.observedCounts, nesting and the layout widths into _decoded/dom-schema.json, keep its titles, labels and cells, and set derivedFrom.capturedOn to this date.",
ceilingDays: 90,
warnDays: 30,
derivedFrom: {
capturedOn: options.now.toISOString().slice(0, 10),
sources: [`Aviary ${options.build} structural observation of the ${route} route`],
notes: "Measured in the signed-in page by Privacy & diagnostics > Copy structural observation."
},
testIds,
roles,
aria,
nesting,
layout,
routes: {
[route]: {
route: ROUTE_TEMPLATES[route],
lang: normalizeLang(documentObject.documentElement.getAttribute("lang")),
observedCounts
}
}
};
}
function routeKind(pathname) {
if (/^\/home\/?$/.test(pathname)) return "home";
if (/^\/[A-Za-z0-9_]{1,30}\/status\/\d+\/?$/.test(pathname)) return "status";
return "other";
}
function hopsTo(node, selector) {
let hops = 0;
for (let current = node?.parentElement ?? null; current; current = current.parentElement) {
hops += 1;
if (current.matches(selector)) return hops;
}
return null;
}
function wrappersBetween(node, selector) {
const hops = hopsTo(node, selector);
return hops === null ? null : hops - 1;
}
function pixels(value) {
const match = /^(\d+(?:\.\d+)?)px$/.exec(value ?? "");
return match ? Math.round(Number(match[1])) : null;
}
function normalizeLang(value) {
return value && /^[a-z]{2,3}(?:-[a-z]{4})?(?:-(?:[a-z]{2}|\d{3}))?$/i.test(value) ? value : null;
}
// src/platform/storage-value-hash.ts
async function hashStorageValue(value) {
const subtle = globalThis.crypto?.subtle;
if (!subtle) {
throw new Error("SHA-256 is unavailable in this browser context");
}
const bytes = new TextEncoder().encode(JSON.stringify(canonicalValue(value, /* @__PURE__ */ new Set())));
const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function storageValueBytes(value) {
const serialized = JSON.stringify(value);
if (serialized === void 0) {
return new TextEncoder().encode(String(value)).byteLength;
}
return new TextEncoder().encode(serialized).byteLength;
}
function canonicalValue(value, seen) {
if (value === null) return null;
if (typeof value === "string") return value;
if (typeof value === "boolean") return ["boolean", value];
if (typeof value === "number") {
if (Number.isNaN(value)) return ["number", "NaN"];
if (value === Number.POSITIVE_INFINITY) return ["number", "Infinity"];
if (value === Number.NEGATIVE_INFINITY) return ["number", "-Infinity"];
if (Object.is(value, -0)) return ["number", "-0"];
return ["number", String(value)];
}
if (typeof value === "bigint") return ["bigint", value.toString()];
if (value === void 0 || typeof value === "function" || typeof value === "symbol") {
return ["undefined"];
}
if (seen.has(value)) {
throw new Error("Storage values must not contain cycles");
}
seen.add(value);
try {
if (value instanceof Date) return ["date", value.toISOString()];
if (value instanceof ArrayBuffer) {
return ["bytes", Array.from(new Uint8Array(value))];
}
if (ArrayBuffer.isView(value)) {
return [
"bytes",
Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
];
}
if (Array.isArray(value)) {
return ["array", value.map((entry) => canonicalValue(entry, seen))];
}
const entries = Object.keys(value).sort().map((key) => [
key,
canonicalValue(value[key], seen)
]);
return ["object", entries];
} finally {
seen.delete(value);
}
}
// src/platform/storage-fence.ts
var STORAGE_FENCE_MESSAGE = "AVIARY_STORAGE_FENCE";
var STORAGE_FENCE_VERSION = 1;
var STORAGE_FENCE_OPERATION_PREFIX = "aviary.fence.v1.op.";
var STORAGE_FENCE_RECEIPT_SUFFIX = ".receipt";
var StorageFenceLostError = class extends Error {
code = "storage-fence-lost";
constructor(message = "The storage lease expired before the write could commit.") {
super(message);
this.name = "StorageFenceLostError";
}
};
var StorageFenceUnavailableError = class extends Error {
code = "storage-fence-unavailable";
constructor(message = "The storage backend cannot prove this write still owns its lease.") {
super(message);
this.name = "StorageFenceUnavailableError";
}
};
function isStorageFenceError(error) {
return error instanceof StorageFenceLostError || error instanceof StorageFenceUnavailableError || isRecord3(error) && (error.code === "storage-fence-lost" || error.code === "storage-fence-unavailable");
}
function isStorageLockFence(value) {
if (!isRecord3(value)) return false;
return value.version === STORAGE_FENCE_VERSION && typeof value.name === "string" && value.name.length > 0 && value.name.length <= 512 && typeof value.owner === "string" && value.owner.length > 0 && value.owner.length <= 256 && typeof value.generation === "number" && Number.isSafeInteger(value.generation) && value.generation >= 0 && typeof value.expiresAt === "number" && Number.isFinite(value.expiresAt);
}
var activeStorageFences = [];
async function withStorageFenceContext(fence, lockKey, exclusive, run) {
if (!fence) return run();
const active2 = { fence, lockKey, exclusive };
activeStorageFences.push(active2);
try {
return await run();
} finally {
const index = activeStorageFences.lastIndexOf(active2);
if (index >= 0) activeStorageFences.splice(index, 1);
}
}
function inferStorageFence(key) {
for (let index = activeStorageFences.length - 1; index >= 0; index -= 1) {
const active2 = activeStorageFences[index];
if (active2.exclusive || key === active2.lockKey || key === `aviary.${active2.lockKey}` || key === active2.fence.name || key === active2.fence.name.replace(/^aviary\./, "")) {
return active2.fence;
}
}
return void 0;
}
function hasExtensionFenceTransport() {
return Boolean(
globalThis.chrome?.runtime?.id && typeof globalThis.chrome.runtime.getManifest === "function" && typeof globalThis.chrome.runtime.sendMessage === "function"
);
}
async function sendStorageFenceControl(operation, fence) {
if (!hasExtensionFenceTransport()) return void 0;
const response = await sendExtensionMessage({
type: STORAGE_FENCE_MESSAGE,
operation,
fence
});
return readFenceResponse(response, operation === "release");
}
async function sendStorageFenceMutation(operation, key, fence, value) {
if (!hasExtensionFenceTransport()) {
throw new StorageFenceUnavailableError(
"The extension background did not expose its fenced storage authority."
);
}
const response = await sendExtensionMessage({
type: STORAGE_FENCE_MESSAGE,
operation,
key,
...operation === "set" ? { value } : {},
fence
});
readFenceResponse(response, true);
}
async function sendExtensionMessage(message) {
const runtime = globalThis.chrome?.runtime;
if (!runtime?.sendMessage) {
throw new StorageFenceUnavailableError(
"The extension background did not expose its fenced storage authority."
);
}
return new Promise((resolve, reject) => {
let settled = false;
const finish2 = (value, error) => {
if (settled) return;
settled = true;
if (error) reject(error);
else resolve(value);
};
try {
const pending = runtime.sendMessage(message, (value) => {
const error = runtime.lastError?.message;
finish2(value, error ? new Error(`Storage fence message failed: ${error}`) : void 0);
});
if (pending && typeof pending.then === "function") {
pending.then(
(value) => finish2(value),
(error) => finish2(void 0, error)
);
}
} catch (error) {
finish2(void 0, error);
}
});
}
function readFenceResponse(value, release) {
const response = isRecord3(value) ? value : {};
if (response.ok !== true) {
const message = typeof response.error === "string" ? response.error : void 0;
if (response.code === "storage-fence-unavailable") {
throw new StorageFenceUnavailableError(message);
}
throw new StorageFenceLostError(message);
}
if (release) return void 0;
if (!isStorageLockFence(response.result)) {
throw new StorageFenceUnavailableError(
"The extension background returned no valid storage fence."
);
}
return response.result;
}
function isRecord3(value) {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
// src/platform/storage.ts
var USERSCRIPT_MANAGER_VALUE_LIMIT_BYTES = 16 * 1024 * 1024;
var userscriptFenceOperationSequence = 0;
var UserscriptStorageCapacityError = class extends Error {
code = "userscript-storage-capacity";
key;
measuredBytes;
limitBytes;
constructor(key, measuredBytes, limitBytes) {
super(
`The userscript manager refused ${key}: ${measuredBytes} bytes exceeds Aviary's ${limitBytes}-byte per-value safety limit.`
);
this.name = "UserscriptStorageCapacityError";
this.key = key;
this.measuredBytes = measuredBytes;
this.limitBytes = limitBytes;
}
};
var onWriteError;
function setStorageErrorSink(sink) {
onWriteError = sink;
}
function reportStorageError(key, error, op) {
onWriteError?.(key, error, op);
}
function createStorageGateway(namespace = "aviary", options = {}) {
const scoped = (key) => {
if (namespace.length === 0 || key.startsWith(`${namespace}.`)) {
return key;
}
return `${namespace}.${key}`;
};
const globals = globalThis;
const mode = options.mode ?? "auto";
const managerLimit = Math.max(
1,
Math.floor(options.userscriptValueLimitBytes ?? USERSCRIPT_MANAGER_VALUE_LIMIT_BYTES)
);
if (mode === "userscript" && (typeof globals.GM_getValue !== "function" || typeof globals.GM_setValue !== "function" || typeof globals.GM_deleteValue !== "function" || typeof globals.GM_listValues !== "function")) {
throw new Error("The userscript manager did not expose its storage API");
}
if (mode === "extension" && !globalThis.chrome?.storage?.local) {
throw new Error("Extension storage is unavailable");
}
return {
async get(key, fallback) {
const storageKey = scoped(key);
try {
if (mode !== "extension" && typeof globals.GM_getValue === "function") {
if (typeof globals.GM_listValues === "function") {
const committed = await readCommittedUserscriptOperation(globals, storageKey, fallback);
if (committed.found) return committed.value;
}
return await globals.GM_getValue(storageKey, fallback);
}
if (mode !== "userscript" && globalThis.chrome?.storage?.local) {
const extensionStorage = globalThis.chrome.storage.local;
const committed = await readCommittedExtensionOperation(extensionStorage, storageKey, fallback);
if (committed.found) return committed.value;
const result = await extensionStorage.get(storageKey);
return result[storageKey] === void 0 ? fallback : result[storageKey];
}
const raw = mode === "auto" ? globalThis.localStorage?.getItem(storageKey) : null;
return raw === null || raw === void 0 ? fallback : JSON.parse(raw);
} catch (error) {
reportStorageError(storageKey, error, "read");
return fallback;
}
},
async set(key, value, fence) {
const storageKey = scoped(key);
const effectiveFence = fence ?? inferStorageFence(storageKey);
try {
if (effectiveFence && typeof globals.GM_setValue === "function") {
await writeUserscriptFenceOperation(globals, storageKey, value, effectiveFence, managerLimit);
return;
}
if (effectiveFence && hasExtensionFenceTransport()) {
await sendStorageFenceMutation("set", storageKey, effectiveFence, value);
return;
}
if (effectiveFence) {
throw new StorageFenceUnavailableError();
}
if (mode !== "extension" && typeof globals.GM_setValue === "function") {
const measuredBytes = storageValueBytes(value);
if (measuredBytes > managerLimit) {
throw new UserscriptStorageCapacityError(storageKey, measuredBytes, managerLimit);
}
await globals.GM_setValue(storageKey, value);
return;
}
if (mode !== "userscript" && globalThis.chrome?.storage?.local) {
await globalThis.chrome.storage.local.set({ [storageKey]: value });
return;
}
if (mode === "auto" && globalThis.localStorage) {
globalThis.localStorage.setItem(storageKey, JSON.stringify(value));
return;
}
throw new Error(`No storage backend is available for ${storageKey}`);
} catch (error) {
reportStorageError(storageKey, error, "write");
throw error;
}
},
async remove(key, fence) {
const storageKey = scoped(key);
const effectiveFence = fence ?? inferStorageFence(storageKey);
try {
if (effectiveFence && typeof globals.GM_deleteValue === "function") {
await writeUserscriptFenceOperation(globals, storageKey, void 0, effectiveFence, managerLimit, "remove");
return;
}
if (effectiveFence && hasExtensionFenceTransport()) {
await sendStorageFenceMutation("remove", storageKey, effectiveFence);
return;
}
if (effectiveFence) {
throw new StorageFenceUnavailableError();
}
if (mode !== "extension" && typeof globals.GM_deleteValue === "function") {
await globals.GM_deleteValue(storageKey);
return;
}
if (mode !== "userscript" && globalThis.chrome?.storage?.local) {
await globalThis.chrome.storage.local.remove(storageKey);
return;
}
if (mode === "auto" && globalThis.localStorage) {
globalThis.localStorage.removeItem(storageKey);
return;
}
throw new Error(`No storage backend is available for ${storageKey}`);
} catch (error) {
reportStorageError(storageKey, error, "write");
throw error;
}
},
async keys() {
const prefix = namespace.length > 0 ? `${namespace}.` : "";
if (mode !== "extension" && typeof globals.GM_listValues === "function") {
return (await globals.GM_listValues()).filter(
(key) => key.startsWith(prefix) && !key.startsWith(STORAGE_FENCE_OPERATION_PREFIX)
);
}
if (mode !== "userscript" && globalThis.chrome?.storage?.local) {
return Object.keys(await globalThis.chrome.storage.local.get(null)).filter((key) => key.startsWith(prefix) && !key.startsWith(STORAGE_FENCE_OPERATION_PREFIX));
}
if (mode === "auto" && globalThis.localStorage) {
const keys = [];
for (let index = 0; index < globalThis.localStorage.length; index += 1) {
const key = globalThis.localStorage.key(index);
if (key?.startsWith(prefix)) keys.push(key);
}
return keys;
}
return [];
}
};
}
async function writeUserscriptFenceOperation(globals, storageKey, value, fence, managerLimit, kind = "set") {
if (!isStorageLockFence(fence)) {
throw new StorageFenceUnavailableError("The storage fence was malformed before the write.");
}
const operationId = `${Date.now().toString(36)}-${(++userscriptFenceOperationSequence).toString(36)}-${Math.random().toString(36).slice(2)}`;
const operationKey = `${STORAGE_FENCE_OPERATION_PREFIX}${encodeURIComponent(storageKey)}.${encodeURIComponent(operationId)}`;
const operation = {
version: 1,
operationId,
storageKey,
kind,
...kind === "set" ? { value } : {},
fence
};
const measuredBytes = storageValueBytes(operation);
if (measuredBytes > managerLimit) {
throw new UserscriptStorageCapacityError(storageKey, measuredBytes, managerLimit);
}
await globals.GM_setValue(operationKey, operation);
const receipt = {
version: 1,
operationId,
committedAt: Date.now()
};
await globals.GM_setValue(`${operationKey}${STORAGE_FENCE_RECEIPT_SUFFIX}`, receipt);
try {
if (kind === "set") await globals.GM_setValue(storageKey, value);
else await globals.GM_deleteValue(storageKey);
} catch (error) {
reportStorageError(storageKey, error, "write");
throw error;
}
}
async function readCommittedUserscriptOperation(globals, storageKey, fallback) {
const keys = await globals.GM_listValues();
const prefix = `${STORAGE_FENCE_OPERATION_PREFIX}${encodeURIComponent(storageKey)}.`;
const rawPresent = keys.includes(storageKey);
const rawValue = rawPresent ? await globals.GM_getValue(storageKey, void 0) : void 0;
let best;
const allOperations = [];
for (const operationKey of keys.filter(
(key) => key.startsWith(prefix) && !key.endsWith(STORAGE_FENCE_RECEIPT_SUFFIX) && !key.endsWith(".materialized")
)) {
const operation = await globals.GM_getValue(operationKey, void 0);
const receipt = await globals.GM_getValue(
`${operationKey}${STORAGE_FENCE_RECEIPT_SUFFIX}`,
void 0
);
if (!isUserscriptFenceOperation(operation, storageKey) || !isUserscriptFenceReceipt(receipt)) {
continue;
}
const candidate = { operationKey, operation, receipt };
allOperations.push(candidate);
if (receipt.operationId !== operation.operationId || receipt.committedAt > operation.fence.expiresAt) {
continue;
}
if (!best || compareUserscriptOperations(best, candidate) < 0) best = candidate;
}
if (!best) return { found: false, value: fallback };
if (rawPresent && !allOperations.some(
(candidate) => candidate.operation.kind === "set" && storageValuesEqual(rawValue, candidate.operation.value)
)) {
return { found: true, value: rawValue };
}
return {
found: true,
value: best.operation.kind === "remove" ? fallback : best.operation.value
};
}
async function readCommittedExtensionOperation(storage, storageKey, fallback) {
const values = await storage.get(null);
const keys = Object.keys(values);
const prefix = `${STORAGE_FENCE_OPERATION_PREFIX}${encodeURIComponent(storageKey)}.`;
const rawPresent = Object.prototype.hasOwnProperty.call(values, storageKey);
const rawValue = rawPresent ? values[storageKey] : void 0;
let best;
const allOperations = [];
for (const operationKey of keys.filter(
(key) => key.startsWith(prefix) && !key.endsWith(STORAGE_FENCE_RECEIPT_SUFFIX) && !key.endsWith(".materialized")
)) {
const operation = values[operationKey];
const receipt = values[`${operationKey}${STORAGE_FENCE_RECEIPT_SUFFIX}`];
if (!isUserscriptFenceOperation(operation, storageKey) || !isUserscriptFenceReceipt(receipt)) {
continue;
}
const candidate = { operationKey, operation, receipt };
allOperations.push(candidate);
if (receipt.operationId !== operation.operationId || receipt.committedAt > operation.fence.expiresAt) {
continue;
}
if (!best || compareUserscriptOperations(best, candidate) < 0) best = candidate;
}
if (!best) return { found: false, value: fallback };
if (rawPresent && !allOperations.some(
(candidate) => candidate.operation.kind === "set" && storageValuesEqual(rawValue, candidate.operation.value)
)) {
return { found: true, value: rawValue };
}
return {
found: true,
value: best.operation.kind === "remove" ? fallback : best.operation.value
};
}
function isUserscriptFenceOperation(value, storageKey) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const operation = value;
return operation.version === 1 && typeof operation.operationId === "string" && operation.operationId.length > 0 && operation.storageKey === storageKey && (operation.kind === "set" || operation.kind === "remove") && isStorageLockFence(operation.fence);
}
function isUserscriptFenceReceipt(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const receipt = value;
return receipt.version === 1 && typeof receipt.operationId === "string" && receipt.operationId.length > 0 && typeof receipt.committedAt === "number" && Number.isFinite(receipt.committedAt);
}
function storageValuesEqual(left, right) {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => storageValuesEqual(value, right[index]));
}
if (left && right && typeof left === "object" && typeof right === "object" && !Array.isArray(left) && !Array.isArray(right)) {
const leftRecord = left;
const rightRecord = right;
const leftKeys = Object.keys(leftRecord).sort();
const rightKeys = Object.keys(rightRecord).sort();
return leftKeys.length === rightKeys.length && leftKeys.every(
(key, index) => key === rightKeys[index] && storageValuesEqual(leftRecord[key], rightRecord[key])
);
}
return false;
}
function compareUserscriptOperations(left, right) {
if (left.operation.fence.name === right.operation.fence.name) {
const generation2 = left.operation.fence.generation - right.operation.fence.generation;
if (generation2 !== 0) return generation2;
}
return left.receipt.committedAt - right.receipt.committedAt || left.operation.fence.owner.localeCompare(right.operation.fence.owner) || left.operationKey.localeCompare(right.operationKey);
}
// src/platform/storage-lock-register.ts
var STORAGE_LOCK_REGISTER_MESSAGE = "AVIARY_STORAGE_LOCK_REGISTER";
async function sendStorageLockRegister(request) {
const runtime = globalThis.chrome?.runtime;
if (typeof runtime?.sendMessage !== "function") {
throw new Error("The extension lock register authority is unavailable.");
}
const response = await new Promise((resolve, reject) => {
let settled = false;
const finish2 = (value, error) => {
if (settled) return;
settled = true;
if (error) reject(error);
else resolve(value);
};
try {
const pending = runtime.sendMessage(request, (value) => {
const error = runtime.lastError?.message;
finish2(value, error ? new Error(`Storage lock register message failed: ${error}`) : void 0);
});
if (pending && typeof pending.then === "function") {
pending.then(
(value) => finish2(value),
(error) => finish2(void 0, error)
);
}
} catch (error) {
finish2(void 0, error);
}
});
if (!response || typeof response !== "object" || response.ok !== true) {
const message = response && typeof response === "object" && typeof response.error === "string" ? response.error : "The extension lock register rejected the request.";
throw new Error(message);
}
return response;
}
// src/platform/storage-lock.ts
var LIBRARY_RESTORE_LOCK = "aviary.library.restore";
var chains = /* @__PURE__ */ new Map();
var storageGateQueue = [];
var storageGateReaders = 0;
var storageGateWriter = false;
var lockOwnerSequence = 0;
var SHARED_LOCK_PREFIX = "aviary.lock.v1";
var SHARED_LOCK_LEASE_MS = 3e4;
var SHARED_LOCK_RENEW_MS = 8e3;
var SHARED_LOCK_POLL_MS = 12;
var USERSCRIPT_LOCK_SETTLE_MS = 200;
function lockManager() {
const locks = globalThis.navigator?.locks;
return typeof locks?.request === "function" ? locks : void 0;
}
async function withStorageLock(name, run, options = {}) {
if (options.restoreGate === false) return withNamedStorageLock(name, run);
return withStorageRestoreGate("shared", () => withNamedStorageLock(name, run));
}
async function withExclusiveStorageGate(run) {
return withStorageRestoreGate("exclusive", run);
}
async function withNamedStorageLock(name, run) {
const previous = chains.get(name) ?? Promise.resolve();
const attempt = previous.then(
() => runUnderBrowserLock(name, run),
// A failure ahead of us released its lock; it is not a reason to refuse this write.
() => runUnderBrowserLock(name, run)
);
const settled = attempt.then(
() => void 0,
() => void 0
);
chains.set(name, settled);
void settled.then(() => {
if (chains.get(name) === settled) {
chains.delete(name);
}
});
return attempt;
}
function withStorageRestoreGate(mode, run) {
return new Promise((resolve, reject) => {
storageGateQueue.push({
mode,
run,
resolve: (value) => resolve(value),
reject
});
drainStorageGate();
});
}
function drainStorageGate() {
if (storageGateWriter || storageGateQueue.length === 0) return;
if (storageGateReaders > 0 && storageGateQueue[0]?.mode === "exclusive") return;
if (storageGateQueue[0]?.mode === "exclusive") {
const request = storageGateQueue.shift();
storageGateWriter = true;
runStorageGateRequest(request);
return;
}
while (storageGateQueue[0]?.mode === "shared" && !storageGateWriter) {
const request = storageGateQueue.shift();
storageGateReaders += 1;
runStorageGateRequest(request);
}
}
function runStorageGateRequest(request) {
void runUnderSharedStorageLock(
LIBRARY_RESTORE_LOCK,
request.mode,
request.run,
LIBRARY_RESTORE_LOCK,
request.mode === "exclusive"
).then(
request.resolve,
request.reject
).finally(() => {
if (request.mode === "exclusive") storageGateWriter = false;
else storageGateReaders -= 1;
drainStorageGate();
});
}
async function runUnderBrowserLock(name, run) {
return runUnderSharedStorageLock(`aviary.${name}`, "exclusive", run, name, false);
}
async function runUnderSharedStorageLock(name, mode, run, contextKey = name, exclusiveContext = false) {
const registerStore = sharedLockRegisterStore();
if (registerStore) {
return runUnderRegisterLock(
registerStore,
name,
mode,
(fence) => withStorageFenceContext(fence, contextKey, exclusiveContext, () => run(fence))
);
}
const locks = lockManager();
if (!locks) {
return run(void 0);
}
let result;
let failure;
let failed = false;
const callback = async () => {
try {
result = await run(void 0);
} catch (error) {
failed = true;
failure = error;
}
};
if (mode === "shared") await locks.request(name, { mode }, callback);
else await locks.request(name, callback);
if (failed) {
throw failure;
}
return result;
}
async function runUnderRegisterLock(store6, name, mode, run) {
const encodedName = encodeURIComponent(name);
if (encodedName.length > 320) throw new Error("The storage lock name is too long");
const prefix = `${SHARED_LOCK_PREFIX}.${encodedName}`;
const owner = nextLockOwner();
const key = `${prefix}.${owner}`;
let contender = {
version: 1,
owner,
phase: "choosing",
ticket: 0,
mode,
expiresAt: Date.now() + SHARED_LOCK_LEASE_MS
};
await store6.write(prefix, key, contender);
try {
const initial = await readLockContenders(store6, prefix, owner);
contender = {
...contender,
phase: "waiting",
ticket: Math.max(0, ...initial.map((entry) => entry.ticket)) + 1,
expiresAt: Date.now() + SHARED_LOCK_LEASE_MS
};
await store6.write(prefix, key, contender);
if (store6.kind === "userscript") await waitForUserscriptLockSettle();
while (!await lockCanEnter(store6, prefix, contender)) {
await waitForLockPoll();
contender = await renewLockContender(store6, key, contender);
}
const localFence = {
version: 1,
name,
owner,
generation: contender.ticket,
expiresAt: contender.expiresAt
};
const needsFence = mode === "exclusive" || store6.kind === "userscript";
const remoteFence = needsFence ? await sendStorageFenceControl("acquire", localFence) : void 0;
let fence = needsFence ? remoteFence ?? (store6.kind === "userscript" ? localFence : void 0) : void 0;
if (needsFence && !fence && hasExtensionFenceTransport()) {
throw new Error("The extension background did not return a storage fence");
}
let renewal = Promise.resolve();
let renewalFailure;
const renew = () => {
renewal = renewal.then(async () => {
try {
contender = await renewLockContender(store6, key, contender);
if (fence) {
const renewed = {
...fence,
expiresAt: contender.expiresAt
};
const remoteRenewed = await sendStorageFenceControl("renew", renewed);
fence = remoteRenewed ?? (store6.kind === "userscript" ? renewed : void 0);
if (!fence && hasExtensionFenceTransport()) {
throw new Error("The extension background did not renew the storage fence");
}
}
} catch (error) {
renewalFailure = error;
}
});
};
const timer2 = globalThis.setInterval(renew, SHARED_LOCK_RENEW_MS);
try {
const result = await run(fence);
await renewal;
if (renewalFailure) throw renewalFailure;
return result;
} finally {
globalThis.clearInterval(timer2);
await renewal;
try {
if (fence) await sendStorageFenceControl("release", fence);
} catch {
}
}
} finally {
await store6.remove(prefix, key);
}
}
async function lockCanEnter(store6, prefix, contender) {
const contenders = await readLockContenders(store6, prefix);
const self = contenders.find((entry) => entry.owner === contender.owner);
if (!self || self.phase !== contender.phase || self.ticket !== contender.ticket || self.mode !== contender.mode) {
await store6.write(prefix, `${prefix}.${contender.owner}`, contender);
return false;
}
const peers = contenders.filter((entry) => entry.owner !== contender.owner);
if (peers.some((peer) => peer.phase === "choosing")) return false;
return !peers.some((peer) => {
if (peer.phase !== "waiting") return false;
if (contender.mode === "shared" && peer.mode === "shared") return false;
return compareLockContenders(peer, contender) < 0;
});
}
async function readLockContenders(store6, prefix, excludedOwner) {
const now6 = Date.now();
const contenders = [];
for (const [key, value] of await store6.entries(prefix)) {
if (!isSharedLockContender(value) || value.expiresAt <= now6) {
await store6.remove(prefix, key);
continue;
}
if (value.owner !== excludedOwner) contenders.push(value);
}
return contenders;
}
async function renewLockContender(store6, key, contender) {
if (contender.expiresAt - Date.now() > SHARED_LOCK_RENEW_MS * 2) return contender;
const renewed = { ...contender, expiresAt: Date.now() + SHARED_LOCK_LEASE_MS };
await store6.write(prefixFromRegisterKey(key), key, renewed);
return renewed;
}
function compareLockContenders(left, right) {
return left.ticket - right.ticket || left.owner.localeCompare(right.owner);
}
function isSharedLockContender(value) {
if (!value || typeof value !== "object") return false;
const contender = value;
return contender.version === 1 && typeof contender.owner === "string" && contender.owner.length > 0 && (contender.phase === "choosing" || contender.phase === "waiting") && typeof contender.ticket === "number" && Number.isSafeInteger(contender.ticket) && contender.ticket >= 0 && (contender.mode === "shared" || contender.mode === "exclusive") && typeof contender.expiresAt === "number" && Number.isFinite(contender.expiresAt);
}
function nextLockOwner() {
const random = globalThis.crypto?.randomUUID?.().replaceAll("-", "") ?? Math.random().toString(36).slice(2);
return `${Date.now().toString(36)}-${(++lockOwnerSequence).toString(36)}-${random}`;
}
function waitForLockPoll() {
return new Promise((resolve) => {
globalThis.setTimeout(resolve, SHARED_LOCK_POLL_MS);
});
}
function waitForUserscriptLockSettle() {
return new Promise((resolve) => {
globalThis.setTimeout(resolve, USERSCRIPT_LOCK_SETTLE_MS);
});
}
function sharedLockRegisterStore() {
const runtime = globalThis.chrome?.runtime;
const extensionStorage = runtime?.id && typeof runtime.getManifest === "function" ? runtime.sendMessage : void 0;
if (typeof extensionStorage === "function") {
return {
kind: "extension",
async entries(prefix) {
const response = await sendStorageLockRegister({
type: STORAGE_LOCK_REGISTER_MESSAGE,
version: 1,
operation: "entries",
prefix
});
return response.entries ?? [];
},
async write(prefix, key, value) {
await sendStorageLockRegister({
type: STORAGE_LOCK_REGISTER_MESSAGE,
version: 1,
operation: "write",
prefix,
key,
value
});
},
async remove(prefix, key) {
await sendStorageLockRegister({
type: STORAGE_LOCK_REGISTER_MESSAGE,
version: 1,
operation: "remove",
prefix,
key
});
}
};
}
if (typeof globalThis.GM_listValues === "function" && typeof globalThis.GM_getValue === "function" && typeof globalThis.GM_setValue === "function" && typeof globalThis.GM_deleteValue === "function") {
return {
kind: "userscript",
async entries(prefix) {
const raw = await ensureUserscriptRoster(prefix);
return readUserscriptRoster(prefix, raw);
},
async write(prefix, key, value) {
await updateUserscriptRoster(prefix, key, value);
},
async remove(prefix, key) {
await updateUserscriptRoster(prefix, key, void 0, true);
}
};
}
return void 0;
}
function prefixFromRegisterKey(key) {
const separator = key.lastIndexOf(".");
return separator > 0 ? key.slice(0, separator) : key;
}
var USERSCRIPT_ROSTER_VERSION = 1;
var migratedUserscriptRosterPrefixes = /* @__PURE__ */ new Set();
function lockRosterKey(prefix) {
return `${SHARED_LOCK_PREFIX}.roster.${encodeURIComponent(prefix)}`;
}
function readUserscriptRoster(prefix, raw) {
if (!raw || typeof raw !== "object") return [];
const value = raw;
if (value.version !== USERSCRIPT_ROSTER_VERSION || value.prefix !== prefix || !Array.isArray(value.entries)) {
return [];
}
return value.entries.filter(
(entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && entry[0].startsWith(`${prefix}.`)
);
}
async function migrateUserscriptRoster(prefix) {
if (typeof globalThis.GM_listValues !== "function") return;
const keys = (await globalThis.GM_listValues()).filter((key) => key.startsWith(`${prefix}.`));
const entries = await Promise.all(keys.map(async (key) => [
key,
await globalThis.GM_getValue(key, void 0)
]));
await globalThis.GM_setValue(lockRosterKey(prefix), {
version: USERSCRIPT_ROSTER_VERSION,
prefix,
entries
});
migratedUserscriptRosterPrefixes.add(prefix);
}
async function ensureUserscriptRoster(prefix) {
const key = lockRosterKey(prefix);
const current = await globalThis.GM_getValue(key, void 0);
if (current !== void 0) return current;
if (migratedUserscriptRosterPrefixes.has(prefix)) return void 0;
await migrateUserscriptRoster(prefix);
return globalThis.GM_getValue(key, void 0);
}
async function updateUserscriptRoster(prefix, key, value, remove = false) {
const rosterKey = lockRosterKey(prefix);
for (let attempt = 0; attempt < 8; attempt += 1) {
const raw = await ensureUserscriptRoster(prefix);
const entries = new Map(readUserscriptRoster(prefix, raw));
if (remove) entries.delete(key);
else entries.set(key, value);
if (entries.size === 0) {
await globalThis.GM_deleteValue(rosterKey);
migratedUserscriptRosterPrefixes.add(prefix);
return;
}
await globalThis.GM_setValue(rosterKey, {
version: USERSCRIPT_ROSTER_VERSION,
prefix,
entries: [...entries.entries()]
});
const confirmed = new Map(readUserscriptRoster(prefix, await globalThis.GM_getValue(rosterKey, void 0)));
if (remove ? !confirmed.has(key) : valuesEqual(confirmed.get(key), value)) return;
}
throw new Error("The userscript lock roster did not settle");
}
function valuesEqual(left, right) {
try {
return JSON.stringify(left) === JSON.stringify(right);
} catch {
return false;
}
}
async function mutateStored(storage, key, fallback, mutate, options = {}) {
return withStorageLock(key, async (fence) => {
const stored = await storage.get(key, fallback);
const next = await mutate(stored);
await storage.set(key, next, fence);
return next;
}, options);
}
async function replaceStored(storage, key, value) {
await withStorageLock(key, async (fence) => {
await storage.set(key, value, fence);
});
}
function mergeKeyed(stored, added, removed = []) {
const merged = new Map(stored);
for (const [key, value] of added) {
merged.set(key, value);
}
for (const key of removed) {
merged.delete(key);
}
return merged;
}
// src/platform/durable-storage.ts
var DURABLE_STORAGE_SCHEMA_VERSION = 1;
var PENDING_WRITES_KEY = "aviary.durable.pending";
var PENDING_WRITES_SCHEMA_VERSION = 2;
var PENDING_WRITES_LOCK = "durable.pending-writes";
var DURABLE_STORAGE_KEYS = [
"aviary.profiles.v1",
"aviary.profile.active.v1",
"aviary.profile.migration.v1",
"aviary.settings.v1",
"aviary.integration.usage.v1",
"aviary.export.checkpoints.v1",
"aviary.queryIds.v1",
"aviary.media.history.v1",
"aviary.media.queue.v1",
"aviary.aria2.history.v1",
"aviary.hiddenPosts.v1",
"aviary.seenPosts.v1",
"aviary.readingMarkers.v1",
"aviary.catchUp.v1",
"aviary.media.last-download.v1",
"aviary.cleanupQueue.v1",
"aviary.accountCleanup.v1",
"aviary.userNotes.v1",
"aviary.audit.v1",
"aviary.firstRun.v1",
"aviary.diagnostics.v1",
"aviary.background.diagnostics.v1",
"aviary.adObservations.v1",
"aviary.library.bookmarks.v1",
"aviary.snapshots.v1",
"aviary.library.underTheHood.v1",
"aviary.semanticIndex.v1",
"aviary.archive.imports.v1",
"aviary.archive.library.v1",
"aviary.waczSigning.v1"
];
var DURABLE_DATABASE_NAME = "aviary.durable.v1";
var DURABLE_OBJECT_STORE = "values";
var DurableStorageGateway = class {
#legacy;
#backend;
#namespace;
#legacyBackend;
#initialized = false;
#usable = true;
#status = {
backend: "legacy",
schemaVersion: 0,
migratedKeys: 0,
usageBytes: null,
quotaBytes: null,
persistence: "unknown",
pendingWrites: 0,
lastError: null
};
constructor(legacy, backend, namespace = "aviary", legacyBackend = "legacy") {
this.#legacy = legacy;
this.#backend = backend;
this.#namespace = namespace;
this.#legacyBackend = legacyBackend;
if (backend) {
this.#status.backend = "indexeddb";
} else {
this.#status.backend = legacyBackend;
if (legacyBackend === "userscript-manager") {
this.#status.schemaVersion = DURABLE_STORAGE_SCHEMA_VERSION;
}
}
}
async initialize(keys = DURABLE_STORAGE_KEYS) {
if (this.#initialized) {
return this.getStatus();
}
this.#initialized = true;
if (!this.#backend) {
return this.getStatus();
}
try {
await this.#reconcilePendingWrites();
const previous = await this.#backend.getMeta();
const migratedKeys = new Set(previous?.migratedKeys ?? []);
const entries = [];
const discoveredKeys = this.#legacy.keys ? await this.#legacy.keys() : [];
const scopedKeys = [.../* @__PURE__ */ new Set([
...keys.map((key) => this.#scope(key)),
...discoveredKeys.filter((key) => this.#isDurable(key)).map((key) => this.#scope(key))
])];
for (const key of scopedKeys) {
const existing = await readDurableBackend(this.#backend, key);
if (existing.found) {
migratedKeys.add(key);
continue;
}
const legacy = await this.#legacy.get(key, void 0);
if (legacy !== void 0) {
entries.push([key, legacy]);
migratedKeys.add(key);
}
}
const meta = {
schemaVersion: DURABLE_STORAGE_SCHEMA_VERSION,
migratedKeys: [...migratedKeys].sort(),
migratedAt: (/* @__PURE__ */ new Date()).toISOString()
};
await this.#backend.putMany(entries, meta);
await Promise.all(
entries.map(([key, value]) => verifyBackendValue(this.#backend, key, value))
);
for (const [key] of entries) {
try {
await this.#legacy.remove(key);
} catch (error) {
reportStorageError(key, error, "write");
}
}
this.#status.schemaVersion = meta.schemaVersion;
this.#status.migratedKeys = meta.migratedKeys.length;
await this.refreshEstimate();
} catch (error) {
this.#fallback(error);
}
return this.getStatus();
}
getStatus() {
return { ...this.#status };
}
async refreshEstimate() {
if (!this.#backend || !this.#usable) {
return this.getStatus();
}
try {
const estimate = await this.#backend.estimate();
this.#status.usageBytes = finiteOrNull(estimate.usage);
this.#status.quotaBytes = finiteOrNull(estimate.quota);
this.#status.persistence = estimate.persisted === void 0 ? "unknown" : estimate.persisted ? "persisted" : "best-effort";
} catch (error) {
reportStorageError("aviary.durable.estimate", error, "read");
}
return this.getStatus();
}
/**
* The measured breakdown, or `null` where this session's backend cannot enumerate itself.
*
* A userscript manager and the legacy fallback both store values behind an API with no cursor,
* so they answer nothing rather than a total assembled from the keys Aviary happens to remember.
*/
async measureCollections() {
await this.#ensureInitialized();
if (!this.#backend || !this.#usable || typeof this.#backend.measure !== "function") {
return null;
}
try {
const breakdown = await this.#backend.measure();
return {
totalBytes: breakdown.totalBytes,
collections: breakdown.collections.map((entry) => ({
...entry,
key: this.#unscope(entry.key)
})),
usageDetails: breakdown.usageDetails
};
} catch (error) {
reportStorageError("aviary.durable.measure", error, "read");
return null;
}
}
async get(key, fallback) {
if (!this.#isDurable(key)) {
return this.#legacy.get(key, fallback);
}
await this.#ensureInitialized();
if (!this.#backend || !this.#usable) {
return this.#backend ? this.#getFallbackValue(key, fallback) : this.#legacy.get(key, fallback);
}
const scopedKey = this.#scope(key);
try {
const stored = await readDurableBackend(this.#backend, scopedKey);
if (stored.found) {
return stored.removed ? fallback : stored.value;
}
const legacy = await this.#legacy.get(key, void 0);
if (legacy !== void 0) {
await this.#backend.put(scopedKey, legacy, void 0, createDurableOperation());
await verifyBackendValue(this.#backend, scopedKey, legacy);
try {
await this.#legacy.remove(key);
} catch (error) {
reportStorageError(scopedKey, error, "write");
}
return legacy;
}
return fallback;
} catch (error) {
this.#fallback(error);
return this.#getFallbackValue(key, fallback);
}
}
async set(key, value, fence) {
const effectiveFence = fence ?? inferStorageFence(this.#scope(key));
if (!this.#isDurable(key)) {
await this.#legacy.set(key, value, effectiveFence);
return;
}
await this.#ensureInitialized();
const operation = createDurableOperation();
if (!this.#backend || !this.#usable) {
if (this.#backend) {
await this.#markPending(key, "put", value, effectiveFence, operation);
} else {
await this.#legacy.set(key, value, effectiveFence);
}
return;
}
const scopedKey = this.#scope(key);
try {
await this.#backend.put(scopedKey, value, effectiveFence, operation);
try {
await this.#legacy.remove(key, effectiveFence);
} catch (error) {
if (effectiveFence && isStorageFenceError(error)) throw error;
reportStorageError(scopedKey, error, "write");
await this.#markPending(key, "put", value, effectiveFence, operation);
}
await this.refreshEstimate();
} catch (error) {
if (effectiveFence && isStorageFenceError(error)) throw error;
this.#fallback(error);
await this.#markPending(key, "put", value, effectiveFence, operation);
}
}
async remove(key, fence) {
const effectiveFence = fence ?? inferStorageFence(this.#scope(key));
if (!this.#isDurable(key)) {
await this.#legacy.remove(key, effectiveFence);
return;
}
await this.#ensureInitialized();
const operation = createDurableOperation();
if (!this.#backend || !this.#usable) {
if (this.#backend) {
await this.#markPending(key, "remove", void 0, effectiveFence, operation);
} else {
await this.#legacy.remove(key, effectiveFence);
}
return;
}
const scopedKey = this.#scope(key);
try {
await this.#backend.remove(scopedKey, effectiveFence, operation);
try {
await this.#legacy.remove(key, effectiveFence);
} catch (error) {
if (effectiveFence && isStorageFenceError(error)) throw error;
reportStorageError(scopedKey, error, "write");
await this.#markPending(key, "remove", void 0, effectiveFence, operation);
}
await this.refreshEstimate();
} catch (error) {
if (effectiveFence && isStorageFenceError(error)) throw error;
this.#fallback(error);
await this.#markPending(key, "remove", void 0, effectiveFence, operation);
}
}
async #ensureInitialized() {
if (!this.#initialized) {
await this.initialize();
}
}
#isDurable(key) {
const scoped = this.#scope(key);
return scoped.startsWith(`${this.#namespace}.`) && /\.v\d+$/.test(scoped);
}
#scope(key) {
if (this.#namespace.length === 0 || key.startsWith(`${this.#namespace}.`)) {
return key;
}
return `${this.#namespace}.${key}`;
}
/** The name a store knows itself by, with this profile's namespace taken back off. */
#unscope(key) {
const prefix = `${this.#namespace}.`;
return this.#namespace.length > 0 && key.startsWith(prefix) ? key.slice(prefix.length) : key;
}
#fallback(error) {
this.#usable = false;
this.#status.backend = "indexeddb-fallback";
this.#status.lastError = error instanceof Error ? error.message : String(error);
reportStorageError("aviary.durable", error, "write");
}
/**
* Records that `key` was written (or removed) into legacy while the backend was unavailable, so
* the next healthy boot knows legacy holds the newer value. Kept in the legacy store on purpose:
* the backend is the thing that just failed.
*/
async #markPending(key, kind, value, _fence, operation) {
try {
this.#status.pendingWrites = await withStorageLock(PENDING_WRITES_LOCK, async (fence) => {
const pending = await this.#readPendingWrites();
addPendingWrite(pending, {
id: createPendingWriteId(),
key,
kind,
...kind === "put" ? { value } : {},
...operation ?? {}
});
await this.#writePendingWrites(pending, fence);
return pending.size;
}, { restoreGate: false });
} catch (error) {
reportStorageError(PENDING_WRITES_KEY, error, "write");
throw error;
}
}
/**
* Folds a previous fallback session's writes back into the backend. Legacy wins here -- and only
* here -- because a key reaches this list only by being written while the backend was down.
*/
async #reconcilePendingWrites() {
if (!this.#backend) {
return;
}
await withStorageLock(PENDING_WRITES_LOCK, async (fence) => {
const pending = await this.#readPendingWrites();
this.#status.pendingWrites = pending.size;
if (pending.size === 0) return;
for (const write of pending.values()) {
const scopedWrite = { ...write, key: this.#scope(write.key) };
try {
await this.#backend.stagePendingWrite(scopedWrite, fence);
const receipt = await this.#backend.commitPendingWrite(scopedWrite, fence);
await verifyPendingWriteReceipt(scopedWrite, receipt);
} catch (error) {
reportStorageError(write.key, error, "write");
throw error;
}
}
for (const write of pending.values()) {
try {
await this.#legacy.remove(write.key, fence);
} catch (error) {
reportStorageError(write.key, error, "write");
throw error;
}
}
try {
await this.#writePendingWrites(/* @__PURE__ */ new Map(), fence);
} catch (error) {
reportStorageError(PENDING_WRITES_KEY, error, "write");
throw error;
}
this.#status.pendingWrites = 0;
}, { restoreGate: false });
}
async #getFallbackValue(key, fallback) {
return withStorageLock(PENDING_WRITES_LOCK, async () => {
const pending = await this.#readPendingWrites();
const write = pending.get(key);
if (write?.kind === "put") return write.value;
if (write?.kind === "remove") return fallback;
return this.#legacy.get(key, fallback);
}, { restoreGate: false });
}
async #readPendingWrites() {
const stored = await this.#legacy.get(PENDING_WRITES_KEY, []);
const pending = /* @__PURE__ */ new Map();
if (Array.isArray(stored)) {
for (const key of stored) {
if (typeof key !== "string" || !this.#isDurable(key) || pending.has(key)) continue;
const value = await this.#legacy.get(key, void 0);
const kind = value === void 0 ? "remove" : "put";
const fingerprint2 = await hashStorageValue({ key, kind, value });
addPendingWrite(pending, {
id: `legacy-${fingerprint2}`,
key,
kind,
...kind === "put" ? { value } : {}
});
}
return pending;
}
if (!isPendingWriteLedger(stored)) return pending;
for (const write of stored.entries) {
if (this.#isDurable(write.key)) addPendingWrite(pending, write);
}
return pending;
}
async #writePendingWrites(pending, fence) {
if (pending.size === 0) {
await this.#legacy.remove(PENDING_WRITES_KEY, fence);
return;
}
const compacted = /* @__PURE__ */ new Map();
for (const write of pending.values()) addPendingWrite(compacted, write);
await this.#legacy.set(PENDING_WRITES_KEY, {
schemaVersion: PENDING_WRITES_SCHEMA_VERSION,
entries: [...compacted.values()]
}, fence);
}
};
function createDurableStorageGateway(legacy, options = {}) {
const backend = options.backend ?? null;
return new DurableStorageGateway(
legacy,
backend,
options.namespace ?? "aviary",
options.legacyBackend ?? "legacy"
);
}
function storageCapReport(input) {
const capBytes = Number.isFinite(input.capBytes) && input.capBytes > 0 ? Math.round(input.capBytes) : 0;
const storedBytes = Math.max(0, Math.round(input.storedBytes));
const incomingBytes = Math.max(0, Math.round(input.incomingBytes ?? 0));
if (capBytes === 0) {
return { state: "off", capBytes: 0, storedBytes, incomingBytes };
}
const state2 = storedBytes >= capBytes ? "over" : storedBytes + incomingBytes >= capBytes ? "would-cross" : "under";
return { state: state2, capBytes, storedBytes, incomingBytes };
}
function isDurableStorageMeta(value) {
if (!value || typeof value !== "object") return false;
const record = value;
return record.schemaVersion === DURABLE_STORAGE_SCHEMA_VERSION && Array.isArray(record.migratedKeys) && record.migratedKeys.every((key) => typeof key === "string") && (record.migratedAt === null || typeof record.migratedAt === "string");
}
function isDurablePendingWrite(value) {
if (!value || typeof value !== "object") return false;
const write = value;
return validPendingWriteId(write.id) && typeof write.key === "string" && write.key.length > 0 && write.key.length <= 512 && (write.kind === "put" && "value" in write || write.kind === "remove") && (write.operationId === void 0 || validPendingWriteId(write.operationId)) && (write.operationOrder === void 0 || validOperationOrder(write.operationOrder));
}
function isDurableOperation(value) {
if (!value || typeof value !== "object") return false;
const operation = value;
return validPendingWriteId(operation.operationId) && validOperationOrder(operation.operationOrder);
}
function finiteOrNull(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
async function readDurableBackend(backend, key) {
if (typeof backend.read === "function") {
return backend.read(key);
}
const value = await backend.get(key);
return value === void 0 ? { found: false, removed: false } : { found: true, removed: false, value };
}
async function verifyBackendValue(backend, key, expectedValue) {
const actualValue = await backend.get(key);
if (actualValue === void 0) {
throw new Error(`Durable migration did not retain ${key}`);
}
const [expectedHash, actualHash] = await Promise.all([
hashStorageValue(expectedValue),
hashStorageValue(actualValue)
]);
if (expectedHash !== actualHash) {
throw new Error(`Durable migration hash mismatch for ${key}`);
}
}
async function verifyPendingWriteReceipt(write, receipt) {
const expectedHash = write.kind === "put" ? await hashStorageValue(write.value) : null;
if (receipt.id !== write.id || receipt.key !== write.key || receipt.kind !== write.kind || receipt.valueHash !== expectedHash) {
throw new Error(`Durable reconciliation receipt mismatch for ${write.key}`);
}
}
function isPendingWriteLedger(value) {
if (!value || typeof value !== "object") return false;
const ledger = value;
return ledger.schemaVersion === PENDING_WRITES_SCHEMA_VERSION && Array.isArray(ledger.entries) && ledger.entries.every(isDurablePendingWrite);
}
function validPendingWriteId(value) {
return typeof value === "string" && value.length > 0 && value.length <= 128 && /^[A-Za-z0-9._:-]+$/.test(value);
}
function validOperationOrder(value) {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
}
function createPendingWriteId() {
const cryptoWithUuid = globalThis.crypto;
if (typeof cryptoWithUuid?.randomUUID === "function") {
return cryptoWithUuid.randomUUID();
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
var durableOperationSequence = 0;
var lastDurableOperationOrder = 0;
function createDurableOperation() {
const now6 = Math.max(0, Math.floor(Date.now()));
const requestedOrder = now6 * 1e3;
const operationOrder = Math.min(
Number.MAX_SAFE_INTEGER,
Math.max(requestedOrder, lastDurableOperationOrder + 1)
);
lastDurableOperationOrder = operationOrder;
return {
operationId: `${now6.toString(36)}-${(++durableOperationSequence).toString(36)}-${Math.random().toString(36).slice(2)}`,
operationOrder
};
}
function normalizePendingWrite(write) {
return { ...write };
}
function addPendingWrite(pending, candidate) {
const normalized = normalizePendingWrite(candidate);
const existing = pending.get(normalized.key);
if (!existing || comparePendingWrites(existing, normalized) <= 0) {
pending.set(normalized.key, normalized);
}
}
function comparePendingWrites(left, right) {
const leftOperation = operationFromWrite(left);
const rightOperation = operationFromWrite(right);
if (!leftOperation && !rightOperation) return left.id.localeCompare(right.id);
if (!leftOperation) return -1;
if (!rightOperation) return 1;
return compareDurableOperations(leftOperation, rightOperation);
}
function operationFromWrite(write) {
if (!isDurableOperation({ operationId: write.operationId, operationOrder: write.operationOrder })) {
return void 0;
}
return { operationId: write.operationId, operationOrder: write.operationOrder };
}
function compareDurableOperations(left, right) {
return left.operationOrder - right.operationOrder || left.operationId.localeCompare(right.operationId);
}
// src/features/library/cleanup-preview.ts
function recordStoredBytes(record) {
let total = 0;
for (const entry of record.media ?? []) {
if (entry.captureStatus !== "captured-bytes") continue;
const bytes = entry.byteLength;
if (typeof bytes === "number" && Number.isFinite(bytes) && bytes > 0) total += Math.round(bytes);
}
return total;
}
function previewCleanup(records, options = {}) {
const whitelist = new Set((options.whitelistHandles ?? []).map((h) => h.toLowerCase()));
const protectedIds = new Set(options.protectedTweetIds ?? []);
const byBucket = {
tweets: 0,
retweets: 0,
replies: 0,
likes: 0,
bookmarks: 0
};
let protectedCount = 0;
const candidates2 = [];
for (const record of records) {
const bucket = classify(record, options.bucketHint);
if (!bucket) continue;
const handle = record.handle?.toLowerCase() ?? null;
const isProtected = record.tweetId !== null && protectedIds.has(record.tweetId) || handle !== null && whitelist.has(handle);
if (isProtected) {
protectedCount += 1;
}
byBucket[bucket] += 1;
candidates2.push({
bucket,
tweetId: record.tweetId,
handle: record.handle,
text: record.text,
permalink: record.permalink,
reason: explain(bucket, record, wasInferred(bucket, record, options.bucketHint)),
protected: isProtected,
storedBytes: recordStoredBytes(record)
});
}
const storedBytes = candidates2.reduce((total, entry) => total + entry.storedBytes, 0);
const freeableBytes = candidates2.filter((entry) => !entry.protected).reduce((total, entry) => total + entry.storedBytes, 0);
const largest = [...candidates2].filter((entry) => entry.storedBytes > 0).sort((left, right) => right.storedBytes - left.storedBytes || (left.tweetId ?? "").localeCompare(right.tweetId ?? "")).slice(0, Math.max(0, options.largestCount ?? 10));
return {
storedBytes,
freeableBytes,
largest,
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
candidates: candidates2,
byBucket,
protectedCount
};
}
function classify(record, hint) {
if (hint) return hint;
if (record.surface.includes("likes")) return "likes";
if (record.surface.includes("bookmarks")) return "bookmarks";
if (record.parentId != null && record.parentId !== "") return "replies";
if (record.text.startsWith("RT @") || record.text.startsWith("Reposted ")) return "retweets";
if (record.text.startsWith("@")) return "replies";
return "tweets";
}
function wasInferred(bucket, record, hint) {
if (hint) return false;
if (record.surface.includes("likes") || record.surface.includes("bookmarks")) return false;
if (bucket === "replies") return record.parentId == null || record.parentId === "";
return bucket === "retweets";
}
function explain(bucket, record, inferred = false) {
switch (bucket) {
case "retweets":
return inferred ? "Looks like reposted content, from how the text begins" : "Reposted content, author retains the original";
case "replies":
return inferred ? "Looks like a reply, from how the text begins" : "Reply to another account";
case "likes":
return "Imported from Likes archive";
case "bookmarks":
return "Imported from Bookmarks archive";
case "tweets":
default:
return record.tweetId ? `Original post ${record.tweetId}` : "Original post";
}
}
// src/platform/network.ts
var NETWORK_TIMEOUTS = {
aria2: 15e3,
ai: 3e4,
semantic: 3e4,
crosspost: 3e4,
mediaProbe: 5e3,
mediaTransfer: 6e4
};
var NetworkTimeoutError = class extends Error {
timeoutMs;
constructor(timeoutMs) {
super(`Network request timed out after ${Math.ceil(timeoutMs / 1e3)} seconds`);
this.name = "NetworkTimeoutError";
this.timeoutMs = timeoutMs;
}
};
async function withNetworkTimeout(operation, timeoutMs) {
const deadline = Math.max(1, Math.trunc(timeoutMs));
const controller2 = new AbortController();
let timer2;
let timedOut = false;
const timeout = new Promise((_, reject) => {
timer2 = setTimeout(() => {
timedOut = true;
controller2.abort();
reject(new NetworkTimeoutError(deadline));
}, deadline);
});
const request = Promise.resolve().then(() => operation(controller2.signal));
try {
return await Promise.race([request, timeout]);
} catch (error) {
if (timedOut) {
throw new NetworkTimeoutError(deadline);
}
throw error;
} finally {
if (timer2 !== void 0) clearTimeout(timer2);
if (timedOut) controller2.abort();
}
}
// src/features/integrations/network-policy.ts
var NetworkPolicyNotInstalledError = class extends Error {
constructor(what) {
super(
`${what} was blocked: Aviary's outbound network policy has not been installed yet. This is a startup fault in Aviary, not the Local-only mode setting.`
);
this.name = "NetworkPolicyNotInstalledError";
}
};
var LocalOnlyError = class extends Error {
constructor(what) {
super(
`${what} was blocked: Aviary is in local-only mode. Turn off "Local-only mode" in Trust to let configured integrations reach the network.`
);
this.name = "LocalOnlyError";
}
};
var SHARED_POLICY_KEY = "__AVIARY_LOCAL_ONLY__";
var localOnly = null;
function setLocalOnlyPolicy(predicate) {
localOnly = predicate;
globalThis[SHARED_POLICY_KEY] = predicate;
}
function localOnlyPolicyInstalled() {
return localOnly !== null || typeof globalThis[SHARED_POLICY_KEY] === "function";
}
function isLocalOnly() {
const shared2 = globalThis[SHARED_POLICY_KEY];
if (shared2) return shared2();
return localOnly === null ? true : localOnly();
}
function assertOutboundAllowed(what) {
if (!localOnlyPolicyInstalled()) {
throw new NetworkPolicyNotInstalledError(what);
}
if (isLocalOnly()) {
throw new LocalOnlyError(what);
}
}
// src/features/integrations/usage.ts
var INTEGRATION_USAGE_KEY = "aviary.integration.usage.v1";
var INTEGRATION_USAGE_SCHEMA_VERSION = 1;
var USAGE_HISTORY_DAYS = 31;
var DEFAULT_AI_MAX_REQUEST_BYTES = 32e3;
var DEFAULT_AI_DAILY_REQUEST_BYTES = 1e6;
var DEFAULT_EMBEDDING_MAX_RECORD_BYTES = 2e4;
var DEFAULT_EMBEDDING_DAILY_RECORD_BYTES = 2e6;
var EMPTY = { schemaVersion: 1, days: [] };
var IntegrationUsageLedger = class {
#storage;
#state = { schemaVersion: 1, days: [] };
#loaded = false;
#lastBlocked = null;
constructor(storage) {
this.#storage = storage;
}
async load() {
if (this.#loaded) return;
const stored = await this.#storage.get(INTEGRATION_USAGE_KEY, EMPTY);
this.#state = normalizeState(stored);
this.#loaded = true;
}
snapshot(now6 = /* @__PURE__ */ new Date()) {
const day = localDay(now6);
const current = this.#state.days.find((entry) => entry.day === day) ?? emptyDay(day);
return {
day,
historyDays: this.#state.days.length,
ai: { ...current.ai },
embedding: { ...current.embedding }
};
}
status(aiBudget, embeddingBudget, localOnly2) {
const snapshot = this.snapshot();
return {
day: snapshot.day,
networkAllowed: !localOnly2,
localOnly: localOnly2,
lastBlocked: this.#lastBlocked ? { ...this.#lastBlocked } : null,
ai: { ...snapshot.ai, dailyLimitBytes: aiBudget.dailyBytes },
embedding: { ...snapshot.embedding, dailyLimitBytes: embeddingBudget.dailyBytes }
};
}
async reserveAi(requestBytes, budget) {
return this.#reserve("ai", requestBytes, 0, budget);
}
async reserveEmbedding(recordBytes, budget) {
return this.#reserve("embedding", recordBytes, 1, budget);
}
async clear() {
await this.load();
const before = this.#state;
this.#state = { schemaVersion: 1, days: [] };
this.#lastBlocked = null;
try {
await replaceStored(this.#storage, INTEGRATION_USAGE_KEY, this.#state);
} catch (error) {
this.#state = before;
throw error;
}
}
/**
* Check the budget and spend from it, atomically across tabs.
*
* The whole body runs inside one lock and re-reads the stored ledger at the top of it. Without
* that the sequence is a textbook time-of-check-to-time-of-use race: two tabs each read the same
* "bytes used today", each conclude there is room, and each write their own total -- so a daily
* budget could be spent once per open tab. The budget is the only promise Aviary makes about
* what a provider is allowed to cost, so it is the one counter that has to be exact.
*/
async #reserve(kind, requestBytes, records, budget) {
await this.load();
return withStorageLock(
INTEGRATION_USAGE_KEY,
() => this.#reserveLocked(kind, requestBytes, records, budget)
);
}
async #reserveLocked(kind, requestBytes, records, budget) {
this.#state = mergeHighest(
this.#state,
normalizeState(await this.#storage.get(INTEGRATION_USAGE_KEY, EMPTY))
);
const bytes = Math.max(0, Math.floor(Number.isFinite(requestBytes) ? requestBytes : 0));
const maxRequestBytes = finiteLimit(budget.maxRequestBytes);
const dailyLimitBytes = finiteLimit(budget.dailyBytes);
const current = this.snapshot();
const usedBytes = kind === "ai" ? current.ai.bytes : current.embedding.bytes;
if (overBudget(maxRequestBytes, bytes)) {
return this.blocked(
kind,
bytes,
usedBytes,
dailyLimitBytes,
maxRequestBytes <= 0 ? `The ${kind} per-request budget is zero, so no provider request was made.` : `The ${kind} request is ${bytes} bytes, over the ${maxRequestBytes}-byte per-request budget.`
);
}
if (overBudget(dailyLimitBytes, usedBytes + bytes)) {
return this.blocked(
kind,
bytes,
usedBytes,
dailyLimitBytes,
dailyLimitBytes <= 0 ? `The ${kind} daily budget is zero, so no provider request was made.` : `The ${kind} daily budget has been reached; no provider request was made.`
);
}
const before = cloneState(this.#state);
const day = this.#getOrCreateDay(current.day);
if (kind === "ai") {
day.ai.requests += 1;
day.ai.bytes += bytes;
} else {
day.embedding.requests += 1;
day.embedding.records += records;
day.embedding.bytes += bytes;
}
this.#trimHistory();
try {
await this.#storage.set(INTEGRATION_USAGE_KEY, this.#state);
} catch {
this.#state = before;
return this.blocked(
kind,
bytes,
usedBytes,
dailyLimitBytes,
"Usage could not be saved locally; the provider request was stopped."
);
}
this.#lastBlocked = null;
return {
allowed: true,
kind,
requestBytes: bytes,
usedBytes: usedBytes + bytes,
dailyLimitBytes
};
}
#getOrCreateDay(day) {
let current = this.#state.days.find((entry) => entry.day === day);
if (!current) {
current = emptyDay(day);
this.#state.days.push(current);
this.#state.days.sort((a, b) => a.day.localeCompare(b.day));
}
return current;
}
#trimHistory() {
if (this.#state.days.length > USAGE_HISTORY_DAYS) {
this.#state.days = this.#state.days.slice(-USAGE_HISTORY_DAYS);
}
}
blocked(kind, requestBytes, usedBytes, dailyLimitBytes, reason) {
this.#lastBlocked = { kind, reason };
return { allowed: false, kind, requestBytes, usedBytes, dailyLimitBytes, reason };
}
};
function defaultAiBudget(config) {
return {
maxRequestBytes: config.maxRequestBytes ?? DEFAULT_AI_MAX_REQUEST_BYTES,
dailyBytes: config.dailyRequestBytes ?? DEFAULT_AI_DAILY_REQUEST_BYTES
};
}
function defaultEmbeddingBudget(config) {
return {
maxRequestBytes: config.maxRecordBytes ?? DEFAULT_EMBEDDING_MAX_RECORD_BYTES,
dailyBytes: config.dailyRecordBytes ?? DEFAULT_EMBEDDING_DAILY_RECORD_BYTES
};
}
function buildAiDisclosure(config, request, usage, networkAllowed) {
const text = [request.systemPrompt ?? "", request.prompt].join("\n");
const budget = defaultAiBudget(config);
const requestBytes = estimateAiRequestBytes(config, request);
const budgetReason = overBudget(budget.maxRequestBytes, requestBytes) ? budget.maxRequestBytes <= 0 ? "The AI per-request budget is zero, so no provider request will be made." : `The AI request is ${requestBytes} bytes, over the ${budget.maxRequestBytes}-byte per-request budget.` : usage && overBudget(budget.dailyBytes, usage.ai.bytes + requestBytes) ? budget.dailyBytes <= 0 ? "The AI daily budget is zero, so no provider request will be made." : "The AI daily budget has been reached; no provider request was made." : null;
return {
provider: config.provider,
endpoint: aiEndpoint(config),
fields: ["model", ...request.systemPrompt ? ["system instruction"] : [], "user prompt"],
characterCount: [...text].length,
requestBytes,
estimatedTokens: estimateTokens(text),
retained: "Aviary stores usage counters only; the provider's retention follows its policy.",
networkAllowed,
budgetAllowed: budgetReason === null,
budgetReason,
dailyUsedBytes: usage?.ai.bytes ?? 0,
dailyLimitBytes: budget.dailyBytes
};
}
function aiEndpoint(config) {
if (config.endpoint) return config.endpoint;
return config.provider === "anthropic" ? "https://api.anthropic.com/v1/messages" : "https://api.openai.com/v1/chat/completions";
}
function estimateAiRequestBytes(config, request) {
return utf8Bytes(JSON.stringify(aiRequestBody(config, request)));
}
function estimateTokens(text) {
return Math.max(0, Math.ceil([...text].length / 4));
}
function utf8Bytes(text) {
return new TextEncoder().encode(text).byteLength;
}
function aiRequestBody(config, request) {
if (config.provider === "anthropic") {
return {
model: config.model,
max_tokens: request.maxTokens ?? 1024,
...request.systemPrompt ? { system: request.systemPrompt } : {},
messages: [{ role: "user", content: request.prompt }]
};
}
return {
model: config.model,
// OpenAI-compatible reasoning endpoints use the longer parameter name first. Estimating
// that shape is conservative for the legacy fallback, whose `max_tokens` key is shorter.
max_completion_tokens: request.maxTokens ?? 1024,
messages: [
...request.systemPrompt ? [{ role: "system", content: request.systemPrompt }] : [],
{ role: "user", content: request.prompt }
]
};
}
function finiteLimit(value) {
if (value === void 0) return 0;
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0;
}
function overBudget(limit, bytes) {
return limit <= 0 || bytes > limit;
}
function localDay(date) {
const pad = (value) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
function emptyDay(day) {
return { day, ai: { requests: 0, bytes: 0 }, embedding: { requests: 0, records: 0, bytes: 0 } };
}
function normalizeState(value) {
if (!isRecord4(value) || value.schemaVersion !== INTEGRATION_USAGE_SCHEMA_VERSION || !Array.isArray(value.days)) {
return { schemaVersion: 1, days: [] };
}
const days = value.days.map(normalizeDay).filter((day) => day !== null).sort((a, b) => a.day.localeCompare(b.day));
return { schemaVersion: 1, days: days.slice(-USAGE_HISTORY_DAYS) };
}
function normalizeDay(value) {
if (!isRecord4(value) || typeof value.day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value.day)) {
return null;
}
const ai = isRecord4(value.ai) ? value.ai : {};
const embedding = isRecord4(value.embedding) ? value.embedding : {};
return {
day: value.day,
ai: {
requests: safeCount(ai.requests),
bytes: safeCount(ai.bytes)
},
embedding: {
requests: safeCount(embedding.requests),
records: safeCount(embedding.records),
bytes: safeCount(embedding.bytes)
}
};
}
function safeCount(value) {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
}
function cloneState(state2) {
return {
schemaVersion: 1,
days: state2.days.map((day) => ({
day: day.day,
ai: { ...day.ai },
embedding: { ...day.embedding }
}))
};
}
function isRecord4(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function mergeHighest(local, stored) {
const byDay = /* @__PURE__ */ new Map();
for (const day of local.days) {
byDay.set(day.day, day);
}
for (const day of stored.days) {
const existing = byDay.get(day.day);
byDay.set(
day.day,
existing ? {
day: day.day,
ai: {
requests: Math.max(existing.ai.requests, day.ai.requests),
bytes: Math.max(existing.ai.bytes, day.ai.bytes)
},
embedding: {
requests: Math.max(existing.embedding.requests, day.embedding.requests),
records: Math.max(existing.embedding.records, day.embedding.records),
bytes: Math.max(existing.embedding.bytes, day.embedding.bytes)
}
} : day
);
}
const days = [...byDay.values()].sort((a, b) => a.day.localeCompare(b.day));
return { schemaVersion: 1, days: days.slice(-USAGE_HISTORY_DAYS) };
}
// src/features/integrations/semantic-search.ts
var SEMANTIC_INDEX_KEY = "aviary.semanticIndex.v1";
var SEMANTIC_INDEX_LIMIT = 400;
var SEMANTIC_INDEX_MAX_BYTES = 8 * 1024 * 1024;
var MAX_VECTOR_DIMENSIONS = 4096;
var VECTOR_PRECISION = 1e5;
function roundVector(vector) {
return vector.map((value) => Math.round(value * VECTOR_PRECISION) / VECTOR_PRECISION);
}
var EMPTY2 = { entries: [], model: "" };
var SemanticIndex = class {
#storage;
#usage;
#state = EMPTY2;
#loaded = false;
constructor(storage, usage) {
this.#storage = storage;
this.#usage = usage;
}
async load() {
if (this.#loaded) return;
const stored = await this.#storage.get(SEMANTIC_INDEX_KEY, EMPTY2);
this.#state = {
entries: Array.isArray(stored?.entries) ? stored.entries.filter(isEntry).slice(-SEMANTIC_INDEX_LIMIT) : [],
model: typeof stored?.model === "string" ? stored.model : ""
};
this.#trimToBudget();
this.#loaded = true;
}
size() {
return this.#state.entries.length;
}
model() {
return this.#state.model;
}
list() {
return this.#state.entries.map((entry) => ({ ...entry, vector: [...entry.vector] }));
}
async embedAndIndex(config, records) {
if (!config.enabled || !config.endpoint || !config.apiKey || !config.model) {
return { added: 0, skipped: records.length, errors: 0, dropped: 0, blocked: 0 };
}
await this.load();
const before = cloneState2(this.#state);
if (this.#state.model && this.#state.model !== config.model) {
this.#state = { entries: [], model: config.model };
} else {
this.#state.model = config.model;
}
const known = new Set(this.#state.entries.map((entry) => entry.id));
let added = 0;
let skipped = 0;
let errors = 0;
let blocked = 0;
for (let index = 0; index < records.length; index += 1) {
const record = records[index];
const id = `${record.tweetId ?? "no-id"}:${(record.text || "").slice(0, 80)}`;
if (known.has(id) || record.text.length === 0) {
skipped += 1;
continue;
}
if (this.#usage) {
const decision = await this.#usage.reserveEmbedding(
utf8Bytes(record.text),
defaultEmbeddingBudget(config)
);
if (!decision.allowed) {
blocked = records.length - index;
break;
}
}
const expectedDimension = this.#state.entries[0]?.vector.length;
const vector = await fetchEmbedding(config, record.text, expectedDimension);
if (!vector) {
errors += 1;
continue;
}
this.#state.entries.push({
id,
tweetId: record.tweetId,
handle: record.handle,
text: record.text,
vector: roundVector(vector),
embeddedAt: (/* @__PURE__ */ new Date()).toISOString()
});
known.add(id);
added += 1;
}
const dropped = this.#trimToBudget();
try {
await this.#persist();
} catch (error) {
this.#state = before;
throw error;
}
return { added, skipped, errors, dropped, blocked };
}
async search(config, query, limit = 10, options = {}) {
if (!config.enabled || !config.endpoint || !config.apiKey || !config.model || query.trim().length === 0) {
return [];
}
if (options.allowProviderRequest === false) return [];
await this.load();
if (this.#state.entries.length === 0) return [];
if (this.#usage) {
const decision = await this.#usage.reserveEmbedding(
utf8Bytes(query),
defaultEmbeddingBudget(config)
);
if (!decision.allowed) return [];
}
const queryVector = await fetchEmbedding(config, query, this.#state.entries[0]?.vector.length);
if (!queryVector) return [];
const hits = this.#state.entries.map((entry) => ({ entry, score: cosineSimilarity(queryVector, entry.vector) })).sort((a, b) => b.score - a.score).slice(0, limit);
return hits;
}
async clear() {
const before = cloneState2(this.#state);
this.#state = { entries: [], model: this.#state.model };
this.#loaded = true;
try {
await this.#persist();
} catch (error) {
this.#state = before;
throw error;
}
}
#trimToBudget() {
const before = this.#state.entries.length;
while (this.#state.entries.length > SEMANTIC_INDEX_LIMIT || serializedBytes(this.#state) > SEMANTIC_INDEX_MAX_BYTES) {
if (this.#state.entries.length === 0) break;
this.#state.entries.shift();
}
return before - this.#state.entries.length;
}
/**
* Written under the lock, and deliberately not merged.
*
* Every entry here is derived from a captured record and can be rebuilt on demand, and the index
* is bounded by a serialized-byte ceiling that a union of two tabs' copies would blow straight
* through. The lock keeps the byte accounting honest: two tabs trimming to the ceiling at once
* would otherwise each write a list the other had already shortened.
*/
async #persist() {
await replaceStored(this.#storage, SEMANTIC_INDEX_KEY, this.#state);
}
};
async function fetchEmbedding(config, text, expectedDimension) {
assertOutboundAllowed("Embedding");
try {
const payload = await withNetworkTimeout(async (signal) => {
const response = await fetch(config.endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${config.apiKey}`
},
body: JSON.stringify({ model: config.model, input: text }),
signal
});
if (!response.ok) return null;
return await response.json();
}, NETWORK_TIMEOUTS.semantic);
if (!payload) return null;
if (Array.isArray(payload?.embedding)) {
return validEmbedding(payload.embedding, expectedDimension) ? payload.embedding : null;
}
const vector = payload?.data?.[0]?.embedding;
return Array.isArray(vector) && validEmbedding(vector, expectedDimension) ? vector : null;
} catch {
return null;
}
}
function cosineSimilarity(a, b) {
if (!validEmbedding(a) || !validEmbedding(b) || a.length !== b.length) return 0;
const length = a.length;
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < length; i++) {
const av = a[i];
const bv = b[i];
dot += av * bv;
normA += av * av;
normB += bv * bv;
}
if (normA === 0 || normB === 0) return 0;
const score = dot / (Math.sqrt(normA) * Math.sqrt(normB));
return Number.isFinite(score) ? score : 0;
}
function isEntry(value) {
if (typeof value !== "object" || value === null) return false;
const candidate = value;
return typeof candidate.id === "string" && (typeof candidate.tweetId === "string" || candidate.tweetId === null) && (typeof candidate.handle === "string" || candidate.handle === null) && typeof candidate.text === "string" && typeof candidate.embeddedAt === "string" && validEmbedding(candidate.vector);
}
function validEmbedding(value, expectedDimension) {
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_VECTOR_DIMENSIONS || expectedDimension !== void 0 && value.length !== expectedDimension) {
return false;
}
return value.every((item) => typeof item === "number" && Number.isFinite(item));
}
function serializedBytes(state2) {
try {
return new TextEncoder().encode(JSON.stringify(state2)).byteLength;
} catch {
return Number.POSITIVE_INFINITY;
}
}
function cloneState2(state2) {
return {
model: state2.model,
entries: state2.entries.map((entry) => ({ ...entry, vector: [...entry.vector] }))
};
}
// src/features/export/language.ts
function normalizePostLanguage(value) {
if (typeof value !== "string") return null;
const candidate = value.trim();
if (candidate.length === 0 || candidate.length > 63 || candidate.includes("_")) return null;
try {
const canonical = Intl.getCanonicalLocales(candidate)[0];
return canonical && /^[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*$/.test(canonical) ? canonical : null;
} catch {
return null;
}
}
function languageAttribute(value) {
const language = normalizePostLanguage(value);
return language ? ` lang="${language}"` : "";
}
// src/features/export/audience.ts
var DEFAULT_EXPORT_AUDIENCE = {
includeProtected: false,
includeUnknown: false
};
function normalizeAudience(value) {
return value === "public" || value === "protected" || value === "unknown" ? value : "unknown";
}
function normalizeAudienceSelection(value) {
return {
includeProtected: value?.includeProtected === true,
includeUnknown: value?.includeUnknown === true
};
}
function audienceOf(record) {
return normalizeAudience(record.audience);
}
function summarizeAudience(records, selection = DEFAULT_EXPORT_AUDIENCE) {
const summary = {
total: records.length,
public: 0,
protected: 0,
unknown: 0,
excludedProtected: 0,
excludedUnknown: 0
};
for (const record of records) {
const audience = audienceOf(record);
summary[audience] += 1;
if (audience === "protected" && !selection.includeProtected) summary.excludedProtected += 1;
if (audience === "unknown" && !selection.includeUnknown) summary.excludedUnknown += 1;
}
return summary;
}
function filterShareRecords(records, selection = DEFAULT_EXPORT_AUDIENCE) {
return records.filter((record) => {
const audience = audienceOf(record);
return audience === "public" || audience === "protected" && selection.includeProtected || audience === "unknown" && selection.includeUnknown;
});
}
function isShareOrientedFormat(format) {
return format === "html" || format === "markdown";
}
// src/features/export/assets.ts
var PERCEPTUAL_HASH_WIDTH = 17;
var PERCEPTUAL_HASH_HEIGHT = 16;
var PERCEPTUAL_MATCH_DISTANCE = 12;
function describeMediaCapture(media, fallbackCapturedAt = null) {
const sourceUrl = cleanText(media.sourceUrl ?? media.url);
const bytes = media.bytes instanceof Uint8Array ? media.bytes : null;
const hasRemoteSource = /^https?:\/\//i.test(sourceUrl);
const status = bytes ? "captured-bytes" : media.captureStatus === "missing" || !hasRemoteSource ? "missing" : "remote-reference";
const byteLength = bytes ? bytes.byteLength : finiteByteLength(media.byteLength);
const sha256 = bytes ? cleanChecksum(media.sha256) ?? sha256Hex(bytes) : cleanChecksum(media.sha256);
const capturedAt = cleanText(media.capturedAt ?? fallbackCapturedAt) || null;
const error = cleanText(media.captureError);
return {
status,
sourceUrl,
capturedAt,
byteLength,
sha256,
retryable: status !== "captured-bytes" && hasRemoteSource,
...status === "captured-bytes" && cleanText(media.assetPath) ? { packagePath: cleanText(media.assetPath) } : {},
...error ? { error } : {}
};
}
function serializeExportMedia(media, fallbackCapturedAt = null) {
const capture = describeMediaCapture(media, fallbackCapturedAt);
const serialized = {
kind: media.kind,
url: media.url,
capture
};
if (media.width !== void 0) serialized.width = media.width;
if (media.height !== void 0) serialized.height = media.height;
if (media.bitrate !== void 0) serialized.bitrate = media.bitrate;
if (media.type !== void 0) serialized.type = media.type;
if (media.altText !== void 0) serialized.altText = media.altText;
if (media.language !== void 0) serialized.language = media.language;
if (media.label !== void 0) serialized.label = media.label;
const httpStatus = media.httpStatus;
if (typeof httpStatus === "number" && Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599) {
serialized.httpStatus = httpStatus;
}
return serialized;
}
function serializeExportRecord(record) {
return {
...record,
language: normalizePostLanguage(record.language),
audience: normalizeAudience(record.audience),
media: mediaOf(record).map((media) => serializeExportMedia(media, record.capturedAt))
};
}
function serializeExportRecords(records) {
return records.map(serializeExportRecord);
}
function prepareExportPackage(records) {
const assets = [];
let assetNumber = 0;
const prepared = records.map((record) => {
const media = mediaOf(record).map((entry) => {
if (!(entry.bytes instanceof Uint8Array)) {
return { ...entry };
}
const extension = mediaExtension(entry);
const path = `media/${String(++assetNumber).padStart(6, "0")}-${entry.kind}.${extension}`;
assets.push({
path,
data: new Uint8Array(entry.bytes),
contentType: mediaContentType(entry, extension)
});
return { ...entry, assetPath: path };
});
return { ...record, media };
});
return { records: prepared, assets };
}
function buildExportPackageManifest(records, files, packageRoot = "", generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
const media = [];
let capturedBytes = 0;
let remoteReferences = 0;
let missing = 0;
let retryable = 0;
records.forEach((record, recordIndex) => {
mediaOf(record).forEach((entry) => {
const capture = describeMediaCapture(entry, record.capturedAt);
const packagePath2 = capture.packagePath ? joinPackagePath(packageRoot, capture.packagePath) : void 0;
const normalizedCapture = packagePath2 ? { ...capture, packagePath: packagePath2 } : capture;
media.push({
recordIndex,
recordId: record.tweetId,
kind: entry.kind,
capture: normalizedCapture
});
if (capture.status === "captured-bytes") {
capturedBytes += capture.byteLength ?? 0;
} else if (capture.status === "remote-reference") {
remoteReferences += 1;
} else {
missing += 1;
}
if (capture.retryable) retryable += 1;
});
});
return {
schemaVersion: 1,
generator: "Aviary",
generatedAt,
recordCount: records.length,
files: files.map((file) => ({ ...file })),
media,
summary: { capturedBytes, remoteReferences, missing, retryable },
offlineReady: remoteReferences === 0 && missing === 0,
networkRequiredToComplete: remoteReferences > 0 || retryable > 0
};
}
function mediaOf(record) {
return Array.isArray(record.media) ? record.media : [];
}
function sha256Hex(data) {
const paddedLength = Math.ceil((data.length + 9) / 64) * 64;
const padded = new Uint8Array(paddedLength);
padded.set(data);
padded[data.length] = 128;
const view = new DataView(padded.buffer);
const bitLength = data.length * 8;
view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
view.setUint32(paddedLength - 4, bitLength >>> 0);
const hash = new Uint32Array([
1779033703,
3144134277,
1013904242,
2773480762,
1359893119,
2600822924,
528734635,
1541459225
]);
const words = new Uint32Array(64);
for (let block = 0; block < padded.length; block += 64) {
for (let index = 0; index < 16; index += 1) {
words[index] = view.getUint32(block + index * 4);
}
for (let index = 16; index < 64; index += 1) {
const s0 = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ words[index - 15] >>> 3;
const s1 = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ words[index - 2] >>> 10;
words[index] = words[index - 16] + s0 + words[index - 7] + s1 >>> 0;
}
let a = hash[0];
let b = hash[1];
let c = hash[2];
let d = hash[3];
let e = hash[4];
let f = hash[5];
let g = hash[6];
let h = hash[7];
for (let index = 0; index < 64; index += 1) {
const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
const choice = e & f ^ ~e & g;
const temp1 = h + s1 + choice + SHA256_K[index] + words[index] >>> 0;
const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
const majority = a & b ^ a & c ^ b & c;
const temp2 = s0 + majority >>> 0;
h = g;
g = f;
f = e;
e = d + temp1 >>> 0;
d = c;
c = b;
b = a;
a = temp1 + temp2 >>> 0;
}
hash[0] = hash[0] + a >>> 0;
hash[1] = hash[1] + b >>> 0;
hash[2] = hash[2] + c >>> 0;
hash[3] = hash[3] + d >>> 0;
hash[4] = hash[4] + e >>> 0;
hash[5] = hash[5] + f >>> 0;
hash[6] = hash[6] + g >>> 0;
hash[7] = hash[7] + h >>> 0;
}
return Array.from(hash, (word) => word.toString(16).padStart(8, "0")).join("");
}
async function sha256HexAsync(data) {
const subtle = globalThis.crypto?.subtle;
if (!subtle) {
throw new Error("Asynchronous SHA-256 is unavailable in this browser.");
}
const digest = await subtle.digest("SHA-256", data.slice().buffer);
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
}
function mediaIdentityHash(kind, sourceUrl, mediaId) {
const identity = cleanText(mediaId) || mediaIdentityFromUrl(sourceUrl) || canonicalMediaUrl(sourceUrl);
return sha256Hex(new TextEncoder().encode(`aviary-media:${kind}:${identity}`));
}
function perceptualHashFromRgba(rgba, width = PERCEPTUAL_HASH_WIDTH, height = PERCEPTUAL_HASH_HEIGHT) {
if (width !== PERCEPTUAL_HASH_WIDTH || height !== PERCEPTUAL_HASH_HEIGHT) {
throw new RangeError(`Perceptual samples must be ${PERCEPTUAL_HASH_WIDTH} by ${PERCEPTUAL_HASH_HEIGHT}.`);
}
if (rgba.length !== width * height * 4) {
throw new RangeError("Perceptual sample length does not match its dimensions.");
}
const bytes = new Uint8Array(32);
let bit = 0;
for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width - 1; x += 1) {
const left = pixelLuma(rgba, (y * width + x) * 4);
const right = pixelLuma(rgba, (y * width + x + 1) * 4);
if (left > right) {
bytes[Math.trunc(bit / 8)] |= 1 << 7 - bit % 8;
}
bit += 1;
}
}
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
}
async function perceptualImageHash(bytes, contentType = "image/*") {
if (typeof createImageBitmap !== "function") {
return null;
}
const bitmap = await createImageBitmap(new Blob([bytes.slice().buffer], { type: contentType }));
try {
const canvas = createHashCanvas();
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) {
return null;
}
context.drawImage(bitmap, 0, 0, PERCEPTUAL_HASH_WIDTH, PERCEPTUAL_HASH_HEIGHT);
const sample = context.getImageData(
0,
0,
PERCEPTUAL_HASH_WIDTH,
PERCEPTUAL_HASH_HEIGHT
);
return perceptualHashFromRgba(sample.data);
} finally {
bitmap.close();
}
}
function hexadecimalHammingDistance(left, right) {
if (!/^[0-9a-f]+$/i.test(left) || left.length !== right.length || !/^[0-9a-f]+$/i.test(right)) {
return Number.POSITIVE_INFINITY;
}
let distance = 0;
for (let index = 0; index < left.length; index += 1) {
distance += NIBBLE_BITS[Number.parseInt(left[index], 16) ^ Number.parseInt(right[index], 16)];
}
return distance;
}
function rotateRight(value, bits) {
return value >>> bits | value << 32 - bits;
}
function pixelLuma(rgba, offset) {
return rgba[offset] * 299 + rgba[offset + 1] * 587 + rgba[offset + 2] * 114;
}
function createHashCanvas() {
if (typeof OffscreenCanvas === "function") {
return new OffscreenCanvas(PERCEPTUAL_HASH_WIDTH, PERCEPTUAL_HASH_HEIGHT);
}
if (typeof document === "undefined") {
throw new Error("No image canvas is available in this context.");
}
const canvas = document.createElement("canvas");
canvas.width = PERCEPTUAL_HASH_WIDTH;
canvas.height = PERCEPTUAL_HASH_HEIGHT;
return canvas;
}
function mediaIdentityFromUrl(sourceUrl) {
try {
const url = new URL(sourceUrl);
if (url.hostname.toLowerCase() === "pbs.twimg.com") {
return /^\/media\/([A-Za-z0-9_-]+)/i.exec(url.pathname)?.[1] ?? "";
}
} catch {
}
return "";
}
function canonicalMediaUrl(sourceUrl) {
try {
const url = new URL(sourceUrl);
for (const key of ["name", "format", "width", "height", "tag"]) {
url.searchParams.delete(key);
}
url.hash = "";
url.searchParams.sort();
return url.toString();
} catch {
return cleanText(sourceUrl);
}
}
function cleanText(value) {
return typeof value === "string" ? value.trim() : "";
}
function cleanChecksum(value) {
const checksum = cleanText(value).toLowerCase();
return /^[0-9a-f]{64}$/.test(checksum) ? checksum : null;
}
function finiteByteLength(value) {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
}
function joinPackagePath(root, path) {
return root ? `${root.replace(/\/+$/g, "")}/${path}` : path;
}
function mediaExtension(media) {
const type = cleanText(media.type).toLowerCase();
const fromType = type.includes("/") ? type.split("/").at(-1) ?? "" : type;
const fromUrl = /[.?](?:format=)?([a-z0-9]{2,5})(?:[?#]|$)/i.exec(media.url)?.[1]?.toLowerCase() ?? "";
const candidate = fromType || fromUrl || defaultMediaExtension(media.kind);
if (candidate.includes("ttml") || candidate.includes("dfxp")) return "ttml";
if (candidate === "jpeg") return "jpg";
return /^[a-z0-9]{2,5}$/.test(candidate) ? candidate : defaultMediaExtension(media.kind);
}
function mediaContentType(media, extension) {
const declared = cleanText(media.type);
if (declared.includes("/")) return declared;
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
if (extension === "png") return "image/png";
if (extension === "webp") return "image/webp";
if (extension === "mp4") return "video/mp4";
if (extension === "m4a") return "audio/mp4";
if (extension === "mp3") return "audio/mpeg";
if (extension === "ogg" || extension === "opus") return "audio/ogg";
if (extension === "vtt") return "text/vtt";
if (extension === "srt") return "text/srt";
if (extension === "ttml") return "application/ttml+xml";
return "application/octet-stream";
}
function defaultMediaExtension(kind) {
if (kind === "video") return "mp4";
if (kind === "audio") return "m4a";
if (kind === "subtitle") return "vtt";
return kind === "photo" || kind === "thumbnail" ? "jpg" : "bin";
}
var SHA256_K = Uint32Array.from([
1116352408,
1899447441,
3049323471,
3921009573,
961987163,
1508970993,
2453635748,
2870763221,
3624381080,
310598401,
607225278,
1426881987,
1925078388,
2162078206,
2614888103,
3248222580,
3835390401,
4022224774,
264347078,
604807628,
770255983,
1249150122,
1555081692,
1996064986,
2554220882,
2821834349,
2952996808,
3210313671,
3336571891,
3584528711,
113926993,
338241895,
666307205,
773529912,
1294757372,
1396182291,
1695183700,
1986661051,
2177026350,
2456956037,
2730485921,
2820302411,
3259730800,
3345764771,
3516065817,
3600352804,
4094571909,
275423344,
430227734,
506948616,
659060556,
883997877,
958139571,
1322822218,
1537002063,
1747873779,
1955562222,
2024104815,
2227730452,
2361852424,
2428436474,
2756734187,
3204031479,
3329325298
]);
var NIBBLE_BITS = Uint8Array.from([0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]);
// src/features/media/urls.ts
var IMAGE_HOST = "pbs.twimg.com";
var FORMAT_PRIORITY = ["jpg", "png", "webp"];
function normalizeImageUrl(rawUrl, options = {}) {
let parsed;
try {
parsed = new URL(rawUrl, "https://x.com");
} catch {
return null;
}
if (parsed.hostname !== IMAGE_HOST) {
return null;
}
if (!parsed.pathname.startsWith("/media/")) {
return null;
}
const params = parsed.searchParams;
const requestedFormat = (params.get("format") ?? "").toLowerCase();
const pathFormat = /\.([a-z0-9]+)$/i.exec(parsed.pathname)?.[1]?.toLowerCase() ?? "";
const validatedPathFormat = pathFormat === "jpeg" ? "jpg" : pathFormat;
const format = FORMAT_PRIORITY.includes(requestedFormat) ? requestedFormat : FORMAT_PRIORITY.includes(validatedPathFormat) ? validatedPathFormat : "jpg";
params.set("format", format);
const preferOriginal = options.preferOriginal ?? true;
if (preferOriginal) {
params.set("name", "orig");
} else if (!params.has("name")) {
params.set("name", "large");
}
const mediaId = mediaIdFromPath(parsed.pathname);
const fallbackUrls = [];
if (preferOriginal) {
const fallbackParams = new URLSearchParams(params);
fallbackParams.set("name", "4096x4096");
fallbackUrls.push(`${parsed.origin}${parsed.pathname}?${fallbackParams.toString()}`);
}
return {
url: `${parsed.origin}${parsed.pathname}?${params.toString()}`,
fallbackUrls,
format,
mediaId
};
}
function mediaIdFromPath(pathname) {
const match = /\/media\/([A-Za-z0-9_-]{6,})(?:\.[A-Za-z0-9]+)?$/.exec(pathname);
return match?.[1] ?? null;
}
function tweetIdFromHref(href) {
if (!href) {
return null;
}
const match = /\/status(?:es)?\/(\d{6,})/.exec(href);
return match?.[1] ?? null;
}
// src/features/media/video-extract.ts
var VIDEO_CONTAINER_SELECTOR = '[data-testid="videoPlayer"], [data-testid="videoComponent"]';
function videoContainers(root) {
const candidates2 = Array.from(
root.querySelectorAll(VIDEO_CONTAINER_SELECTOR)
);
return candidates2.filter((container) => {
const video = container.querySelector("video");
if (!video) {
return false;
}
return !candidates2.some(
(candidate) => candidate !== container && container.contains(candidate) && candidate.contains(video)
);
});
}
function extractVideo(container, metadata = {}) {
const video = container.querySelector("video");
if (!video) {
return null;
}
const variants = [];
const seen = /* @__PURE__ */ new Set();
if (video.currentSrc) {
pushVariant(variants, seen, video.currentSrc, video.dataset.contentType ?? "video/mp4");
if (typeof video.readyState === "number" && video.readyState >= 2) {
markPlaybackObserved(variants, video.currentSrc);
}
}
if (video.src) {
pushVariant(variants, seen, video.src, "video/mp4");
}
for (const source of Array.from(video.querySelectorAll("source"))) {
pushVariant(
variants,
seen,
source.src,
source.type || "video/mp4",
source.dataset.width,
source.dataset.height,
source.dataset.bitrate,
source.dataset.codec,
void 0,
source.dataset.codec ? "source-element" : void 0
);
}
for (const variant of metadata.variants ?? []) {
pushVariantObject(variants, seen, variant);
}
const audioVariants = [];
const seenAudio = /* @__PURE__ */ new Set();
for (const source of Array.from(video.querySelectorAll("source"))) {
const markedAudio = typeof source.hasAttribute === "function" ? source.hasAttribute("data-av-audio") : source.dataset?.avAudio !== void 0;
if (!/^audio\//i.test(source.type) && !markedAudio) continue;
pushVariant(
audioVariants,
seenAudio,
source.src,
source.type || "audio/mp4",
void 0,
void 0,
source.dataset.bitrate
);
}
for (const variant of metadata.audioVariants ?? []) {
pushVariantObject(audioVariants, seenAudio, variant);
}
const subtitleTracks = [
...Array.from(video.querySelectorAll("track")).map(readSubtitleTrack).filter((track) => track !== null),
...metadata.subtitleTracks ?? []
].filter((track, index, all) => all.findIndex((candidate) => candidate.url === track.url) === index);
const poster = video.poster || metadata.poster || null;
if (variants.length === 0 && !poster) {
return null;
}
const stableVariants = mergeVideoVariants([], variants);
const stableAudioVariants = mergeVideoVariants([], audioVariants);
const preferred = stableVariants.length > 0 ? pickPreferred(stableVariants) : null;
const isGif = metadata.isGif === true || looksLikeGif(container, video, variants);
return {
container,
poster,
isGif,
variants: stableVariants,
preferred,
audioVariants: stableAudioVariants,
subtitleTracks
};
}
function readSubtitleTrack(track) {
if (typeof track.getAttribute !== "function" && typeof track.kind !== "string") {
return null;
}
const url = track.src || track.getAttribute("src") || "";
if (!/^https?:\/\//i.test(url)) return null;
const kind = (track.kind || "").toLowerCase();
if (kind && kind !== "subtitles" && kind !== "captions") return null;
return {
url,
type: track.getAttribute?.("type") || "text/vtt",
language: track.srclang?.trim() || null,
label: track.label?.trim() || null
};
}
function pushVariantObject(variants, seen, variant) {
pushVariant(
variants,
seen,
variant.url,
variant.type,
variant.width === null ? void 0 : String(variant.width),
variant.height === null ? void 0 : String(variant.height),
variant.bitrate === null ? void 0 : String(variant.bitrate),
variant.codec ?? void 0,
variant.provenance ?? void 0,
variant.codecSource ?? (variant.codec ? "graphql-variant" : void 0)
);
}
function markPlaybackObserved(variants, url) {
const index = variants.findIndex((variant) => variant.url === url);
if (index >= 0) variants[index] = { ...variants[index], playbackObserved: true };
}
function pushVariant(variants, seen, src, type, width, height, bitrate, codec, provenance, codecSource) {
if (!src) {
return;
}
const candidate = {
url: src,
type,
width: parsePositiveInt(width),
height: parsePositiveInt(height),
bitrate: parsePositiveInt(bitrate)
};
if (codec?.trim()) {
candidate.codec = codec.trim();
if (codecSource) candidate.codecSource = codecSource;
}
if (provenance?.trim()) candidate.provenance = provenance.trim();
const existingIndex = variants.findIndex((variant) => variant.url === src);
if (existingIndex >= 0) {
variants[existingIndex] = mergeVideoVariant(variants[existingIndex], candidate);
seen.add(src);
return;
}
seen.add(src);
variants.push(candidate);
}
function mergeVideoVariant(left, right) {
if (left.url !== right.url) {
return { ...left };
}
const merged = {
url: left.url,
type: chooseVariantType(left.url, left.type, right.type),
width: maxKnown(left.width, right.width),
height: maxKnown(left.height, right.height),
bitrate: maxKnown(left.bitrate, right.bitrate)
};
if ("codec" in left || "codec" in right) {
merged.codec = chooseKnownText(left.codec, right.codec);
const source = merged.codec === left.codec ? left.codecSource : right.codecSource;
merged.codecSource = merged.codec ? source ?? null : null;
}
if (left.playbackObserved || right.playbackObserved) {
merged.playbackObserved = true;
}
if ("provenance" in left || "provenance" in right) {
merged.provenance = mergeProvenance(left.provenance, right.provenance);
}
return merged;
}
function mergeVideoVariants(existing, incoming) {
const merged = /* @__PURE__ */ new Map();
for (const variant of [...existing, ...incoming]) {
const previous = merged.get(variant.url);
merged.set(
variant.url,
previous ? mergeVideoVariant(previous, variant) : { ...variant }
);
}
return [...merged.values()].sort((left, right) => compareStable(left.url, right.url));
}
function isSaveableVariantUrl(url, type = "") {
if (!/^https?:\/\//i.test(url)) {
return false;
}
if (/\.(?:m3u8|mpd|m4s)(?:[?#]|$)/i.test(url)) {
return false;
}
return !/(?:mpegurl|dash\+xml)/i.test(type);
}
function compareVariantQuality(left, right) {
const height = compareKnown(left.height, right.height);
if (height !== 0) return height;
const width = compareKnown(left.width, right.width);
if (width !== 0) return width;
if (left.bitrate !== null && right.bitrate !== null) {
return left.bitrate - right.bitrate;
}
return 0;
}
function compareKnown(left, right) {
if (left === right) return 0;
if (left === null) return -1;
if (right === null) return 1;
return left - right;
}
function pickPreferred(variants) {
const sorted = [...variants].sort((a, b) => {
const saveableDiff = Number(isSaveableVariantUrl(b.url, b.type)) - Number(isSaveableVariantUrl(a.url, a.type));
if (saveableDiff !== 0) {
return saveableDiff;
}
const mp4Diff = Number(isProgressiveMp4(b)) - Number(isProgressiveMp4(a));
if (mp4Diff !== 0) {
return mp4Diff;
}
const qualityDiff = compareVariantQuality(b, a);
if (qualityDiff !== 0) {
return qualityDiff;
}
return compareStable(a.url, b.url);
});
return sorted[0] ?? variants[0];
}
function maxKnown(left, right) {
if (left === null && right === null) return null;
return Math.max(left ?? 0, right ?? 0) || null;
}
function chooseKnownText(left, right) {
const values = [left, right].filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim());
return [...new Set(values)].sort(compareStable)[0] ?? null;
}
function mergeProvenance(left, right) {
const values = [left, right].flatMap((value) => typeof value === "string" ? value.split("|") : []).map((value) => value.trim()).filter(Boolean);
return [...new Set(values)].sort(compareStable).join("|") || null;
}
function chooseVariantType(url, left, right) {
const types = [...new Set([left, right].map((value) => value.trim()).filter(Boolean))];
if (types.length === 0) return "video/mp4";
return types.sort((a, b) => {
const scoreDiff = variantTypeScore(url, b) - variantTypeScore(url, a);
return scoreDiff !== 0 ? scoreDiff : compareStable(a, b);
})[0];
}
function variantTypeScore(url, type) {
const lowerType = type.toLowerCase();
const lowerUrl = url.toLowerCase();
let score = 0;
if (lowerType === "video/mp4") score += 40;
else if (lowerType.startsWith("video/")) score += 30;
else if (lowerType === "audio/mp4") score += 35;
else if (lowerType.startsWith("audio/")) score += 25;
if (/(?:mpegurl|dash\+xml)/i.test(lowerType)) score -= 100;
for (const extension of ["mp4", "webm", "mov", "m4a", "mp3", "ogg", "opus", "wav"]) {
if (new RegExp(`\\.${extension}(?:[?#]|$)`, "i").test(lowerUrl) && lowerType.includes(extension)) {
score += 50;
}
}
return score;
}
function compareStable(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function isProgressiveMp4(variant) {
return /video\/mp4/i.test(variant.type) || /\.mp4(?:[?#]|$)/i.test(variant.url);
}
function looksLikeGif(container, video, variants) {
if (video.loop && video.muted) {
return true;
}
const label = (container.getAttribute("aria-label") ?? "").toLowerCase();
if (label.includes("gif")) {
return true;
}
return variants.some((variant) => variant.url.toLowerCase().includes("tweet_video"));
}
function parsePositiveInt(value) {
if (!value) {
return null;
}
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
// src/features/media/extract.ts
var QUOTE_BOUNDARY_SELECTOR = '[data-testid="quoteTweet"], [aria-labelledby="quoted"], div[role="link"][tabindex="0"]';
var CARD_BOUNDARY_SELECTOR2 = '[data-testid="card.wrapper"]';
function quotedPost(article) {
for (const candidate of Array.from(article.querySelectorAll(QUOTE_BOUNDARY_SELECTOR))) {
if (isQuoteBoundary2(candidate)) {
return candidate;
}
}
return null;
}
function extractTweet(article, options = {}) {
const tweetId = readTweetId(article);
const handle = readHandle(article);
const text = readText(article);
const own = { scope: "post", tweetId, handle, text };
const media = [];
const imageOptions = { preferOriginal: options.preferOriginalImages ?? true };
for (const img of Array.from(
article.querySelectorAll('[data-testid="tweetPhoto"] img')
)) {
const normalized = normalizeImageUrl(img.src, imageOptions);
if (normalized) {
media.push({ kind: "photo", source: img, image: normalized, owner: ownerOf(article, img, own) });
}
}
for (const container of videoContainers(article)) {
const localPoster = container.querySelector("video")?.poster || null;
const owner = ownerOf(article, container, own);
const captured = options.mediaMetadata?.({
// A quoted player's poster belongs to the quoted post, so filtering the lookup by the
// outer post's id would rule out the only entry that can match it.
tweetId: owner.scope === "post" ? tweetId : owner.tweetId,
mediaId: mediaIdFromUrl(localPoster),
poster: localPoster
});
const video = extractVideo(container, captured ?? void 0);
if (!video) {
continue;
}
const resolved = owner.scope === "quote" && !owner.tweetId && captured?.tweetId ? { ...owner, tweetId: captured.tweetId } : owner;
if (video.preferred) {
media.push({ kind: "video", source: video.container, video, owner: resolved });
}
const audio = preferredAudio(video.audioVariants);
if (audio) {
media.push({
kind: "audio",
source: video.container,
audio: { container: video.container, variants: video.audioVariants, preferred: audio },
owner: resolved
});
}
for (const track of video.subtitleTracks) {
media.push({
kind: "subtitle",
source: video.container,
subtitle: { container: video.container, track },
owner: resolved
});
}
if (video.poster) {
const normalized = normalizeImageUrl(video.poster, imageOptions);
if (normalized) {
media.push({ kind: "thumbnail", source: video.container, image: normalized, owner: resolved });
}
}
}
for (const audio of Array.from(article.querySelectorAll("audio"))) {
const owner = ownerOf(article, audio, own);
const variants = readAudioVariants(audio);
const preferred = preferredAudio(variants);
if (preferred) {
media.push({ kind: "audio", source: audio, audio: { container: audio, variants, preferred }, owner });
}
for (const track of Array.from(audio.querySelectorAll("track"))) {
const subtitle = readSubtitleTrack2(track);
if (subtitle) {
media.push({ kind: "subtitle", source: audio, subtitle: { container: audio, track: subtitle }, owner });
}
}
}
return { article, tweetId, handle, text, media };
}
function readAudioVariants(audio) {
const variants = [];
const seen = /* @__PURE__ */ new Set();
const push = (url, type, bitrate) => {
if (!/^https?:\/\//i.test(url) || seen.has(url)) return;
seen.add(url);
variants.push({
url,
type: type || "audio/mp4",
width: null,
height: null,
bitrate: bitrate && Number.isFinite(Number(bitrate)) ? Number(bitrate) : null
});
};
if (audio.currentSrc) push(audio.currentSrc, audio.dataset.contentType ?? "audio/mp4");
if (audio.src) push(audio.src, audio.dataset.contentType ?? "audio/mp4");
for (const source of Array.from(audio.querySelectorAll("source"))) {
push(source.src, source.type || "audio/mp4", source.dataset.bitrate);
}
return variants;
}
function preferredAudio(variants) {
return [...variants].sort((left, right) => (right.bitrate ?? 0) - (left.bitrate ?? 0))[0] ?? null;
}
function readSubtitleTrack2(track) {
const url = track.src || track.getAttribute("src") || "";
if (!/^https?:\/\//i.test(url)) return null;
const kind = (track.kind || "").toLowerCase();
if (kind && kind !== "subtitles" && kind !== "captions") return null;
return {
url,
type: track.getAttribute("type") || "text/vtt",
language: track.srclang?.trim() || null,
label: track.label?.trim() || null
};
}
function mediaIdentity(tweet, media) {
const owner = media.owner;
if (owner.scope === "post") {
return { handle: tweet.handle, tweetId: tweet.tweetId, text: tweet.text };
}
return {
handle: owner.handle ?? tweet.handle,
tweetId: owner.tweetId ?? tweet.tweetId,
text: owner.text
};
}
function isQuoteBoundary2(element2) {
if (element2.matches('[data-testid="quoteTweet"], [aria-labelledby="quoted"]')) {
return true;
}
return element2.matches('div[role="link"][tabindex="0"]') && element2.querySelector('[data-testid="User-Name"]') !== null;
}
function boundaryFor(article, node) {
let found = null;
let current = node;
while (current && current !== article) {
if (isQuoteBoundary2(current)) {
found = { element: current, scope: "quote" };
} else if (!found && current.matches(CARD_BOUNDARY_SELECTOR2)) {
found = { element: current, scope: "card" };
}
current = current.parentElement;
}
return found;
}
function ownerOf(article, node, own) {
const boundary = boundaryFor(article, node);
if (!boundary) {
return own;
}
if (boundary.scope === "card") {
return { ...own, scope: "card" };
}
return {
scope: "quote",
tweetId: readTweetId(boundary.element, false),
handle: readHandle(boundary.element, false),
text: readText(boundary.element, false)
};
}
function mediaIdFromUrl(url) {
return /\/media\/([A-Za-z0-9_-]+)/i.exec(url ?? "")?.[1] ?? null;
}
function readTweetId(article, skipNested = true) {
const links = article.querySelectorAll('a[href*="/status/"]');
for (const link of Array.from(links)) {
if (skipNested && boundaryFor(article, link)) {
continue;
}
const candidate = tweetIdFromHref(link.getAttribute("href"));
if (candidate) {
return candidate;
}
}
return null;
}
function readHandle(article, skipNested = true) {
const userNames = Array.from(article.querySelectorAll('[data-testid="User-Name"]'));
for (const userName of userNames) {
if (skipNested && boundaryFor(article, userName)) {
continue;
}
for (const link of Array.from(userName.querySelectorAll('a[href^="/"]'))) {
const href = link.getAttribute("href") ?? "";
const match = /^\/([A-Za-z0-9_]{1,15})(?:[/?#]|$)/.exec(href);
const candidate = match?.[1];
if (candidate) {
return candidate;
}
}
}
return null;
}
function readText(article, skipNested = true) {
for (const node of Array.from(article.querySelectorAll('[data-testid="tweetText"]'))) {
if (skipNested && boundaryFor(article, node)) {
continue;
}
return node.textContent?.trim() ?? "";
}
return "";
}
// src/features/export/collector.ts
function collectExportRecords(root, surface, options = {}) {
const articles = root instanceof Element && root.matches('article[data-testid="tweet"]') ? [root] : Array.from(root.querySelectorAll('article[data-testid="tweet"]'));
const seen = /* @__PURE__ */ new Set();
const records = [];
const now6 = (/* @__PURE__ */ new Date()).toISOString();
for (const article of articles) {
const tweet = extractTweet(
article,
options.mediaMetadata ? { mediaMetadata: options.mediaMetadata } : {}
);
const key = `${tweet.tweetId ?? "noid"}:${tweet.handle ?? "noh"}:${(tweet.text || "").slice(0, 60)}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
const media = [];
for (const item of tweet.media) {
if (item.kind === "video" && item.video?.preferred) {
const url = item.video.preferred.url;
const entry = {
kind: "video",
url: isSaveableVariantUrl(url) ? url : "",
type: item.video.preferred.type
};
if (item.video.poster) entry.poster = item.video.poster;
if (item.video.preferred.width !== null) entry.width = item.video.preferred.width;
if (item.video.preferred.height !== null) entry.height = item.video.preferred.height;
if (item.video.preferred.bitrate !== null) entry.bitrate = item.video.preferred.bitrate;
media.push(entry);
} else if (item.kind === "audio" && item.audio?.preferred) {
const variant = item.audio.preferred;
if (!isSaveableVariantUrl(variant.url, variant.type)) continue;
media.push({
kind: "audio",
url: variant.url,
type: variant.type,
...variant.bitrate !== null ? { bitrate: variant.bitrate } : {}
});
} else if (item.kind === "subtitle" && item.subtitle?.track) {
const track = item.subtitle.track;
if (!isSaveableVariantUrl(track.url, track.type)) continue;
media.push({
kind: "subtitle",
url: track.url,
type: track.type,
...track.language ? { language: track.language } : {},
...track.label ? { label: track.label } : {}
});
} else if (item.image) {
const entry = {
kind: item.kind === "thumbnail" ? "thumbnail" : "photo",
url: item.image.url,
type: item.image.format
};
if (item.source instanceof HTMLImageElement && item.source.alt) {
entry.altText = item.source.alt;
}
media.push(entry);
} else {
continue;
}
const attributed = media.at(-1);
if (attributed && item.owner.scope !== "post") {
attributed.attribution = { scope: item.owner.scope, handle: item.owner.handle };
}
}
const displayName = readDisplayName(article);
const permalink2 = readPermalink(article, tweet.handle, tweet.tweetId);
const poll = readPoll(article);
const quote = readQuote(article);
const articleSummary = readArticle(article);
const birdwatch = readBirdwatch(article);
const record = {
tweetId: tweet.tweetId,
handle: tweet.handle,
displayName,
text: tweet.text,
capturedAt: now6,
surface,
media,
permalink: permalink2,
language: readPostLanguage(article),
audience: "unknown"
};
for (const entry of record.media) {
if (entry.attribution) entry.attribution = { ...entry.attribution, audience: record.audience ?? "unknown" };
}
if (poll) record.poll = poll;
if (quote) record.quote = quote;
if (articleSummary) record.article = articleSummary;
if (birdwatch) record.birdwatch = birdwatch;
records.push(record);
}
return records;
}
function readPostLanguage(article) {
const textNode = article.querySelector('[data-testid="tweetText"]');
return normalizePostLanguage(
textNode?.getAttribute("lang") ?? textNode?.closest("[lang]")?.getAttribute("lang") ?? article.getAttribute("lang")
);
}
function readPoll(article) {
const bars = Array.from(article.querySelectorAll('[data-testid$="-progress-bar"], [data-testid="cardPoll"] li'));
if (bars.length === 0) {
return null;
}
const choices = [];
for (const bar of bars) {
const label = readFirstText(bar);
if (!label) continue;
const percentMatch = /([0-9]+(?:\.[0-9]+)?)\s*%/.exec(bar.textContent ?? "");
const choice = { label };
if (percentMatch) {
choice.percent = Number(percentMatch[1]);
choice.voteShare = `${percentMatch[1]}%`;
}
choices.push(choice);
}
if (choices.length === 0) {
return null;
}
const totals = article.querySelector('[data-testid="cardPoll"] span');
const totalText = totals?.textContent?.trim() ?? "";
const poll = { choices };
if (totalText.length > 0) {
poll.totalVotes = totalText;
}
return poll;
}
function readQuote(article) {
const quote = quotedPost(article);
if (!quote) {
return null;
}
const handleLink = quote.querySelector('a[href^="/"]');
const handle = handleLink ? /\/([A-Za-z0-9_]{1,15})/.exec(handleLink.getAttribute("href") ?? "")?.[1] ?? null : null;
const text = quote.querySelector('[data-testid="tweetText"]')?.textContent?.trim() ?? "";
return { handle, text };
}
function readArticle(article) {
const card = article.querySelector('[data-testid="card.wrapper"], [data-testid="article"]');
if (!card) {
return null;
}
const titleNode = card.querySelector('[data-testid="card.layoutLarge.detail"] span, [data-testid="article-title"], h2');
const link = card.querySelector("a[href]");
return {
title: titleNode?.textContent?.trim() ?? null,
url: link?.href ?? link?.getAttribute("href") ?? null
};
}
function readBirdwatch(article) {
const pivot = article.querySelector('[data-testid="birdwatch-pivot"]');
if (!pivot) return void 0;
return pivot.textContent?.trim() || void 0;
}
function readFirstText(node) {
if (!node) return null;
const text = node.textContent?.trim() ?? "";
return text.length > 0 ? text : null;
}
function readDisplayName(article) {
const userName = article.querySelector('[data-testid="User-Name"]');
if (!userName) {
return null;
}
const spans = Array.from(userName.querySelectorAll("span"));
for (const span of spans) {
const text = span.textContent?.trim() ?? "";
if (text.length > 0 && !text.startsWith("@") && !text.startsWith("\xB7")) {
return text;
}
}
return null;
}
function readPermalink(article, handle, tweetId) {
if (handle && tweetId) {
return `https://x.com/${handle}/status/${tweetId}`;
}
const link = article.querySelector('a[href*="/status/"]');
const href = link?.getAttribute("href") ?? null;
if (href && tweetIdFromHref(href)) {
return new URL(href, "https://x.com").toString();
}
return null;
}
// src/features/integrations/aria2.ts
var ARIA2_HISTORY_KEY = "aviary.aria2.history.v1";
var ARIA2_HISTORY_LIMIT = 1e3;
async function addUriToAria2(config, request) {
assertOutboundAllowed("The Aria2 handoff");
if (!config.endpoint) {
return { ok: false, error: "Aria2 endpoint not configured" };
}
const token = config.secret ? `token:${config.secret}` : void 0;
const params = token ? [token] : [];
params.push([request.url]);
params.push({ out: request.filename });
const body = JSON.stringify({
jsonrpc: "2.0",
id: `aviary-${Date.now()}`,
method: "aria2.addUri",
params
});
try {
const networkResult = await withNetworkTimeout(async (signal) => {
const response = await fetch(`${config.endpoint}/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json" },
body,
signal
});
if (!response.ok) return { status: response.status };
return { payload: await response.json() };
}, NETWORK_TIMEOUTS.aria2);
if ("status" in networkResult) {
return { ok: false, error: `Aria2 HTTP ${networkResult.status}` };
}
const payload = networkResult.payload;
if (payload?.error) {
return { ok: false, error: payload.error.message ?? "Aria2 error" };
}
const ariaResult = { ok: true };
if (typeof payload?.result === "string") ariaResult.gid = payload.result;
return ariaResult;
} catch (error) {
return { ok: false, error: String(error?.message ?? error) };
}
}
function shouldHandoffToAria2(integration, estimatedBytes) {
if (!integration.enabled || !integration.endpoint) return false;
if (estimatedBytes === null) return true;
return estimatedBytes >= integration.minBytes;
}
var Aria2History = class {
#storage;
#limit;
#entries = [];
#loaded = false;
constructor(storage, limit = ARIA2_HISTORY_LIMIT) {
this.#storage = storage;
this.#limit = Math.max(50, Math.trunc(limit));
}
async load() {
if (this.#loaded) return;
const fallback = { entries: [] };
const raw = await this.#storage.get(ARIA2_HISTORY_KEY, fallback);
const entries = Array.isArray(raw?.entries) ? raw.entries : [];
this.#entries = entries.filter(isHistoryEntry).slice(-this.#limit);
this.#loaded = true;
}
hasUrl(url) {
return this.#entries.some((entry) => entry.url === url);
}
snapshot() {
return { entries: this.#entries.map((entry) => ({ ...entry })) };
}
async rememberQueued(entry) {
await this.load();
if (this.hasUrl(entry.url)) return;
this.#entries.push({
...entry,
status: "queued",
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
});
while (this.#entries.length > this.#limit) {
this.#entries.shift();
}
await this.#persist();
}
async reconcile(config) {
await this.load();
let completed = 0;
let removed = 0;
const retained = [];
for (const entry of this.#entries) {
if (entry.status !== "queued") {
retained.push(entry);
continue;
}
const status = await tellAria2Status(config, entry.gid);
if (status === "complete") {
retained.push({ ...entry, status: "complete", completedAt: (/* @__PURE__ */ new Date()).toISOString() });
completed += 1;
} else if (status === "error" || status === "removed") {
removed += 1;
} else {
retained.push(entry);
}
}
this.#entries = retained;
if (completed > 0 || removed > 0) {
await this.#persist();
}
return { completed, removed };
}
async clear() {
this.#entries = [];
this.#loaded = true;
await this.#persist();
}
/**
* Written under the lock, and deliberately *not* merged.
*
* The aria2 daemon is the authority on what it is holding, and `reconcile()` rebuilds this list
* from it. Merging two tabs' copies would resurrect handoffs the daemon has already finished and
* this tab has already reconciled away. The lock is still worth taking: it stops a second tab's
* write landing in the middle of this one's read-modify-write.
*/
async #persist() {
try {
await replaceStored(this.#storage, ARIA2_HISTORY_KEY, this.snapshot());
} catch {
}
}
};
async function pingAria2Version(config) {
assertOutboundAllowed("The Aria2 connection test");
if (!config.endpoint) {
return { ok: false, error: "Aria2 endpoint not configured" };
}
try {
const result = await withNetworkTimeout(async (signal) => {
const response = await fetch(`${config.endpoint}/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: `aviary-${Date.now()}`,
method: "aria2.getVersion",
params: config.secret ? [`token:${config.secret}`] : []
}),
signal
});
if (!response.ok) return { status: response.status };
return {
payload: await response.json()
};
}, NETWORK_TIMEOUTS.aria2);
if ("status" in result) {
return { ok: false, error: `Aria2 HTTP ${result.status}` };
}
const payload = result.payload;
if (payload?.error) {
return { ok: false, error: payload.error.message ?? "Aria2 rejected the request" };
}
return typeof payload?.result?.version === "string" ? { ok: true, gid: payload.result.version } : { ok: false, error: "Aria2 did not report a version" };
} catch (error) {
return { ok: false, error: String(error?.message ?? error) };
}
}
async function tellActiveAria2(config) {
assertOutboundAllowed("The Aria2 sweep");
const payload = await callAria2(config, "aria2.tellActive", []);
if (!Array.isArray(payload)) return [];
return payload.map((row) => ({
gid: typeof row.gid === "string" ? row.gid : "",
status: typeof row.status === "string" ? row.status : "unknown",
totalLength: Number(row.totalLength ?? 0),
completedLength: Number(row.completedLength ?? 0),
files: Array.isArray(row.files) ? row.files.map((file) => typeof file === "object" && file !== null ? file : {}).map((file) => ({ path: typeof file.path === "string" ? file.path : "" })) : []
}));
}
async function removeAria2Download(config, gid) {
assertOutboundAllowed("The Aria2 cancel");
if (!gid) return { ok: false, error: "Missing GID" };
const payload = await callAria2(config, "aria2.remove", [gid]);
if (typeof payload === "string") {
return { ok: true, gid: payload };
}
return { ok: false, error: "Aria2 did not return a GID" };
}
async function tellAria2Status(config, gid) {
if (!gid) return null;
if (!config.endpoint) return null;
try {
assertOutboundAllowed("The Aria2 status check");
} catch {
return null;
}
const token = config.secret ? `token:${config.secret}` : void 0;
const params = token ? [token, gid] : [gid];
try {
const payload = await withNetworkTimeout(async (signal) => {
const response = await fetch(`${config.endpoint}/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: `aviary-${Date.now()}`,
method: "aria2.tellStatus",
params
}),
signal
});
if (!response.ok) return null;
return await response.json();
}, NETWORK_TIMEOUTS.aria2);
if (!payload) return null;
if (payload.error) {
return isUnknownGidError(payload.error) ? "removed" : null;
}
return typeof payload.result?.status === "string" ? payload.result.status : null;
} catch {
return null;
}
}
async function callAria2(config, method, args) {
if (!config.endpoint) return null;
const token = config.secret ? `token:${config.secret}` : void 0;
const params = token ? [token, ...args] : args;
const body = JSON.stringify({
jsonrpc: "2.0",
id: `aviary-${Date.now()}`,
method,
params
});
try {
const json = await withNetworkTimeout(async (signal) => {
const response = await fetch(`${config.endpoint}/jsonrpc`, {
method: "POST",
headers: { "content-type": "application/json" },
body,
signal
});
if (!response.ok) return null;
return await response.json();
}, NETWORK_TIMEOUTS.aria2);
if (!json) return null;
return json?.result ?? null;
} catch {
return null;
}
}
function isUnknownGidError(error) {
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
return /gid/.test(message) && /(not found|is not found|cannot be found)/.test(message);
}
function isHistoryEntry(value) {
if (typeof value !== "object" || value === null) return false;
const entry = value;
return typeof entry.gid === "string" && typeof entry.url === "string" && typeof entry.filename === "string" && (entry.status === "queued" || entry.status === "complete") && typeof entry.queuedAt === "string" && (entry.completedAt === void 0 || typeof entry.completedAt === "string");
}
// src/extension/download-state.ts
var DOWNLOAD_STATE_MESSAGE = "AVIARY_DOWNLOAD_STATE";
var DOWNLOAD_QUERY_MESSAGE = "AVIARY_DOWNLOAD_QUERY";
function unknownDownloadQuality() {
return {
label: "quality-unknown",
width: null,
height: null,
bitrate: null,
mime: null,
codec: null,
codecSource: null,
playbackProven: false
};
}
function normalizeDownloadQuality(value) {
if (!value || typeof value !== "object") {
return unknownDownloadQuality();
}
const candidate = value;
const label = candidate.label === "original" || candidate.label === "fallback" || candidate.label === "best-direct" || candidate.label === "adaptive-remux" || candidate.label === "quality-unknown" ? candidate.label : "quality-unknown";
const codecSource = candidate.codecSource === "graphql-variant" || candidate.codecSource === "source-element" ? candidate.codecSource : null;
const codec = codecSource !== null && typeof candidate.codec === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(candidate.codec.trim()) ? candidate.codec.trim().toLowerCase() : null;
return {
label,
width: boundedPositive(candidate.width, 2e4),
height: boundedPositive(candidate.height, 2e4),
bitrate: boundedPositive(candidate.bitrate, 1e9),
mime: typeof candidate.mime === "string" && /^[a-z][a-z0-9!#$&^_.+-]*\/[a-z0-9!#$&^_.+-]+$/i.test(candidate.mime) ? candidate.mime.slice(0, 120).toLowerCase() : null,
codec: codec === null ? null : codec,
codecSource: codec === null ? null : codecSource,
playbackProven: candidate.playbackProven === true
};
}
function isDownloadQualityReceipt(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const normalized = normalizeDownloadQuality(value);
for (const [key, provided] of Object.entries(value)) {
if (!(key in normalized)) return false;
if (provided !== normalized[key]) return false;
}
return true;
}
function boundedPositive(value, max) {
const number = typeof value === "number" ? value : Number(value);
return Number.isFinite(number) && number > 0 && number <= max ? Math.round(number) : null;
}
function isDownloadStateMessage(value) {
if (typeof value !== "object" || value === null) {
return false;
}
const candidate = value;
return candidate.type === DOWNLOAD_STATE_MESSAGE && typeof candidate.id === "number" && (candidate.state === "complete" || candidate.state === "interrupted") && (candidate.error === void 0 || typeof candidate.error === "string") && (candidate.quality === void 0 || isDownloadQualityReceipt(candidate.quality));
}
// src/features/media/downloader.ts
var MEDIA_FINGERPRINT_TIMEOUT_MS = 1500;
var DOWNLOAD_PERMISSION_CODE = "downloads-permission-missing";
async function captureMediaBytes(url, options = {}) {
const sourceUrl = url.trim();
if (!/^https?:\/\//i.test(sourceUrl)) {
throw new TypeError("Only HTTP(S) media URLs can be captured into an archive.");
}
const maxBytes = Math.max(1, Math.trunc(options.maxBytes ?? 50 * 1024 * 1024));
const captured = await withNetworkTimeout(async (signal) => {
const response = await fetch(sourceUrl, { signal });
if (!response.ok) {
throw new Error(`Media request failed with HTTP ${response.status}.`);
}
const declaredLength = Number(response.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
throw new RangeError(`Media response exceeds the ${maxBytes}-byte capture limit.`);
}
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > maxBytes) {
throw new RangeError(`Media response exceeds the ${maxBytes}-byte capture limit.`);
}
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
if (signal.aborted) {
throw signal.reason;
}
const sha256 = await sha256HexAsync(bytes);
return {
bytes,
contentType: response.headers.get("content-type")?.split(";", 1)[0]?.trim() || "application/octet-stream",
httpStatus: response.status,
httpHeaders: safeResponseHeaders(response.headers),
sha256
};
}, options.timeoutMs ?? NETWORK_TIMEOUTS.mediaTransfer);
return {
sourceUrl,
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
bytes: captured.bytes,
byteLength: captured.bytes.byteLength,
sha256: captured.sha256,
contentType: captured.contentType,
httpStatus: captured.httpStatus,
httpHeaders: captured.httpHeaders
};
}
async function fingerprintMediaDownload(request) {
const identityHash = mediaIdentityHash(request.kind, request.url, request.mediaId);
if (request.kind === "video") {
return { identityHash };
}
const timeoutMs = Math.max(
1,
Math.min(MEDIA_FINGERPRINT_TIMEOUT_MS, Math.trunc(request.timeoutMs ?? MEDIA_FINGERPRINT_TIMEOUT_MS))
);
const deadline = Date.now() + timeoutMs;
for (const url of downloadCandidates(request)) {
const remaining = deadline - Date.now();
if (remaining <= 0) break;
try {
const captured = await captureMediaBytes(url, { timeoutMs: remaining });
let perceptualHash = null;
if (request.includePerceptual && captured.contentType.startsWith("image/")) {
const decodeBudget = deadline - Date.now();
if (decodeBudget <= 0) {
return { identityHash, exactHash: captured.sha256 };
}
try {
perceptualHash = await withNetworkTimeout(async (signal) => {
const hash = await perceptualImageHash(captured.bytes, captured.contentType);
if (signal.aborted) throw signal.reason;
return hash;
}, decodeBudget);
} catch {
}
}
return {
identityHash,
exactHash: captured.sha256,
...perceptualHash ? { perceptualHash } : {}
};
} catch {
}
}
return { identityHash };
}
async function captureExportRecordMedia(record, options = {}) {
const media = await Promise.all(record.media.map(async (entry) => {
const wantsPosterOnly = options.posterFrameOnly === true && entry.kind === "video";
const posterUrl = wantsPosterOnly ? (entry.poster ?? "").trim() : "";
const videoUrl = (entry.sourceUrl ?? entry.url).trim();
if (wantsPosterOnly && !posterUrl) {
return {
...entry,
sourceUrl: videoUrl,
capturedAt: entry.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
captureStatus: "remote-reference",
captureError: "Poster frames only is on and this video has no poster, so nothing was stored.",
reduction: { posterFrameOnly: true, posterMissing: true }
};
}
const posterOnly = wantsPosterOnly && Boolean(posterUrl);
const sourceUrl = posterOnly ? posterUrl : videoUrl;
try {
const captured = await captureMediaBytes(sourceUrl, options);
const reduced = await applyCaptureSize(
captured,
options,
posterOnly,
posterOnly ? typeof entry.byteLength === "number" ? entry.byteLength : null : null
);
const { reduction: _previousReduction, ...base } = entry;
return {
...base,
// A poster is a photo. Leaving `kind: "video"` on it made the ZIP write a still under a
// video name and let any consumer keying off `kind` or `url` read it as the video.
...posterOnly ? { kind: "thumbnail", url: posterUrl } : {},
sourceUrl: captured.sourceUrl,
capturedAt: captured.capturedAt,
byteLength: reduced.bytes.byteLength,
sha256: reduced.sha256,
bytes: reduced.bytes,
httpStatus: captured.httpStatus,
httpHeaders: captured.httpHeaders,
// Only a re-encode changes the type. Otherwise the DOM-observed type wins over the response
// header, which matters because `captureMediaBytes` defaults a missing `Content-Type` to
// `application/octet-stream` -- and that value goes straight into the ZIP entry and the
// WARC record. The exception is a poster capture: the entry's declared type describes the
// video that was left out, and the bytes on disk are a still.
type: reduced.reencodedType ?? (posterOnly || !entry.type?.includes("/") ? captured.contentType : entry.type),
captureStatus: "captured-bytes",
...reduced.reduction ? { reduction: reduced.reduction } : {}
};
} catch (error) {
return {
...entry,
sourceUrl,
capturedAt: entry.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
captureStatus: /^https?:\/\//i.test(sourceUrl) ? "remote-reference" : "missing",
captureError: error instanceof Error ? error.message : String(error)
};
}
}));
return { ...record, media };
}
async function applyCaptureSize(captured, options, posterFrameOnly, replacedByteLength) {
const scale = options.imageScale ?? 1;
const isImage = captured.contentType.startsWith("image/");
const wantsScale = isImage && Number.isFinite(scale) && scale > 0 && scale < 1;
const reduction = {};
if (posterFrameOnly) {
reduction.posterFrameOnly = true;
if (replacedByteLength !== null) reduction.replacedByteLength = replacedByteLength;
}
if (!wantsScale) {
return {
bytes: captured.bytes,
sha256: captured.sha256,
...posterFrameOnly ? { reduction } : {}
};
}
const rescaled = await downscaleImageBytes(captured.bytes, captured.contentType, scale);
if (!rescaled) {
return {
bytes: captured.bytes,
sha256: captured.sha256,
...posterFrameOnly ? { reduction } : {}
};
}
return {
bytes: rescaled.bytes,
sha256: await sha256HexAsync(rescaled.bytes),
reencodedType: rescaled.contentType,
reduction: { ...reduction, imageScale: scale, originalByteLength: captured.byteLength }
};
}
async function downscaleImageBytes(bytes, contentType, scale) {
const decode = globalThis.createImageBitmap;
const Canvas = globalThis.OffscreenCanvas;
if (typeof decode !== "function" || typeof Canvas !== "function") return null;
try {
const source = new Blob([new Uint8Array(bytes)], { type: contentType });
const bitmap = await decode(source);
const width = Math.max(1, Math.round(bitmap.width * scale));
const height = Math.max(1, Math.round(bitmap.height * scale));
const canvas = new Canvas(width, height);
const context = canvas.getContext("2d");
if (!context) {
bitmap.close?.();
return null;
}
context.drawImage(bitmap, 0, 0, width, height);
bitmap.close?.();
const type = contentType === "image/png" ? "image/png" : "image/jpeg";
const blob = await canvas.convertToBlob({ type, quality: 0.92 });
return { bytes: new Uint8Array(await blob.arrayBuffer()), contentType: type };
} catch {
return null;
}
}
function safeResponseHeaders(headers) {
const output = {};
headers.forEach((value, name) => {
const normalized = name.toLowerCase();
if (/^(?:accept-ranges|cache-control|content-disposition|content-range|content-type|etag|expires|last-modified)$/.test(normalized)) {
output[normalized] = value;
}
});
return output;
}
var DownloadPermissionError = class extends Error {
code = DOWNLOAD_PERMISSION_CODE;
constructor(message = "Aviary needs the browser download permission to save this file.") {
super(message);
this.name = "DownloadPermissionError";
}
};
async function queryExtensionDownload(id) {
if (!Number.isSafeInteger(id) || id < 0) {
return void 0;
}
const runtime = globalThis.chrome?.runtime;
if (!runtime?.sendMessage) {
return void 0;
}
try {
const response = await runtime.sendMessage({
type: DOWNLOAD_QUERY_MESSAGE,
id
});
if (response?.ok !== true || response.id !== id || !isDownloadQueryState(response.state)) {
return void 0;
}
return {
ok: true,
id,
state: response.state,
...typeof response.error === "string" ? { error: response.error } : {},
...response.quality ? { quality: normalizeDownloadQuality(response.quality) } : {}
};
} catch {
return void 0;
}
}
function createDownloader(options = {}) {
return async (request) => {
if (options.integrations) {
const aria = options.integrations.aria2;
if (options.aria2History?.hasUrl(request.url)) {
return { ok: true, via: "aria2", deduplicated: true };
}
const estimatedBytes = request.estimatedBytes ?? (aria.enabled && aria.endpoint ? await estimateBytes(request.url) : null);
if (shouldHandoffToAria2(aria, estimatedBytes)) {
const result = await addUriToAria2(
{ endpoint: aria.endpoint, secret: aria.secret },
{ url: request.url, filename: request.filename }
);
if (result.ok) {
if (result.gid && options.aria2History) {
await options.aria2History.rememberQueued({
gid: result.gid,
url: request.url,
filename: request.filename
});
}
return { ok: true, via: "aria2", ...result.gid ? { gid: result.gid } : {} };
}
options.onWarn?.("Aria2 refused the handoff, saving through the browser instead", {
error: result.error ?? "unknown",
filename: request.filename
});
}
}
const gmResult = await tryGmDownload(request);
if (gmResult) {
return { ok: true, via: "gm", ...gmResult.quality ? { quality: gmResult.quality } : {} };
}
const extResult = await tryExtensionDownload(request);
if (extResult.status === "ok") {
return {
ok: true,
via: "extension",
...extResult.pending && extResult.id !== void 0 ? { pending: true, downloadId: extResult.id } : {}
};
}
if (extResult.status === "needs-permission") {
throw new DownloadPermissionError();
}
if (extResult.status === "failed") {
throw new Error(extResult.error);
}
triggerAnchor(request);
return isCrossOrigin(request.url) ? { ok: true, via: "anchor", degraded: true } : { ok: true, via: "anchor" };
};
}
function isCrossOrigin(url) {
if (/^(blob|data):/i.test(url)) {
return false;
}
const base = typeof location === "undefined" ? void 0 : location.href;
try {
const parsed = new URL(url, base);
return base === void 0 ? true : parsed.origin !== new URL(base).origin;
} catch {
return false;
}
}
async function estimateBytes(url) {
if (typeof fetch !== "function") {
return null;
}
try {
const response = await withNetworkTimeout(
(signal) => fetch(url, { method: "HEAD", signal }),
NETWORK_TIMEOUTS.mediaProbe
);
if (!response.ok) {
return null;
}
const length = Number(response.headers.get("content-length"));
return Number.isFinite(length) && length > 0 ? length : null;
} catch {
return null;
}
}
async function tryGmDownload(request) {
const globals = globalThis;
if (typeof globals.GM_download !== "function") {
return void 0;
}
const candidates2 = [
{ url: request.url, quality: request.quality },
...(request.fallbackUrls ?? []).map((url, index) => ({
url,
quality: request.fallbackQualities?.[index]
}))
].filter(
(candidate, index, all) => /^https?:\/\//i.test(candidate.url) && all.findIndex((entry) => entry.url === candidate.url) === index
).slice(0, 4);
for (let index = 0; index < candidates2.length; index += 1) {
const candidate = candidates2[index];
const saved = await new Promise((resolve) => {
try {
globals.GM_download?.({
url: candidate.url,
name: request.filename,
onload: () => resolve(true),
onerror: () => resolve(false),
ontimeout: () => resolve(false)
});
} catch {
resolve(false);
}
});
if (saved) {
return candidate.quality ? { quality: candidate.quality } : {};
}
}
return void 0;
}
async function tryExtensionDownload(request) {
const runtime = globalThis.chrome?.runtime;
if (!runtime?.sendMessage) {
return { status: "unavailable" };
}
try {
const response = await runtime.sendMessage({
type: "AVIARY_DOWNLOAD",
url: request.url,
...request.fallbackUrls ? { fallbackUrls: request.fallbackUrls } : {},
...request.quality ? { quality: request.quality } : {},
...request.fallbackQualities ? { fallbackQualities: request.fallbackQualities } : {},
filename: request.filename
});
if (response?.ok === true) {
return {
status: "ok",
...typeof response.id === "number" ? { id: response.id } : {},
...response.pending === true ? { pending: true } : {}
};
}
if (response?.code === DOWNLOAD_PERMISSION_CODE) {
return { status: "needs-permission" };
}
if (response === void 0) {
return { status: "unavailable" };
}
return { status: "failed", error: response.error ?? "download failed" };
} catch {
return { status: "unavailable" };
}
}
function downloadCandidates(request) {
return [request.url, ...request.fallbackUrls ?? []].filter((url, index, all) => /^https?:\/\//i.test(url) && all.indexOf(url) === index).slice(0, 4);
}
function isDownloadQueryState(value) {
return value === "in_progress" || value === "complete" || value === "interrupted" || value === "missing";
}
async function requestDownloadPermissionSurface() {
const runtime = globalThis.chrome?.runtime;
if (!runtime?.sendMessage) {
return false;
}
try {
const response = await runtime.sendMessage({ type: "AVIARY_OPEN_OPTIONS" });
return response?.ok === true;
} catch {
return false;
}
}
function triggerAnchor(request) {
if (typeof document === "undefined") {
return;
}
const anchor = document.createElement("a");
anchor.href = request.url;
anchor.download = request.filename;
anchor.rel = "noopener noreferrer";
anchor.target = "_blank";
anchor.style.display = "none";
document.body.append(anchor);
anchor.click();
anchor.remove();
}
// src/extension/native-i18n.ts
var NATIVE_I18N_COPY = {
extensionName: "Aviary for X",
extensionDescription: "Local-first X/Twitter enhancer with reversible controls and privacy-first defaults.",
actionTitle: "Aviary",
contextDownloadMedia: "Download media with Aviary"
};
// src/extension/media-context-menu.ts
var MEDIA_CONTEXT_MENU_TITLE = NATIVE_I18N_COPY.contextDownloadMedia;
var MEDIA_CONTEXT_DOWNLOAD_MESSAGE = "AVIARY_DOWNLOAD_CONTEXT_MEDIA";
var MEDIA_CONTEXT_PERMISSION_DENIED_MESSAGE = "AVIARY_CONTEXT_DOWNLOAD_PERMISSION_DENIED";
function isMediaContextDownloadMessage(message, sender) {
return isContextMessage(message, MEDIA_CONTEXT_DOWNLOAD_MESSAGE, sender);
}
function isMediaContextPermissionDeniedMessage(message, sender) {
return isContextMessage(message, MEDIA_CONTEXT_PERMISSION_DENIED_MESSAGE, sender);
}
function isSupportedXDocumentUrl(value) {
if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
return false;
}
try {
const url = new URL(value);
return url.protocol === "https:" && !url.username && !url.password && ["x.com", "www.x.com", "twitter.com", "www.twitter.com", "pro.x.com"].includes(url.hostname);
} catch {
return false;
}
}
function isContextMessage(message, type, sender) {
return typeof message === "object" && message !== null && Object.keys(message).length === 2 && Object.prototype.hasOwnProperty.call(message, "type") && Object.prototype.hasOwnProperty.call(message, "documentUrl") && message.type === type && isSupportedXDocumentUrl(message.documentUrl) && isTrustedExtensionSender(sender);
}
function isTrustedExtensionSender(sender) {
const extensionId = globalThis.chrome?.runtime?.id;
return typeof extensionId === "string" && extensionId.length > 0 && typeof sender === "object" && sender !== null && sender.id === extensionId;
}
// src/features/media/media-metadata.ts
function extractMediaMetadata(payload) {
const body = readBody(payload);
if (!body || body.length === 0 || body.length > MAX_BODY_CHARS) {
return [];
}
let parsed;
try {
parsed = JSON.parse(body);
} catch {
return [];
}
const found = [];
const state2 = { nodes: 0 };
collectMetadata(parsed, null, 0, state2, found);
return found.map(cloneMetadata);
}
var MAX_BODY_CHARS = 15e5;
var MAX_ENTRIES = 256;
var MAX_NODES = 5e4;
var MAX_DEPTH = 32;
var MEDIA_URL_PATTERN = /\/media\/([A-Za-z0-9_-]+)/i;
var MediaMetadataCache = class {
#entries = /* @__PURE__ */ new Map();
#version = 0;
get version() {
return this.#version;
}
get size() {
return this.#entries.size;
}
ingest(payload) {
return this.ingestMetadata(extractMediaMetadata(payload));
}
ingestMetadata(found) {
let changed = 0;
for (const metadata of found) {
if (upsert(this.#entries, metadata)) {
changed += 1;
this.#version += 1;
}
}
while (this.#entries.size > MAX_ENTRIES) {
const oldest = this.#entries.keys().next().value;
if (!oldest) {
break;
}
this.#entries.delete(oldest);
}
return changed;
}
find(tweetId, mediaId = null, poster = null) {
const wantedTweet = cleanId(tweetId);
const wantedMedia = cleanId(mediaId) ?? mediaIdFromUrl2(poster);
const wantedPoster = cleanUrl(poster);
let winner = null;
let winnerScore = 0;
let tied = false;
for (const entry of this.#entries.values()) {
let score = 0;
if (wantedTweet && entry.tweetId === wantedTweet) {
score += 4;
} else if (wantedTweet && entry.tweetId) {
continue;
}
if (wantedMedia) {
const mediaMatches = entry.mediaId === wantedMedia || mediaIdFromUrl2(entry.poster) === wantedMedia;
if (!mediaMatches && (!wantedPoster || cleanUrl(entry.poster) !== wantedPoster)) {
continue;
}
if (mediaMatches) {
score += 8;
}
}
if (wantedPoster && cleanUrl(entry.poster) === wantedPoster) {
score += 6;
} else if (wantedMedia && mediaIdFromUrl2(entry.poster) === wantedMedia) {
score += 3;
}
if (score === 0 || score < winnerScore) {
continue;
}
if (score === winnerScore) {
tied = true;
continue;
}
winner = entry;
winnerScore = score;
tied = false;
}
return winner && !tied ? cloneMetadata(winner) : null;
}
clear() {
this.#entries.clear();
this.#version += 1;
}
};
function mediaMetadataIdentity(metadata) {
const identity = [
metadata.tweetId,
metadata.mediaId,
mediaIdFromUrl2(metadata.poster),
metadata.poster
].filter(Boolean);
if (identity.length === 0) {
identity.push(metadata.variants[0]?.url ?? metadata.audioVariants[0]?.url ?? "unknown");
}
return JSON.stringify(identity);
}
function mergeMediaMetadata(existing, incoming) {
return mergeMetadata(existing, incoming);
}
function mediaIdFromUrl2(url) {
if (!url) {
return null;
}
return MEDIA_URL_PATTERN.exec(url)?.[1] ?? null;
}
function readBody(payload) {
if (typeof payload === "string") {
return payload;
}
if (!payload || typeof payload !== "object") {
return null;
}
const body = payload.body;
return typeof body === "string" ? body : null;
}
function collectMetadata(value, inheritedTweetId, depth, state2, found) {
if (depth > MAX_DEPTH || state2.nodes >= MAX_NODES || !value || typeof value !== "object") {
return;
}
state2.nodes += 1;
if (Array.isArray(value)) {
for (const child of value) {
collectMetadata(child, inheritedTweetId, depth + 1, state2, found);
if (state2.nodes >= MAX_NODES) {
return;
}
}
return;
}
const record = value;
const tweetId = isMediaRecord(record) ? inheritedTweetId : readTweetId2(record) ?? inheritedTweetId;
if (isMediaRecord(record)) {
const metadata = readMediaMetadata(record, tweetId);
if (metadata) {
found.push(metadata);
}
}
for (const child of Object.values(record)) {
collectMetadata(child, tweetId, depth + 1, state2, found);
if (state2.nodes >= MAX_NODES) {
return;
}
}
}
function readTweetId2(record) {
const restId = cleanId(record.rest_id);
if (restId && looksLikeTweet(record)) {
return restId;
}
const id = cleanId(record.id_str);
return id && looksLikeTweet(record) ? id : null;
}
function looksLikeTweet(record) {
return "legacy" in record || "core" in record || "conversation_id_str" in record || "full_text" in record || "note_tweet" in record || "quoted_status_result" in record;
}
function isMediaRecord(record) {
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
return type === "video" || type === "animated_gif" || isRecord5(record.video_info) || Boolean(record.preview_image_url || record.preview_image_url_https) && (type.includes("video") || type.includes("gif"));
}
function readMediaMetadata(record, tweetId) {
const videoInfo = isRecord5(record.video_info) ? record.video_info : {};
const allVariants = readVariants(videoInfo.variants);
const audioVariants = allVariants.filter(isAudioVariant);
const variants = allVariants.filter((variant) => !isAudioVariant(variant));
const subtitleTracks = readSubtitleTracks(record, videoInfo);
const poster = firstUrl(
record.preview_image_url_https,
record.preview_image_url,
record.media_url_https,
record.media_url
);
const mediaId = (poster ? mediaIdFromUrl2(poster) : null) ?? cleanId(record.media_key) ?? cleanId(record.media_id_string) ?? cleanId(record.media_id);
const isGif = record.type === "animated_gif" || record.is_gif === true || variants.some((variant) => variant.url.toLowerCase().includes("tweet_video"));
if (!poster && variants.length === 0 && audioVariants.length === 0 && subtitleTracks.length === 0) {
return null;
}
return { tweetId, mediaId, poster, variants, audioVariants, subtitleTracks, isGif };
}
function readVariants(value) {
if (!Array.isArray(value)) {
return [];
}
const variants = [];
for (const entry of value) {
if (!isRecord5(entry)) {
continue;
}
const url = httpUrl(entry.url);
if (!url) {
continue;
}
const candidate = {
url,
type: typeof entry.content_type === "string" ? entry.content_type : "video/mp4",
width: positiveNumber(entry.width) ?? dimensionsFromUrl(url)?.width ?? null,
height: positiveNumber(entry.height) ?? dimensionsFromUrl(url)?.height ?? null,
bitrate: positiveNumber(entry.bitrate) ?? positiveNumber(entry.bit_rate) ?? null
};
if (typeof entry.codec === "string" && entry.codec.trim()) {
candidate.codec = entry.codec.trim();
candidate.codecSource = "graphql-variant";
}
if (typeof entry.provenance === "string" && entry.provenance.trim()) {
candidate.provenance = entry.provenance.trim();
}
const existingIndex = variants.findIndex((variant) => variant.url === url);
if (existingIndex < 0) {
variants.push(candidate);
} else {
variants[existingIndex] = mergeVideoVariant(variants[existingIndex], candidate);
}
}
return mergeVideoVariants([], variants);
}
function isAudioVariant(variant) {
return /^audio\//i.test(variant.type) || /\.(?:aac|m4a|mp3|ogg|opus|wav)(?:[?#]|$)/i.test(variant.url);
}
function readSubtitleTracks(...values) {
const found = /* @__PURE__ */ new Map();
const seen = /* @__PURE__ */ new Set();
const visit = (value, depth) => {
if (depth > 6 || !value || typeof value !== "object" || seen.has(value)) return;
seen.add(value);
if (Array.isArray(value)) {
for (const child of value) visit(child, depth + 1);
return;
}
const record = value;
const type = typeof record.type === "string" ? record.type : "";
const url = firstUrl(record.url, record.src, record.source_url, record.href);
if (url && isSubtitleUrl(url, type)) {
found.set(url, {
url,
type: type.includes("/") ? type : subtitleType(url),
language: cleanOptional(record.language ?? record.lang ?? record.srclang),
label: cleanOptional(record.label ?? record.name ?? record.title)
});
}
for (const child of Object.values(record)) visit(child, depth + 1);
};
for (const value of values) visit(value, 0);
return [...found.values()].slice(0, 8);
}
function isSubtitleUrl(url, type) {
return /(?:text\/vtt|text\/srt|application\/ttml|caption|subtitle)/i.test(type) || /\.(?:vtt|srt|ttml|dfxp)(?:[?#]|$)/i.test(url);
}
function subtitleType(url) {
if (/\.(?:srt)(?:[?#]|$)/i.test(url)) return "text/srt";
if (/\.(?:ttml|dfxp)(?:[?#]|$)/i.test(url)) return "application/ttml+xml";
return "text/vtt";
}
function cleanOptional(value) {
return typeof value === "string" && value.trim().length > 0 ? value.trim().slice(0, 80) : null;
}
function upsert(entries, incoming) {
const existingKey = findExistingKey(entries, incoming);
if (!existingKey) {
entries.set(metadataKey(incoming), cloneMetadata(incoming));
return true;
}
const existing = entries.get(existingKey);
const merged = mergeMetadata(existing, incoming);
if (!metadataEqual(existing, merged)) {
entries.delete(existingKey);
entries.set(metadataKey(merged), merged);
return true;
}
entries.delete(existingKey);
entries.set(existingKey, existing);
return false;
}
function findExistingKey(entries, incoming) {
const incomingPosterId = mediaIdFromUrl2(incoming.poster);
for (const [key, entry] of entries) {
if (incoming.mediaId && entry.mediaId === incoming.mediaId) {
return key;
}
if (incomingPosterId && mediaIdFromUrl2(entry.poster) === incomingPosterId) {
return key;
}
if (incoming.tweetId && entry.tweetId === incoming.tweetId && incoming.poster && entry.poster === incoming.poster) {
return key;
}
}
return null;
}
function mergeMetadata(existing, incoming) {
const variants = mergeVideoVariants(existing.variants, incoming.variants);
const audioVariants = mergeVideoVariants(existing.audioVariants, incoming.audioVariants);
const subtitleTracks = [...existing.subtitleTracks];
const seenTracks = new Set(subtitleTracks.map((track) => track.url));
for (const track of incoming.subtitleTracks) {
if (!seenTracks.has(track.url)) {
subtitleTracks.push(track);
seenTracks.add(track.url);
}
}
return {
tweetId: existing.tweetId ?? incoming.tweetId,
mediaId: existing.mediaId ?? incoming.mediaId,
poster: existing.poster ?? incoming.poster,
variants,
audioVariants,
subtitleTracks,
isGif: existing.isGif || incoming.isGif
};
}
function metadataEqual(a, b) {
return a.tweetId === b.tweetId && a.mediaId === b.mediaId && a.poster === b.poster && a.isGif === b.isGif && a.variants.length === b.variants.length && a.variants.every((variant, index) => {
const other = b.variants[index];
return other !== void 0 && videoVariantEqual(variant, other);
}) && a.audioVariants.length === b.audioVariants.length && a.audioVariants.every((variant, index) => {
const other = b.audioVariants[index];
return other !== void 0 && videoVariantEqual(variant, other);
}) && a.subtitleTracks.length === b.subtitleTracks.length && a.subtitleTracks.every((track, index) => {
const other = b.subtitleTracks[index];
return track.url === other?.url && track.type === other.type && track.language === other.language && track.label === other.label;
});
}
function videoVariantEqual(left, right) {
return left.url === right.url && left.type === right.type && left.width === right.width && left.height === right.height && left.bitrate === right.bitrate && left.codec === right.codec && left.codecSource === right.codecSource && left.provenance === right.provenance;
}
function metadataKey(metadata) {
return [
metadata.tweetId,
metadata.mediaId,
mediaIdFromUrl2(metadata.poster),
metadata.poster,
metadata.variants[0]?.url
].filter(Boolean).join(":") || "unknown";
}
function cloneMetadata(metadata) {
return {
...metadata,
variants: metadata.variants.map((variant) => ({ ...variant })),
audioVariants: metadata.audioVariants.map((variant) => ({ ...variant })),
subtitleTracks: metadata.subtitleTracks.map((track) => ({ ...track }))
};
}
function firstUrl(...values) {
for (const value of values) {
const url = httpUrl(value);
if (url) {
return url;
}
}
return null;
}
function httpUrl(value) {
if (typeof value !== "string" || !/^https?:\/\//i.test(value)) {
return null;
}
return value;
}
function cleanId(value) {
if (typeof value !== "string" && typeof value !== "number") {
return null;
}
const result = String(value).trim();
return result.length > 0 ? result : null;
}
function cleanUrl(value) {
return httpUrl(value);
}
function positiveNumber(value) {
const parsed = typeof value === "number" ? value : Number(value);
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : null;
}
function dimensionsFromUrl(url) {
const match = /\/(\d{2,5})x(\d{2,5})\//.exec(url);
if (!match) {
return null;
}
return { width: Number(match[1]), height: Number(match[2]) };
}
function isRecord5(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
// src/features/media/download-watch.ts
var DOWNLOAD_TERMINAL_TIMEOUT_MS = 3e5;
var MAX_BUFFERED = 64;
var DownloadWatcher = class {
#waiting = /* @__PURE__ */ new Map();
#settled = /* @__PURE__ */ new Map();
#quality = /* @__PURE__ */ new Map();
#listener;
start() {
if (this.#listener) {
return;
}
const runtime = globalThis.chrome?.runtime;
if (!runtime?.onMessage?.addListener) {
return;
}
this.#listener = (message) => {
if (isDownloadStateMessage(message)) {
this.settle(message.id, message.state, message.quality);
}
};
runtime.onMessage.addListener(this.#listener);
}
stop() {
const runtime = globalThis.chrome?.runtime;
if (this.#listener && runtime?.onMessage?.removeListener) {
runtime.onMessage.removeListener(this.#listener);
}
this.#listener = void 0;
for (const waiter of this.#waiting.values()) {
waiter.resolve("pending");
}
this.#waiting.clear();
this.#settled.clear();
this.#quality.clear();
}
/** Also the entry point the background's message takes; exposed so tests can drive it. */
settle(id, state2, quality) {
if (this.#settled.has(id)) {
return;
}
if (quality) this.#quality.set(id, { ...quality });
const waiter = this.#waiting.get(id);
if (waiter) {
this.#waiting.delete(id);
this.#rememberSettled(id, state2);
waiter.resolve(state2);
return;
}
this.#rememberSettled(id, state2);
}
/**
* Drops a terminal result after the caller has finished all work derived from it.
*
* Most callers can leave the bounded replay buffer alone. This hook is useful for long-lived
* pages that explicitly know a download's queue, history, and UI consumers have all settled.
*/
forget(id) {
this.#settled.delete(id);
this.#quality.delete(id);
}
/** Quality is available only after a terminal message has proved which candidate completed. */
receipt(id) {
const quality = this.#quality.get(id);
return quality ? { ...quality } : void 0;
}
#rememberSettled(id, state2) {
if (this.#settled.size >= MAX_BUFFERED) {
const oldest = this.#settled.keys().next().value;
if (oldest !== void 0) {
this.#settled.delete(oldest);
}
}
this.#settled.set(id, state2);
}
wait(id, timeoutMs = DOWNLOAD_TERMINAL_TIMEOUT_MS) {
const terminal = this.terminal(id);
return new Promise((resolve) => {
const timer2 = setTimeout(() => resolve("pending"), timeoutMs);
void terminal.then((outcome) => {
clearTimeout(timer2);
resolve(outcome);
});
});
}
/**
* Keeps listening until the browser reports a terminal result or the feature is destroyed.
* Unlike `wait`, this promise does not disappear when the UI switches from busy to Started.
*/
terminal(id) {
const already = this.#settled.get(id);
if (already) {
return Promise.resolve(already);
}
const existing = this.#waiting.get(id);
if (existing) {
return existing.promise;
}
let settle;
const promise = new Promise((resolve) => {
settle = resolve;
});
this.#waiting.set(id, { promise, resolve: settle });
return promise;
}
};
var shared;
function sharedDownloadWatcher() {
shared ??= new DownloadWatcher();
return shared;
}
// src/features/media/yt-dlp-transport.ts
var YTDLP_PROXY_MESSAGE = "AVIARY_YTDLP_PROXY";
var YTDLP_DEFAULT_ENDPOINT = "http://127.0.0.1:8787";
function normalizeYtDlpEndpoint(endpoint) {
try {
const parsed = new URL(endpoint || YTDLP_DEFAULT_ENDPOINT);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
if (host !== "localhost" && host !== "::1" && !host.endsWith(".localhost") && !/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) {
return null;
}
parsed.username = "";
parsed.password = "";
parsed.search = "";
parsed.hash = "";
return parsed.toString().replace(/\/$/, "");
} catch {
return null;
}
}
async function performYtDlpCall(call) {
const response = await fetch(call.url, {
method: call.method,
headers: {
authorization: `Bearer ${call.secret}`,
...call.body === void 0 ? {} : { "content-type": "application/json" }
},
...call.body === void 0 ? {} : { body: call.body }
});
let payload = null;
try {
payload = await response.json();
} catch {
payload = null;
}
return { status: response.status, payload };
}
async function sendYtDlpCall(call) {
assertOutboundAllowed("The local yt-dlp helper call");
const runtime = globalThis.chrome?.runtime;
if (runtime?.id && typeof runtime.sendMessage === "function") {
const response = await runtime.sendMessage({ type: YTDLP_PROXY_MESSAGE, call });
if (response?.ok === true && typeof response.status === "number") {
return { status: response.status, payload: response.payload ?? null };
}
if (response?.ok === false) {
throw new Error(typeof response.error === "string" ? response.error : "The extension background refused the helper call.");
}
}
return performYtDlpCall(call);
}
// src/features/media/yt-dlp-helper.ts
var YTDLP_FORMAT_POLICY = "bv*+ba/b";
var YTDLP_MERGE_POLICY = "mp4/mkv";
function isObservedAdaptiveManifest(variant) {
let parsed;
try {
parsed = new URL(variant.url);
} catch {
return false;
}
if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "video.twimg.com") {
return false;
}
return /\.(?:m3u8|mpd)$/i.test(parsed.pathname) || /(?:mpegurl|dash\+xml)/i.test(variant.type);
}
function observedAdaptiveCandidates(variants) {
return variants.filter(isObservedAdaptiveManifest).map((variant) => ({ manifestUrl: variant.url, variant })).sort((left, right) => {
const quality = compareVariantQuality(right.variant, left.variant);
return quality !== 0 ? quality : left.manifestUrl.localeCompare(right.manifestUrl);
});
}
function bestObservedAdaptive(variants) {
return observedAdaptiveCandidates(variants)[0] ?? null;
}
function shouldOfferAdaptiveHelper(direct, variants) {
const adaptive = bestObservedAdaptive(variants);
return adaptive !== null && (direct === null || compareVariantQuality(adaptive.variant, direct) > 0);
}
function buildYtDlpCommand(request) {
return [
"yt-dlp",
"--no-playlist",
"--format",
quotePowerShell(request.formatPolicy),
"--merge-output-format",
quotePowerShell(YTDLP_MERGE_POLICY),
"--output",
quotePowerShell(request.filename),
quotePowerShell(request.manifestUrl)
].join(" ");
}
async function handoffToYtDlp(settings, request) {
if (!settings.enabled || !settings.secret) {
return { state: "refused", error: "Enable the local yt-dlp helper and set its shared secret." };
}
const endpoint = normalizeYtDlpEndpoint(settings.endpoint);
if (!endpoint) {
return { state: "refused", error: "The yt-dlp helper endpoint must be on this machine." };
}
if (!isObservedAdaptiveManifest({ url: request.manifestUrl, type: "application/x-mpegURL" })) {
return { state: "refused", error: "Only an observed X adaptive manifest can be handed off." };
}
assertOutboundAllowed("The local yt-dlp handoff");
try {
const { status, payload } = await sendYtDlpCall({
method: "POST",
url: `${endpoint}/v1/jobs`,
secret: settings.secret,
body: JSON.stringify({
manifestUrl: request.manifestUrl,
filename: request.filename,
formatPolicy: request.formatPolicy
})
});
if (status === 401 || status === 403) {
return { state: "refused", error: textError(payload, "The local yt-dlp helper refused authorization.") };
}
if (status < 200 || status > 299) {
return { state: "failed", error: textError(payload, `The local yt-dlp helper returned HTTP ${status}.`) };
}
return normalizeJobStatus(payload, "The local yt-dlp helper returned an invalid job.");
} catch (error) {
return { state: "failed", error: error instanceof Error ? error.message : String(error) };
}
}
async function readYtDlpJob(settings, jobId) {
if (!settings.enabled || !settings.secret) return { state: "refused", error: "The local yt-dlp helper is not configured." };
const endpoint = normalizeYtDlpEndpoint(settings.endpoint);
if (!endpoint || !/^[A-Za-z0-9_-]{8,80}$/.test(jobId)) return { state: "missing" };
assertOutboundAllowed("The local yt-dlp status check");
try {
const { status, payload } = await sendYtDlpCall({
method: "GET",
url: `${endpoint}/v1/jobs/${encodeURIComponent(jobId)}`,
secret: settings.secret
});
if (status === 401 || status === 403) {
return { state: "refused", error: textError(payload, "The local yt-dlp helper refused authorization.") };
}
if (status === 404) return { state: "missing" };
if (status < 200 || status > 299) return { state: "failed", error: textError(payload, `The local yt-dlp helper returned HTTP ${status}.`) };
return normalizeJobStatus(payload, "The local yt-dlp helper returned an invalid status.");
} catch (error) {
return { state: "failed", error: error instanceof Error ? error.message : String(error) };
}
}
function normalizeJobStatus(value, fallback) {
if (!value || typeof value !== "object") return { state: "failed", error: fallback };
const record = value;
const state2 = record.state;
if (state2 !== "missing" && state2 !== "refused" && state2 !== "running" && state2 !== "completed" && state2 !== "failed") {
return { state: "failed", error: fallback };
}
return {
...typeof record.jobId === "string" ? { jobId: record.jobId } : {},
state: state2,
...typeof record.error === "string" ? { error: record.error } : {}
};
}
function textError(value, fallback) {
const error = value && typeof value === "object" ? value.error : void 0;
if (typeof error === "string") {
return error;
}
return fallback;
}
function quotePowerShell(value) {
return `'${value.replace(/['‘’‚‛]/g, "$&$&")}'`;
}
// src/features/media/history.ts
var MEDIA_HISTORY_KEY = "aviary.media.history.v1";
var MEDIA_HISTORY_LIMIT = 1500;
var MEDIA_HISTORY_RESERVATION_TTL_MS = 10 * 60 * 1e3;
var MEDIA_HISTORY_RESERVATION_LIMIT = 128;
var MEDIA_HISTORY_SCHEMA_VERSION = 5;
function buildMediaHistoryExportArtifacts(snapshot, options = {}) {
const range = normalizeHistoryRange(options);
const entries = snapshot.entries.filter((entry) => {
const at = Date.parse(entry.at);
return Number.isFinite(at) && at >= range.fromMs && at <= range.toMs;
}).sort((left, right) => left.at.localeCompare(right.at));
const rangeLabel = `${range.fromLabel ?? "all"}-to-${range.toLabel ?? "all"}`;
const payload = {
schemaVersion: 1,
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
range: { from: range.fromLabel, to: range.toLabel },
count: entries.length,
matches: { ...snapshot.matches },
lastMatch: snapshot.lastMatch ? { ...snapshot.lastMatch } : null,
entries: entries.map((entry) => ({
...entry,
quality: entry.quality ?? unknownDownloadQuality()
}))
};
const csv = [
"identity_hash,exact_hash,perceptual_hash,quality,width,height,bitrate,mime,codec,codec_source,playback_proven,downloaded_at",
...entries.map((entry) => {
const quality = entry.quality ?? unknownDownloadQuality();
return [
entry.identityHash,
entry.exactHash ?? "",
entry.perceptualHash ?? "",
quality.label,
quality.width === null ? "" : String(quality.width),
quality.height === null ? "" : String(quality.height),
quality.bitrate === null ? "" : String(quality.bitrate),
quality.mime ?? "",
quality.codec ?? "",
quality.codecSource ?? "",
quality.playbackProven ? "yes" : "no",
entry.at
].map(csvCell).join(",");
})
].join("\n") + "\n";
return [
{
filename: `aviary-media-history-${rangeLabel}.json`,
contentType: "application/json",
data: new TextEncoder().encode(`${JSON.stringify(payload, null, 2)}
`)
},
{
filename: `aviary-media-history-${rangeLabel}.csv`,
contentType: "text/csv",
data: new TextEncoder().encode(csv)
}
];
}
var MediaHistory = class {
#storage;
#limit;
#onPersistError;
#entries = [];
#reservations = [];
#matches = emptyMatches();
#lastMatch = null;
#loaded = false;
#loading;
constructor(storage, limit = MEDIA_HISTORY_LIMIT, onPersistError) {
this.#storage = storage;
this.#limit = Math.max(50, limit);
this.#onPersistError = onPersistError;
}
async load() {
if (this.#loaded) {
return;
}
if (!this.#loading) {
this.#loading = this.#hydrate();
}
await this.#loading;
}
has(key) {
return this.findMatch({ identityHash: legacyIdentityHash(key) }, false) !== null;
}
findMatch(fingerprint2, allowPerceptual = false) {
const candidate = normalizeFingerprint(fingerprint2);
return findFingerprintMatch(
this.#entries,
activeReservations(this.#reservations),
candidate,
allowPerceptual
);
}
/** Completed history only. In-flight claims must not paint a media item as already saved. */
wasDownloaded(fingerprint2, allowPerceptual = false) {
const candidate = normalizeFingerprint(fingerprint2);
return findFingerprintMatch(this.#entries, [], candidate, allowPerceptual) !== null;
}
async record(fingerprintOrLegacyKey, quality) {
await this.load();
const fingerprint2 = typeof fingerprintOrLegacyKey === "string" ? { identityHash: legacyIdentityHash(fingerprintOrLegacyKey) } : normalizeFingerprint(fingerprintOrLegacyKey);
const entry = {
...fingerprint2,
quality: normalizeDownloadQuality(quality),
at: (/* @__PURE__ */ new Date()).toISOString()
};
return await this.#persist({ added: [entry] }) > 0;
}
/** Returns the bounded quality receipt for a previously downloaded identity. */
findQuality(fingerprint2, allowPerceptual = false) {
const candidate = normalizeFingerprint(fingerprint2);
const entry = findEntry(this.#entries, candidate, allowPerceptual);
return entry ? { ...entry.quality } : null;
}
/** Atomically claims a fingerprint so two tabs cannot start the same transfer. */
async reserve(fingerprint2, allowPerceptual = false) {
await this.load();
const candidate = normalizeFingerprint(fingerprint2);
const now6 = Date.now();
const reservation = {
...candidate,
token: reservationToken(candidate, now6),
at: new Date(now6).toISOString(),
expiresAt: now6 + MEDIA_HISTORY_RESERVATION_TTL_MS
};
let result = { match: null, token: reservation.token };
try {
const merged = await mutateStored(
this.#storage,
MEDIA_HISTORY_KEY,
emptySnapshot2(),
(stored) => {
const entries = readEntries(stored);
const reservations = readReservations(stored, now6);
const match = findFingerprintMatch(entries, reservations, candidate, allowPerceptual);
if (match) {
result = { match, token: null };
} else {
reservations.push(reservation);
}
return snapshotFrom(stored, entries, reservations, this.#limit);
}
);
this.#adopt(merged);
return result;
} catch (error) {
this.#onPersistError?.(error);
const match = this.findMatch(candidate, allowPerceptual);
if (match) return { match, token: null };
this.#reservations.push(reservation);
this.#reservations = activeReservations(this.#reservations).slice(-MEDIA_HISTORY_RESERVATION_LIMIT);
return result;
}
}
/** Turns a successful in-flight claim into durable completed history. */
async commit(token, fingerprint2, quality) {
await this.load();
if (!validToken(token)) return false;
const candidate = normalizeFingerprint(fingerprint2);
const entry = {
...candidate,
quality: normalizeDownloadQuality(quality),
at: (/* @__PURE__ */ new Date()).toISOString()
};
let added = false;
try {
const merged = await mutateStored(
this.#storage,
MEDIA_HISTORY_KEY,
emptySnapshot2(),
(stored) => {
const entries = readEntries(stored);
const reservations = readReservations(stored).filter((item) => item.token !== token);
const existing = findEntry(entries, candidate, false);
if (existing) {
const before = JSON.stringify(existing.quality);
mergeEntry(entries, entry);
added = JSON.stringify(existing.quality) !== before;
} else {
mergeEntry(entries, entry);
added = true;
}
return snapshotFrom(stored, entries, reservations, this.#limit);
}
);
this.#adopt(merged);
return added;
} catch (error) {
this.#onPersistError?.(error);
this.#reservations = this.#reservations.filter((item) => item.token !== token);
const existing = findEntry(this.#entries, candidate, false);
if (existing) {
const before = JSON.stringify(existing.quality);
mergeEntry(this.#entries, entry);
return JSON.stringify(existing.quality) !== before;
}
mergeEntry(this.#entries, entry);
return true;
}
}
/** Releases a failed transfer claim so a retry can start immediately. */
async release(token) {
await this.load();
if (!validToken(token)) return;
try {
const merged = await mutateStored(
this.#storage,
MEDIA_HISTORY_KEY,
emptySnapshot2(),
(stored) => snapshotFrom(
stored,
readEntries(stored),
readReservations(stored).filter((item) => item.token !== token),
this.#limit
)
);
this.#adopt(merged);
} catch (error) {
this.#onPersistError?.(error);
this.#reservations = this.#reservations.filter((item) => item.token !== token);
}
}
async noteMatch(kind) {
await this.load();
const match = { kind, at: (/* @__PURE__ */ new Date()).toISOString() };
await this.#persist({ matched: match });
}
async clear() {
this.#entries = [];
this.#reservations = [];
this.#matches = emptyMatches();
this.#lastMatch = null;
this.#loaded = true;
try {
await replaceStored(this.#storage, MEDIA_HISTORY_KEY, emptySnapshot2());
} catch (error) {
this.#onPersistError?.(error);
}
}
size() {
return this.#entries.length;
}
snapshot() {
return {
schemaVersion: MEDIA_HISTORY_SCHEMA_VERSION,
entries: this.#entries.map((entry) => ({ ...entry })),
reservations: activeReservations(this.#reservations).map((entry) => ({ ...entry })),
matches: { ...this.#matches },
lastMatch: this.#lastMatch ? { ...this.#lastMatch } : null
};
}
async #hydrate() {
const stored = await this.#storage.get(
MEDIA_HISTORY_KEY,
emptySnapshot2()
);
this.#entries = readEntries(stored).slice(-this.#limit);
this.#reservations = readReservations(stored);
this.#matches = readMatches(stored);
this.#lastMatch = readLastMatch(stored);
this.#loaded = true;
if (!isCurrentSnapshot(stored)) {
await this.#persist({});
}
}
/**
* Folds this tab's entries into what is stored, under a cross-tab lock.
*
* Overwriting cost real work: two tabs saving media each wrote their own list, so the loser's
* dedup keys disappeared and the same files were offered again as new. `added` is what this call
* recorded -- never the whole local list -- then the same cap the in-memory list applies.
*/
async #persist(delta) {
let added = 0;
try {
const merged = await mutateStored(
this.#storage,
MEDIA_HISTORY_KEY,
emptySnapshot2(),
(stored) => {
const entries = readEntries(stored);
const reservations = readReservations(stored);
for (const entry of delta.added ?? []) {
const existing = findEntry(entries, entry, false);
if (existing) {
const before = JSON.stringify(existing.quality);
mergeEntry(entries, entry);
if (JSON.stringify(existing.quality) !== before) added += 1;
continue;
}
mergeEntry(entries, entry);
added += 1;
}
const ordered = entries.sort(
(left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0
);
const matches2 = readMatches(stored);
if (delta.matched) {
matches2[delta.matched.kind] += 1;
}
const storedLastMatch = readLastMatch(stored);
const lastMatch = delta.matched && (!storedLastMatch || storedLastMatch.at <= delta.matched.at) ? delta.matched : storedLastMatch;
return {
schemaVersion: MEDIA_HISTORY_SCHEMA_VERSION,
entries: ordered.slice(-this.#limit),
reservations: reservations.slice(-MEDIA_HISTORY_RESERVATION_LIMIT),
matches: matches2,
lastMatch
};
}
);
this.#adopt(merged);
} catch (error) {
this.#onPersistError?.(error);
}
return added;
}
#adopt(snapshot) {
this.#entries = readEntries(snapshot).slice(-this.#limit);
this.#reservations = readReservations(snapshot);
this.#matches = readMatches(snapshot);
this.#lastMatch = readLastMatch(snapshot);
}
};
function readEntries(stored) {
const entries = Array.isArray(stored?.entries) ? stored.entries : [];
const normalized = [];
for (const candidate of entries) {
if (!candidate || typeof candidate !== "object" || typeof candidate.at !== "string") continue;
if ("identityHash" in candidate && validHash(candidate.identityHash)) {
const exactHash = validHash(candidate.exactHash) ? candidate.exactHash.toLowerCase() : void 0;
const perceptualHash = validHash(candidate.perceptualHash) ? candidate.perceptualHash.toLowerCase() : void 0;
mergeEntry(normalized, {
identityHash: candidate.identityHash.toLowerCase(),
...exactHash ? { exactHash } : {},
...perceptualHash ? { perceptualHash } : {},
quality: normalizeDownloadQuality("quality" in candidate ? candidate.quality : void 0),
at: candidate.at
});
continue;
}
if ("key" in candidate && typeof candidate.key === "string") {
mergeEntry(normalized, {
identityHash: legacyIdentityHash(candidate.key),
quality: normalizeDownloadQuality(void 0),
at: candidate.at
});
}
}
return normalized;
}
function emptySnapshot2() {
return {
schemaVersion: MEDIA_HISTORY_SCHEMA_VERSION,
entries: [],
reservations: [],
matches: emptyMatches(),
lastMatch: null
};
}
function normalizeHistoryRange(options) {
const from = normalizeDateInput(options.from, false);
const to = normalizeDateInput(options.to, true);
if (from && to && from.ms > to.ms) {
throw new RangeError("The history start date must be on or before the end date.");
}
return {
fromMs: from?.ms ?? Number.NEGATIVE_INFINITY,
toMs: to?.ms ?? Number.POSITIVE_INFINITY,
fromLabel: from?.label ?? null,
toLabel: to?.label ?? null
};
}
function normalizeDateInput(value, endOfDay) {
if (value === null || value === void 0 || value.trim() === "") return null;
const trimmed = value.trim();
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
if (!match) throw new RangeError("History dates must use YYYY-MM-DD.");
const date = /* @__PURE__ */ new Date(`${trimmed}T${endOfDay ? "23:59:59.999" : "00:00:00.000"}Z`);
if (!Number.isFinite(date.getTime())) throw new RangeError("History date is invalid.");
if (date.getUTCFullYear() !== Number(match[1]) || date.getUTCMonth() + 1 !== Number(match[2]) || date.getUTCDate() !== Number(match[3])) {
throw new RangeError("History date is invalid.");
}
return { ms: date.getTime(), label: trimmed.replaceAll("-", "") };
}
function csvCell(value) {
const safe = value.replaceAll('"', '""');
return /[",\n\r]/.test(safe) ? `"${safe}"` : safe;
}
function readReservations(stored, now6 = Date.now()) {
const candidates2 = Array.isArray(stored?.reservations) ? stored.reservations : [];
const reservations = [];
for (const candidate of candidates2) {
if (!candidate || typeof candidate !== "object") continue;
const value = candidate;
if (!validToken(value.token) || !validHash(value.identityHash) || typeof value.at !== "string" || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt) || value.expiresAt <= now6) {
continue;
}
reservations.push({
token: value.token,
identityHash: value.identityHash.toLowerCase(),
...validHash(value.exactHash) ? { exactHash: value.exactHash.toLowerCase() } : {},
...validHash(value.perceptualHash) ? { perceptualHash: value.perceptualHash.toLowerCase() } : {},
at: value.at,
expiresAt: Math.trunc(value.expiresAt)
});
}
return reservations.slice(-MEDIA_HISTORY_RESERVATION_LIMIT);
}
function activeReservations(reservations, now6 = Date.now()) {
return reservations.filter((entry) => entry.expiresAt > now6);
}
function snapshotFrom(stored, entries, reservations, limit) {
return {
schemaVersion: MEDIA_HISTORY_SCHEMA_VERSION,
entries: entries.sort((left, right) => left.at < right.at ? -1 : left.at > right.at ? 1 : 0).slice(-limit),
reservations: activeReservations(reservations).slice(-MEDIA_HISTORY_RESERVATION_LIMIT),
matches: readMatches(stored),
lastMatch: readLastMatch(stored)
};
}
function emptyMatches() {
return { identity: 0, exact: 0, perceptual: 0 };
}
function readMatches(stored) {
const candidate = stored?.matches;
return {
identity: validCount(candidate?.identity),
exact: validCount(candidate?.exact),
perceptual: validCount(candidate?.perceptual)
};
}
function readLastMatch(stored) {
const candidate = stored?.lastMatch;
return candidate && isMatchKind(candidate.kind) && typeof candidate.at === "string" ? { kind: candidate.kind, at: candidate.at } : null;
}
function mergeEntry(entries, incoming) {
const existing = entries.find(
(entry) => entry.identityHash === incoming.identityHash || Boolean(entry.exactHash && incoming.exactHash && entry.exactHash === incoming.exactHash)
);
if (!existing) {
entries.push({ ...incoming });
return;
}
if (!existing.exactHash && incoming.exactHash) existing.exactHash = incoming.exactHash;
if (!existing.perceptualHash && incoming.perceptualHash) {
existing.perceptualHash = incoming.perceptualHash;
}
existing.quality = mergeQuality(existing.quality, incoming.quality);
if (existing.at < incoming.at) existing.at = incoming.at;
}
function normalizeFingerprint(fingerprint2) {
const identityHash = validHash(fingerprint2.identityHash) ? fingerprint2.identityHash.toLowerCase() : sha256Hex(new TextEncoder().encode(String(fingerprint2.identityHash)));
const exactHash = validHash(fingerprint2.exactHash) ? fingerprint2.exactHash.toLowerCase() : void 0;
const perceptualHash = validHash(fingerprint2.perceptualHash) ? fingerprint2.perceptualHash.toLowerCase() : void 0;
return {
identityHash,
...exactHash ? { exactHash } : {},
...perceptualHash ? { perceptualHash } : {}
};
}
function legacyIdentityHash(key) {
const parts = key.split(":");
const kind = parts.at(-1);
const index = parts.at(-2);
if (isMediaKind(kind) && /^\d+$/.test(index ?? "") && parts.length >= 4) {
const source = parts.slice(1, -2).join(":");
return mediaIdentityHash(kind, source, /^https?:\/\//i.test(source) ? null : source);
}
return sha256Hex(new TextEncoder().encode(`aviary-media-legacy:${key}`));
}
function isCurrentSnapshot(stored) {
const now6 = Date.now();
return stored.schemaVersion === MEDIA_HISTORY_SCHEMA_VERSION && Array.isArray(stored.entries) && Array.isArray(stored.reservations) && stored.entries.every(
(entry) => typeof entry === "object" && entry !== null && "identityHash" in entry && validHash(entry.identityHash) && !("key" in entry) && "quality" in entry && isDownloadQualityReceipt(entry.quality)
) && stored.reservations.every((entry) => isActiveStoredReservation(entry, now6));
}
function findEntry(entries, fingerprint2, allowPerceptual) {
const exact = fingerprint2.exactHash && entries.find((entry) => entry.exactHash === fingerprint2.exactHash);
if (exact) return exact;
const identity = entries.find((entry) => entry.identityHash === fingerprint2.identityHash);
if (identity) return identity;
if (allowPerceptual && fingerprint2.perceptualHash) {
return entries.find(
(entry) => entry.perceptualHash && hexadecimalHammingDistance(entry.perceptualHash, fingerprint2.perceptualHash) <= PERCEPTUAL_MATCH_DISTANCE
) ?? null;
}
return null;
}
function qualityRank(value) {
return value.label === "original" ? 5 : value.label === "adaptive-remux" ? 4 : value.label === "best-direct" ? 3 : value.label === "fallback" ? 2 : 1;
}
function mergeQuality(existing, incoming) {
const existingRank = qualityRank(existing);
const incomingRank = qualityRank(incoming);
if (incomingRank > existingRank) return { ...incoming };
if (incomingRank < existingRank) return { ...existing };
return {
...existing,
width: incoming.width ?? existing.width,
height: incoming.height ?? existing.height,
bitrate: incoming.bitrate ?? existing.bitrate,
mime: incoming.mime ?? existing.mime,
// Codec evidence only ever arrives with its source, so the pair moves together or not at all.
...incoming.codec !== null ? { codec: incoming.codec, codecSource: incoming.codecSource } : {},
// Playback is one-way: a later observation that did not play does not unprove an earlier one.
playbackProven: existing.playbackProven || incoming.playbackProven
};
}
function isActiveStoredReservation(value, now6) {
if (!value || typeof value !== "object") return false;
const entry = value;
return validToken(entry.token) && validHash(entry.identityHash) && (entry.exactHash === void 0 || validHash(entry.exactHash)) && (entry.perceptualHash === void 0 || validHash(entry.perceptualHash)) && typeof entry.at === "string" && typeof entry.expiresAt === "number" && Number.isFinite(entry.expiresAt) && entry.expiresAt > now6;
}
function findFingerprintMatch(entries, reservations, fingerprint2, allowPerceptual) {
const candidates2 = [...entries, ...reservations];
if (fingerprint2.exactHash && candidates2.some((entry) => entry.exactHash === fingerprint2.exactHash)) {
return "exact";
}
if (candidates2.some((entry) => entry.identityHash === fingerprint2.identityHash)) {
return "identity";
}
if (allowPerceptual && fingerprint2.perceptualHash) {
for (const entry of candidates2) {
if (entry.perceptualHash && hexadecimalHammingDistance(entry.perceptualHash, fingerprint2.perceptualHash) <= PERCEPTUAL_MATCH_DISTANCE) {
return "perceptual";
}
}
}
return null;
}
var reservationSequence = 0;
function reservationToken(fingerprint2, now6) {
reservationSequence += 1;
return sha256Hex(new TextEncoder().encode(
`${fingerprint2.exactHash ?? fingerprint2.identityHash}:${now6}:${reservationSequence}:${Math.random()}`
));
}
function validToken(value) {
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
}
function validHash(value) {
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
}
function validCount(value) {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : 0;
}
function isMatchKind(value) {
return value === "identity" || value === "exact" || value === "perceptual";
}
function isMediaKind(value) {
return value === "photo" || value === "video" || value === "thumbnail" || value === "audio" || value === "subtitle";
}
// src/features/media/last-download.ts
var LAST_DOWNLOAD_KEY = "aviary.media.last-download.v1";
async function rememberLastDownload(storage, input) {
if (!isHttpUrl(input.url) || input.filename.trim().length === 0) return;
try {
await replaceStored(storage, LAST_DOWNLOAD_KEY, {
...input,
filename: input.filename.slice(0, 240),
downloadedAt: (/* @__PURE__ */ new Date()).toISOString()
});
} catch {
}
}
async function getLastDownload(storage) {
const stored = await storage.get(LAST_DOWNLOAD_KEY, null);
return isLastDownload(stored) ? stored : null;
}
function isLastDownload(value) {
if (typeof value !== "object" || value === null) return false;
const candidate = value;
return typeof candidate.url === "string" && isHttpUrl(candidate.url) && typeof candidate.filename === "string" && candidate.filename.length > 0 && (candidate.kind === "photo" || candidate.kind === "video" || candidate.kind === "thumbnail" || candidate.kind === "audio" || candidate.kind === "subtitle") && typeof candidate.downloadedAt === "string";
}
function isHttpUrl(value) {
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
// src/features/media/sidecar.ts
var MAX_SIDECAR_TEXT = 1e4;
function buildMediaSidecar(format, input) {
if (format === "off") return null;
const normalized = normalizeMediaSidecarInput(input);
const extension = format === "json" ? "json" : "txt";
const filename = replaceExtension(normalized.mediaFilename, extension);
if (format === "json") {
const payload = {
schemaVersion: 1,
savedAt: normalized.savedAt,
post: {
handle: normalized.handle,
tweetId: normalized.tweetId,
text: normalized.text,
permalink: normalized.permalink
},
media: {
kind: normalized.kind,
filename: normalized.mediaFilename
}
};
return {
filename,
contentType: "application/json",
data: new TextEncoder().encode(`${JSON.stringify(payload, null, 2)}
`)
};
}
const lines = [
`Saved: ${normalized.savedAt}`,
`Account: ${normalized.handle ? `@${normalized.handle}` : "unknown"}`,
`Post ID: ${normalized.tweetId ?? "unknown"}`,
`Post: ${normalized.permalink ?? "unavailable"}`,
`Media: ${normalized.kind} (${normalized.mediaFilename})`,
"",
normalized.text
];
return {
filename,
contentType: "text/plain",
data: new TextEncoder().encode(`${lines.join("\n")}
`)
};
}
function mediaSidecarRequest(format, input) {
if (format === "off") return void 0;
const normalized = normalizeMediaSidecarInput({
...input,
savedAt: input.queuedAt ?? (/* @__PURE__ */ new Date()).toISOString()
});
const { savedAt: queuedAt, ...metadata } = normalized;
return { format, ...metadata, queuedAt };
}
function normalizeMediaSidecarRequest(value) {
if (!value || typeof value !== "object") return void 0;
const record = value;
if (record.format !== "json" && record.format !== "text") return void 0;
if (typeof record.mediaFilename !== "string" || record.kind !== "photo" && record.kind !== "video" && record.kind !== "thumbnail" && record.kind !== "audio" && record.kind !== "subtitle") {
return void 0;
}
const normalized = normalizeMediaSidecarInput({
mediaFilename: record.mediaFilename,
kind: record.kind,
handle: typeof record.handle === "string" ? record.handle : null,
tweetId: typeof record.tweetId === "string" ? record.tweetId : null,
text: typeof record.text === "string" ? record.text : "",
permalink: typeof record.permalink === "string" ? record.permalink : null,
savedAt: typeof record.queuedAt === "string" ? record.queuedAt : typeof record.savedAt === "string" ? record.savedAt : (/* @__PURE__ */ new Date()).toISOString()
});
const { savedAt: queuedAt, ...metadata } = normalized;
return { format: record.format, ...metadata, queuedAt };
}
function saveMediaSidecar(request, savedAt = (/* @__PURE__ */ new Date()).toISOString()) {
if (!request || typeof document === "undefined") return false;
const artifact = buildMediaSidecar(request.format, { ...request, savedAt });
if (!artifact) return false;
let url = null;
try {
const blob = new Blob([new Uint8Array(artifact.data)], { type: artifact.contentType });
url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = artifact.filename;
anchor.rel = "noopener noreferrer";
document.body.append(anchor);
anchor.click();
anchor.remove();
const handed = url;
url = null;
setTimeout(() => URL.revokeObjectURL(handed), 4e3);
return true;
} catch {
return false;
} finally {
if (url !== null) URL.revokeObjectURL(url);
}
}
function normalizeMediaSidecarInput(input) {
return {
mediaFilename: bounded(input.mediaFilename, 240) || "media",
kind: input.kind,
handle: nullable(input.handle, 80),
tweetId: nullable(input.tweetId, 80),
text: bounded(input.text, MAX_SIDECAR_TEXT),
permalink: safePermalink(input.permalink),
savedAt: validInstant(input.savedAt) ? input.savedAt : (/* @__PURE__ */ new Date()).toISOString()
};
}
function replaceExtension(filename, extension) {
const slash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
const dot = filename.lastIndexOf(".");
const base = dot > slash ? filename.slice(0, dot) : filename;
return `${base || "media"}.${extension}`;
}
function safePermalink(value) {
if (!value || value.length > 2048) return null;
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:" ? url.href : null;
} catch {
return null;
}
}
function bounded(value, max) {
return String(value ?? "").normalize("NFC").slice(0, max);
}
function nullable(value, max) {
const normalized = value === null ? "" : bounded(value, max).trim();
return normalized || null;
}
function validInstant(value) {
return typeof value === "string" && Number.isFinite(Date.parse(value));
}
// src/features/media/queue.ts
var MEDIA_QUEUE_KEY = "aviary.media.queue.v1";
var RECENT_LIMIT = 40;
var QUEUE_LIMIT = 5e3;
var DownloadQueue = class {
#jobs = [];
#listeners = /* @__PURE__ */ new Set();
#storage;
#onPersistError;
#seq = 0;
#loaded = false;
#persistTail = Promise.resolve();
#changeSequence = 0;
#pendingChanges = [];
#lastPersistFailure;
constructor(storage, onPersistError) {
this.#storage = storage;
this.#onPersistError = onPersistError;
}
async load() {
if (this.#loaded) return;
this.#loaded = true;
if (!this.#storage) return;
try {
const raw = await this.#storage.get(MEDIA_QUEUE_KEY, { sequence: 0, jobs: [] });
const jobs = Array.isArray(raw?.jobs) ? raw.jobs.filter(isDownloadJob).map(normalizeJob) : [];
for (const job of jobs) {
if (job.status === "running") {
job.status = "paused";
job.resumeOnBoot = true;
job.error = "Interrupted before completion; resume when ready.";
}
}
this.#jobs.push(...jobs.slice(-QUEUE_LIMIT));
this.#seq = Math.max(
Number.isFinite(raw?.sequence) ? Math.trunc(raw.sequence) : 0,
...this.#jobs.map((job) => sequenceFromId(job.id))
);
const interrupted = jobs.filter((job) => job.status === "paused" && job.resumeOnBoot).map((job) => ({
id: job.id,
set: {
status: "paused",
resumeOnBoot: true,
...job.error ? { error: job.error } : {}
}
}));
if (interrupted.length > 0) {
this.#persist({ kind: "updates", updates: interrupted });
}
this.#notify();
} catch (error) {
this.#onPersistError?.(error);
}
}
async flush() {
await this.#persistTail;
}
/** Persists every queued item before a batch starts its first external handoff. */
async checkpoint() {
const through = this.#changeSequence;
await this.#persistTail;
const failure = this.#lastPersistFailure;
if (failure && failure.token <= through) {
this.#lastPersistFailure = void 0;
throw failure.error;
}
}
enqueue(job) {
const entry = {
id: `job-${++this.#seq}-${randomSuffix()}`,
status: "queued",
resumeOnBoot: true,
...job
};
this.#jobs.push(entry);
this.#trim();
this.#persist({ kind: "add", job: cloneJob(entry), sequence: this.#seq });
this.#notify();
return entry;
}
/** Refreshes a queued target after a late metadata observation improves its direct URL. */
updateTarget(jobId, target) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || job.status !== "queued" && job.status !== "paused") return false;
job.url = target.url;
if (target.fallbackUrls === void 0) delete job.fallbackUrls;
else job.fallbackUrls = [...target.fallbackUrls];
if (target.fallbackQualities === void 0) delete job.fallbackQualities;
else job.fallbackQualities = target.fallbackQualities.map((quality) => ({ ...quality }));
if (target.quality === void 0) delete job.quality;
else job.quality = { ...target.quality };
if (target.mediaId === void 0) delete job.mediaId;
else job.mediaId = target.mediaId;
const remove = [];
if (target.fallbackUrls === void 0) remove.push("fallbackUrls");
if (target.fallbackQualities === void 0) remove.push("fallbackQualities");
if (target.quality === void 0) remove.push("quality");
if (target.mediaId === void 0) remove.push("mediaId");
this.#persist({
kind: "update",
id: jobId,
set: {
url: target.url,
...target.fallbackUrls === void 0 ? {} : { fallbackUrls: [...target.fallbackUrls] },
...target.fallbackQualities === void 0 ? {} : {
fallbackQualities: target.fallbackQualities.map((quality) => ({ ...quality }))
},
...target.quality === void 0 ? {} : { quality: { ...target.quality } },
...target.mediaId === void 0 ? {} : { mediaId: target.mediaId }
},
...remove.length > 0 ? { remove } : {}
});
this.#notify();
return true;
}
mark(jobId, status, error) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job) {
return;
}
if (status === "running" && !job.startedAt) {
job.startedAt = (/* @__PURE__ */ new Date()).toISOString();
}
if (status === "completed" || status === "failed" || status === "opened" || status === "duplicate") {
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
job.resumeOnBoot = false;
delete job.downloadId;
}
if (status === "cancelled") {
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
job.resumeOnBoot = false;
delete job.downloadId;
}
job.status = status;
if (error) {
job.error = error;
} else if (status !== "failed") {
delete job.error;
}
const set = { status };
const remove = [];
if (status === "running" && job.startedAt) set.startedAt = job.startedAt;
if (status === "completed" || status === "failed" || status === "opened" || status === "duplicate" || status === "cancelled") {
if (job.finishedAt) set.finishedAt = job.finishedAt;
set.resumeOnBoot = false;
remove.push("downloadId");
}
if (error) set.error = error;
else if (status !== "failed") remove.push("error");
this.#persist({ kind: "update", id: jobId, set, remove });
this.#notify();
}
trackDownload(jobId, downloadId) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || !Number.isSafeInteger(downloadId) || downloadId < 0) return;
job.downloadId = downloadId;
this.#persist({ kind: "update", id: jobId, set: { downloadId } });
this.#notify();
}
setQuality(jobId, quality) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job) return false;
job.quality = { ...quality };
this.#persist({ kind: "update", id: jobId, set: { quality: { ...quality } } });
this.#notify();
return true;
}
/** Clears a retained browser id after reconciliation says the transfer is gone or failed. */
forgetDownload(jobId) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || job.downloadId === void 0) return false;
delete job.downloadId;
this.#persist({ kind: "update", id: jobId, set: {}, remove: ["downloadId"] });
this.#notify();
return true;
}
pause(jobId) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || job.status === "completed" || job.status === "failed" || job.status === "opened" || job.status === "duplicate" || job.status === "cancelled") return false;
job.status = "paused";
job.resumeOnBoot = false;
job.error = "Paused by user.";
this.#persist({
kind: "update",
id: jobId,
set: { status: "paused", resumeOnBoot: false, error: "Paused by user." }
});
this.#notify();
return true;
}
resume(jobId) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || job.status !== "paused" && job.status !== "queued") return false;
job.status = "queued";
job.resumeOnBoot = false;
delete job.error;
this.#persist({ kind: "update", id: jobId, set: { status: "queued", resumeOnBoot: false }, remove: ["error"] });
this.#notify();
return true;
}
cancel(jobId) {
const job = this.#jobs.find((entry) => entry.id === jobId);
if (!job || job.status === "completed" || job.status === "failed" || job.status === "opened" || job.status === "duplicate" || job.status === "cancelled") return false;
job.status = "cancelled";
job.resumeOnBoot = false;
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
this.#persist({
kind: "update",
id: jobId,
set: { status: "cancelled", resumeOnBoot: false, finishedAt: job.finishedAt }
});
this.#notify();
return true;
}
retryFailed() {
const retryable = this.#jobs.filter(
(job) => job.status === "failed" || job.status === "cancelled"
);
for (const job of retryable) {
job.status = "queued";
job.resumeOnBoot = true;
delete job.error;
delete job.finishedAt;
}
if (retryable.length > 0) {
this.#persist({
kind: "updates",
updates: retryable.map((job) => ({
id: job.id,
set: { status: "queued", resumeOnBoot: true },
remove: ["error", "finishedAt"]
}))
});
this.#notify();
}
return retryable.map((job) => ({ ...job }));
}
pending(resumeOnBoot = false) {
return this.#jobs.filter((job) => (job.status === "queued" || job.status === "paused") && (!resumeOnBoot || job.resumeOnBoot === true)).map((job) => ({ ...job }));
}
snapshot() {
const counts = {
queued: 0,
running: 0,
completed: 0,
failed: 0,
opened: 0,
duplicate: 0,
paused: 0,
cancelled: 0
};
for (const job of this.#jobs) {
counts[job.status] += 1;
}
return {
total: this.#jobs.length,
queued: counts.queued,
running: counts.running,
completed: counts.completed,
failed: counts.failed,
opened: counts.opened,
duplicate: counts.duplicate,
paused: counts.paused,
cancelled: counts.cancelled,
recent: this.#jobs.slice(-RECENT_LIMIT)
};
}
subscribe(listener) {
this.#listeners.add(listener);
listener(this.snapshot());
return () => {
this.#listeners.delete(listener);
};
}
clear() {
this.#jobs.length = 0;
this.#persist({ kind: "clear", sequence: this.#seq });
this.#notify();
}
remove(jobId) {
const index = this.#jobs.findIndex((job) => job.id === jobId);
if (index < 0) return false;
this.#jobs.splice(index, 1);
this.#persist({ kind: "remove", id: jobId });
this.#notify();
return true;
}
#trim() {
while (this.#jobs.length > QUEUE_LIMIT) {
this.#jobs.shift();
}
}
#notify() {
const snapshot = this.snapshot();
for (const listener of this.#listeners) {
try {
listener(snapshot);
} catch {
}
}
}
#persist(change) {
void this.#queuePersist(change).catch(() => void 0);
}
#queuePersist(change) {
if (!this.#storage) return Promise.resolve();
const token = ++this.#changeSequence;
this.#pendingChanges.push({ token, change });
const write = this.#persistTail.then(
() => mutateStored(
this.#storage,
MEDIA_QUEUE_KEY,
{ sequence: 0, jobs: [] },
(stored) => applyQueueChange(stored, change)
)
).then((next) => {
this.#pendingChanges = this.#pendingChanges.filter((entry) => entry.token !== token);
this.#adopt(next, change);
for (const pending of this.#pendingChanges) {
this.#adopt(applyQueueChange(this.#currentState(), pending.change));
}
});
this.#persistTail = write.catch((error) => {
this.#pendingChanges = this.#pendingChanges.filter((entry) => entry.token !== token);
this.#lastPersistFailure = { token, error };
this.#onPersistError?.(error);
});
return write;
}
#currentState() {
return { sequence: this.#seq, jobs: this.#jobs.map(cloneJob) };
}
#adopt(next, change) {
const normalized = normalizeQueueState(next);
const preserveMissing = change?.kind === "update" || change?.kind === "updates";
const existing = preserveMissing ? new Map(this.#jobs.map((job) => [job.id, cloneJob(job)])) : void 0;
if (existing) {
for (const job of normalized.jobs) existing.set(job.id, cloneJob(job));
for (const job of existing.values()) {
if (!normalized.jobs.some((entry) => entry.id === job.id)) normalized.jobs.push(job);
}
}
this.#jobs.length = 0;
this.#jobs.push(...normalized.jobs.map(cloneJob));
this.#seq = Math.max(normalized.sequence, ...this.#jobs.map((job) => sequenceFromId(job.id)));
}
};
function isDownloadJob(value) {
if (!value || typeof value !== "object") return false;
const record = value;
return typeof record.id === "string" && typeof record.url === "string" && typeof record.filename === "string";
}
function normalizeJob(value) {
const valid = value.status === "queued" || value.status === "running" || value.status === "completed" || value.status === "failed" || value.status === "opened" || value.status === "duplicate" || value.status === "paused" || value.status === "cancelled";
const sidecar = normalizeMediaSidecarRequest(value.sidecar);
return {
id: value.id,
url: value.url,
...Array.isArray(value.fallbackUrls) ? {
fallbackUrls: value.fallbackUrls.filter((url) => typeof url === "string" && /^https?:\/\//i.test(url)).slice(0, 8)
} : {},
...Array.isArray(value.fallbackQualities) ? { fallbackQualities: value.fallbackQualities.map(normalizeQuality) } : {},
filename: value.filename,
...value.kind === "photo" || value.kind === "video" || value.kind === "thumbnail" || value.kind === "audio" || value.kind === "subtitle" ? { kind: value.kind } : {},
...typeof value.mediaId === "string" ? { mediaId: value.mediaId.slice(0, 160) } : value.mediaId === null ? { mediaId: null } : {},
...value.quality ? { quality: normalizeQuality(value.quality) } : {},
...sidecar ? { sidecar } : {},
...typeof value.downloadId === "number" && Number.isSafeInteger(value.downloadId) && value.downloadId >= 0 ? { downloadId: value.downloadId } : {},
status: valid ? value.status : "failed",
...typeof value.error === "string" ? { error: value.error } : {},
...typeof value.startedAt === "string" ? { startedAt: value.startedAt } : {},
...typeof value.finishedAt === "string" ? { finishedAt: value.finishedAt } : {},
resumeOnBoot: value.resumeOnBoot === true
};
}
function sequenceFromId(id) {
const match = /^job-(\d+)/.exec(id);
return match ? Number.parseInt(match[1], 10) : 0;
}
function randomSuffix() {
const uuid = globalThis.crypto?.randomUUID?.();
if (uuid) return uuid.replaceAll("-", "").slice(0, 12);
return Math.random().toString(36).slice(2, 14);
}
function cloneJob(job) {
return {
...job,
...job.fallbackUrls ? { fallbackUrls: [...job.fallbackUrls] } : {},
...job.fallbackQualities ? { fallbackQualities: job.fallbackQualities.map((quality) => ({ ...quality })) } : {},
...job.sidecar ? { sidecar: { ...job.sidecar } } : {},
...job.quality ? { quality: { ...job.quality } } : {}
};
}
function normalizeQuality(value) {
return normalizeDownloadQuality(value);
}
function normalizeQueueState(value) {
if (!value || typeof value !== "object") return { sequence: 0, jobs: [] };
const raw = value;
const jobs = Array.isArray(raw.jobs) ? raw.jobs.filter(isDownloadJob).map(normalizeJob).slice(-QUEUE_LIMIT) : [];
const sequence = Math.max(
Number.isFinite(raw.sequence) ? Math.trunc(raw.sequence) : 0,
...jobs.map((job) => sequenceFromId(job.id))
);
return { sequence, jobs };
}
function applyQueueChange(value, change) {
const state2 = normalizeQueueState(value);
if (change.kind === "clear") {
return { sequence: Math.max(state2.sequence, change.sequence), jobs: [] };
}
const byId = new Map(state2.jobs.map((job) => [job.id, cloneJob(job)]));
if (change.kind === "add") {
byId.set(change.job.id, cloneJob(change.job));
} else if (change.kind === "remove") {
byId.delete(change.id);
} else {
const updates = change.kind === "update" ? [change] : change.updates;
for (const update of updates) {
const current = byId.get(update.id);
if (!current) continue;
const next = { ...current, ...update.set };
for (const field2 of update.remove ?? []) delete next[field2];
byId.set(update.id, next);
}
}
return {
sequence: Math.max(state2.sequence, change.kind === "add" ? change.sequence : 0),
jobs: [...byId.values()].slice(-QUEUE_LIMIT)
};
}
// src/features/media/template.ts
var FORBIDDEN = /[<>:"/\\|?*\u0000-\u001f]/g;
var COLLAPSING_WHITESPACE = /\s+/g;
function renderFilename(template, fields) {
const datePart = formatDate(fields.date);
const safeHandle = sanitizeSegment(fields.handle ?? "unknown");
const safeText = sanitizeSegment(fields.text).slice(0, 60);
const tweetId = sanitizeSegment(fields.tweetId ?? "0");
const mediaId = sanitizeSegment(fields.mediaId ?? tweetId);
const indexLabel = String(fields.index + 1).padStart(2, "0");
const totalLabel = String(Math.max(fields.total, 1)).padStart(2, "0");
const substitutions = {
handle: safeHandle,
account: safeHandle,
tweetId,
mediaId,
index: indexLabel,
total: totalLabel,
date: datePart,
text: safeText,
ext: fields.ext.toLowerCase()
};
let name = template.replace(/\{(\w+)\}/g, (match, key) => {
const value = substitutions[key];
return value === void 0 ? match : value;
}).replace(/[\\/]+/g, "/").trim();
if (name.length === 0) {
name = `${safeHandle}_${tweetId}_${indexLabel}`;
}
if (!name.toLowerCase().endsWith(`.${fields.ext.toLowerCase()}`)) {
name = `${name}.${fields.ext.toLowerCase()}`;
}
return name;
}
function sanitizeSegment(value) {
return value.replace(FORBIDDEN, "").replace(COLLAPSING_WHITESPACE, " ").trim();
}
function formatDate(date) {
const y = date.getUTCFullYear();
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
const d = String(date.getUTCDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
// src/features/media/media-buttons.ts
var STYLE_ID4 = "av-media-buttons";
var BUTTON_ATTR = "data-av-media-button";
var ACTION_ATTR = "data-av-media-action";
var COPY_ACTION_ATTR = "data-av-media-copy";
var ACTION_SLOT_ATTR = "data-av-media-action-slot";
var HELPER_SLOT_ATTR = "data-av-media-helper-slot";
var HELPER_ACTION_ATTR = "data-av-media-helper-action";
var PROCESSED_ATTR = "data-av-media-processed";
var DOWNLOADED_ATTR = "data-av-downloaded";
var MEDIA_HOST_SELECTOR = '[data-testid="tweetPhoto"], [data-testid="videoPlayer"], [data-testid="videoComponent"], audio';
var MEDIA_MUTATION_SELECTOR = '[data-testid="tweetPhoto"], [data-testid="tweetPhoto"] img, [data-testid="videoPlayer"], [data-testid="videoComponent"], video, source, audio, track, [role="group"], [data-testid="reply"]';
var CONTEXT_TARGET_MAX_AGE_MS = 3e4;
var downloader;
var downloadWatcher = sharedDownloadWatcher();
var history;
var aria2History;
var queue;
var appliedPreferOriginalImages;
var appliedCopyMediaLinks;
var appliedMetadataVersion;
var mediaMetadataCache = new MediaMetadataCache();
var subscribedBridge2;
var permissionSurfaceOpened = false;
var pendingContextTarget;
var contextMenuListener;
var extensionMessageListener;
var buttonResetTimers = /* @__PURE__ */ new WeakMap();
var mediaButtonsFeature = {
id: "media.buttons",
title: "One-click media",
category: "media",
async init(ctx) {
if (ctx.settings.media.buttons) {
subscribeToMediaMetadata(ctx);
}
downloadWatcher.start();
if (ctx.settings.media.buttons) {
ensureMediaStyle();
}
aria2History = new Aria2History(ctx.storage);
await aria2History.load();
if (ctx.settings.integrations.aria2.enabled && ctx.settings.integrations.aria2.endpoint) {
try {
await aria2History.reconcile({
endpoint: ctx.settings.integrations.aria2.endpoint,
secret: ctx.settings.integrations.aria2.secret
});
} catch (error) {
ctx.diagnostics.warn("Aria2 history reconcile skipped", errorDetails2(error));
}
}
downloader = createDownloader({
integrations: ctx.settings.integrations,
aria2History,
onWarn: (message, details) => ctx.diagnostics.warn(message, details)
});
queue = new DownloadQueue(ctx.storage, (error) => {
ctx.diagnostics.error("Media queue failed to save", errorDetails2(error));
});
await queue.load();
history = new MediaHistory(ctx.storage, void 0, (error) => {
ctx.diagnostics.error("Media history failed to save", errorDetails2(error));
});
try {
await history.load();
} catch (error) {
ctx.diagnostics.warn("Media history failed to load", errorDetails2(error));
}
installContextDownload(ctx);
applyToggleClass(ctx);
appliedPreferOriginalImages = ctx.settings.media.preferOriginalImages;
appliedCopyMediaLinks = ctx.settings.media.copyMediaLinks;
appliedMetadataVersion = mediaMetadataCache.version;
scanArticles(document, ctx);
ctx.diagnostics.info("Media buttons initialized", { history: history.size() });
},
apply(ctx, root, addedNodes) {
applyToggleClass(ctx);
if (!ctx.settings.media.buttons) {
unsubscribeFromMediaMetadata();
clearDecorations();
pendingContextTarget = void 0;
appliedPreferOriginalImages = void 0;
appliedCopyMediaLinks = void 0;
appliedMetadataVersion = void 0;
return;
}
subscribeToMediaMetadata(ctx);
if (appliedPreferOriginalImages !== void 0 && appliedPreferOriginalImages !== ctx.settings.media.preferOriginalImages) {
clearDecorations();
}
if (appliedCopyMediaLinks !== void 0 && appliedCopyMediaLinks !== ctx.settings.media.copyMediaLinks) {
clearDecorations();
}
if (appliedMetadataVersion !== void 0 && appliedMetadataVersion !== mediaMetadataCache.version) {
clearDecorations();
}
appliedPreferOriginalImages = ctx.settings.media.preferOriginalImages;
appliedCopyMediaLinks = ctx.settings.media.copyMediaLinks;
appliedMetadataVersion = mediaMetadataCache.version;
ensureMediaStyle();
if (!addedNodes || addedNodes.length === 0) {
scanArticles(root, ctx);
return;
}
for (const node of addedNodes) {
if (node.hasAttribute(BUTTON_ATTR) || node.hasAttribute(COPY_ACTION_ATTR) || node.hasAttribute(ACTION_SLOT_ATTR) || node.hasAttribute(HELPER_SLOT_ATTR) || node.hasAttribute(HELPER_ACTION_ATTR)) {
continue;
}
if (needsMutationReconcile(node)) {
scanArticles(node, ctx, true);
}
}
},
async destroy(ctx) {
uninstallContextDownload();
downloadWatcher.stop();
clearDecorations();
downloader = void 0;
history = void 0;
aria2History = void 0;
await queue?.flush();
queue = void 0;
appliedPreferOriginalImages = void 0;
appliedCopyMediaLinks = void 0;
appliedMetadataVersion = void 0;
mediaMetadataCache.clear();
removeFeatureToast();
unsubscribeFromMediaMetadata();
subscribedBridge2 = void 0;
ctx.diagnostics.info("Media buttons destroyed");
},
getStatus() {
if (!queue) {
return { ok: true, message: "Media buttons idle" };
}
const snapshot = queue.snapshot();
return {
ok: snapshot.failed === 0,
message: `Downloads: ${snapshot.completed} ok / ${snapshot.duplicate} dup / ${snapshot.failed} fail`,
details: { ...snapshot }
};
}
};
function getMediaQueue() {
return queue;
}
function getMediaHistory() {
return history;
}
function refreshMediaDownloadMarkers(ctx) {
clearDecorations();
if (!ctx.settings.media.buttons) return;
ensureMediaStyle();
scanArticles(document, ctx);
}
function getCapturedMediaMetadata(args) {
return mediaMetadataCache.find(args.tweetId, args.mediaId, args.poster);
}
var graphqlHandler;
var mediaMetadataHandler;
function subscribeToMediaMetadata(ctx) {
const bridge = ctx.pageBridge;
if (!bridge || subscribedBridge2 === bridge) {
return;
}
unsubscribeFromMediaMetadata();
subscribedBridge2 = bridge;
const onMediaMetadata = bridge.onMediaMetadata;
const ingest = (payload) => {
const changed = onMediaMetadata ? mediaMetadataCache.ingestMetadata([payload]) : mediaMetadataCache.ingest(payload);
if (changed > 0 && ctx.settings.media.buttons) {
ctx.requestApply();
}
};
if (typeof onMediaMetadata === "function") {
mediaMetadataHandler = ingest;
onMediaMetadata.call(bridge, mediaMetadataHandler);
} else {
graphqlHandler = ingest;
bridge.on("graphql", graphqlHandler);
}
}
function unsubscribeFromMediaMetadata() {
if (graphqlHandler) subscribedBridge2?.off("graphql", graphqlHandler);
if (mediaMetadataHandler) subscribedBridge2?.offMediaMetadata(mediaMetadataHandler);
graphqlHandler = void 0;
mediaMetadataHandler = void 0;
}
function applyToggleClass(ctx) {
document.documentElement.classList.toggle(
"av-media-buttons-enabled",
ctx.settings.media.buttons
);
}
function clearDecorations() {
document.getElementById(STYLE_ID4)?.remove();
document.documentElement.classList.remove("av-media-buttons-enabled");
for (const article of Array.from(document.querySelectorAll(`[${PROCESSED_ATTR}]`))) {
article.removeAttribute(PROCESSED_ATTR);
}
for (const button3 of Array.from(document.querySelectorAll(`[${BUTTON_ATTR}]`))) {
button3.remove();
}
for (const slot of Array.from(document.querySelectorAll(`[${ACTION_SLOT_ATTR}]`))) {
slot.remove();
}
for (const slot of Array.from(document.querySelectorAll(`[${HELPER_SLOT_ATTR}]`))) {
slot.remove();
}
}
function installContextDownload(ctx) {
const runtime = globalThis.chrome?.runtime;
if (!runtime?.id || !runtime.onMessage || contextMenuListener || extensionMessageListener) {
return;
}
contextMenuListener = (event) => {
pendingContextTarget = ctx.settings.media.buttons ? contextTarget(event.target) : void 0;
};
document.addEventListener("contextmenu", contextMenuListener, true);
extensionMessageListener = (message, sender, sendResponse) => {
if (isMediaContextPermissionDeniedMessage(message, sender)) {
showFeatureToast(
ft(ctx, "Download access was not granted. Open Aviary Options to enable browser downloads."),
{ tone: "error", ctx }
);
sendResponse({ ok: false, reason: "permission-denied" });
return false;
}
if (!isMediaContextDownloadMessage(message, sender)) {
return false;
}
void downloadContextTarget(ctx).then(
(ok) => sendResponse({ ok }),
(error) => {
ctx.diagnostics.error("Context media download failed", errorDetails2(error));
showFeatureToast(ft(ctx, "Media download failed. Try the on-post button again."), {
tone: "error",
ctx
});
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
}
);
return true;
};
runtime.onMessage.addListener(extensionMessageListener);
}
function uninstallContextDownload() {
if (contextMenuListener) {
document.removeEventListener("contextmenu", contextMenuListener, true);
contextMenuListener = void 0;
}
const onMessage = globalThis.chrome?.runtime?.onMessage;
if (extensionMessageListener && onMessage?.removeListener) {
onMessage.removeListener(extensionMessageListener);
}
extensionMessageListener = void 0;
pendingContextTarget = void 0;
}
function contextTarget(rawTarget) {
if (!(rawTarget instanceof Element)) {
return void 0;
}
const host = rawTarget.closest(VIDEO_CONTAINER_SELECTOR) ?? rawTarget.closest('[data-testid="tweetPhoto"]');
const article = host?.closest('article[data-testid="tweet"]');
if (!host || !article) {
return void 0;
}
return { article, host, capturedAt: Date.now() };
}
async function downloadContextTarget(ctx) {
const pending = pendingContextTarget;
pendingContextTarget = void 0;
if (!ctx.settings.media.buttons || !pending || !pending.article.isConnected || !pending.host.isConnected || Date.now() - pending.capturedAt > CONTEXT_TARGET_MAX_AGE_MS) {
showFeatureToast(ft(ctx, "Right-click an image or video first, then choose Aviary download."), {
tone: "error",
ctx
});
return false;
}
const tweet = extractTweetForContext(pending.article, ctx);
const wantsVideo = pending.host.matches(VIDEO_CONTAINER_SELECTOR);
const index = tweet.media.findIndex((media2) => {
const target = resolveTarget(media2);
if (wantsVideo) {
return media2.kind === "video" && media2.video?.container === pending.host && target !== null && target.helperOnly !== true;
}
return media2.kind === "photo" && media2.source.closest('[data-testid="tweetPhoto"]') === pending.host && target !== null && target.helperOnly !== true;
});
const media = tweet.media[index];
if (!media) {
showFeatureToast(
ft(
ctx,
wantsVideo ? "The direct video is still loading. Try again in a moment." : "This image is not available to download."
),
{ tone: "error", ctx }
);
return false;
}
const container = resolveContainer(media);
if (!container) {
return false;
}
let button3 = container.querySelector(
`[${BUTTON_ATTR}="${media.kind}"]`
);
if (!button3) {
button3 = buildButton(media, index, tweet, ctx);
container.append(button3);
positionButton(button3, container);
}
await handleDownload(media, index, tweet, ctx, button3);
return true;
}
function extractTweetForContext(article, ctx) {
return extractTweet(article, {
preferOriginalImages: ctx.settings.media.preferOriginalImages,
mediaMetadata: ({ tweetId, mediaId, poster }) => mediaMetadataCache.find(tweetId, mediaId, poster)
});
}
function scanArticles(root, ctx, force = false) {
if (!ctx.settings.media.buttons) {
return;
}
const articles = collectArticles(root);
for (const article of articles) {
if (!force && article.getAttribute(PROCESSED_ATTR) === "1") {
if (article.querySelector(`[${BUTTON_ATTR}]`) && (article.querySelector(`[${ACTION_ATTR}]`) || !findActionGroup(article)) || !article.querySelector(MEDIA_HOST_SELECTOR)) {
continue;
}
}
const tweet = extractTweetForContext(article, ctx);
if (tweet.media.length === 0) {
article.setAttribute(PROCESSED_ATTR, "1");
continue;
}
decorateArticle(tweet, ctx);
article.setAttribute(PROCESSED_ATTR, "1");
}
}
function needsMutationReconcile(node) {
const closestArticle = node.closest('article[data-testid="tweet"]');
if (closestArticle && closestArticle.getAttribute(PROCESSED_ATTR) !== "1") {
return true;
}
if (node.matches('article[data-testid="tweet"]')) {
return node.getAttribute(PROCESSED_ATTR) !== "1" || (!node.querySelector(`[${BUTTON_ATTR}]`) || findActionGroup(node) !== null && !node.querySelector(`[${ACTION_ATTR}]`)) && node.querySelector(MEDIA_HOST_SELECTOR) !== null;
}
return node.matches(MEDIA_MUTATION_SELECTOR) || node.querySelector(MEDIA_MUTATION_SELECTOR) !== null;
}
function collectArticles(root) {
const found = /* @__PURE__ */ new Set();
if (root instanceof Element) {
const article = root.matches('article[data-testid="tweet"]') ? root : root.closest('article[data-testid="tweet"]');
if (article) {
found.add(article);
}
}
if ("querySelectorAll" in root) {
for (const article of Array.from(
root.querySelectorAll('article[data-testid="tweet"]')
)) {
found.add(article);
}
}
return [...found];
}
function decorateArticle(tweet, ctx) {
decoratePostAction(tweet, ctx);
tweet.media.forEach((media, index) => {
const container = resolveContainer(media);
if (!container || hasOwnButton(container, media.kind)) {
return;
}
const target = resolveTarget(media);
if (!target || target.helperOnly) {
return;
}
const button3 = buildButton(media, index, tweet, ctx);
container.append(button3);
positionButton(button3, container);
});
}
function decoratePostAction(tweet, ctx) {
const group = findActionGroup(tweet.article);
if (group?.querySelector(`[${ACTION_ATTR}], [${HELPER_ACTION_ATTR}]`)) {
return;
}
if (!group) return;
const assets = primaryDownloadAssets(tweet);
const adaptiveAssets = primaryAdaptiveAssets(tweet);
const hasPendingVideo = tweet.media.some((media) => media.kind === "video" && resolveTarget(media) === null) || tweet.article.querySelector(VIDEO_CONTAINER_SELECTOR) !== null && !tweet.media.some((media) => media.kind === "video");
if (assets.length === 0 && adaptiveAssets.length === 0 && !hasPendingVideo) {
return;
}
if (assets.length > 0 || hasPendingVideo) {
const slot = document.createElement("div");
slot.className = "av-media-action-slot";
slot.setAttribute(ACTION_SLOT_ATTR, "1");
const button3 = buildPostAction(tweet, hasPendingVideo ? [] : assets, ctx);
slot.append(button3);
if (ctx.settings.media.copyMediaLinks && !hasPendingVideo && assets.length > 0) {
slot.append(buildCopyMediaAction(assets, ctx));
}
group.append(slot);
}
for (const asset of adaptiveAssets) {
group.append(buildYtDlpSlot(tweet, asset, ctx));
}
}
function findActionGroup(article) {
const reply = article.querySelector('[data-testid="reply"]');
const group = reply?.closest('[role="group"]') ?? null;
return group && article.contains(group) ? group : null;
}
function primaryDownloadAssets(tweet) {
const assets = [];
tweet.media.forEach((media, index) => {
if (media.kind !== "photo" && media.kind !== "video" && media.kind !== "audio" && media.kind !== "subtitle") {
return;
}
if (media.owner.scope !== "post") {
return;
}
const target = resolveTarget(media);
if (target && !target.helperOnly) {
assets.push({ media, index, target });
}
});
return assets;
}
function primaryAdaptiveAssets(tweet) {
const assets = [];
tweet.media.forEach((media, index) => {
if (media.kind !== "video" || media.owner.scope !== "post" || !media.video) return;
const direct = media.video.variants.filter((variant) => isSaveableVariantUrl(variant.url, variant.type)).sort((left, right) => compareVariantQuality(right, left))[0] ?? null;
const candidate = bestObservedAdaptive(media.video.variants);
if (candidate && shouldOfferAdaptiveHelper(direct, media.video.variants)) {
assets.push({ media, index, candidate });
}
});
return assets;
}
function buildPostAction(tweet, assets, ctx) {
const button3 = document.createElement("button");
button3.type = "button";
button3.className = "av-media-action";
button3.setAttribute(ACTION_ATTR, "1");
const idleLabel = primaryAdaptiveAssets(tweet).length > 0 ? ft(ctx, "Download \xB7 Best direct MP4") : ft(ctx, "Download");
const accessibleLabel = tweet.media.some((media) => media.owner.scope !== "post") ? ft(ctx, "Download this post's own media. Quoted and card media has its own Download button.") : ft(ctx, "Download all media in this post");
button3.dataset.idleLabel = idleLabel;
button3.dataset.idleIcon = "\u2193";
button3.dataset.baseIdleAriaLabel = accessibleLabel;
button3.dataset.idleAriaLabel = accessibleLabel;
button3.setAttribute("aria-label", accessibleLabel);
button3.setAttribute("aria-live", "polite");
button3.setAttribute("aria-busy", "false");
const icon = document.createElement("span");
icon.className = "av-media-action-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "\u2193 ";
const label = document.createElement("span");
label.className = "av-media-action-label";
label.textContent = idleLabel;
button3.append(icon, label);
if (assets.length > 0) {
setDownloadedMarker(
button3,
assets.every((asset) => wasDownloaded(asset.media.kind, asset.target)),
ctx
);
}
if (assets.length === 0) {
button3.dataset.pendingVideo = "true";
button3.setAttribute(
"aria-label",
ft(ctx, "The direct video is still loading. Try again in a moment.")
);
button3.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void handlePendingPostDownload(tweet.article, ctx, button3);
});
return button3;
}
const completed = /* @__PURE__ */ new Set();
button3.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void handlePostDownload(tweet, assets, completed, ctx, button3);
});
return button3;
}
function buildCopyMediaAction(assets, ctx) {
const button3 = document.createElement("button");
button3.type = "button";
button3.className = "av-media-action av-media-copy-action";
button3.setAttribute(ACTION_ATTR, "copy");
button3.setAttribute(COPY_ACTION_ATTR, "1");
const idleLabel = ft(ctx, "Copy links");
const accessibleLabel = ft(ctx, "Copy this post's best direct media links");
button3.dataset.idleLabel = idleLabel;
button3.dataset.idleIcon = "\u29C9";
button3.dataset.baseIdleAriaLabel = accessibleLabel;
button3.dataset.idleAriaLabel = accessibleLabel;
button3.setAttribute("aria-label", accessibleLabel);
button3.setAttribute("aria-live", "polite");
button3.setAttribute("aria-busy", "false");
const icon = document.createElement("span");
icon.className = "av-media-action-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "\u29C9 ";
const label = document.createElement("span");
label.className = "av-media-action-label";
label.textContent = idleLabel;
button3.append(icon, label);
const urls = [...new Set(assets.map((asset) => asset.target.url))];
button3.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void copyMediaLinks(urls, button3, ctx);
});
return button3;
}
async function copyMediaLinks(urls, button3, ctx) {
try {
const clipboard = globalThis.navigator?.clipboard;
if (!clipboard?.writeText) throw new Error("This browser exposes no clipboard write.");
await clipboard.writeText(urls.join("\n"));
setButtonFeedback(button3, {
label: ft(ctx, "Copied"),
icon: "\u2713",
className: "is-success"
});
void ctx.auditLog.record("media.links.copied", { count: urls.length });
} catch (error) {
setButtonFeedback(button3, {
label: ft(ctx, "Failed"),
icon: "!",
className: "is-error"
});
ctx.diagnostics.warn("Media links could not be copied", errorDetails2(error));
showFeatureToast(ft(ctx, "The clipboard refused this copy."), { tone: "error", ctx });
}
scheduleButtonRestore(button3);
}
function buildYtDlpSlot(tweet, asset, ctx) {
const slot = document.createElement("div");
slot.className = "av-media-helper-slot";
slot.setAttribute(HELPER_SLOT_ATTR, "1");
const height = asset.candidate.variant.height;
const quality = height ? `${height}p` : ft(ctx, "adaptive");
const request = helperRequest(tweet, asset, ctx);
const send = document.createElement("button");
send.type = "button";
send.className = "av-media-helper-button av-media-helper-send";
send.setAttribute(HELPER_ACTION_ATTR, "send");
send.textContent = ft(ctx, "Send to yt-dlp");
send.setAttribute("aria-label", ft(ctx, "Send the observed adaptive video to local yt-dlp"));
send.dataset.idleLabel = send.textContent;
send.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void sendYtDlp(request, send, ctx);
});
const copy2 = document.createElement("button");
copy2.type = "button";
copy2.className = "av-media-helper-button av-media-helper-copy";
copy2.setAttribute(HELPER_ACTION_ATTR, "copy");
copy2.textContent = ft(ctx, "Copy yt-dlp command");
copy2.setAttribute("aria-label", ft(ctx, "Copy the yt-dlp command for the observed adaptive video"));
copy2.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void copyYtDlpCommand(request, copy2, ctx);
});
slot.append(send, copy2);
slot.setAttribute("aria-label", ft(ctx, "Higher quality adaptive video") + ` \xB7 ${quality}`);
return slot;
}
function helperRequest(tweet, asset, ctx) {
const identity = mediaIdentity(tweet, asset.media);
const baseFilename = renderFilename(ctx.settings.media.filenameTemplate, {
handle: identity.handle,
tweetId: identity.tweetId,
index: asset.index,
total: tweet.media.length,
date: /* @__PURE__ */ new Date(),
ext: "mp4",
text: identity.text,
mediaId: asset.candidate.variant.url.match(/\/([A-Za-z0-9_-]{6,})\.(?:m3u8|mpd)(?:[?#]|$)/i)?.[1] ?? null
}).replace(/\.mp4$/i, ".%(ext)s");
return {
manifestUrl: asset.candidate.manifestUrl,
filename: baseFilename,
formatPolicy: YTDLP_FORMAT_POLICY
};
}
async function sendYtDlp(request, button3, ctx) {
setHelperFeedback(button3, ft(ctx, "Sending\u2026"), true, "running");
try {
const result = await handoffToYtDlp(ctx.settings.integrations.ytDlp, request);
if (result.state === "refused") {
setHelperFeedback(button3, ft(ctx, "Refused"), false, "refused");
showFeatureToast(result.error ?? ft(ctx, "The local yt-dlp helper refused this job."), { tone: "error", ctx });
return;
}
if (result.state === "failed" || !result.jobId) {
setHelperFeedback(button3, ft(ctx, "Failed"), false, "failed");
showFeatureToast(result.error ?? ft(ctx, "The local yt-dlp helper failed."), { tone: "error", ctx });
return;
}
const terminal = await waitForYtDlp(ctx.settings.integrations.ytDlp, result.jobId);
if (terminal.state === "completed") {
setHelperFeedback(button3, ft(ctx, "Saved"), false, "completed");
showFeatureToast(ft(ctx, "Adaptive video saved by yt-dlp."), { tone: "info", ctx });
} else if (terminal.state === "missing") {
setHelperFeedback(button3, ft(ctx, "Missing"), false, "missing");
showFeatureToast(ft(ctx, "The yt-dlp job is no longer available. Copy the command to retry."), { tone: "error", ctx });
} else if (terminal.state === "refused") {
setHelperFeedback(button3, ft(ctx, "Refused"), false, "refused");
showFeatureToast(terminal.error ?? ft(ctx, "The local yt-dlp helper refused this job."), { tone: "error", ctx });
} else {
setHelperFeedback(button3, ft(ctx, "Failed"), false, "failed");
showFeatureToast(terminal.error ?? ft(ctx, "The local yt-dlp helper failed."), { tone: "error", ctx });
}
} catch (error) {
setHelperFeedback(button3, ft(ctx, "Failed"), false, "failed");
ctx.diagnostics.warn("yt-dlp handoff failed", errorDetails2(error));
showFeatureToast(ft(ctx, "The local yt-dlp helper could not be reached."), { tone: "error", ctx });
}
}
async function waitForYtDlp(settings, jobId) {
const deadline = Date.now() + 12e4;
let current = await readYtDlpJob(settings, jobId);
while (current.state === "running" && Date.now() < deadline) {
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
current = await readYtDlpJob(settings, jobId);
}
return current;
}
async function copyYtDlpCommand(request, button3, ctx) {
try {
const clipboard = globalThis.navigator?.clipboard;
if (!clipboard?.writeText) throw new Error("Clipboard unavailable");
await clipboard.writeText(buildYtDlpCommand(request));
setHelperFeedback(button3, ft(ctx, "Copied"), false, "copied");
showFeatureToast(ft(ctx, "yt-dlp command copied."), { tone: "info", ctx });
setTimeout(() => {
if (button3.isConnected) setHelperFeedback(button3, ft(ctx, "Copy yt-dlp command"), false, "idle");
}, 2200);
} catch (error) {
ctx.diagnostics.warn("yt-dlp command copy failed", errorDetails2(error));
showFeatureToast(ft(ctx, "The yt-dlp command could not be copied."), { tone: "error", ctx });
}
}
function setHelperFeedback(button3, label, disabled, state2) {
button3.textContent = label;
button3.disabled = disabled;
button3.dataset.state = state2;
button3.setAttribute("aria-busy", String(disabled));
}
async function handlePendingPostDownload(article, ctx, button3) {
setButtonFeedback(button3, {
label: ft(ctx, "Downloading\u2026"),
icon: "\u21BB",
className: "is-active",
disabled: true,
busy: true
});
for (let attempt = 0; attempt < 8; attempt += 1) {
const refreshed = extractTweetForContext(article, ctx);
const assets = primaryDownloadAssets(refreshed);
const stillWaiting = refreshed.media.some((media) => media.kind === "video" && resolveTarget(media) === null) || article.querySelector(VIDEO_CONTAINER_SELECTOR) !== null && !refreshed.media.some((media) => media.kind === "video");
if (assets.length > 0 && !stillWaiting) {
delete button3.dataset.pendingVideo;
await handlePostDownload(refreshed, assets, /* @__PURE__ */ new Set(), ctx, button3);
return;
}
await new Promise((resolve) => {
setTimeout(resolve, 250);
});
}
setButtonFeedback(button3, {
label: ft(ctx, "Retry"),
icon: "\u21BB",
className: "is-error"
});
button3.setAttribute(
"aria-label",
ft(ctx, "The direct video is still loading. Try again in a moment.")
);
}
function positionButton(button3, container) {
const anchor = positionedAncestor(container);
const media = container.getBoundingClientRect();
const base = anchor?.getBoundingClientRect();
const siblings = Array.from(container.querySelectorAll(`[${BUTTON_ATTR}]`));
const slot = Math.max(0, siblings.indexOf(button3));
const topOffset = 8 + slot * 40;
if (!anchor || !base || media.width === 0 || media.height === 0) {
button3.style.top = `${topOffset}px`;
button3.style.right = "8px";
return;
}
button3.style.top = `${Math.round(media.top - base.top + topOffset)}px`;
button3.style.right = `${Math.round(base.right - media.right + 8)}px`;
}
function positionedAncestor(node) {
let current = node;
while (current && current !== document.body) {
if (getComputedStyle(current).position !== "static") {
return current;
}
current = current.parentElement;
}
return null;
}
function resolveContainer(media) {
if (media.kind === "video" && media.video) {
return media.video.container;
}
if (media.kind === "thumbnail" && (media.source.matches?.('[data-testid="videoPlayer"], [data-testid="videoComponent"]') ?? false)) {
return media.source;
}
if (media.kind === "audio" && media.audio) {
return media.audio.container;
}
if (media.kind === "subtitle" && media.subtitle) {
return media.subtitle.container;
}
return media.source.closest('[data-testid="tweetPhoto"]') ?? media.source.parentElement;
}
function hasOwnButton(container, kind) {
return container.querySelector(`[${BUTTON_ATTR}="${kind}"]`) !== null;
}
function buildButton(media, index, tweet, ctx) {
const button3 = document.createElement("button");
button3.type = "button";
button3.className = "av-media-button";
button3.setAttribute(BUTTON_ATTR, media.kind);
button3.dataset.kind = media.kind;
const accessibleLabel = ft(ctx, buttonAriaLabel(media));
const idleLabel = ft(ctx, buttonLabel(media));
button3.setAttribute("aria-label", accessibleLabel);
button3.setAttribute("aria-live", "polite");
button3.setAttribute("aria-busy", "false");
button3.dataset.idleLabel = idleLabel;
button3.dataset.idleIcon = "\u2193";
button3.dataset.baseIdleAriaLabel = accessibleLabel;
button3.dataset.idleAriaLabel = accessibleLabel;
button3.textContent = `\u2193 ${ft(ctx, buttonLabel(media))}`;
const icon = document.createElement("span");
icon.className = "av-media-button-icon";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "\u2193 ";
const label = document.createElement("span");
label.className = "av-media-button-label";
label.textContent = idleLabel;
button3.replaceChildren(icon, label);
const target = resolveTarget(media);
if (target) {
const receipt = downloadReceipt(media.kind, target);
setDownloadedMarker(button3, receipt !== null, ctx, receipt);
updateOriginalRetryState(button3, media, target, receipt, ctx);
}
button3.addEventListener("click", (event) => {
event.stopPropagation();
event.preventDefault();
void handleDownload(media, index, tweet, ctx, button3);
});
return button3;
}
function buttonLabel(media) {
if (media.kind === "thumbnail") {
return "Thumb";
}
if (media.kind === "video") {
return media.video?.isGif ? "GIF" : "Video";
}
if (media.kind === "audio") return "Audio";
if (media.kind === "subtitle") return "Captions";
return "Download";
}
function buttonAriaLabel(media) {
if (media.kind === "thumbnail") {
return "Download thumbnail";
}
if (media.kind === "video") {
return media.video?.isGif ? "Download GIF" : "Download video";
}
if (media.kind === "audio") return "Download audio";
if (media.kind === "subtitle") return "Download captions";
return "Download image";
}
async function handleDownload(media, index, tweet, ctx, button3) {
if (!downloader || !queue || !history) {
return;
}
const target = resolveTarget(media);
if (!target) {
setButtonFeedback(button3, {
label: ft(ctx, "Unavailable"),
icon: "!",
className: "is-error",
disabled: true
});
ctx.diagnostics.warn("Media target unavailable", { kind: media.kind });
return;
}
setButtonFeedback(button3, {
label: ft(ctx, "Downloading\u2026"),
icon: "\u21BB",
className: "is-active",
disabled: true,
busy: true
});
try {
const outcome = await performMediaDownload(
media,
index,
tweet,
target,
ctx,
() => {
setButtonFeedback(button3, {
label: ft(ctx, "Started"),
icon: "\u2193",
className: "is-active",
disabled: true,
busy: true
});
},
button3.dataset.retryOriginal === "true"
);
setButtonFeedback(button3, {
label: ft(
ctx,
outcome.degraded ? "Opened" : outcome.status === "aria2-duplicate" ? "Queued" : outcome.status === "started" ? "Started" : successLabel(media)
),
icon: outcome.degraded ? "\u2197" : outcome.status === "started" ? "\u2193" : "\u2713",
className: outcome.status === "completed" ? "is-success" : outcome.status === "opened" ? "is-opened" : "is-duplicate"
});
if (outcome.status === "started") {
button3.setAttribute(
"aria-label",
ft(ctx, "The browser is still transferring this file. Check your downloads for the result.")
);
}
if (outcome.matchKind) {
button3.setAttribute("aria-label", duplicateMatchTitle(outcome.matchKind, ctx));
}
if (outcome.status === "completed" || outcome.status === "history-duplicate") {
const receipt = downloadReceipt(media.kind, target);
setDownloadedMarker(button3, true, ctx, receipt);
updateOriginalRetryState(button3, media, target, receipt, ctx);
}
if (outcome.degraded) {
button3.setAttribute(
"aria-label",
ft(
ctx,
"Your browser opened this file instead of saving it. Grant Aviary the download permission for a real save."
)
);
}
scheduleButtonRestore(button3);
} catch (error) {
showDownloadError(button3, error, ctx);
}
}
async function handlePostDownload(tweet, assets, completed, ctx, button3) {
if (!downloader || !queue || !history) {
return;
}
setButtonFeedback(button3, {
label: ft(ctx, "Downloading\u2026"),
icon: "\u21BB",
className: "is-active",
disabled: true,
busy: true
});
const outcomes = [];
let firstError;
for (const asset of assets) {
const key = primaryAssetKey(asset);
if (completed.has(key)) {
continue;
}
try {
outcomes.push(
await performMediaDownload(asset.media, asset.index, tweet, asset.target, ctx, () => {
setButtonFeedback(button3, {
label: ft(ctx, "Started"),
icon: "\u2193",
className: "is-active",
disabled: true,
busy: true
});
})
);
completed.add(key);
} catch (error) {
firstError ??= error;
if (error instanceof DownloadPermissionError) {
break;
}
}
}
if (firstError) {
showDownloadError(button3, firstError, ctx);
return;
}
completed.clear();
const degraded = outcomes.some((outcome) => outcome.degraded);
const aria2Duplicate = outcomes.length > 0 && outcomes.every((outcome) => outcome.status === "aria2-duplicate");
const anyStarted = outcomes.some((outcome) => outcome.status === "started");
const allDuplicate = outcomes.length > 0 && outcomes.every((outcome) => outcome.status !== "completed");
setButtonFeedback(button3, {
label: ft(
ctx,
degraded ? "Opened" : aria2Duplicate ? "Queued" : anyStarted ? "Started" : "Saved"
),
icon: degraded ? "\u2197" : anyStarted ? "\u2193" : "\u2713",
className: allDuplicate ? "is-duplicate" : "is-success"
});
if (degraded) {
button3.setAttribute(
"aria-label",
ft(
ctx,
"Your browser opened this file instead of saving it. Grant Aviary the download permission for a real save."
)
);
}
if (outcomes.length > 0 && outcomes.every(
(outcome) => outcome.status === "completed" || outcome.status === "history-duplicate"
)) {
setDownloadedMarker(button3, true, ctx);
}
scheduleButtonRestore(button3);
}
function primaryAssetKey(asset) {
return `${asset.index}:${asset.media.kind}:${asset.target.mediaId ?? asset.target.url}`;
}
async function performMediaDownload(media, index, tweet, target, ctx, onStarted, allowQualityUpgrade = false) {
if (!downloader || !queue || !history) {
throw new Error("Media downloader is not ready.");
}
const identity = mediaIdentity(tweet, media);
const filename = renderFilename(ctx.settings.media.filenameTemplate, {
handle: identity.handle,
tweetId: identity.tweetId,
index,
total: tweet.media.length,
date: /* @__PURE__ */ new Date(),
ext: target.ext,
text: identity.text,
mediaId: target.mediaId
});
const sidecar = mediaSidecarRequest(ctx.settings.media.sidecarFormat, {
mediaFilename: filename,
kind: media.kind,
handle: identity.handle,
tweetId: identity.tweetId,
text: identity.text,
permalink: identity.tweetId ? `https://x.com/${identity.handle ?? "i"}/status/${identity.tweetId}` : null,
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
});
let fingerprint2 = {
identityHash: mediaIdentityHash(media.kind, target.url, target.mediaId)
};
let historyMatch = null;
let reservationToken2 = null;
if (ctx.settings.media.downloadHistory && !allowQualityUpgrade) {
historyMatch = history.findMatch(fingerprint2, false);
if (historyMatch) {
const reservation = await history.reserve(fingerprint2, false);
historyMatch = reservation.match;
reservationToken2 = reservation.token;
} else {
fingerprint2 = await fingerprintMediaDownload({
kind: media.kind,
url: target.url,
...target.fallbackUrls ? { fallbackUrls: target.fallbackUrls } : {},
mediaId: target.mediaId,
includePerceptual: ctx.settings.media.perceptualDedup
});
const reservation = await history.reserve(
fingerprint2,
ctx.settings.media.perceptualDedup
);
historyMatch = reservation.match;
reservationToken2 = reservation.token;
}
}
if (historyMatch) {
await history.noteMatch(historyMatch);
const duplicate = queue.enqueue({
url: target.url,
...target.fallbackUrls ? { fallbackUrls: target.fallbackUrls } : {},
quality: target.quality,
filename,
kind: media.kind,
mediaId: target.mediaId
});
queue.mark(duplicate.id, "duplicate");
ctx.diagnostics.info("Media skipped because its fingerprint is already in history", {
matchKind: historyMatch
});
void ctx.auditLog.record("media.download.duplicate", { matchKind: historyMatch });
return { status: "history-duplicate", degraded: false, matchKind: historyMatch };
}
const job = queue.enqueue({
url: target.url,
...target.fallbackUrls ? { fallbackUrls: target.fallbackUrls } : {},
quality: target.quality,
filename,
kind: media.kind,
mediaId: target.mediaId,
...sidecar ? { sidecar } : {}
});
queue.mark(job.id, "running");
const completeConfirmedSave = async (via, quality = target.quality) => {
queue.mark(job.id, "completed");
queue.setQuality(job.id, quality);
await rememberLastDownload(ctx.storage, {
url: target.url,
filename,
kind: media.kind
});
if (ctx.settings.media.downloadHistory) {
if (reservationToken2) {
await history.commit(reservationToken2, fingerprint2, quality);
reservationToken2 = null;
} else {
await history.record(fingerprint2, quality);
}
}
saveSidecarOrWarn(ctx, sidecar, filename);
ctx.diagnostics.info("Media saved", { filename, kind: media.kind });
void ctx.auditLog.record("media.download", { filename, kind: media.kind, via });
};
const failInterruptedSave = async () => {
if (reservationToken2) {
await history.release(reservationToken2);
reservationToken2 = null;
}
queue.mark(job.id, "failed", "the browser interrupted this transfer");
ctx.diagnostics.warn("Media transfer was interrupted", { filename, kind: media.kind });
void ctx.auditLog.record("media.download.failed", { filename, kind: media.kind });
};
try {
const result = await downloader({
url: target.url,
...target.fallbackUrls ? { fallbackUrls: target.fallbackUrls } : {},
quality: target.quality,
...target.fallbackQualities ? { fallbackQualities: target.fallbackQualities } : {},
filename
});
if (result.deduplicated) {
if (reservationToken2) await history.release(reservationToken2);
queue.mark(job.id, "duplicate");
ctx.diagnostics.info("Media skipped: already queued in Aria2 history", {
url: target.url
});
void ctx.auditLog.record("media.download.duplicate", {
source: "aria2-history"
});
return { status: "aria2-duplicate", degraded: false };
}
if (result.degraded) {
if (reservationToken2) {
await history.release(reservationToken2);
reservationToken2 = null;
}
queue.mark(job.id, "opened", "the browser opened this URL; a saved file was not confirmed");
ctx.diagnostics.info("Media opened without a confirmed save", {
filename,
kind: media.kind,
via: result.via
});
void ctx.auditLog.record("media.download.opened", {
filename,
kind: media.kind,
via: result.via
});
return { status: "opened", degraded: true };
}
if (result.pending && result.downloadId !== void 0) {
const downloadId = result.downloadId;
queue.trackDownload(job.id, downloadId);
onStarted?.();
const terminalResult = downloadWatcher.terminal(downloadId);
const terminal = await downloadWatcher.wait(downloadId);
if (terminal === "interrupted") {
await failInterruptedSave();
throw new Error("The browser interrupted this transfer before it finished.");
}
if (terminal === "pending") {
void terminalResult.then(async (eventual) => {
if (eventual === "complete") {
await completeConfirmedSave(result.via, downloadWatcher.receipt(downloadId) ?? target.quality);
} else if (eventual === "interrupted") {
await failInterruptedSave();
}
}).catch((error) => {
ctx.diagnostics.warn("Could not reconcile media transfer", errorDetails2(error));
});
ctx.diagnostics.info("Media transfer still running", { filename, kind: media.kind });
return { status: "started", degraded: false };
}
}
await completeConfirmedSave(result.via, result.quality ?? target.quality);
return { status: "completed", degraded: false };
} catch (error) {
if (reservationToken2) {
await history.release(reservationToken2);
reservationToken2 = null;
}
const needsPermission = error instanceof DownloadPermissionError;
queue.mark(job.id, "failed", String(error?.message ?? error));
ctx.diagnostics.error("Media download failed", errorDetails2(error));
void ctx.auditLog.record("media.download.failed", {
filename,
kind: media.kind,
...needsPermission ? { reason: "downloads-permission-missing" } : {}
});
throw error;
}
}
function duplicateMatchTitle(kind, ctx) {
if (kind === "exact") {
return ft(ctx, "Skipped because the downloaded bytes match an item in history.");
}
if (kind === "perceptual") {
return ft(ctx, "Skipped because the image looks like an item in history.");
}
return ft(ctx, "Skipped because this is the same X media asset at another size.");
}
function showDownloadError(button3, error, ctx) {
const needsPermission = error instanceof DownloadPermissionError;
setButtonFeedback(button3, {
label: ft(ctx, needsPermission ? "Allow" : "Retry"),
icon: needsPermission ? "\u2197" : "!",
className: "is-error"
});
if (needsPermission) {
button3.setAttribute(
"aria-label",
ft(ctx, "Aviary needs the browser download permission. Opening its options page.")
);
if (!permissionSurfaceOpened) {
permissionSurfaceOpened = true;
void requestDownloadPermissionSurface();
}
}
}
function setButtonFeedback(button3, feedback) {
clearButtonRestore(button3);
button3.classList.remove("is-active", "is-success", "is-opened", "is-duplicate", "is-error");
button3.classList.add(feedback.className);
button3.dataset.state = feedback.className.slice(3);
button3.disabled = feedback.disabled === true;
button3.setAttribute("aria-busy", String(feedback.busy === true));
const icon = button3.querySelector(
".av-media-button-icon, .av-media-action-icon"
);
const label = button3.querySelector(
".av-media-button-label, .av-media-action-label"
);
if (icon) icon.textContent = `${feedback.icon} `;
if (label) label.textContent = feedback.label;
button3.setAttribute("aria-label", feedback.label);
button3.removeAttribute("title");
}
function scheduleButtonRestore(button3) {
clearButtonRestore(button3);
const timer2 = setTimeout(() => {
buttonResetTimers.delete(button3);
if (button3.isConnected) {
restoreIdleButton(button3);
}
}, 2200);
buttonResetTimers.set(button3, timer2);
}
function clearButtonRestore(button3) {
const timer2 = buttonResetTimers.get(button3);
if (timer2 !== void 0) {
clearTimeout(timer2);
buttonResetTimers.delete(button3);
}
}
function restoreIdleButton(button3) {
button3.classList.remove("is-active", "is-success", "is-opened", "is-duplicate", "is-error");
delete button3.dataset.state;
button3.disabled = false;
button3.setAttribute("aria-busy", "false");
const icon = button3.querySelector(
".av-media-button-icon, .av-media-action-icon"
);
const label = button3.querySelector(
".av-media-button-label, .av-media-action-label"
);
if (icon) icon.textContent = `${button3.dataset.idleIcon ?? "\u2193"} `;
if (label) label.textContent = button3.dataset.idleLabel ?? "";
const accessibleLabel = button3.dataset.idleAriaLabel ?? button3.dataset.idleLabel ?? "";
button3.setAttribute("aria-label", accessibleLabel);
button3.removeAttribute("title");
}
function wasDownloaded(kind, target) {
return downloadReceipt(kind, target) !== null;
}
function updateOriginalRetryState(button3, media, target, receipt, ctx) {
if (media.kind === "photo" && target.quality.label === "original" && receipt?.label === "fallback") {
button3.dataset.retryOriginal = "true";
button3.dataset.idleLabel = ft(ctx, "Original");
button3.dataset.idleAriaLabel = ft(ctx, "Retry original image");
const label = button3.querySelector(".av-media-button-label, .av-media-action-label");
if (label && !button3.dataset.state) label.textContent = button3.dataset.idleLabel;
if (!button3.dataset.state) button3.setAttribute("aria-label", button3.dataset.idleAriaLabel);
return;
}
delete button3.dataset.retryOriginal;
}
function downloadReceipt(kind, target) {
return history?.findQuality({
identityHash: mediaIdentityHash(kind, target.url, target.mediaId)
}) ?? null;
}
function setDownloadedMarker(button3, downloaded, ctx, receipt = null) {
if (downloaded) {
button3.setAttribute(DOWNLOADED_ATTR, "true");
} else {
button3.removeAttribute(DOWNLOADED_ATTR);
}
const base = button3.dataset.baseIdleAriaLabel ?? button3.dataset.idleAriaLabel ?? "";
const qualityLabel = receipt ? qualityReceiptLabel(receipt, ctx) : null;
const accessibleLabel = downloaded ? `${base}. ${ft(ctx, "Previously downloaded")}${qualityLabel ? `. ${qualityLabel}` : ""}.` : base;
button3.dataset.idleAriaLabel = accessibleLabel;
if (!button3.dataset.state) {
button3.setAttribute("aria-label", accessibleLabel);
button3.removeAttribute("title");
}
}
function qualityReceiptLabel(receipt, ctx) {
if (receipt.label === "original") return ft(ctx, "Original quality");
if (receipt.label === "fallback") return ft(ctx, "Fallback quality. Click to retry original");
if (receipt.label === "best-direct") return ft(ctx, "Best direct quality");
return ft(ctx, "Quality unknown");
}
function saveSidecarOrWarn(ctx, request, mediaFilename) {
if (!request || saveMediaSidecar(request)) return;
ctx.diagnostics.warn("Media sidecar could not be saved", { mediaFilename });
}
function resolveTarget(media) {
if (media.kind === "video" && media.video?.preferred) {
const direct = media.video.variants.filter((variant2) => isSaveableVariantUrl(variant2.url, variant2.type)).sort((left, right) => compareVariantQuality(right, left))[0] ?? null;
const adaptive = bestObservedAdaptive(media.video.variants);
if (!direct && adaptive) {
return {
url: adaptive.manifestUrl,
quality: qualityForVariant(adaptive.variant, "adaptive-remux"),
mediaId: mediaIdFromVideo(adaptive.manifestUrl),
ext: "mp4",
helperOnly: true,
adaptive
};
}
if (!direct) return null;
const url = direct.url;
const variant = direct;
return {
url,
quality: qualityForVariant(variant, "best-direct"),
mediaId: mediaIdFromVideo(url),
ext: extensionForVideo(variant.type, url),
...adaptive && shouldOfferAdaptiveHelper(direct, media.video.variants) ? { adaptive } : {}
};
}
if (media.kind === "audio" && media.audio?.preferred) {
const variant = media.audio.preferred;
if (!isSaveableVariantUrl(variant.url, variant.type)) return null;
return {
url: variant.url,
quality: qualityForVariant(variant, "best-direct"),
mediaId: mediaIdFromVideo(variant.url),
ext: extensionForAudio(variant.type, variant.url)
};
}
if (media.kind === "subtitle" && media.subtitle?.track) {
const track = media.subtitle.track;
if (!isSaveableVariantUrl(track.url, track.type)) return null;
return {
url: track.url,
quality: qualityForMime(track.type, "quality-unknown"),
mediaId: mediaIdFromVideo(track.url),
ext: extensionForSubtitle(track.type, track.url)
};
}
if (media.image) {
const fallbackQualities = media.image.fallbackUrls.map((url) => qualityForImageUrl(url, "fallback"));
return {
url: media.image.url,
fallbackUrls: media.image.fallbackUrls,
quality: qualityForImageUrl(media.image.url, "original"),
...fallbackQualities.length > 0 ? { fallbackQualities } : {},
mediaId: media.image.mediaId,
ext: media.image.format
};
}
return null;
}
function qualityForVariant(variant, label) {
return normalizeDownloadQuality({
label,
width: variant.width,
height: variant.height,
bitrate: variant.bitrate,
mime: variant.type,
codec: variant.codec ?? null,
codecSource: variant.codecSource ?? null,
playbackProven: variant.playbackObserved === true
});
}
function qualityForMime(mime, label) {
return normalizeDownloadQuality({ label, mime });
}
function qualityForImageUrl(url, label) {
const size = /(?:^|[?&])name=(\d{2,5})x(\d{2,5})(?:&|$)/i.exec(url);
const format = /(?:^|[?&])format=([a-z0-9]+)/i.exec(url)?.[1]?.toLowerCase() ?? (/\.([a-z0-9]+)(?:\?|$)/i.exec(url)?.[1]?.toLowerCase() ?? "jpg");
const mime = format === "png" ? "image/png" : format === "webp" ? "image/webp" : "image/jpeg";
return normalizeDownloadQuality({
label,
width: size ? Number(size[1]) : null,
height: size ? Number(size[2]) : null,
mime
});
}
function mediaIdFromVideo(url) {
const match = /\/([A-Za-z0-9_-]{6,})\.(mp4|m4s|m3u8|webm|mov|m4a|mp3|ogg|opus|wav|vtt|srt|ttml|dfxp)(?:[?#]|$)/i.exec(url);
return match?.[1] ?? null;
}
function extensionForVideo(mime, url) {
if (/mp4/i.test(mime) || /\.mp4(?:[?#]|$)/i.test(url)) return "mp4";
if (/webm/i.test(mime) || /\.webm(?:[?#]|$)/i.test(url)) return "webm";
if (/m3u8/i.test(mime) || /\.m3u8(?:[?#]|$)/i.test(url)) return "m3u8";
if (/mov/i.test(mime) || /\.mov(?:[?#]|$)/i.test(url)) return "mov";
return "mp4";
}
function extensionForAudio(mime, url) {
if (/mpeg|mp3/i.test(mime) || /\.mp3(?:[?#]|$)/i.test(url)) return "mp3";
if (/ogg|opus/i.test(mime) || /\.(?:ogg|opus)(?:[?#]|$)/i.test(url)) return "ogg";
if (/wav/i.test(mime) || /\.wav(?:[?#]|$)/i.test(url)) return "wav";
return "m4a";
}
function extensionForSubtitle(mime, url) {
if (/srt/i.test(mime) || /\.srt(?:[?#]|$)/i.test(url)) return "srt";
if (/ttml|dfxp/i.test(mime) || /\.(?:ttml|dfxp)(?:[?#]|$)/i.test(url)) return "ttml";
return "vtt";
}
function successLabel(media) {
if (media.kind === "thumbnail") return "Got it";
if (media.kind === "video") return media.video?.isGif ? "GIF saved" : "Saved";
if (media.kind === "audio") return "Audio saved";
if (media.kind === "subtitle") return "Captions saved";
return "Saved";
}
function errorDetails2(error) {
if (error instanceof Error) {
return { name: error.name, message: error.message };
}
return { message: String(error) };
}
function ensureMediaStyle() {
if (document.getElementById(STYLE_ID4)) {
return;
}
const style = document.createElement("style");
style.id = STYLE_ID4;
style.textContent = MEDIA_CSS;
(document.head ?? document.documentElement).append(style);
}
var MEDIA_CSS = `
html:not(.av-media-buttons-enabled) [${BUTTON_ATTR}],
html:not(.av-media-buttons-enabled) [${ACTION_SLOT_ATTR}] {
display: none !important;
}
[${ACTION_SLOT_ATTR}] {
display: flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
gap: 5px;
min-width: 112px;
margin-inline-start: 4px;
}
[${HELPER_SLOT_ATTR}] {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 4px;
margin-inline-start: 4px;
}
[${HELPER_ACTION_ATTR}] {
appearance: none;
min-height: 40px;
padding: 6px 10px;
border: 1px solid color-mix(in srgb, var(--av-accent, rgb(29, 155, 240)) 48%, transparent);
border-radius: 999px;
background: transparent;
color: var(--av-accent, rgb(29, 155, 240));
cursor: pointer;
font: 700 12px/1.1 TwitterChirp, Inter, ui-sans-serif, system-ui, sans-serif;
white-space: nowrap;
transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
[${HELPER_ACTION_ATTR}]:hover:not(:disabled) {
border-color: var(--av-accent, rgb(29, 155, 240));
background: color-mix(in srgb, var(--av-accent, rgb(29, 155, 240)) 12%, transparent);
}
[${HELPER_ACTION_ATTR}]:focus-visible {
outline: 2px solid var(--av-accent, rgb(29, 155, 240));
outline-offset: 2px;
}
[${HELPER_ACTION_ATTR}][data-state="completed"],
[${HELPER_ACTION_ATTR}][data-state="copied"] {
border-color: var(--av-media-success, rgb(120, 200, 130));
color: var(--av-media-success-text, rgb(206, 240, 210));
}
[${HELPER_ACTION_ATTR}][data-state="failed"],
[${HELPER_ACTION_ATTR}][data-state="refused"],
[${HELPER_ACTION_ATTR}][data-state="missing"] {
border-color: var(--av-media-error, rgb(220, 110, 110));
color: var(--av-media-error-text, rgb(248, 200, 200));
}
[${HELPER_ACTION_ATTR}]:disabled {
cursor: progress;
opacity: 0.72;
}
[${ACTION_ATTR}] {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 108px;
min-height: 40px;
padding: 7px 12px;
border: 0;
border-radius: 8px;
background: var(--av-accent, rgb(29, 155, 240));
color: rgb(3, 20, 24);
cursor: pointer;
font: 750 13px/1.1 TwitterChirp, Inter, ui-sans-serif, system-ui, sans-serif;
white-space: nowrap;
transition: transform 140ms ease, background-color 140ms ease, color 140ms ease;
}
[${ACTION_ATTR}][${DOWNLOADED_ATTR}],
[${BUTTON_ATTR}][${DOWNLOADED_ATTR}] {
position: relative;
}
[${ACTION_ATTR}][${DOWNLOADED_ATTR}]::after,
[${BUTTON_ATTR}][${DOWNLOADED_ATTR}]::after {
content: "";
flex: 0 0 auto;
width: 7px;
height: 7px;
border-radius: 2px;
background: var(--av-media-success, rgb(120, 200, 130));
box-shadow: 0 0 0 2px color-mix(in srgb, var(--av-media-success, rgb(120, 200, 130)) 20%, transparent);
}
[${ACTION_ATTR}] .av-media-action-icon {
display: inline-grid;
place-items: center;
width: 16px;
height: 16px;
font-size: 17px;
line-height: 1;
}
[${ACTION_ATTR}]:hover:not(:disabled) {
background: color-mix(in srgb, var(--av-accent, rgb(29, 155, 240)) 86%, white);
transform: translateY(-1px);
}
[${ACTION_ATTR}]:focus-visible {
outline: 2px solid var(--av-accent, rgb(29, 155, 240));
outline-offset: 2px;
}
[${COPY_ACTION_ATTR}] {
min-width: 88px;
border: 1px solid color-mix(in srgb, var(--av-accent, rgb(29, 155, 240)) 48%, transparent);
background: transparent;
color: var(--av-accent, rgb(29, 155, 240));
}
[${COPY_ACTION_ATTR}]:hover:not(:disabled) {
border-color: var(--av-accent, rgb(29, 155, 240));
background: color-mix(in srgb, var(--av-accent, rgb(29, 155, 240)) 12%, transparent);
color: var(--av-accent, rgb(29, 155, 240));
}
[${ACTION_ATTR}].is-success {
color: rgb(3, 20, 24);
background: var(--av-media-success, rgb(120, 200, 130));
}
[${ACTION_ATTR}].is-opened,
[${ACTION_ATTR}].is-duplicate {
color: var(--av-text, rgb(239, 243, 244));
background: color-mix(in srgb, var(--av-muted, rgb(132, 139, 145)) 46%, transparent);
}
[${ACTION_ATTR}].is-error {
color: rgb(28, 8, 8);
background: var(--av-media-error, rgb(220, 110, 110));
}
[${ACTION_ATTR}].is-active {
cursor: progress;
}
[${ACTION_ATTR}].is-active .av-media-action-icon {
animation: av-media-spin 700ms linear infinite;
}
[${ACTION_ATTR}]:disabled {
cursor: wait;
opacity: 0.84;
transform: none;
}
[${BUTTON_ATTR}] {
position: absolute;
z-index: 12;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 72px;
min-height: 38px;
padding: 6px 11px;
border: 0;
border-radius: 8px;
background: color-mix(in srgb, var(--av-surface-raised, rgb(15, 20, 25)) 90%, black);
color: var(--av-text, rgb(239, 243, 244));
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.38);
cursor: pointer;
font: 700 12.5px/1.1 TwitterChirp, Inter, ui-sans-serif, system-ui, sans-serif;
opacity: 1;
transition: transform 140ms ease, border-color 140ms ease, background-color 140ms ease, color 140ms ease;
}
[${BUTTON_ATTR}] .av-media-button-icon {
display: inline-grid;
place-items: center;
width: 14px;
height: 14px;
font-size: 15px;
line-height: 1;
}
[${BUTTON_ATTR}] .av-media-button-label {
min-width: 0;
}
/* Aviary no longer makes X's media containers the positioning context.
Forcing position:relative onto [data-testid="tweetPhoto"] made a box X keeps at zero height
-- it is a flex container whose two children are both position:absolute inset:0, with the real
height carried by an ancestor -- into the containing block for those children. They collapsed
to zero height, so the photo vanished the moment the Save button was switched on and came back
the moment it was switched off. positionButton() measures against whatever ancestor X has
already positioned, which is the same box the photo itself resolves against. */
[${BUTTON_ATTR}]:hover {
background: color-mix(in srgb, var(--av-surface-raised, rgb(15, 20, 25)) 86%, var(--av-accent, rgb(29, 155, 240)));
transform: translateY(-1px);
}
[${BUTTON_ATTR}]:focus-visible {
outline: 2px solid var(--av-accent, rgb(29, 155, 240));
outline-offset: 2px;
}
[${BUTTON_ATTR}].is-success {
color: var(--av-media-success-text, rgb(206, 240, 210));
}
[${BUTTON_ATTR}].is-opened,
[${BUTTON_ATTR}].is-duplicate {
color: var(--av-muted, rgb(132, 139, 145));
}
[${BUTTON_ATTR}].is-error {
color: var(--av-media-error-text, rgb(248, 200, 200));
}
[${BUTTON_ATTR}].is-active {
cursor: progress;
}
[${BUTTON_ATTR}].is-active .av-media-button-icon {
animation: av-media-spin 700ms linear infinite;
}
[${BUTTON_ATTR}]:disabled {
transform: none;
}
[${BUTTON_ATTR}]:disabled:not(.is-active) {
cursor: default;
opacity: 0.68;
}
@keyframes av-media-spin {
to { transform: rotate(360deg); }
}
@media (pointer: coarse) {
[${ACTION_ATTR}] {
min-height: 44px;
}
}
@media (max-width: 520px) {
article[data-testid="tweet"] [role="group"]:has(> [${ACTION_SLOT_ATTR}]) {
flex-wrap: wrap;
row-gap: 8px;
}
[${ACTION_SLOT_ATTR}] {
flex: 1 0 100%;
width: 100%;
min-width: 0;
margin: 4px 0 0;
}
[${HELPER_SLOT_ATTR}] {
flex: 1 0 100%;
width: 100%;
margin: 4px 0 0;
}
[${HELPER_ACTION_ATTR}] {
flex: 1 1 0;
min-height: 46px;
}
[${ACTION_ATTR}] {
flex: 1 1 0;
width: auto;
min-width: 0;
min-height: 46px;
padding-inline: 14px;
}
[${ACTION_ATTR}] .av-media-action-label {
display: inline;
}
[${BUTTON_ATTR}] {
min-width: 44px;
width: 44px;
min-height: 44px;
padding: 8px;
color: var(--av-accent, rgb(29, 155, 240));
}
[${BUTTON_ATTR}] .av-media-button-label {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
[${BUTTON_ATTR}],
[${ACTION_ATTR}] {
transition: none;
}
[${BUTTON_ATTR}].is-active .av-media-button-icon,
[${ACTION_ATTR}].is-active .av-media-action-icon {
animation: none;
}
}
`;
// src/features/export/jobs.ts
var CHECKPOINT_KEY = "aviary.export.checkpoints.v1";
var RETENTION_KEYS = {
maxJobs: "aviary.retention.maxJobs",
maxRecordsPerJob: "aviary.retention.maxRecordsPerJob",
maxAgeDays: "aviary.retention.maxAgeDays"
};
var DEFAULT_RETENTION_POLICY = {
maxJobs: 0,
maxRecordsPerJob: 0,
maxAgeDays: 0
};
var EMPTY3 = { jobs: {}, records: {} };
var CheckpointStore = class {
#storage;
#state = EMPTY3;
#policy = DEFAULT_RETENTION_POLICY;
#loaded = false;
constructor(storage) {
this.#storage = storage;
}
async load() {
if (this.#loaded) return emptySweep(this.#policy, Object.keys(this.#state.jobs).length);
const raw = await this.#storage.get(CHECKPOINT_KEY, EMPTY3);
this.#state = {
jobs: normalizeJobs(raw?.jobs),
records: normalizeRecords(raw?.records)
};
this.#policy = await loadRetentionPolicy(this.#storage);
this.#loaded = true;
const interrupted = Object.entries(raw?.jobs ?? {}).filter(([, value]) => Boolean(value && typeof value === "object" && value.status === "running")).map(([jobId]) => ({
jobId,
set: {
status: "paused",
done: false,
resumeOnBoot: true,
error: "Interrupted before completion; resume when ready."
}
}));
if (interrupted.length > 0) {
await this.#persistMany(interrupted);
}
return this.sweep();
}
get retentionPolicy() {
return { ...this.#policy };
}
list() {
return Object.values(this.#state.jobs);
}
records(jobId) {
return this.#state.records[jobId] ?? [];
}
async start(jobId, surface, formats, preserveRawPayloads) {
await this.load();
this.#state.jobs[jobId] = {
jobId,
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
surface,
recordCount: 0,
done: false,
formats,
preserveRawPayloads,
status: "running",
progress: { completed: 0, total: null },
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
resumeOnBoot: false
};
this.#state.records[jobId] = [];
await this.#persist({ kind: "start", job: cloneJob2(this.#state.jobs[jobId]) });
await this.sweep();
}
async append(jobId, batch) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || isTerminal(job.status)) return;
const seen = new Set(this.#state.records[jobId]?.map((entry) => recordKey(entry)) ?? []);
const records = this.#state.records[jobId] ?? [];
for (const record of batch) {
const key = recordKey(record);
if (!seen.has(key)) {
seen.add(key);
records.push(record);
}
}
const retained = this.#policy.maxRecordsPerJob > 0 ? records.slice(-this.#policy.maxRecordsPerJob) : records;
this.#state.records[jobId] = retained;
job.recordCount = retained.length;
job.progress = { completed: retained.length, total: job.progress.total };
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await this.#persist({
kind: "append",
jobId,
records: batch.map(cloneRecord),
total: job.progress.total,
updatedAt: job.updatedAt
});
}
async finish(jobId) {
await this.load();
const job = this.#state.jobs[jobId];
if (job) {
job.done = true;
job.status = "completed";
job.progress = { completed: job.recordCount, total: job.progress.total ?? job.recordCount };
job.resumeOnBoot = false;
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
delete job.error;
await this.#persist({
kind: "update",
jobId,
set: {
done: true,
status: "completed",
progress: { ...job.progress },
resumeOnBoot: false,
updatedAt: job.updatedAt
},
remove: ["error"]
});
}
}
async pause(jobId) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || isTerminal(job.status)) return false;
job.status = "paused";
job.done = false;
job.resumeOnBoot = false;
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await this.#persist({
kind: "update",
jobId,
set: { status: "paused", done: false, resumeOnBoot: false, updatedAt: job.updatedAt }
});
return true;
}
async resume(jobId) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || job.status !== "paused" && job.status !== "queued") return false;
job.status = "running";
job.done = false;
job.resumeOnBoot = false;
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
delete job.error;
await this.#persist({
kind: "update",
jobId,
set: { status: "running", done: false, resumeOnBoot: false, updatedAt: job.updatedAt },
remove: ["error"]
});
return true;
}
async cancel(jobId) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || isTerminal(job.status)) return false;
job.status = "cancelled";
job.done = true;
job.resumeOnBoot = false;
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await this.#persist({
kind: "update",
jobId,
set: { status: "cancelled", done: true, resumeOnBoot: false, updatedAt: job.updatedAt }
});
return true;
}
async fail(jobId, error) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || isTerminal(job.status)) return false;
job.status = "failed";
job.done = true;
job.resumeOnBoot = false;
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
job.error = error instanceof Error ? error.message : String(error);
await this.#persist({
kind: "update",
jobId,
set: {
status: "failed",
done: true,
resumeOnBoot: false,
updatedAt: job.updatedAt,
error: job.error
}
});
return true;
}
async updateProgress(jobId, progress) {
await this.load();
const job = this.#state.jobs[jobId];
if (!job || isTerminal(job.status)) return false;
job.progress = {
completed: nonNegativeInteger(progress.completed, job.progress.completed),
total: progress.total === null ? null : nonNegativeInteger(progress.total, job.progress.total ?? 0)
};
job.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await this.#persist({
kind: "update",
jobId,
set: { progress: { ...job.progress }, updatedAt: job.updatedAt }
});
return true;
}
listResumable() {
return this.list().filter((job) => job.status === "paused" && job.resumeOnBoot).sort(compareJobs);
}
async remove(jobId) {
await this.load();
delete this.#state.jobs[jobId];
delete this.#state.records[jobId];
await this.#persist({ kind: "remove", jobId });
}
async sweep(policy) {
await this.load();
if (policy) {
this.#policy = normalizeRetentionPolicy(policy);
}
let result = emptySweep(this.#policy, Object.keys(this.#state.jobs).length);
await this.#persist({ kind: "sweep", policy: this.#policy }, (nextResult) => {
result = nextResult;
});
return result;
}
async #persist(change, onResult) {
try {
const next = await mutateStored(
this.#storage,
CHECKPOINT_KEY,
EMPTY3,
(stored) => {
const applied = applyCheckpointChange(stored, change, this.#policy);
onResult?.(applied.result);
return applied.state;
}
);
this.#state = normalizeCheckpointState(next);
} catch {
}
}
async #persistMany(updates) {
await this.#persist({ kind: "updates", updates });
}
};
function normalizeCheckpointState(value) {
if (!value || typeof value !== "object") return { jobs: {}, records: {} };
const raw = value;
return {
jobs: normalizeJobs(raw.jobs, false),
records: normalizeRecords(raw.records)
};
}
function cloneJob2(job) {
return {
...job,
formats: [...job.formats],
progress: { ...job.progress }
};
}
function cloneRecord(record) {
return { ...record };
}
function applyCheckpointChange(value, change, policy) {
const state2 = normalizeCheckpointState(value);
let result = emptySweep(policy, Object.keys(state2.jobs).length);
if (change.kind === "start") {
state2.jobs[change.job.jobId] = cloneJob2(change.job);
state2.records[change.job.jobId] = [];
} else if (change.kind === "append") {
const job = state2.jobs[change.jobId];
if (job && !isTerminal(job.status)) {
const records = state2.records[change.jobId] ?? [];
const seen = new Set(records.map((entry) => recordKey(entry)));
for (const record of change.records) {
const key = recordKey(record);
if (!seen.has(key)) {
seen.add(key);
records.push(cloneRecord(record));
}
}
const retained = policy.maxRecordsPerJob > 0 ? records.slice(-policy.maxRecordsPerJob) : records;
state2.records[change.jobId] = retained;
job.recordCount = retained.length;
job.progress = {
completed: retained.length,
total: job.progress.total ?? change.total
};
job.updatedAt = change.updatedAt;
}
} else if (change.kind === "update") {
applyCheckpointUpdate(state2, change.jobId, change.set, change.remove);
} else if (change.kind === "updates") {
for (const update of change.updates) {
applyCheckpointUpdate(state2, update.jobId, update.set, update.remove);
}
} else if (change.kind === "remove") {
delete state2.jobs[change.jobId];
delete state2.records[change.jobId];
} else {
const swept = sweepCheckpointState(state2, policy);
result = swept.result;
return { state: swept.state, result };
}
return { state: state2, result };
}
function applyCheckpointUpdate(state2, jobId, set, remove) {
const job = state2.jobs[jobId];
if (!job) return;
state2.jobs[jobId] = { ...job, ...set, progress: set.progress ? { ...set.progress } : { ...job.progress } };
for (const field2 of remove ?? []) delete state2.jobs[jobId][field2];
}
function sweepCheckpointState(input, policy) {
const state2 = normalizeCheckpointState(input);
const beforeJobs = Object.keys(state2.jobs).length;
const beforeRecords = Object.values(state2.records).reduce((total, records) => total + records.length, 0);
const removeIds = /* @__PURE__ */ new Set();
if (policy.maxAgeDays > 0) {
const cutoff = Date.now() - policy.maxAgeDays * 24 * 60 * 60 * 1e3;
for (const job of Object.values(state2.jobs)) {
const startedAt = Date.parse(job.startedAt);
if (Number.isFinite(startedAt) && startedAt < cutoff) removeIds.add(job.jobId);
}
}
if (policy.maxJobs > 0) {
const newest = Object.values(state2.jobs).filter((job) => !removeIds.has(job.jobId)).sort(compareJobs).slice(-policy.maxJobs).map((job) => job.jobId);
const keepIds = new Set(newest);
for (const job of Object.values(state2.jobs)) {
if (!removeIds.has(job.jobId) && !keepIds.has(job.jobId)) removeIds.add(job.jobId);
}
}
for (const jobId of removeIds) {
delete state2.jobs[jobId];
delete state2.records[jobId];
}
for (const job of Object.values(state2.jobs)) {
const records = state2.records[job.jobId] ?? [];
if (policy.maxRecordsPerJob > 0 && records.length > policy.maxRecordsPerJob) {
state2.records[job.jobId] = records.slice(-policy.maxRecordsPerJob);
}
job.recordCount = state2.records[job.jobId]?.length ?? 0;
}
const afterRecords = Object.values(state2.records).reduce((total, records) => total + records.length, 0);
return {
state: state2,
result: {
removedJobs: beforeJobs - Object.keys(state2.jobs).length,
removedRecords: beforeRecords - afterRecords,
retainedJobs: Object.keys(state2.jobs).length,
policy: { ...policy }
}
};
}
function normalizeRetentionPolicy(input) {
const record = input && typeof input === "object" ? input : {};
return {
maxJobs: retentionNumber(record.maxJobs, DEFAULT_RETENTION_POLICY.maxJobs, 1e4),
maxRecordsPerJob: retentionNumber(
record.maxRecordsPerJob,
DEFAULT_RETENTION_POLICY.maxRecordsPerJob,
1e5
),
maxAgeDays: retentionNumber(record.maxAgeDays, DEFAULT_RETENTION_POLICY.maxAgeDays, 3650)
};
}
async function loadRetentionPolicy(storage) {
const [maxJobs, maxRecordsPerJob, maxAgeDays] = await Promise.all([
storage.get(RETENTION_KEYS.maxJobs, DEFAULT_RETENTION_POLICY.maxJobs),
storage.get(RETENTION_KEYS.maxRecordsPerJob, DEFAULT_RETENTION_POLICY.maxRecordsPerJob),
storage.get(RETENTION_KEYS.maxAgeDays, DEFAULT_RETENTION_POLICY.maxAgeDays)
]);
return normalizeRetentionPolicy({ maxJobs, maxRecordsPerJob, maxAgeDays });
}
async function saveRetentionPolicy(storage, input) {
const policy = normalizeRetentionPolicy(input);
await Promise.all([
replaceStored(storage, RETENTION_KEYS.maxJobs, policy.maxJobs),
replaceStored(storage, RETENTION_KEYS.maxRecordsPerJob, policy.maxRecordsPerJob),
replaceStored(storage, RETENTION_KEYS.maxAgeDays, policy.maxAgeDays)
]);
return policy;
}
function retentionNumber(value, fallback, max) {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
return Math.max(0, Math.min(max, Math.trunc(value)));
}
function compareJobs(left, right) {
const leftAt = Date.parse(left.startedAt);
const rightAt = Date.parse(right.startedAt);
if (leftAt !== rightAt) return leftAt - rightAt;
return left.jobId.localeCompare(right.jobId);
}
function emptySweep(policy, retainedJobs) {
return { removedJobs: 0, removedRecords: 0, retainedJobs, policy: { ...policy } };
}
function recordKey(record) {
const identity = record.tweetId ? `tweet:${record.tweetId}` : `source:${record.permalink ?? ""}|${record.surface}|${record.handle ?? ""}`;
return `${identity}|${record.text.length}|${hashRecordText(record.text)}`;
}
function normalizeJobs(input, recoverRunning = true) {
if (!input || typeof input !== "object") return {};
const result = {};
for (const [jobId, value] of Object.entries(input)) {
const job = normalizeJob2(value, jobId, recoverRunning);
if (job) result[job.jobId] = job;
}
return result;
}
function normalizeRecords(input) {
if (!input || typeof input !== "object") return {};
const result = {};
for (const [jobId, records] of Object.entries(input)) {
if (Array.isArray(records)) result[jobId] = records;
}
return result;
}
function normalizeJob2(value, fallbackId, recoverRunning = true) {
if (!value || typeof value !== "object") return null;
const raw = value;
const jobId = typeof raw.jobId === "string" && raw.jobId.length > 0 ? raw.jobId : fallbackId;
const startedAt = typeof raw.startedAt === "string" ? raw.startedAt : (/* @__PURE__ */ new Date(0)).toISOString();
const recordCount = nonNegativeInteger(raw.recordCount, 0);
const persistedStatus = validStatus(raw.status) ? raw.status : raw.done === true ? "completed" : "paused";
const status = recoverRunning && persistedStatus === "running" ? "paused" : persistedStatus;
const progress = normalizeProgress(raw.progress, recordCount);
return {
jobId,
startedAt,
surface: typeof raw.surface === "string" ? raw.surface : "unknown",
recordCount,
done: status === "completed" || status === "cancelled" || status === "failed",
formats: Array.isArray(raw.formats) ? raw.formats.filter(isExportFormat) : ["json"],
preserveRawPayloads: raw.preserveRawPayloads === true,
status,
progress,
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : startedAt,
resumeOnBoot: status === "paused" && (persistedStatus === "running" || raw.resumeOnBoot !== false),
...typeof raw.error === "string" && raw.error.length > 0 ? { error: raw.error } : {}
};
}
function normalizeProgress(value, fallback) {
if (!value || typeof value !== "object") return { completed: fallback, total: null };
const raw = value;
return {
completed: nonNegativeInteger(raw.completed, fallback),
total: raw.total === null ? null : nonNegativeInteger(raw.total, fallback)
};
}
function validStatus(value) {
return value === "queued" || value === "running" || value === "paused" || value === "cancelled" || value === "failed" || value === "completed";
}
function isExportFormat(value) {
return value === "json" || value === "csv" || value === "html" || value === "markdown" || value === "xlsx";
}
function isTerminal(status) {
return status === "cancelled" || status === "failed" || status === "completed";
}
function nonNegativeInteger(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : fallback;
}
function hashRecordText(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619) >>> 0;
}
return hash.toString(36);
}
// src/features/export/text-safety.ts
function safeExternalHref(value) {
if (typeof value !== "string" || value.trim().length === 0) return "";
try {
const parsed = new URL(value, "https://x.com");
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : "";
} catch {
return "";
}
}
function safeRelativePath(value) {
if (!/^(?:[A-Za-z0-9._-]+\/)*[A-Za-z0-9._-]+$/.test(value)) return "";
const segments = value.split("/");
if (segments.some((segment) => segment === "." || segment === ".." || isReservedFileName(segment))) return "";
return value;
}
var RESERVED_FILE_NAMES = /^(?:CON|PRN|AUX|NUL|CLOCK\$|COM[1-9²³¹]|LPT[1-9²³¹])(?:\.|$)/i;
function isReservedFileName(value) {
return RESERVED_FILE_NAMES.test(value.trim());
}
function safeFileSlug(value) {
const slug = value.replace(/[^A-Za-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
if (slug.length === 0) return "post";
return isReservedFileName(slug) ? `${slug}-post` : slug;
}
function stripInvalidXmlChars(value) {
let output = "";
for (const char of value) {
const code = char.codePointAt(0) ?? 0;
const allowed = code === 9 || code === 10 || code === 13 || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
if (allowed) output += char;
}
return output;
}
function escapeXmlText(value) {
return stripInvalidXmlChars(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
function parseExportDate(raw) {
if (typeof raw !== "string" || raw.trim().length === 0) return null;
const value = raw.trim();
const offsetless = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)?$/.test(value);
const parsed = new Date(offsetless ? `${value.replace(" ", "T")}Z` : value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function compareRecordIds(left, right) {
const a = left ?? "";
const b = right ?? "";
const numericA = /^\d+$/.test(a);
const numericB = /^\d+$/.test(b);
if (numericA && numericB) {
const trimmedA = a.replace(/^0+(?=\d)/, "");
const trimmedB = b.replace(/^0+(?=\d)/, "");
if (trimmedA.length !== trimmedB.length) return trimmedA.length - trimmedB.length;
return trimmedA < trimmedB ? -1 : trimmedA > trimmedB ? 1 : compareCodeUnits(a, b);
}
if (numericA !== numericB) return numericA ? -1 : 1;
return compareCodeUnits(a, b);
}
function compareCodeUnits(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
// src/features/export/thread-reconstruction.ts
function reconstructThreads(records) {
const nodes = dedupeNodes(records);
if (nodes.length === 0) return [];
const union = new UnionFind(nodes.length);
const byTweetId = /* @__PURE__ */ new Map();
const contextBuckets = /* @__PURE__ */ new Map();
nodes.forEach((node, index) => {
if (node.tweetId) byTweetId.set(node.tweetId, index);
const context = node.conversationId ?? node.rootId ?? node.threadId;
if (context) {
const bucket = contextBuckets.get(context) ?? [];
bucket.push(index);
contextBuckets.set(context, bucket);
}
});
for (const bucket of contextBuckets.values()) {
for (let index = 1; index < bucket.length; index += 1) union.join(bucket[0], bucket[index]);
}
nodes.forEach((node, index) => {
const parent = node.parentId ? byTweetId.get(node.parentId) : void 0;
if (parent !== void 0) union.join(index, parent);
});
const components = /* @__PURE__ */ new Map();
nodes.forEach((node, index) => {
const root = union.find(index);
const component = components.get(root) ?? [];
component.push(node);
components.set(root, component);
});
return [...components.values()].sort(compareComponents).map((component) => buildThread(component, byNodeId(component)));
}
function reconstructExportOrder(records) {
const threads = reconstructThreads(records);
const ordered = [];
const seen = /* @__PURE__ */ new Set();
for (const thread of threads) {
for (const item of thread.items) {
if (item.kind !== "post") continue;
const key = threadRecordKey(item.record);
if (seen.has(key)) continue;
seen.add(key);
ordered.push(item.record);
}
}
for (const record of records) {
const key = threadRecordKey(record);
if (!seen.has(key)) {
seen.add(key);
ordered.push(record);
}
}
return ordered;
}
function threadRecordKey(record) {
if (record.tweetId) return `tweet:${record.tweetId}`;
return [
"record",
record.surface,
record.permalink ?? "",
record.capturedAt,
record.text.slice(0, 160)
].join("|");
}
function dedupeNodes(records) {
const nodes = [];
const byKey = /* @__PURE__ */ new Map();
records.forEach((record, index) => {
const key = threadRecordKey(record);
const previous = byKey.get(key);
if (previous !== void 0) {
nodes[previous] = mergeNode(nodes[previous], record);
return;
}
const node = {
record,
key,
index,
tweetId: cleanId2(record.tweetId),
conversationId: cleanId2(record.conversationId),
rootId: cleanId2(record.rootId),
parentId: cleanId2(record.parentId),
threadId: cleanId2(record.threadId),
authorId: cleanId2(record.authorId),
handle: cleanHandle(record.handle),
displayName: cleanText2(record.displayName),
timestamp: timestampOf(record.createdAt ?? record.capturedAt)
};
byKey.set(key, nodes.length);
nodes.push(node);
});
return nodes;
}
function mergeNode(existing, incoming) {
const merged = { ...existing.record };
const stringFields = [
"tweetId",
"handle",
"displayName",
"capturedAt",
"surface",
"permalink",
"conversationId",
"parentId",
"rootId",
"authorId",
"createdAt",
"threadId",
"birdwatch",
"language"
];
for (const field2 of stringFields) {
const value = incoming[field2];
if (typeof value !== "string" || value.trim().length === 0) continue;
if (field2 === "capturedAt" && existing.record.capturedAt) continue;
if (field2 === "surface" && existing.record.surface) continue;
merged[field2] = value;
}
if (incoming.text.trim().length > existing.record.text.trim().length) merged.text = incoming.text;
if (incoming.media.length > existing.record.media.length) merged.media = incoming.media;
const incomingParticipants = incoming.participants;
if (incomingParticipants && incomingParticipants.length > (existing.record.participants?.length ?? 0)) {
merged.participants = incomingParticipants;
}
const incomingExpandedUrls = incoming.expandedUrls;
if (incomingExpandedUrls && incomingExpandedUrls.length > (existing.record.expandedUrls?.length ?? 0)) {
merged.expandedUrls = incomingExpandedUrls;
}
if (incoming.poll && !existing.record.poll) merged.poll = incoming.poll;
if (incoming.quote && !existing.record.quote) merged.quote = incoming.quote;
if (incoming.article && !existing.record.article) merged.article = incoming.article;
return {
...existing,
record: merged,
conversationId: cleanId2(merged.conversationId) ?? existing.conversationId,
rootId: cleanId2(merged.rootId) ?? existing.rootId,
parentId: cleanId2(merged.parentId) ?? existing.parentId,
threadId: cleanId2(merged.threadId) ?? existing.threadId,
authorId: cleanId2(merged.authorId) ?? existing.authorId,
handle: cleanHandle(merged.handle) ?? existing.handle,
displayName: cleanText2(merged.displayName) ?? existing.displayName,
timestamp: timestampOf(merged.createdAt ?? merged.capturedAt) || existing.timestamp
};
}
function byNodeId(component) {
const result = /* @__PURE__ */ new Map();
for (const node of component) {
if (node.tweetId) result.set(node.tweetId, node);
}
return result;
}
function buildThread(component, byTweetId) {
const orderedNodes = [...component].sort(compareNodes);
const rootId = firstNonEmpty(orderedNodes.map((node) => node.rootId ?? node.conversationId));
const id = rootId ?? orderedNodes[0]?.tweetId ?? `context:${orderedNodes[0]?.handle ?? orderedNodes[0]?.key}`;
const children = /* @__PURE__ */ new Map();
for (const node of orderedNodes) {
if (!node.parentId || !byTweetId.has(node.parentId)) continue;
const list = children.get(node.parentId) ?? [];
list.push(node);
children.set(node.parentId, list);
}
for (const list of children.values()) list.sort(compareNodes);
const items = [];
const visited = /* @__PURE__ */ new Set();
const emittedGaps = /* @__PURE__ */ new Set();
const firstNode = orderedNodes.find((node) => node.tweetId === rootId);
if (rootId && !firstNode && !orderedNodes.some((node) => node.parentId === rootId)) {
emitGap(items, emittedGaps, rootId, null, 0);
}
const emit2 = (node, depth) => {
const key = node.key;
if (visited.has(key)) return;
if (node.parentId && !byTweetId.has(node.parentId)) {
emitGap(items, emittedGaps, node.parentId, null, Math.max(0, depth - 1));
}
visited.add(key);
items.push({
kind: "post",
record: node.record,
depth,
parentId: node.parentId,
differentAuthor: false
});
for (const child of children.get(node.tweetId ?? "") ?? []) emit2(child, depth + 1);
};
const roots = orderedNodes.filter((node) => !node.parentId || !byTweetId.has(node.parentId));
for (const node of roots) emit2(node, node.parentId && !byTweetId.has(node.parentId) ? 1 : 0);
for (const node of orderedNodes) {
if (!visited.has(node.key)) emit2(node, node.parentId ? 1 : 0);
}
const firstAuthor = items.find((item) => item.kind === "post");
const firstIdentity = firstAuthor ? authorIdentity(firstAuthor.record) : "";
for (const item of items) {
if (item.kind !== "post") continue;
item.differentAuthor = firstIdentity !== "" && authorIdentity(item.record) !== firstIdentity;
}
const records = items.filter((item) => item.kind === "post").map((item) => item.record);
const participants = participantList(records);
const authorRuns = collapseAuthorRuns(items);
return {
id,
rootId,
kind: participants.length > 1 ? "conversation" : "thread",
items,
records,
gaps: items.filter((item) => item.kind === "gap"),
participants,
authorRuns
};
}
function collapseAuthorRuns(items) {
const runs = [];
let current = null;
items.forEach((item, index) => {
if (item.kind === "gap") {
current = null;
return;
}
const identity = authorIdentity(item.record);
if (!current || authorIdentityFromRun(current) !== identity) {
current = {
authorId: cleanId2(item.record.authorId),
handle: cleanHandle(item.record.handle),
displayName: cleanText2(item.record.displayName),
start: index,
count: 0,
postIds: []
};
runs.push(current);
}
current.count += 1;
current.postIds.push(item.record.tweetId ?? threadRecordKey(item.record));
});
return runs;
}
function authorIdentityFromRun(run) {
return run.authorId ?? run.handle ?? run.displayName ?? "unknown";
}
function participantList(records) {
const seen = /* @__PURE__ */ new Set();
const result = [];
for (const record of records) {
const participant = {
authorId: cleanId2(record.authorId),
handle: cleanHandle(record.handle),
displayName: cleanText2(record.displayName)
};
const key = authorIdentity(record);
if (seen.has(key)) continue;
seen.add(key);
result.push(participant);
}
return result;
}
function emitGap(items, emitted, missingId, parentId, depth) {
if (emitted.has(missingId)) return;
emitted.add(missingId);
items.push({ kind: "gap", missingId, parentId, depth });
}
function compareComponents(left, right) {
return compareNodes(left[0], right[0]);
}
function compareNodes(left, right) {
return left.timestamp - right.timestamp || left.index - right.index || left.key.localeCompare(right.key);
}
function authorIdentity(record) {
return cleanId2(record.authorId) ?? cleanHandle(record.handle) ?? cleanText2(record.displayName) ?? "unknown";
}
function cleanId2(value) {
if (typeof value !== "string") return null;
const cleaned = value.trim();
return cleaned.length > 0 ? cleaned.slice(0, 128) : null;
}
function cleanHandle(value) {
if (typeof value !== "string") return null;
const cleaned = value.replace(/^@/, "").trim().toLowerCase();
return cleaned.length > 0 ? cleaned.slice(0, 64) : null;
}
function cleanText2(value) {
if (typeof value !== "string") return null;
const cleaned = value.trim();
return cleaned.length > 0 ? cleaned.slice(0, 256) : null;
}
function firstNonEmpty(values) {
return values.find((value) => Boolean(value)) ?? null;
}
function timestampOf(value) {
if (!value) return 0;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : 0;
}
var UnionFind = class {
#parents;
#ranks;
constructor(size) {
this.#parents = Array.from({ length: size }, (_, index) => index);
this.#ranks = Array.from({ length: size }, () => 0);
}
find(value) {
let current = value;
while (this.#parents[current] !== current) current = this.#parents[current];
while (this.#parents[value] !== value) {
const next = this.#parents[value];
this.#parents[value] = current;
value = next;
}
return current;
}
join(left, right) {
const leftRoot = this.find(left);
const rightRoot = this.find(right);
if (leftRoot === rightRoot) return;
if (this.#ranks[leftRoot] < this.#ranks[rightRoot]) {
this.#parents[leftRoot] = rightRoot;
} else if (this.#ranks[leftRoot] > this.#ranks[rightRoot]) {
this.#parents[rightRoot] = leftRoot;
} else {
this.#parents[rightRoot] = leftRoot;
this.#ranks[leftRoot] += 1;
}
}
};
// src/features/export/zip-store.ts
var CRC32_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
})();
function crc32(data) {
let c = 4294967295;
for (let i = 0; i < data.length; i++) {
c = (CRC32_TABLE[(c ^ data[i]) & 255] ^ c >>> 8) >>> 0;
}
return (c ^ 4294967295) >>> 0;
}
var FLAG_UTF8_NAMES = 2048;
var MAX_UINT16 = 65535;
var MAX_UINT32 = 4294967295;
var METHOD_STORE = 0;
var METHOD_DEFLATE = 8;
async function deflateRaw(data) {
const Compression = globalThis.CompressionStream;
if (typeof Compression !== "function") {
return null;
}
try {
const stream = new Blob([data]).stream().pipeThrough(new Compression("deflate-raw"));
const chunks = [];
let total = 0;
const reader = stream.getReader();
for (; ; ) {
const next = await reader.read();
if (next.done) break;
const chunk = next.value;
chunks.push(chunk);
total += chunk.length;
}
const output = new Uint8Array(total);
let at = 0;
for (const chunk of chunks) {
output.set(chunk, at);
at += chunk.length;
}
return output;
} catch {
return null;
}
}
async function buildZip(entries) {
const compressed = [];
const methods = [];
for (const entry of entries) {
const deflated = entry.data.length > 0 ? await deflateRaw(entry.data) : null;
if (deflated && deflated.length < entry.data.length) {
compressed.push({ ...entry, data: deflated });
methods.push(METHOD_DEFLATE);
} else {
compressed.push(entry);
methods.push(METHOD_STORE);
}
}
return writeZip(compressed, methods, entries.map((entry) => entry.data));
}
function buildStoreZip(entries) {
return writeZip(entries, entries.map(() => METHOD_STORE), entries.map((entry) => entry.data));
}
function writeZip(entries, methods, originals) {
const encoder = new TextEncoder();
const localBlocks = [];
const centralBlocks = [];
let offset = 0;
if (entries.length > MAX_UINT16) {
throw new RangeError(
`A zip holds at most ${MAX_UINT16} entries without ZIP64; got ${entries.length}.`
);
}
for (const [index, entry] of entries.entries()) {
const method = methods[index] ?? METHOD_STORE;
const original = originals[index] ?? entry.data;
if (original.length > MAX_UINT32) {
throw new RangeError(
`"${entry.filename}" is ${entry.data.length} bytes; a zip entry cannot exceed ${MAX_UINT32} without ZIP64.`
);
}
const nameBytes = encoder.encode(entry.filename);
if (nameBytes.length > MAX_UINT16) {
throw new RangeError(`"${entry.filename}" has a name longer than ${MAX_UINT16} bytes.`);
}
const crc = crc32(original);
const size = original.length;
const stored = entry.data.length;
const date = entry.date ?? /* @__PURE__ */ new Date();
const dosDate = toDosDate(date);
const dosTime = toDosTime(date);
const localHeader = new ArrayBuffer(30 + nameBytes.length);
const lhView = new DataView(localHeader);
lhView.setUint32(0, 67324752, true);
lhView.setUint16(4, 20, true);
lhView.setUint16(6, FLAG_UTF8_NAMES, true);
lhView.setUint16(8, method, true);
lhView.setUint16(10, dosTime, true);
lhView.setUint16(12, dosDate, true);
lhView.setUint32(14, crc, true);
lhView.setUint32(18, stored, true);
lhView.setUint32(22, size, true);
lhView.setUint16(26, nameBytes.length, true);
lhView.setUint16(28, 0, true);
const localHeaderBytes = new Uint8Array(localHeader);
localHeaderBytes.set(nameBytes, 30);
localBlocks.push(localHeaderBytes);
localBlocks.push(entry.data);
const centralHeader = new ArrayBuffer(46 + nameBytes.length);
const chView = new DataView(centralHeader);
chView.setUint32(0, 33639248, true);
chView.setUint16(4, 20, true);
chView.setUint16(6, 20, true);
chView.setUint16(8, FLAG_UTF8_NAMES, true);
chView.setUint16(10, method, true);
chView.setUint16(12, dosTime, true);
chView.setUint16(14, dosDate, true);
chView.setUint32(16, crc, true);
chView.setUint32(20, stored, true);
chView.setUint32(24, size, true);
chView.setUint16(28, nameBytes.length, true);
chView.setUint16(30, 0, true);
chView.setUint16(32, 0, true);
chView.setUint16(34, 0, true);
chView.setUint16(36, 0, true);
chView.setUint32(38, 0, true);
chView.setUint32(42, offset, true);
const centralBytes = new Uint8Array(centralHeader);
centralBytes.set(nameBytes, 46);
centralBlocks.push(centralBytes);
offset += localHeaderBytes.length + entry.data.length;
}
const centralStart = offset;
let centralSize = 0;
for (const block of centralBlocks) {
centralSize += block.length;
}
if (centralStart > MAX_UINT32 || centralSize > MAX_UINT32) {
throw new RangeError(
`The archive is too large for a non-ZIP64 zip (central directory at ${centralStart}, size ${centralSize}).`
);
}
const endRecord = new Uint8Array(22);
const erView = new DataView(endRecord.buffer);
erView.setUint32(0, 101010256, true);
erView.setUint16(4, 0, true);
erView.setUint16(6, 0, true);
erView.setUint16(8, entries.length, true);
erView.setUint16(10, entries.length, true);
erView.setUint32(12, centralSize, true);
erView.setUint32(16, centralStart, true);
erView.setUint16(20, 0, true);
const total = offset + centralSize + endRecord.length;
const output = new Uint8Array(total);
let cursor = 0;
for (const block of localBlocks) {
output.set(block, cursor);
cursor += block.length;
}
for (const block of centralBlocks) {
output.set(block, cursor);
cursor += block.length;
}
output.set(endRecord, cursor);
return output;
}
function toDosDate(date) {
const year = Math.max(date.getUTCFullYear() - 1980, 0);
return (year & 127) << 9 | (date.getUTCMonth() + 1 & 15) << 5 | date.getUTCDate() & 31;
}
function toDosTime(date) {
return (date.getUTCHours() & 31) << 11 | (date.getUTCMinutes() & 63) << 5 | Math.floor(date.getUTCSeconds() / 2) & 31;
}
// src/features/export/xlsx.ts
var ENCODER = new TextEncoder();
function formatXlsx(records) {
const sheetRows = [
[
"tweetId",
"handle",
"displayName",
"capturedAt",
"surface",
"permalink",
"language",
"text",
"mediaUrls",
"mediaStatus",
"mediaManifest"
]
];
for (const record of records) {
const captures = record.media.map((media) => describeMediaCapture(media, record.capturedAt));
sheetRows.push([
record.tweetId ?? "",
record.handle ?? "",
record.displayName ?? "",
record.capturedAt,
record.surface,
record.permalink ?? "",
normalizePostLanguage(record.language) ?? "",
record.text,
captures.map((media) => media.sourceUrl).filter(Boolean).join("|"),
captures.map((media) => media.status).join("|"),
JSON.stringify(captures)
]);
}
const sheetXml = buildSheetXml(sheetRows);
const workbookXml = `
`;
const workbookRelsXml = `
`;
const rootRelsXml = `
`;
const contentTypesXml = `
`;
const archive = buildStoreZip([
{ filename: "[Content_Types].xml", data: ENCODER.encode(contentTypesXml) },
{ filename: "_rels/.rels", data: ENCODER.encode(rootRelsXml) },
{ filename: "xl/_rels/workbook.xml.rels", data: ENCODER.encode(workbookRelsXml) },
{ filename: "xl/workbook.xml", data: ENCODER.encode(workbookXml) },
{ filename: "xl/worksheets/sheet1.xml", data: ENCODER.encode(sheetXml) }
]);
return {
filename: "tweets.xlsx",
contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
data: archive
};
}
function buildSheetXml(rows) {
const xmlRows = [];
for (let r = 0; r < rows.length; r++) {
const row = rows[r];
const cells = [];
for (let c = 0; c < row.length; c++) {
const value = row[c] ?? "";
const cellRef = `${columnLetter(c)}${r + 1}`;
cells.push(`${escapeXml(value)}`);
}
xmlRows.push(`${cells.join("")}
`);
}
return `
${xmlRows.join("")}`;
}
function columnLetter(index) {
let label = "";
let n = index;
while (n >= 0) {
label = String.fromCharCode(n % 26 + 65) + label;
n = Math.floor(n / 26) - 1;
}
return label;
}
function escapeXml(value) {
return stripInvalidXmlChars(value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
}
// src/features/export/formatters.ts
var TEXT_ENCODER = new TextEncoder();
function formatExport(format, records) {
switch (format) {
case "json":
return jsonArtifact(records);
case "csv":
return csvArtifact(records);
case "html":
return htmlArtifact(records);
case "markdown":
return markdownArtifact(records);
case "xlsx":
return formatXlsx(records);
default:
return jsonArtifact(records);
}
}
function jsonArtifact(records) {
const json = JSON.stringify(
{
generator: "Aviary",
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
count: records.length,
records: serializeExportRecords(records),
threads: reconstructThreads(records).map((thread) => ({
id: thread.id,
rootId: thread.rootId,
kind: thread.kind,
postIds: thread.records.map((record) => record.tweetId ?? null),
gaps: thread.gaps.map((gap) => ({ missingId: gap.missingId, parentId: gap.parentId })),
authorRuns: thread.authorRuns
}))
},
null,
2
);
return {
filename: "tweets.json",
contentType: "application/json",
data: TEXT_ENCODER.encode(json)
};
}
function csvArtifact(records) {
const headers = [
"tweetId",
"handle",
"displayName",
"capturedAt",
"surface",
"permalink",
"language",
"audience",
"text",
"mediaUrls",
"mediaStatus",
"mediaManifest"
];
const lines = [headers.join(",")];
for (const record of records) {
const captures = record.media.map((media) => describeMediaCapture(media, record.capturedAt));
const mediaUrls = captures.map((media) => media.sourceUrl).filter(Boolean).join("|");
const mediaStatus = captures.map((media) => media.status).join("|");
lines.push(
[
record.tweetId ?? "",
record.handle ?? "",
record.displayName ?? "",
record.capturedAt,
record.surface,
record.permalink ?? "",
normalizePostLanguage(record.language) ?? "",
normalizeAudience(record.audience),
record.text,
mediaUrls,
mediaStatus,
JSON.stringify(captures)
].map(csvCell2).join(",")
);
}
return {
filename: "tweets.csv",
contentType: "text/csv",
data: TEXT_ENCODER.encode(`${lines.join("\n")}
`)
};
}
function neutralizeFormula(value) {
return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
}
function csvCell2(value) {
if (value === void 0 || value === null) {
return "";
}
const safe = neutralizeFormula(value);
const needsQuotes = /[,"\r\n]/.test(safe);
const escaped = safe.replace(/"/g, '""');
return needsQuotes ? `"${escaped}"` : escaped;
}
function htmlArtifact(records) {
const rows = records.map((record) => {
const media = record.media.map((entry) => {
const capture = describeMediaCapture(entry, record.capturedAt);
const href = capture.packagePath ? safeRelativeHref(capture.packagePath) : safeExternalHref(capture.sourceUrl);
const label = `${entry.kind} \xB7 ${capture.status}`;
const link = href ? `${escapeHtml(label)}` : escapeHtml(label);
const details = [
capture.capturedAt ? `captured ${capture.capturedAt}` : "capture time unknown",
capture.byteLength === null ? "bytes unknown" : `${capture.byteLength} bytes`,
capture.sha256 ? `sha256 ${capture.sha256}` : "checksum unknown"
].join(" \xB7 ");
const source = capture.sourceUrl && capture.status !== "captured-bytes" ? ` source: ${escapeHtml(capture.sourceUrl)}` : "";
return `${link} ${escapeHtml(details)}${source}`;
}).join("");
const permalinkHref = record.permalink ? safeExternalHref(record.permalink) : "";
const permalink2 = permalinkHref ? `${escapeHtml(permalinkHref)}` : "";
return `
${escapeHtml(record.displayName ?? record.handle ?? "Unknown")}
@${escapeHtml(record.handle ?? "")}
${escapeHtml(record.text).replace(/\n/g, "
")}
`;
}).join("\n");
const html = `
Aviary export
Aviary export
${records.length} records, generated ${(/* @__PURE__ */ new Date()).toISOString()}.
Media status is explicit: captured bytes use package-relative links; remote references are not fetched until a link is activated.
${rows}
`;
return {
filename: "tweets.html",
contentType: "text/html",
data: TEXT_ENCODER.encode(html)
};
}
function markdownArtifact(records) {
const sections = records.map((record) => {
const header = `## ${record.displayName ?? record.handle ?? "Unknown"} (@${record.handle ?? "anon"}) \xB7 ${record.capturedAt}`;
const language = normalizePostLanguage(record.language);
const frontmatter = `---
language: ${language ?? "null"}
---`;
const body = record.text.split("\n").map((line) => `> ${line}`).join("\n");
const media = record.media.length === 0 ? "" : `
${record.media.map((entry) => {
const capture = describeMediaCapture(entry, record.capturedAt);
const target = capture.packagePath ?? capture.sourceUrl;
const label = `${entry.kind} \xB7 ${capture.status}`;
const link = target ? `[${label}](${escapeMarkdownUrl(target)})` : label;
const details = `captured ${capture.capturedAt ?? "unknown"}; bytes ${capture.byteLength ?? "unknown"}; sha256 ${capture.sha256 ?? "unknown"}`;
return `- ${link} (${details})`;
}).join("\n")}`;
const permalink2 = record.permalink ? `
${record.permalink}` : "";
return `${frontmatter}
${header}
Audience: ${normalizeAudience(record.audience)}
${body}${media}${permalink2}`;
});
const md = `# Aviary export
Generated ${(/* @__PURE__ */ new Date()).toISOString()} \xB7 ${records.length} records. Media links marked remote-reference are not fetched automatically.
${sections.join("\n\n---\n\n")}
`;
return {
filename: "tweets.md",
contentType: "text/markdown",
data: TEXT_ENCODER.encode(md)
};
}
function escapeHtml(value) {
return value.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'");
}
function safeRelativeHref(value) {
return safeRelativePath(value);
}
function escapeMarkdownUrl(value) {
return value.replace(/\\/g, "%5C").replace(/\)/g, "%29").replace(/\s/g, "%20");
}
// src/features/export/query-discovery.ts
var QUERY_REGISTRY_KEY = "aviary.queryIds.v1";
var QUERY_REGEX = /\/i\/api\/graphql\/([A-Za-z0-9_-]{6,})\/([A-Za-z0-9_]{2,80})/g;
var MAX_INLINE_SCRIPT_BYTES = 256e3;
async function discoverQueryIds(storage) {
const fallback = { queries: {}, observedAt: null };
const existing = await storage.get(QUERY_REGISTRY_KEY, fallback);
const queries = { ...existing.queries };
if (typeof document !== "undefined") {
const scripts = Array.from(document.querySelectorAll("script"));
for (const script of scripts) {
const src = script.getAttribute("src");
if (src) {
mergeFromString(queries, src);
} else if (script.textContent && script.textContent.length <= MAX_INLINE_SCRIPT_BYTES) {
mergeFromString(queries, script.textContent);
}
}
}
const result = {
queries,
observedAt: (/* @__PURE__ */ new Date()).toISOString()
};
try {
await replaceStored(storage, QUERY_REGISTRY_KEY, result);
} catch {
}
return result;
}
function mergeFromString(target, source) {
QUERY_REGEX.lastIndex = 0;
let match;
while ((match = QUERY_REGEX.exec(source)) !== null) {
const [, id, operation] = match;
if (id && operation) {
target[operation] = id;
}
}
}
// src/features/export/viewer.ts
var VIEWER_COPY = {
title: "Aviary archive",
subtitle: "Local archive viewer. Remote references are never fetched automatically.",
language: "Language",
search: "Search records",
status: "Media status",
all: "All media",
captured: "Captured bytes",
remote: "Remote reference",
missing: "Missing",
sort: "Sort",
newest: "Newest first",
oldest: "Oldest first",
handle: "Handle",
thread: "Thread view",
media: "Media",
records: "records",
shown: "shown",
noResults: "No matching records.",
source: "source",
bytes: "bytes",
checksum: "SHA-256",
openSource: "Open source URL",
threadPosts: "posts",
offline: "offline-ready",
network: "network may be required",
gap: "Missing captured post",
gapDetail: "This parent was not captured locally.",
conversation: "Conversation",
authorRun: "{count} posts by {author}"
};
function buildViewerLabels() {
const table = {};
for (const locale of supportedLocales()) {
const entry = { name: locale.label };
for (const [key, english] of Object.entries(VIEWER_COPY)) {
entry[key] = translateText(locale.code, english);
}
table[locale.code] = entry;
}
return table;
}
var LOCALE_ORDER = supportedLocales().map((locale) => locale.code);
var RTL_CODES = supportedLocales().filter((locale) => locale.direction === "rtl").map((locale) => locale.code);
function buildExportViewer(records, options = {}) {
const visibleRecords = filterShareRecords(records, normalizeAudienceSelection(options.audience));
const data = safeJson(serializeExportRecords(visibleRecords));
const threads = safeJson(serializeThreads(reconstructThreads(visibleRecords)));
const labels = safeJson(buildViewerLabels());
const script = viewerScript(labels, LOCALE_ORDER, RTL_CODES);
const html = `
Aviary archive