Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit fc175654d01b10966ba45fa5933e39f5b6c3b742
parent 7f91177c75088089fdf8556b05ca9af26d5a5c89
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 10 Oct 2026 00:42:46 -0400

channels: release 21 D2 — a clip window of a saved video is cut from its container, not fetched

fetchWindowAction asks the saved-video store after the clip-window cache and
the in-flight jobs, before the network: a container that covers the window is
cut locally (evidenceClip-server's evidenceCutArgs) into data/<id>/clips/ with
the fetched sidecar shape plus `source: "saved-video"`, and answered
`cached: true`. No pointer, or a container that ends before the window, goes
to the network as before; a pointer whose container cannot be read (missing,
an unmounted or unanswering store drive, a store mid-move, unreadable by
ffprobe) is refused with the media guard's (or the store guard's) sentence.

The fetch-windows batch puts covered windows in their own group, platform
`saved-video` on `clips:saved-video` (no platform's hold, cooldown or gap);
the controller cuts them with no request and no pause, and fails an
unreadable one as `unreachable`. The video page marks a cut window. The MCP
plan says a locally held video is cut without a fetch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/controller/fetchWindows.test.ts | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/controller/fetchWindows.ts | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcommon/lib/clipWindow.ts | 10++++++++++
Mcommon/lib/queueKeys.ts | 8++++++++
Acommon/lib/savedVideoWindow-server.test.ts | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon/lib/savedVideoWindow-server.ts | 208+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/api/media/fetch-window/route.saved.test.ts | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/channels/[slug]/videos/[id]/components/cards/FetchedWindows.tsx | 1+
Meditor/app/channels/[slug]/videos/[id]/videoActions.ts | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/channels/[slug]/videos/fetchWindowsAction.test.ts | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/channels/[slug]/videos/fetchWindowsAction.ts | 44+++++++++++++++++++++++++++++++++++++++++++-
Mmcp/src/instructions.ts | 3++-
12 files changed, 982 insertions(+), 4 deletions(-)

diff --git a/common/controller/fetchWindows.test.ts b/common/controller/fetchWindows.test.ts @@ -274,3 +274,79 @@ test("a second run on the same platform waits out the gap the first one set", as await go(await corpus(), [item("v3", "/ok"), item("v4", "/ok")], second); assert.deepEqual(second.rec.sleeps, [1000, 1000], "the first fetch waits for the earlier run's gap"); }); + +// THE SAVED CONTAINER (release 21 D2): a window it holds is cut, not fetched; +// a pointer it cannot be read for fails the item, never a fetch in its place; +// a container that ends too early is fetched as before. +test("saved containers: a covered window is cut with no request and no pause; unreadable fails; not-covering is fetched", async () => { + const paths = await corpus(); + const cuts: { id: string; from: number; to: number; clipId?: string }[] = []; + const h = harness({ + savedWindowSource: async ({ videoDir }) => { + const id = path.basename(videoDir); + if (id === "held") { + return { + kind: "covers", + pointer: { storedAt: "", dir: "/store", file: "source-media.mp4", bytes: 1 }, + container: "/store/source-media.mp4", + durationSec: 600, + probe: { durationSec: 600, width: 160, height: 90, hasVideo: true, hasAudio: true }, + }; + } + if (id === "unplugged") { + return { kind: "unreadable", error: `Channel "c": media is not reachable — its saved video is not there` }; + } + if (id === "short") return { kind: "not-covering", container: "/store/x.mp4", durationSec: 5 }; + return { kind: "none" }; + }, + cutSavedWindow: async ({ videoDir, from, to, provenance }) => { + cuts.push({ id: path.basename(videoDir), from, to, clipId: provenance.clipId }); + return { file: "x.mp4", path: "/x.mp4", from, to, bytes: 1, provenance: { requestedBy: "test", source: "saved-video" } }; + }, + }); + const before = await spawns(); + const r = await go( + paths, + [ + { ...item("held", "/a"), clipId: "r#1" }, + item("unplugged", "/b"), + item("short", "/c"), + { ...item("held", "/d", 30, 40), clipId: "r#2" }, + ], + h, + ); + assert.deepEqual(cuts, [ + { id: "held", from: 10, to: 20, clipId: "r#1" }, + { id: "held", from: 30, to: 40, clipId: "r#2" }, + ]); + assert.deepEqual(r.cut.map((i) => i.id), ["held", "held"]); + assert.equal(r.failed.length, 1); + assert.equal(r.failed[0].class, "unreachable"); + assert.equal(r.failed[0].item.id, "unplugged"); + assert.match(r.failed[0].message, /media is not reachable/); + assert.deepEqual(r.fetched.map((i) => i.id), ["short"]); + assert.equal((await spawns()) - before, 1, "only the not-covering window touched the network"); + assert.deepEqual(h.rec.sleeps, [], "a cut owes no pause, and one fetch owes none either"); + assert.deepEqual(h.rec.progress.at(-1), { metric: "clips", initial: 0, target: 4, current: 4 }); +}); + +test("saved containers: a cut that fails is a cut-failed item and the run carries on", async () => { + const h = harness({ + savedWindowSource: async () => ({ + kind: "covers", + pointer: { storedAt: "", dir: "/store", file: "source-media.mp4", bytes: 1 }, + container: "/store/source-media.mp4", + durationSec: 600, + probe: { durationSec: 600, width: 160, height: 90, hasVideo: true, hasAudio: true }, + }), + cutSavedWindow: async ({ from }) => { + if (from === 10) throw new Error("ffmpeg failed cutting 10.00–20.00"); + return { file: "x.mp4", path: "/x.mp4", from, to: from + 10, bytes: 1, provenance: null }; + }, + }); + const r = await go(await corpus(), [item("v1", "/a"), item("v2", "/b", 30, 40)], h); + assert.equal(r.failed.length, 1); + assert.equal(r.failed[0].class, "cut-failed"); + assert.deepEqual(r.cut.map((i) => i.id), ["v2"]); + assert.equal(r.stopped, undefined); +}); diff --git a/common/controller/fetchWindows.ts b/common/controller/fetchWindows.ts @@ -8,6 +8,10 @@ import { resolveCookiePolicy } from "../lib/cookiePolicy"; import { assertChannelTextReadable } from "../lib/channelMedia"; import { detectPlatform } from "../lib/platform"; import { findContainingClipWindow } from "../lib/clipWindow-server"; +import { + cutSavedVideoWindow, + savedWindowSource, +} from "../lib/savedVideoWindow-server"; import type { JobProgress } from "../jobs/registry"; import { downloadGapMs } from "../jobs/platformBackoff"; import { notePlatformGap, platformGapRemainingMs } from "../jobs/platformGap"; @@ -49,6 +53,10 @@ import { findVideoSourceUrl } from "./undownloadedVideos"; // - A CACHED WINDOW costs nothing: no request, no pause. The cache is asked // here first (the same containing-window rule fetchWindowManaged applies), // so a re-run of a half-done list walks straight to what is missing. +// - A WINDOW HELD BY A SAVED CONTAINER (release 21 D2) is CUT from it, with +// no request and no pause either (lib/savedVideoWindow-server.ts). A +// pointer whose container cannot be read fails the item as `unreachable` +// — never a fetch in its place; one that ends too early is fetched. // - BETWEEN TWO NETWORK FETCHES the batch downloads' own gap // (downloadGapMs), with a floor of CLIP_WINDOW_MIN_GAP_SECONDS — jittered // up to half again, so a platform is never asked on a fixed beat. @@ -107,8 +115,11 @@ export type FetchWindowsFailureClass = | DownloadFailureClass // No channel config for the slug. | "unknown-channel" - // The channel's text (and so its clips/) is not readable. + // The channel's text (and so its clips/) is not readable, or the video's + // saved container is not. | "unreachable" + // ffmpeg could not cut the window out of a covering saved container. + | "cut-failed" // No page URL to fetch from. | "no-url"; @@ -129,6 +140,8 @@ export type FetchWindowsStop = export type FetchWindowsResult = { fetched: FetchWindowsItem[]; cached: FetchWindowsItem[]; + // Cut from the video's saved container, not fetched (release 21 D2). + cut: FetchWindowsItem[]; failed: FetchWindowsFailure[]; // Due a fetch but not attempted because the run stopped first. notAttempted: FetchWindowsItem[]; @@ -146,6 +159,9 @@ export type FetchWindowsProvenance = { // is. export type FetchWindowsDeps = { fetchWindow: (opts: FetchWindowOpts) => Promise<FetchWindowResult>; + // The saved-container tier (lib/savedVideoWindow-server.ts). + savedWindowSource: typeof savedWindowSource; + cutSavedWindow: typeof cutSavedVideoWindow; sleep: (ms: number, signal?: AbortSignal) => Promise<void>; getSettings: () => SiteSettings; readChannelConfig: (paths: Paths, slug: string) => Promise<ChannelConfig | null>; @@ -188,6 +204,8 @@ function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> { const DEFAULT_DEPS: FetchWindowsDeps = { fetchWindow: fetchWindowManaged, + savedWindowSource, + cutSavedWindow: cutSavedVideoWindow, sleep: abortableSleep, getSettings, readChannelConfig, @@ -262,6 +280,7 @@ export async function fetchWindows({ const result: FetchWindowsResult = { fetched: [], cached: [], + cut: [], failed: [], notAttempted: [], }; @@ -351,6 +370,49 @@ export async function fetchWindows({ continue; } + // THEN THE SAVED CONTAINER: cut, no request, no pause, not a network + // attempt. + const saved = await deps.savedWindowSource({ + paths, + slug: item.slug, + videoDir, + from: item.from, + to: item.to, + }); + if (saved.kind === "unreadable") { + fail(item, "unreachable", saved.error); + done += 1; + progress(); + continue; + } + if (saved.kind === "covers") { + try { + const cut = await deps.cutSavedWindow({ + paths, + videoDir, + source: saved, + from: item.from, + to: item.to, + provenance: { + requestedBy: provenance.requestedBy, + manifest: provenance.manifest, + clipId: item.clipId, + reason: item.reason, + pad: item.pad, + requestedAt, + }, + signal: fetchSignal, + }); + result.cut.push(item); + log(` ✂ ${label}: cut from the saved video into clips/${cut.file}`); + } catch (e) { + fail(item, "cut-failed", (e as Error).message); + } + done += 1; + progress(); + continue; + } + const url = item.webpageUrl?.trim() || (await deps.findVideoSourceUrl(paths, item.slug, item.id, config)); @@ -490,7 +552,9 @@ export async function fetchWindows({ } log( - `Fetch windows: ${result.fetched.length} fetched, ${result.cached.length} cached, ` + + `Fetch windows: ${result.fetched.length} fetched, ` + + (result.cut.length ? `${result.cut.length} cut from saved videos, ` : "") + + `${result.cached.length} cached, ` + `${result.failed.length} failed` + (result.notAttempted.length ? `, ${result.notAttempted.length} not attempted (stopped: ${result.stopped})` diff --git a/common/lib/clipWindow.ts b/common/lib/clipWindow.ts @@ -152,8 +152,15 @@ export type ClipProvenance = { // The argv the fetch actually ran, minus the binary. For the case where a // window looks wrong and the question is which format selector produced it. ytdlp?: { args: string[] }; + // WHERE THE BYTES CAME FROM, when it was not the network: "saved-video" for + // a window cut from the video's saved container (release 21 D2, + // lib/savedVideoWindow-server.ts). Absent on every fetched window. + source?: ClipWindowSource; }; +export const CLIP_WINDOW_SOURCES = ["saved-video"] as const; +export type ClipWindowSource = (typeof CLIP_WINDOW_SOURCES)[number]; + const str = (v: unknown): string | undefined => typeof v === "string" && v !== "" ? v : undefined; const num = (v: unknown): number | undefined => @@ -188,6 +195,9 @@ export function parseClipProvenance(raw: unknown): ClipProvenance | null { ); out.ytdlp = { args }; } + if ((CLIP_WINDOW_SOURCES as readonly unknown[]).includes(r.source)) { + out.source = r.source as ClipWindowSource; + } return out; } diff --git a/common/lib/queueKeys.ts b/common/lib/queueKeys.ts @@ -110,3 +110,11 @@ export function resolveQueueKey( ): string { return override === undefined ? defaultKey : override.trim(); } + +// WINDOWS CUT FROM SAVED CONTAINERS (release 21 D2) run on a queue of their own: +// no platform is asked, so none of a platform's queues — or its hold, cooldown +// and gap — applies, and a batch of local cuts never waits behind a network +// window. The pseudo-platform a batch's group of them carries is +// SAVED_VIDEO_CLIP_PLATFORM. +export const SAVED_VIDEO_CLIP_QUEUE = "clips:saved-video"; +export const SAVED_VIDEO_CLIP_PLATFORM = "saved-video"; diff --git a/common/lib/savedVideoWindow-server.test.ts b/common/lib/savedVideoWindow-server.test.ts @@ -0,0 +1,206 @@ +// A clip window cut from a saved container (release 21 D2): which pointer +// answers which way, and the cut itself. +// +// Real ffmpeg over tiny lavfi containers (160×90, a few seconds), so a run +// costs a second or two. +// +// Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/savedVideoWindow-server.test.ts + +import { after, before, test } from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { cutSavedVideoWindow, savedWindowSource } from "./savedVideoWindow-server"; +import { findContainingClipWindow } from "./clipWindow-server"; +import { probeMedia } from "./evidenceClip-server"; +import { SAVED_VIDEOS_MARKER_FILENAME } from "./savedVideoStore"; + +const SLUG = "demo-channel"; +const ID = "abc123"; + +let ROOT = ""; +// 8 s of 160×90 picture with sound, and 8 s of sound alone. +let PICTURE = ""; +let SOUND = ""; + +function ff(args: string[]): void { + execFileSync("ffmpeg", ["-nostdin", "-v", "error", "-y", ...args]); +} + +before(async () => { + ROOT = await mkdtemp(path.join(tmpdir(), "saved-window-")); + const fx = path.join(ROOT, "fixtures"); + await mkdir(fx, { recursive: true }); + PICTURE = path.join(fx, "picture.mp4"); + SOUND = path.join(fx, "sound.m4a"); + ff([ + "-f", "lavfi", "-i", "testsrc=size=160x90:rate=10:duration=8", + "-f", "lavfi", "-i", "sine=frequency=440:duration=8", + "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", PICTURE, + ]); + ff(["-f", "lavfi", "-i", "sine=frequency=220:duration=8", "-c:a", "aac", SOUND]); +}); + +after(() => rm(ROOT, { recursive: true, force: true })); + +let n = 0; +// A fresh corpus per test: `<root>/<n>/transcripts/channels/<slug>/data/<id>/` +// and its saved-video store `<root>/<n>/transcripts/saved-videos/<slug>/<id>/`, +// holding `container` (when given) under the pointer's name. +async function corpus(container?: string, opts: { pointer?: boolean } = {}) { + const root = path.join(ROOT, String(++n)); + const transcriptsDir = path.join(root, "transcripts"); + const savedVideosDir = path.join(transcriptsDir, "saved-videos"); + const videoDir = path.join(transcriptsDir, "channels", SLUG, "data", ID); + const storeDir = path.join(savedVideosDir, SLUG, ID); + await mkdir(videoDir, { recursive: true }); + await mkdir(storeDir, { recursive: true }); + const file = `source-media${container ? path.extname(container) : ".mp4"}`; + if (container) await copyFile(container, path.join(storeDir, file)); + if (opts.pointer !== false) { + await writeFile( + path.join(videoDir, "saved-video.json"), + JSON.stringify({ + storedAt: "2026-10-10T00:00:00.000Z", + dir: storeDir, + file, + bytes: 1, + keepReason: "pin", + origin: { + requestedBy: "attach-media", + kind: "local-archive", + archive: "/archive/example.zip", + entry: "Example (abc123)/example.mp4", + }, + }), + ); + } + const paths = { transcriptsDir, savedVideosDir, ffmpegBin: "ffmpeg", ffprobeBin: "ffprobe" }; + return { paths, videoDir, storeDir, transcriptsDir }; +} + +test("no pointer: none — the caller goes to the network", async () => { + const c = await corpus(undefined, { pointer: false }); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 3 }); + assert.deepEqual(r, { kind: "none" }); +}); + +test("a container that ends before the window: not-covering", async () => { + const c = await corpus(PICTURE); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 5, to: 12 }); + assert.equal(r.kind, "not-covering"); + assert.ok(r.kind === "not-covering" && Math.abs(r.durationSec - 8) < 0.2); +}); + +test("a pointer whose container is missing: unreadable, in the media guard's words", async () => { + const c = await corpus(undefined); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 3 }); + assert.equal(r.kind, "unreadable"); + assert.match( + (r as { error: string }).error, + /^Channel "demo-channel": media is not reachable — its saved video .*source-media\.mp4 is not there \(drive not mounted\?\)$/, + ); +}); + +test("a pointer whose store is unmounted (its directory gone): unreadable", async () => { + const c = await corpus(PICTURE); + await rm(c.storeDir, { recursive: true, force: true }); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 3 }); + assert.equal(r.kind, "unreadable"); + assert.match((r as { error: string }).error, /media is not reachable/); +}); + +test("a missing container while the store is being moved: the store guard's sentence", async () => { + const c = await corpus(undefined); + await writeFile( + path.join(c.transcriptsDir, SAVED_VIDEOS_MARKER_FILENAME), + JSON.stringify({ target: "/mnt/elsewhere/saved-videos", direction: "out", startedAt: "", phase: "swap" }), + ); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 3 }); + assert.equal(r.kind, "unreadable"); + assert.match((r as { error: string }).error, /^The saved-video store is being moved \(out to \/mnt\/elsewhere\/saved-videos, phase "swap"\)/); +}); + +test("a container ffprobe cannot read: unreadable, never a guess", async () => { + const c = await corpus(undefined); + await writeFile(path.join(c.storeDir, "source-media.mp4"), "not a container"); + const r = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 3 }); + assert.equal(r.kind, "unreadable"); + assert.match((r as { error: string }).error, /could not be read by ffprobe/); +}); + +test("a covering container is cut into clips/ with a saved-video sidecar, and the cache then answers it", async () => { + const c = await corpus(PICTURE); + const src = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 2, to: 5 }); + assert.equal(src.kind, "covers"); + if (src.kind !== "covers") return; + const logs: string[] = []; + const w = await cutSavedVideoWindow({ + paths: c.paths, + videoDir: c.videoDir, + source: src, + from: 2, + to: 5, + provenance: { requestedBy: "test", manifest: "m", clipId: "c1", pad: 1, requestedAt: "2026-10-10T01:00:00.000Z" }, + onLog: (l) => logs.push(l), + }); + assert.equal(w.file, "2.00-5.00.mp4"); + assert.equal(path.dirname(w.path), path.join(c.videoDir, "clips")); + // Only the window and its sidecar: no temp file left behind. + assert.deepEqual((await readdir(path.join(c.videoDir, "clips"))).sort(), ["2.00-5.00.json", "2.00-5.00.mp4"]); + const probe = await probeMedia(w.path); + assert.ok(probe?.hasVideo && probe.hasAudio); + assert.ok(Math.abs((probe?.durationSec ?? 0) - 3) < 0.25, `duration ${probe?.durationSec}`); + const sidecar = JSON.parse(await readFile(path.join(c.videoDir, "clips", "2.00-5.00.json"), "utf8")); + assert.equal(sidecar.source, "saved-video"); + assert.equal(sidecar.requestedBy, "test"); + assert.equal(sidecar.manifest, "m"); + assert.equal(sidecar.clipId, "c1"); + assert.equal(sidecar.pad, 1); + assert.equal(sidecar.bytes, w.bytes); + assert.equal(sidecar.ytdlp, undefined, "nothing was fetched"); + assert.match(logs.join(""), /Cut 2\.00–5\.00 from the saved video source-media\.mp4/); + // The fetched-window cache reads it back like any window. + const hit = await findContainingClipWindow(c.videoDir, 2.5, 4.5); + assert.equal(hit?.file, "2.00-5.00.mp4"); + assert.equal(hit?.provenance?.source, "saved-video"); +}); + +test("a container with no picture is cut as sound", async () => { + const c = await corpus(SOUND); + const src = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 4 }); + assert.equal(src.kind, "covers"); + if (src.kind !== "covers") return; + const w = await cutSavedVideoWindow({ + paths: c.paths, + videoDir: c.videoDir, + source: src, + from: 1, + to: 4, + provenance: { requestedBy: "test" }, + }); + const probe = await probeMedia(w.path); + assert.equal(probe?.hasVideo, false); + assert.equal(probe?.hasAudio, true); +}); + +test("an ffmpeg that fails leaves no window behind", async () => { + const c = await corpus(PICTURE); + const src = await savedWindowSource({ paths: c.paths, slug: SLUG, videoDir: c.videoDir, from: 1, to: 2 }); + assert.equal(src.kind, "covers"); + if (src.kind !== "covers") return; + await assert.rejects( + cutSavedVideoWindow({ + paths: { ...c.paths, ffmpegBin: "false" }, + videoDir: c.videoDir, + source: src, + from: 1, + to: 2, + provenance: { requestedBy: "test" }, + }), + /ffmpeg failed cutting 1\.00–2\.00 from the saved video source-media\.mp4/, + ); + assert.deepEqual(await readdir(path.join(c.videoDir, "clips")), []); +}); diff --git a/common/lib/savedVideoWindow-server.ts b/common/lib/savedVideoWindow-server.ts @@ -0,0 +1,208 @@ +// A CLIP WINDOW CUT FROM A SAVED CONTAINER, not fetched (release 21 D2). +// +// A video whose whole source is in the saved-video store (a persist, a +// `full: true` fetch, or a container attached from a local archive — release 21 +// D1's `origin.kind: "local-archive"`) holds every second a window could ask +// for. Fetching those seconds again spends the source's patience for bytes +// already on a disk here — and for a deleted channel there is no source to ask: +// the fetch fails every time. So the window paths (the single +// `fetchWindowAction` and the `fetch-windows` batch) ask here after the +// clip-window cache and BEFORE the network. +// +// THE THREE ANSWERS, and what each caller does with them (ruled 2026-10-10): +// none no pointer: the network path, exactly as before. +// unreadable a pointer whose container cannot be read — the store's drive +// is not mounted or not answering, the file is missing, ffprobe +// cannot open it. REFUSED with the sentence below, never a +// silent fall-through to the network: the operator who can plug +// the drive in is the one who needs to hear it. +// not-covering a pointer whose container ends before the window does: the +// network path. +// covers cut it here. +// +// THE CUT IS evidenceClip-server's (`evidenceCutArgs`): seeked on the input +// and re-encoded, so the window's first frame is the asked-for second; H.264 + +// AAC in an mp4, fitted inside 1280×720 — the container a fetched window is +// pinned to (lib/clipWindow.ts), at most the default clip height. A container +// with no picture is cut as sound (still an `.mp4` name, the window's one +// extension). The window lands in `data/<id>/clips/<from>-<to>.mp4` with a +// provenance sidecar of the fetched shape plus `source: "saved-video"`, so +// every reader of the cache (the video page, the build, umtool) sees it as a +// window like any other. +// +// SERVER-ONLY (node:fs, ffmpeg). + +import { mkdir, rename, rm, stat } from "node:fs/promises"; +import path from "node:path"; +import { execa } from "execa"; +import { + WIN_EPS, + clipWindowFile, + type ClipProvenance, +} from "./clipWindow"; +import { + clipWindowPath, + writeClipProvenance, + type ClipWindow, +} from "./clipWindow-server"; +import { evidenceCutArgs, probeMedia, type MediaProbe } from "./evidenceClip-server"; +import { tmpPathFor } from "./jsonFile-server"; +import { savedVideoPath, type SavedVideoPointer } from "./savedVideo"; +import { loadSavedVideo } from "./savedVideo-server"; +import { readSavedVideosMarker, SavedVideosStoreInTransitionError } from "./savedVideoStore"; +import { isDriveNotAnswering, onDrive } from "./storageHealth"; +import type { Paths } from "./paths"; + +// A window cut from a container is seconds of re-encoding; a whole container +// on a platter seeks once. Generous, so only a wedged ffmpeg reaches it — +// evidenceClip-server's limit for the same work. +const CUT_TIMEOUT_MS = 10 * 60_000; + +export type SavedWindowSource = + | { kind: "none" } + | { kind: "unreadable"; error: string } + | { kind: "not-covering"; container: string; durationSec: number } + | { + kind: "covers"; + pointer: SavedVideoPointer; + container: string; + durationSec: number; + probe: MediaProbe; + }; + +// The media guard's sentence (lib/channelMedia.ts, ChannelMediaUnreachableError) +// with the saved container as its detail, so a refusal here reads like every +// other unreachable-media refusal the operator has seen. +function unreachable(slug: string, detail: string): SavedWindowSource { + return { kind: "unreadable", error: `Channel "${slug}": media is not reachable — ${detail}` }; +} + +// Does this video's saved container hold [from, to]? Reads the pointer, stats +// the container through the drive watchdog (an unanswering drive is refused in +// seconds, not waited on), and probes it once for its duration and streams. +export async function savedWindowSource(opts: { + paths: Pick<Paths, "transcriptsDir" | "savedVideosDir" | "ffprobeBin">; + slug: string; + videoDir: string; + from: number; + to: number; +}): Promise<SavedWindowSource> { + const pointer = await loadSavedVideo(opts.videoDir); + if (!pointer) return { kind: "none" }; + const container = savedVideoPath(pointer); + let isFile = false; + try { + isFile = await onDrive(pointer.dir, () => + stat(container).then( + (s) => s.isFile(), + () => false, + ), + ); + } catch (err) { + if (isDriveNotAnswering(err)) { + return unreachable(opts.slug, `its saved video ${container}: ${(err as Error).message}`); + } + throw err; + } + if (!isFile) { + // The store mid-move is the one case with a sentence of its own: the + // store guard's (assertSavedVideosStoreWritable), which names the move. + const marker = await readSavedVideosMarker(opts.paths); + if (marker) { + return { kind: "unreadable", error: new SavedVideosStoreInTransitionError(marker).message }; + } + return unreachable( + opts.slug, + `its saved video ${container} is not there (drive not mounted?)`, + ); + } + const probe = await probeMedia(container, opts.paths.ffprobeBin); + if (!probe || probe.durationSec === null || (!probe.hasVideo && !probe.hasAudio)) { + return unreachable(opts.slug, `its saved video ${container} could not be read by ffprobe`); + } + const durationSec = probe.durationSec; + if (opts.from < 0 || opts.to > durationSec + WIN_EPS) { + return { kind: "not-covering", container, durationSec }; + } + return { kind: "covers", pointer, container, durationSec, probe }; +} + +export type SavedWindowProvenance = { + requestedBy: string; + manifest?: string; + clipId?: string; + reason?: string; + pad?: number; + requestedAt?: string; +}; + +export class SavedWindowCutError extends Error { + constructor(message: string) { + super(message); + this.name = "SavedWindowCutError"; + } +} + +// Cut [from, to] out of a covering container into the video's clips/, with its +// sidecar. Written to a temp name first (which no window parser accepts) and +// renamed, so a reader never sees half a window. +export async function cutSavedVideoWindow(opts: { + paths: Pick<Paths, "ffmpegBin" | "ffprobeBin">; + videoDir: string; + source: Extract<SavedWindowSource, { kind: "covers" }>; + from: number; + to: number; + provenance: SavedWindowProvenance; + onLog?: (line: string) => void; + signal?: AbortSignal; +}): Promise<ClipWindow> { + const { from, to, source } = opts; + const dest = clipWindowPath(opts.videoDir, from, to); + await mkdir(path.dirname(dest), { recursive: true }); + const tmp = tmpPathFor(dest); + const kind = source.probe.hasVideo ? "video" : "audio"; + try { + const r = await execa(opts.paths.ffmpegBin, evidenceCutArgs(source.container, from, to, kind, tmp), { + reject: false, + timeout: CUT_TIMEOUT_MS, + ...(opts.signal ? { cancelSignal: opts.signal } : {}), + }); + if (r.exitCode !== 0) { + const err = String(r.stderr ?? "").trim().split("\n").slice(-3).join(" | "); + throw new SavedWindowCutError( + `ffmpeg failed cutting ${from.toFixed(2)}–${to.toFixed(2)} from the saved video ` + + `${source.pointer.file}${err ? `: ${err}` : ` (exit ${r.exitCode ?? "?"})`}`, + ); + } + const bytes = (await stat(tmp)).size; + await rename(tmp, dest); + const now = new Date().toISOString(); + const provenance: ClipProvenance = { + requestedBy: opts.provenance.requestedBy, + ...(opts.provenance.manifest ? { manifest: opts.provenance.manifest } : {}), + ...(opts.provenance.clipId ? { clipId: opts.provenance.clipId } : {}), + ...(opts.provenance.reason ? { reason: opts.provenance.reason } : {}), + requestedAt: opts.provenance.requestedAt ?? now, + ...(typeof opts.provenance.pad === "number" ? { pad: opts.provenance.pad } : {}), + bytes, + // When the window landed — for a cut, when it was cut. + fetchedAt: now, + source: "saved-video", + }; + await writeClipProvenance(opts.videoDir, from, to, provenance); + opts.onLog?.( + `Cut ${from.toFixed(2)}–${to.toFixed(2)} from the saved video ${source.pointer.file} into ` + + `clips/${clipWindowFile(from, to)} (${bytes} bytes); nothing fetched.\n`, + ); + return { + file: clipWindowFile(from, to), + path: dest, + from, + to, + bytes, + provenance, + }; + } finally { + await rm(tmp, { force: true }).catch(() => {}); + } +} diff --git a/editor/app/api/media/fetch-window/route.saved.test.ts b/editor/app/api/media/fetch-window/route.saved.test.ts @@ -0,0 +1,151 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +// Run with: +// pnpm -C editor exec tsx --test "app/api/media/fetch-window/route.saved.test.ts" +// +// A window of a video whose source is in the saved-video store (release 21 +// D2): cut on the spot and answered `cached: true`, with no job and no +// network; a container that does not reach the window goes to the network +// path exactly as before; a pointer whose container is missing is refused. +// +// THE NETWORK PATH IS SEEN BY ITS DOOR: YouTube is put in a rate-limit +// cooldown before the first request, so a request that reaches the network +// path answers 409 with the cooldown's sentence — before any job is queued, +// and never a real fetch. Real ffmpeg over an 8 s 160×90 lavfi container. + +const ROOT = await mkdtemp(path.join(os.tmpdir(), "fetch-window-saved-")); +const SLUG = "demo-channel"; +// Set before the route (and getPaths, which caches) is first imported. +process.env.WORKER_TOKEN = "test-token"; +process.env.TRANSCRIPTS_DIR = ROOT; +process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); +delete process.env.FFPROBE_BIN; +delete process.env.FFMPEG_BIN; + +await mkdir(path.join(ROOT, "channels", SLUG), { recursive: true }); +await writeFile( + path.join(ROOT, "channels", SLUG, "config.json"), + JSON.stringify({ name: "Demo", handling: "youtube", url: "https://www.youtube.com/@demo" }), +); + +// `held1` has its container in the store; `gone1` has a pointer to a file that +// is not there (the store's drive unplugged). +async function pointTo(id: string, withFile: boolean): Promise<string> { + const videoDir = path.join(ROOT, "channels", SLUG, "data", id); + const store = path.join(ROOT, "saved-videos", SLUG, id); + await mkdir(videoDir, { recursive: true }); + await mkdir(store, { recursive: true }); + if (withFile) { + execFileSync("ffmpeg", [ + "-nostdin", "-v", "error", "-y", + "-f", "lavfi", "-i", "testsrc=size=160x90:rate=10:duration=8", + "-f", "lavfi", "-i", "sine=frequency=440:duration=8", + "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", + path.join(store, "source-media.mp4"), + ]); + } + await writeFile( + path.join(videoDir, "saved-video.json"), + JSON.stringify({ + storedAt: "2026-10-10T00:00:00.000Z", + dir: store, + file: "source-media.mp4", + bytes: 1, + keepReason: "pin", + origin: { requestedBy: "attach-media", kind: "local-archive", archive: "/archive/example.zip", entry: "e.mp4" }, + }), + ); + return videoDir; +} +const HELD = await pointTo("held1", true); +const GONE = await pointTo("gone1", false); + +const { POST } = await import("./route"); +const { getRegistry } = await import("yt-dlp-transcript-common/jobs/registry"); +const { recordDownloadBackoff } = await import("yt-dlp-transcript-common/jobs/downloadBackoff"); +const { getPaths } = await import("yt-dlp-transcript-common/lib/paths"); +await recordDownloadBackoff("youtube", getPaths()); +const NETWORK_DOOR = /youtube is in a rate-limit cooldown/; +test.after(() => rm(ROOT, { recursive: true, force: true })); + +function post(body: Record<string, unknown>): Promise<Response> { + return POST( + new Request("http://localhost/api/media/fetch-window", { + method: "POST", + headers: { authorization: "Bearer test-token", "content-type": "application/json" }, + body: JSON.stringify(body), + }), + ); +} + +const base = { channelSlug: SLUG, requestedBy: "test", manifest: "m", clipId: "c1", pad: 1 }; +const jobsNow = () => getRegistry().list().length; + +test("a window the saved container holds is cut and answered cached, with no job", async () => { + const jobs = jobsNow(); + const res = await post({ ...base, videoId: "held1", from: 2, to: 5 }); + assert.equal(res.status, 200); + const body = (await res.json()) as { + cached: boolean; + file: string; + from: number; + to: number; + bytes: number; + height?: number; + provenance: Record<string, unknown>; + }; + assert.equal(body.cached, true); + assert.equal(body.from, 2); + assert.equal(body.to, 5); + assert.equal(body.file, path.join(HELD, "clips", "2.00-5.00.mp4")); + assert.ok(body.bytes > 0); + assert.equal(body.height, 90); + assert.equal(body.provenance.source, "saved-video"); + assert.equal(body.provenance.requestedBy, "test"); + assert.equal(body.provenance.clipId, "c1"); + assert.equal(jobsNow(), jobs, "no job was queued"); + const sidecar = JSON.parse(await readFile(path.join(HELD, "clips", "2.00-5.00.json"), "utf8")); + assert.equal(sidecar.source, "saved-video"); + + // Asked again (or for seconds inside it), the clip cache answers it. + const again = await post({ ...base, videoId: "held1", from: 2.5, to: 4.5 }); + assert.equal(again.status, 200); + const b2 = (await again.json()) as { file: string; provenance: Record<string, unknown> }; + assert.equal(b2.file, body.file); + assert.equal(b2.provenance.source, "saved-video"); + assert.deepEqual((await readdir(path.join(HELD, "clips"))).sort(), ["2.00-5.00.json", "2.00-5.00.mp4"]); +}); + +test("a window past the container's end goes to the network path, as before", async () => { + const res = await post({ ...base, videoId: "held1", from: 6, to: 12 }); + assert.equal(res.status, 409); + const body = (await res.json()) as { error: string }; + assert.match(body.error, NETWORK_DOOR); + assert.ok( + !(await readdir(path.join(HELD, "clips"))).some((f) => f.startsWith("6.00-")), + "nothing was cut", + ); +}); + +test("a pointer whose container is missing is refused with the media guard's sentence, never fetched", async () => { + const res = await post({ ...base, videoId: "gone1", from: 2, to: 5 }); + assert.equal(res.status, 503); + const body = (await res.json()) as { error: string }; + assert.match( + body.error, + /^Channel "demo-channel": media is not reachable — its saved video .*source-media\.mp4 is not there \(drive not mounted\?\)$/, + ); + assert.deepEqual(await readdir(GONE), ["saved-video.json"]); +}); + +test("a video with no pointer goes to the network path, as before", async () => { + await mkdir(path.join(ROOT, "channels", SLUG, "data", "plain1"), { recursive: true }); + const res = await post({ ...base, videoId: "plain1", from: 2, to: 5 }); + assert.equal(res.status, 409); + assert.match(((await res.json()) as { error: string }).error, NETWORK_DOOR); +}); diff --git a/editor/app/channels/[slug]/videos/[id]/components/cards/FetchedWindows.tsx b/editor/app/channels/[slug]/videos/[id]/components/cards/FetchedWindows.tsx @@ -46,6 +46,7 @@ export function FetchedWindows({ windows }: { windows: FetchedWindow[] }) { requested by {p.requestedBy} {what ? ` for ${what}` : ""} {p.reason ? ` · \u201c${p.reason}\u201d` : ""} + {p.source === "saved-video" ? " · cut from the saved video" : ""} {p.fetchedAt ? ` · ${new Date(p.fetchedAt).toLocaleString()}` : ""} diff --git a/editor/app/channels/[slug]/videos/[id]/videoActions.ts b/editor/app/channels/[slug]/videos/[id]/videoActions.ts @@ -43,6 +43,7 @@ import { } from "yt-dlp-transcript-common/controller/keepVideosMatching"; import { ChannelMediaUnreachableError, + assertChannelTextReadable, channelTextStall, inspectChannelMedia, } from "yt-dlp-transcript-common/lib/channelMedia"; @@ -78,6 +79,10 @@ import { findContainingClipWindow, } from "yt-dlp-transcript-common/lib/clipWindow-server"; import { + cutSavedVideoWindow, + savedWindowSource, +} from "yt-dlp-transcript-common/lib/savedVideoWindow-server"; +import { fetchWindowManaged, type FetchWindowProvenance, } from "yt-dlp-transcript-common/ytdlp/fetchWindowManaged"; @@ -1133,6 +1138,57 @@ export async function fetchWindowAction(req: { } } + // ...THEN THE SAVED CONTAINER (release 21 D2), before any network: a video + // whose whole source is in the saved-video store is CUT here, not fetched, + // and the answer is a cached window like the one above + // (lib/savedVideoWindow-server.ts). No + // pointer, or a container that ends before the window, goes on to the + // network as before; a pointer whose container cannot be read is REFUSED — + // falling through would spend the source's patience (or, for a deleted + // channel, fail) on seconds that are on a drive somebody can plug in. + const saved = await savedWindowSource({ paths, slug, videoDir, from, to }); + if (saved.kind === "unreadable") { + return { ok: false, status: 503, error: saved.error }; + } + if (saved.kind === "covers") { + // The window lands in data/<id>/clips/, the channel's TEXT tier: the guard a + // fetch-window job's `needsText` would have asked, asked here because no + // job runs. + try { + await assertChannelTextReadable(paths, slug); + } catch (e) { + return { ok: false, status: 503, error: (e as Error).message }; + } + let cut; + try { + cut = await cutSavedVideoWindow({ + paths, + videoDir, + source: saved, + from, + to, + provenance: req.provenance, + }); + } catch (e) { + return { ok: false, status: 500, error: (e as Error).message }; + } + const height = await probeVideoHeight({ + ffprobeBin: paths.ffprobeBin, + file: cut.path, + }); + safeRevalidate([`/channels/${slug}/videos/${videoId}`]); + return { + ok: true, + cached: true, + file: cut.path, + from: cut.from, + to: cut.to, + bytes: cut.bytes, + provenance: cut.provenance, + ...(height !== null ? { height } : {}), + }; + } + const url = req.webpageUrl?.trim() || (await findVideoSourceUrl(paths, slug, videoId, r.config)); diff --git a/editor/app/channels/[slug]/videos/fetchWindowsAction.test.ts b/editor/app/channels/[slug]/videos/fetchWindowsAction.test.ts @@ -0,0 +1,155 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +// Run with: +// pnpm -C editor exec tsx --test "app/channels/*/videos/fetchWindowsAction.test.ts" +// (a wildcard: node's --test reads `[slug]` as a glob class) +// +// The batch fetch over videos whose source is in the saved-video store +// (release 21 D2): a window a container holds joins the `saved-video` group on +// `clips:saved-video` — no platform's queue, hold or cooldown — and its job +// cuts it; a pointer whose container is missing is unresolved with the media +// guard's sentence; a window past the container's end joins its platform's +// group as before. YouTube is in a cooldown throughout, so its group is +// refused at the door and nothing here ever reaches the network. + +const ROOT = await mkdtemp(path.join(os.tmpdir(), "fetch-windows-saved-")); +const SLUG = "demo-channel"; +process.env.TRANSCRIPTS_DIR = ROOT; +process.env.SETTINGS_FILE = path.join(ROOT, "settings.json"); +delete process.env.FFPROBE_BIN; +delete process.env.FFMPEG_BIN; + +// The temp corpus sits on whatever disk os.tmpdir() is; the floor is not what +// is under test. +await writeFile(process.env.SETTINGS_FILE, JSON.stringify({ minFreeDiskGB: 0 })); +await mkdir(path.join(ROOT, "channels", SLUG), { recursive: true }); +await writeFile( + path.join(ROOT, "channels", SLUG, "config.json"), + JSON.stringify({ name: "Demo", handling: "youtube", url: "https://www.youtube.com/@demo" }), +); + +async function pointTo(id: string, withFile: boolean): Promise<string> { + const videoDir = path.join(ROOT, "channels", SLUG, "data", id); + const store = path.join(ROOT, "saved-videos", SLUG, id); + await mkdir(videoDir, { recursive: true }); + await mkdir(store, { recursive: true }); + await writeFile( + path.join(videoDir, "metadata.info.json"), + JSON.stringify({ id, webpage_url: `https://www.youtube.com/watch?v=${id}` }), + ); + if (withFile) { + execFileSync("ffmpeg", [ + "-nostdin", "-v", "error", "-y", + "-f", "lavfi", "-i", "testsrc=size=160x90:rate=10:duration=8", + "-f", "lavfi", "-i", "sine=frequency=440:duration=8", + "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", + path.join(store, "source-media.mp4"), + ]); + } + await writeFile( + path.join(videoDir, "saved-video.json"), + JSON.stringify({ storedAt: "", dir: store, file: "source-media.mp4", bytes: 1, keepReason: "pin" }), + ); + return videoDir; +} +const HELD = await pointTo("held1", true); +await pointTo("gone1", false); + +const { fetchWindowsAction } = await import("./fetchWindowsAction"); +const { getRegistry } = await import("yt-dlp-transcript-common/jobs/registry"); +const { recordDownloadBackoff } = await import("yt-dlp-transcript-common/jobs/downloadBackoff"); +const { getPaths } = await import("yt-dlp-transcript-common/lib/paths"); +await recordDownloadBackoff("youtube", getPaths()); +test.after(() => rm(ROOT, { recursive: true, force: true })); + +const items = [ + { slug: SLUG, id: "held1", from: 1, to: 3, clipId: "r#1" }, + { slug: SLUG, id: "held1", from: 6, to: 12, clipId: "r#2" }, // past the container's end + { slug: SLUG, id: "gone1", from: 1, to: 3, clipId: "r#3" }, +]; + +test("a dry run groups held windows on clips:saved-video, refuses the unreadable, and sends the rest to their platform", async () => { + const r = await fetchWindowsAction({ items, requestedBy: "test", dryRun: true }); + assert.ok(r.ok); + if (!r.ok) return; + assert.deepEqual( + r.groups.map((g) => [g.platform, g.queueKey, g.items.map((i) => i.clipId)]).sort(), + [ + ["saved-video", "clips:saved-video", ["r#1"]], + ["youtube", "clips:youtube", ["r#2"]], + ], + ); + assert.equal(r.unresolved.length, 1); + assert.equal(r.unresolved[0].item.id, "gone1"); + assert.match(r.unresolved[0].error, /^Channel "demo-channel": media is not reachable — its saved video .* is not there/); +}); + +test("a real run cuts the held window in a saved-video job while the cooling platform is refused", async () => { + const r = await fetchWindowsAction({ items, requestedBy: "test", manifest: "m" }); + assert.ok(r.ok, JSON.stringify(r)); + if (!r.ok) return; + assert.deepEqual( + r.jobs.map((j) => [j.platform, j.queueKey, j.items]), + [["saved-video", "clips:saved-video", 1]], + ); + assert.deepEqual(r.refused.map((x) => x.platform), ["youtube"]); + const jobId = r.jobs[0].jobId; + const reg = getRegistry(); + for (let i = 0; i < 200; i++) { + const s = reg.get(jobId)?.status; + if (s !== "queued" && s !== "running") break; + await new Promise((res) => setTimeout(res, 50)); + } + assert.equal(reg.get(jobId)?.status, "done", `job ${jobId}: ${reg.get(jobId)?.status}`); + assert.deepEqual((await readdir(path.join(HELD, "clips"))).sort(), ["1.00-3.00.json", "1.00-3.00.mp4"]); + const sidecar = JSON.parse(await readFile(path.join(HELD, "clips", "1.00-3.00.json"), "utf8")); + assert.equal(sidecar.source, "saved-video"); + assert.equal(sidecar.manifest, "m"); + assert.equal(sidecar.clipId, "r#1"); + + // Sent again, the cut window is cached and joins no job. + const again = await fetchWindowsAction({ items: [items[0]], requestedBy: "test", dryRun: true }); + assert.ok(again.ok); + if (!again.ok) return; + assert.deepEqual(again.cached.map((i) => i.clipId), ["r#1"]); + assert.deepEqual(again.groups, []); +}); + +test("a held window a queued saved-video job will already cut is answered in inFlight (A5's dedupe)", async () => { + const reg = getRegistry(); + const make = (id: string, params: Record<string, unknown>) => { + const record = { + id, + kind: "fetch-windows", + queueKey: "clips:saved-video", + channelSlug: SLUG, + status: "queued" as const, + queuedAt: Date.now(), + logPath: "/dev/null", + spec: { kind: "fetch-windows", slug: SLUG, params }, + }; + reg.register(record); + reg.enqueue(record, { start: () => {}, onCancel: () => {} }); + }; + make("HOLDER", { items: [{ slug: SLUG, id: "other", from: 0, to: 1 }] }); + make("CUT4", { items: [{ slug: SLUG, id: "held1", from: 4, to: 7 }] }); + try { + const r = await fetchWindowsAction({ + items: [{ slug: SLUG, id: "held1", from: 4.5, to: 6.5 }], + requestedBy: "test", + dryRun: true, + }); + assert.ok(r.ok); + if (!r.ok) return; + assert.deepEqual(r.inFlight.map((f) => f.jobId), ["CUT4"]); + assert.deepEqual(r.groups, []); + } finally { + reg.cancel("CUT4"); + reg.forceRelease("HOLDER"); + } +}); diff --git a/editor/app/channels/[slug]/videos/fetchWindowsAction.ts b/editor/app/channels/[slug]/videos/fetchWindowsAction.ts @@ -5,7 +5,12 @@ import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { diskGate } from "yt-dlp-transcript-common/lib/diskSpace"; import { formatBytes } from "yt-dlp-transcript-common/lib/format"; -import { clipWindowQueueKey, resolveQueueKey } from "yt-dlp-transcript-common/lib/queueKeys"; +import { + SAVED_VIDEO_CLIP_PLATFORM, + SAVED_VIDEO_CLIP_QUEUE, + clipWindowQueueKey, + resolveQueueKey, +} from "yt-dlp-transcript-common/lib/queueKeys"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { windowInFlight } from "yt-dlp-transcript-common/jobs/windowJobs"; import { detectPlatform, queueKeyForUrl } from "yt-dlp-transcript-common/lib/platform"; @@ -14,6 +19,7 @@ import { isFetchMaxHeight, } from "yt-dlp-transcript-common/lib/clipWindow"; import { findContainingClipWindow } from "yt-dlp-transcript-common/lib/clipWindow-server"; +import { savedWindowSource } from "yt-dlp-transcript-common/lib/savedVideoWindow-server"; import type { ChannelConfig } from "yt-dlp-transcript-common/lib/channelConfig"; import { isValidChannelSlug, @@ -59,6 +65,14 @@ import { safeRevalidate } from "../../../lib/safeRevalidate"; // queued or running job will already write (`inFlight`, with that job's id — // jobs/windowJobs.ts): the same manifest sent twice queues nothing twice. // +// A WINDOW A SAVED CONTAINER HOLDS (release 21 D2) joins no platform's job: it +// goes to one group of its own, platform `saved-video` on the +// `clips:saved-video` queue, whose job CUTS each window from the container +// (controller/fetchWindows.ts) — no request, no pause, no hold or cooldown to +// pass. A pointer whose container cannot be read is `unresolved` with the +// media guard's sentence, never sent to the network; one whose container ends +// before the window is fetched as before. +// // Like the single fetch, NOT GATED BY THE DOWNLOAD PAUSE: an operator (or a // tool they are driving) asked for these seconds by hand. @@ -80,6 +94,8 @@ export type FetchWindowsUnresolved = { item: FetchWindowsItem; error: string }; export type FetchWindowsGroup = { // The cooldown key (`detectPlatform(url)`), and the queue the job runs on. + // `saved-video` (SAVED_VIDEO_CLIP_PLATFORM) for the windows cut from saved + // containers, on SAVED_VIDEO_CLIP_QUEUE. platform: string; queueKey: string; items: FetchWindowsItem[]; @@ -174,6 +190,30 @@ async function planFetchWindows( inFlight.push({ item, jobId: running.jobId }); continue; } + // The saved container: a window it holds is cut, on a queue no platform + // shares; one it cannot be read for is refused here, not fetched. + const saved = await savedWindowSource({ + paths, + slug: item.slug, + videoDir, + from: item.from, + to: item.to, + }); + if (saved.kind === "unreadable") { + unresolved.push({ item, error: saved.error }); + continue; + } + if (saved.kind === "covers") { + const queueKey = resolveQueueKey(SAVED_VIDEO_CLIP_QUEUE, queueOverride); + const group = groups.get(queueKey) ?? { + platform: SAVED_VIDEO_CLIP_PLATFORM, + queueKey, + items: [], + }; + group.items.push(item); + groups.set(queueKey, group); + continue; + } const url = item.webpageUrl?.trim() || (await findVideoSourceUrl(paths, item.slug, item.id, config)); @@ -204,6 +244,8 @@ async function planFetchWindows( // The door each group passes before its job is queued: the platform's hold, // then its cooldown — fetchWindowAction's checks and sentences. async function groupRefusal(platform: string): Promise<string | null> { + // Cuts from saved containers ask no platform. + if (platform === SAVED_VIDEO_CLIP_PLATFORM) return null; const paths = getPaths(); const held = await heldPlatformRefusal(platform, "This batch", paths); if (held) return held; diff --git a/mcp/src/instructions.ts b/mcp/src/instructions.ts @@ -155,7 +155,8 @@ function clipStep(ctx: PlanContext): string { `NEVER run yt-dlp yourself, in any form. If the tool reports no editor is ` + `configured, say so and stop — the README's yt-dlp command is the ` + `operator's fallback, not yours. If it returns queued, call it again with ` + - `the job it names.` + `the job it names. A video the archive holds locally is cut from its ` + + `saved copy without a fetch (cached at once).` ); }