// THE METADATA REFRESH AS A JOB — what the video page's "Refresh metadata" and // `/api/ops/refresh-metadata` enqueue. The work is refreshVideoMetadata.ts; its // own module, as feedMetadataJob.ts is, so the work can be tested (and run by // anything else) without loading the job registry. // // THE REFUSALS COME BEFORE ANY JOB, in the sentences the rest of the app // already uses for them, so a click and an ops call get the same answer: // // - a channel whose text cannot be read (`assertChannelTextReadable`, the // guard every `needsText` kind gets — asked first here so its sentence // wins over "not a video of this channel", which is what an unreadable // `data/` would otherwise look like); // - an id with no data// — a refresh re-reads a video already // archived and never creates its directory (resolveRefreshTarget); // - a HELD platform (`heldPlatformRefusal`), then one in a rate-limit // cooldown — both `info: true`, as the metadata scan answers them: nothing // is wrong, the source is resting. // // ON THE PLATFORM'S DOWNLOAD QUEUE, like the metadata scan and every per-video // download: it contends for the source's patience, and the queue serialises it // with a download of the same video on that queue rewriting the same file. // `needsText` (jobs/jobKinds.ts): it writes one text file and opens no media, // so a stalled media drive does not hold it. Replayable — a Retry re-resolves // the target and re-asks the cooldown. import { detectPlatform } from "../lib/platform"; import { downloadQueueKey, resolveQueueKey } from "../lib/queueKeys"; import { assertChannelTextReadable } from "../lib/channelMedia"; import type { ChannelConfig } from "../lib/channelConfig"; import type { ResolvedCookiePolicy } from "../lib/cookiePolicy"; import type { Paths } from "../lib/paths"; import { heldPlatformRefusal, platformCooldownRemainingMs, recordDownloadBackoff, recordPlatformClean, } from "../jobs/downloadBackoff"; import { runManagedFunction, type StreamActionResult, } from "../jobs/streamCommand"; import { refreshVideoMetadata, resolveRefreshTarget, } from "./refreshVideoMetadata"; export const REFRESH_METADATA_JOB_KIND = "refresh-metadata"; export type RefreshMetadataJobOpts = { paths: Paths; slug: string; videoId: string; channelConfig: ChannelConfig; cookiePolicy: ResolvedCookiePolicy; // Per-run override of the platform queue key. queueKey?: string; requestedBy?: string; // Called after a successful refresh, inside the job. The editor revalidates // the video and channel pages here. afterRun?: () => void | Promise; }; export async function runRefreshMetadataJob( opts: RefreshMetadataJobOpts, ): Promise { const { paths, slug, videoId, channelConfig } = opts; try { await assertChannelTextReadable(paths, slug, channelConfig); } catch (err) { return { ok: false, error: (err as Error).message }; } const target = await resolveRefreshTarget(paths, slug, videoId, channelConfig); if (!target.ok) return { ok: false, error: target.error }; // The VIDEO's platform, as the clip fetch keys it: a record whose URL is on // another host than its channel's (a mirror, a Wayback copy) answers to // that host's cooldown. const platform = detectPlatform(target.url) ?? "unknown"; const held = await heldPlatformRefusal(platform, "The metadata refresh", paths); if (held) return { ok: false, info: true, error: held }; const remainingMs = await platformCooldownRemainingMs(platform, paths); if (remainingMs > 0) { const secs = Math.ceil(remainingMs / 1000); return { ok: false, info: true, error: `${platform} is in a rate-limit cooldown (${secs}s remaining). ` + `Refresh the metadata once the cooldown lapses.`, }; } return runManagedFunction({ kind: REFRESH_METADATA_JOB_KIND, queueKey: resolveQueueKey(downloadQueueKey(channelConfig), opts.queueKey), paths, channelSlug: slug, videoId, spec: { kind: REFRESH_METADATA_JOB_KIND, slug, params: { videoId, queueKey: opts.queueKey }, }, fn: async (onLog, signal) => { await refreshVideoMetadata({ paths, slug, videoId, videoUrl: target.url, channelConfig, cookiePolicy: opts.cookiePolicy, onLog, signal, ...(opts.requestedBy ? { requestedBy: opts.requestedBy } : {}), onPlatformBackoff: () => recordDownloadBackoff(platform, paths), onPlatformClean: () => recordPlatformClean(platform, paths), }); await opts.afterRun?.(); }, }); }