"use server"; // THE ACTIONS BEHIND /review: the two detectors a human runs, and the two // decisions a human records about what they found. Both pairs are flag-only — // nothing here deletes anything — which is exactly why they belong to a review // page rather than to a lane. import { revalidatePath } from "next/cache"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { runManagedFunction } from "yt-dlp-transcript-common/jobs/streamCommand"; import { drainStream } from "yt-dlp-transcript-common/jobs/drainStream"; import { detectDuplicateShorts, readDuplicateReport, updateDuplicateOverride, } from "yt-dlp-transcript-common/controller/duplicateShorts"; import { scanCorruptMedia, updateMediaScanOverride, } from "yt-dlp-transcript-common/controller/scanCorruptMedia"; import { mediaScanTotals } from "yt-dlp-transcript-common/lib/mediaScan"; import { shareClusterFromCanonical } from "yt-dlp-transcript-common/controller/digestSharing"; export type DuplicateScope = "shorts" | "all"; export type RunDuplicateDetectionResult = | { ok: true; clusters: number; videosInClusters: number } | { ok: false; error: string }; // Runs the global cross-platform duplicate-shorts pass. `scope: "all"` removes // the duration cutoff (a heavier one-off run that also surfaces clip-of-longer // containment). Reads the cues + statsByPath written by build:index/build:stats, // so a build must have run first for meaningful results. export async function runDuplicateDetectionAction( scope: DuplicateScope = "shorts", ): Promise { const paths = getPaths(); let clusters = 0; let videosInClusters = 0; const result = await runManagedFunction({ kind: "detect-duplicates", // Empty queueKey: a local read-only scan, no reason to wait behind // sync/download work (see refreshAllChannelSnapshotsAction). queueKey: "", paths, fn: async (onLog, signal) => { const report = await detectDuplicateShorts({ paths, thresholdSeconds: scope === "all" ? null : undefined, onLog, signal, }); clusters = report.totals.clusters; videosInClusters = report.totals.videosInClusters; }, }); if (!result.ok) return { ok: false, error: result.error }; await drainStream(result.stream); revalidatePath("/review"); return { ok: true, clusters, videosInClusters }; } // What a human can say about a cluster the detector could not decide. // confirmed — "I looked; these really are the same video." Unblocks // sharing AND publication for a title+duration suspect. // not-duplicate — "They are not." Suppresses the cluster entirely. // clear — undo, back to awaiting review. export type DuplicateClusterDecision = "confirmed" | "not-duplicate" | "clear"; export type ReviewDuplicateClusterResult = | { ok: true; shared: number; misaligned: number } | { ok: false; error: string }; // Record a review decision for one cluster. // // `updateDuplicateOverride` has existed — with the `confirmed` flag, the // read-modify-write, the atomic rename and the "an empty patch clears the // decision" rule — since duplicate review was built, and until now **nothing in // the repo called it**. `clusterMaySharePartial` fails closed, so every // needsReview cluster shared nothing and there was no way for a human to change // that. This is that missing caller. // // Confirming also attempts the share immediately, via the equally-uncalled // `shareClusterFromCanonical`: the point of confirming is to let derived work // flow, and making the operator wait for the canonical member's next sweep to // find out whether it would have would make the button feel inert. It is a // no-op when the canonical has no digest yet, which today is almost always. export async function reviewDuplicateClusterAction( clusterId: string, decision: DuplicateClusterDecision, ): Promise { const paths = getPaths(); try { const overrides = await updateDuplicateOverride( paths, clusterId, decision === "confirmed" ? { confirmed: true, notDuplicate: false } : decision === "not-duplicate" ? { notDuplicate: true, confirmed: false } : { confirmed: false, notDuplicate: false }, ); let shared = 0; let misaligned = 0; if (decision === "confirmed") { const report = await readDuplicateReport(paths); const cluster = report?.clusters.find((c) => c.clusterId === clusterId); if (cluster) { for (const outcome of await shareClusterFromCanonical(paths, cluster, { overrides, })) { if (outcome.status === "shared") shared++; else if (outcome.status === "misaligned") misaligned++; } } } revalidatePath("/review"); return { ok: true, shared, misaligned }; } catch (e) { return { ok: false, error: (e as Error)?.message ?? String(e) }; } } // --------------------------------------------------------------------------- // Corrupt-media scan // --------------------------------------------------------------------------- export type RunMediaScanResult = | { ok: true; findings: number; filesScanned: number } | { ok: false; error: string }; // Scan media already on disk. Reports; deletes nothing — deletion stays the // separate, explicit per-file click that already exists on the video page. // // `deepProbe` turns on the tier-2 full decode for files whose duration looks // wrong. Off by default because that tier is a real transcode per file: it has // to be budgeted like the digest sweep, not like a stat() walk. export async function runMediaScanAction( opts: { channels?: string[]; deepProbe?: boolean } = {}, ): Promise { const paths = getPaths(); let findings = 0; let filesScanned = 0; const perChannel = (opts.channels?.length ?? 0) > 0; const result = await runManagedFunction({ kind: perChannel ? "scan-media-channel" : "scan-media", // Empty queueKey: local read-only disk work, no reason to wait behind // sync/download work (see runDuplicateDetectionAction). queueKey: "", paths, fn: async (onLog, signal) => { const report = await scanCorruptMedia({ paths, channels: opts.channels, deepProbe: opts.deepProbe, onLog, signal, }); findings = report.findings.length; filesScanned = mediaScanTotals(report).filesScanned; }, }); if (!result.ok) return { ok: false, error: result.error }; await drainStream(result.stream); revalidatePath("/review"); return { ok: true, findings, filesScanned }; } export type ReviewMediaFindingResult = | { ok: true } | { ok: false; error: string }; // "Looked at it; it is fine." Recorded in a sibling overrides file, because the // report is regenerated wholesale by every scan and a decision written into it // would be destroyed by the next run. export async function reviewMediaFindingAction( key: string, reviewed: boolean, ): Promise { try { await updateMediaScanOverride(getPaths(), key, { reviewed }); revalidatePath("/review"); return { ok: true }; } catch (e) { return { ok: false, error: (e as Error).message }; } }