"use server"; import { safeRevalidate } from "../../lib/safeRevalidate"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { platformQueueKey, queueKeyForUrl, } from "yt-dlp-transcript-common/lib/platform"; import { resolveQueueKey } from "yt-dlp-transcript-common/lib/queueKeys"; import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels"; import { CHECK_AVAILABILITY_MODES, runAvailabilityCheck, type CheckAvailabilityMode, } from "yt-dlp-transcript-common/controller/checkAvailability"; import { loadMaybeMissing, runQuickAvailabilityCheck, } from "yt-dlp-transcript-common/controller/quickAvailabilityCheck"; import { runManagedFunction, type StreamActionResult, } from "yt-dlp-transcript-common/jobs/streamCommand"; function sanitizeConcurrency(c: number | undefined): number | undefined { if (c === undefined) return undefined; if (!Number.isFinite(c) || c < 1) return undefined; return Math.floor(c); } export async function checkAvailabilityAction( slug: string, mode: CheckAvailabilityMode, queueKey?: string, concurrency?: number, shardTotal?: number, shardIndex?: number, ): Promise { if (!(CHECK_AVAILABILITY_MODES as string[]).includes(mode)) { return { ok: false, error: `Unsupported mode: ${mode}` }; } const paths = getPaths(); const config = await readChannelConfig(paths, slug); // Default to the platform queue so availability checks share the per-source // rate-limit budget with downloads instead of fighting them. const defaultQueue = config?.platform ? platformQueueKey(config.platform) : queueKeyForUrl(config?.url); const limit = sanitizeConcurrency(concurrency); return runManagedFunction({ kind: "check-availability", queueKey: resolveQueueKey(defaultQueue, queueKey), paths, channelSlug: slug, fn: async (onLog, signal) => { await runAvailabilityCheck({ channelSlug: slug, paths, mode, concurrency: limit, shardTotal, shardIndex, onLog, signal, }); safeRevalidate([`/channels/${slug}`]); }, }); } // Cheap first pass: one yt-dlp spawn fetches the channel's fresh flat playlist // and diffs it against the on-disk videos, persisting the maybe-missing set. export async function quickAvailabilityCheckAction( slug: string, queueKey?: string, ): Promise { const paths = getPaths(); const config = await readChannelConfig(paths, slug); const defaultQueue = config?.platform ? platformQueueKey(config.platform) : queueKeyForUrl(config?.url); return runManagedFunction({ kind: "quick-availability-check", queueKey: resolveQueueKey(defaultQueue, queueKey), paths, channelSlug: slug, fn: async (onLog, signal) => { await runQuickAvailabilityCheck({ channelSlug: slug, paths, onLog, signal, }); safeRevalidate([`/channels/${slug}`]); }, }); } // Targeted full check: run the per-video --dump-json pass on only the // maybe-missing ids, skipping any already known permanently gone. Resolves // deleted vs. unlisted without re-probing the whole channel. export async function checkMaybeMissingAction( slug: string, queueKey?: string, concurrency?: number, ): Promise { const paths = getPaths(); const config = await readChannelConfig(paths, slug); const defaultQueue = config?.platform ? platformQueueKey(config.platform) : queueKeyForUrl(config?.url); const limit = sanitizeConcurrency(concurrency); return runManagedFunction({ kind: "check-maybe-missing", queueKey: resolveQueueKey(defaultQueue, queueKey), paths, channelSlug: slug, fn: async (onLog, signal) => { const record = await loadMaybeMissing(paths, slug); const ids = record?.ids ?? []; if (ids.length === 0) { onLog("No maybe-missing videos. Run a quick check first.\n"); return; } // recheck-all (not resume) so videos with a stale `public` // availability.json are re-probed; skipExpectedAbsent avoids wasted // probes of videos already known deleted/private/members-only. await runAvailabilityCheck({ channelSlug: slug, paths, mode: "recheck-all", onlyIds: ids, skipExpectedAbsent: true, concurrency: limit, onLog, signal, }); safeRevalidate([`/channels/${slug}`]); }, }); }