import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { isValidChannelSlug, readChannelConfig, readChannelSnapshot, readChannelStat, } from "yt-dlp-transcript-common/controller/channels"; import { inspectChannelMedia } from "yt-dlp-transcript-common/lib/channelMedia"; import { overridesOf, rankOf, tierOf, } from "yt-dlp-transcript-common/lib/channelPriority"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { opsAuth } from "../../_lib"; export const dynamic = "force-dynamic"; // GET /api/ops/channel/ // // THE READ SIDE, so a caller never opens transcripts/ itself. Everything a // script needs to decide what to do next about one channel: its stored config, // its report totals and bucket SIZES (not the id lists — a bucket can hold tens // of thousands of ids and a caller deciding "is there work" only needs the // count; /api/ops/retry-bucket resolves the ids server-side anyway), its // priority tier, and — the one that cannot be inferred from the filesystem — // whether its media is reachable. // // `media.status` is `inspectChannelMedia`'s, which is the ONE module that can // tell an unmounted drive from an empty channel. Every enumerator else swallows // ENOENT on data/ as "no videos", so a script reading counts alone would read an // unmounted platter as a channel that has downloaded nothing. // // `?counts=1` ADDS THE LIVE ON-DISK COUNTS, and it is opt-in because // readChannelStat walks every video directory — eleven thousand readdirs on the // largest channel here. The report's totals answer the same question from a // file, and a poll loop asking for them every few seconds must not be the thing // that hammers the platter. export async function GET( request: Request, { params }: { params: Promise<{ slug: string }> }, ) { const denied = opsAuth(request); if (denied) return denied; const { slug } = await params; // The same shape check every POST route applies through reqSlug (_lib.ts); // spelled out here only because the slug arrives as a route param, not in a // body. readChannelConfig swallows its own errors, so a traversing segment // would fail silently rather than loudly. if (!isValidChannelSlug(slug)) { return NextResponse.json( { ok: false, error: `"${slug}" is not a valid channel slug` }, { status: 400 }, ); } const paths = getPaths(); const config = await readChannelConfig(paths, slug); if (!config) { return NextResponse.json( { ok: false, error: `Channel "${slug}" not found` }, { status: 404 }, ); } const priority = getSettings().channelPriority; const wantCounts = new URL(request.url).searchParams.get("counts") === "1"; const [snapshot, stat, media] = await Promise.all([ readChannelSnapshot(paths, slug), wantCounts ? readChannelStat(paths, slug) : Promise.resolve(null), inspectChannelMedia(paths, slug, config), ]); const buckets = snapshot ? Object.fromEntries( Object.entries(snapshot.buckets).map(([k, v]) => [ k, Array.isArray(v) ? v.length : v, ]), ) : null; return NextResponse.json({ ok: true, slug, config, priority: { tier: tierOf(priority, slug), overrides: overridesOf(priority, slug), rank: rankOf(priority, slug), }, // null unless ?counts=1 — see the header. counts: stat, media, report: snapshot ? { generatedAt: snapshot.generatedAt, totals: snapshot.totals, buckets, } : null, }); }