commit 922071fc240f6564c9665f387b9dea83c2f26ec2
parent 512de151cf395f602530b4bc2434bb3e18f51b24
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sun, 20 Sep 2026 19:23:12 -0400
editor: show a fetched window, and who asked for it
A `clips/` directory that nothing renders is bytes nobody can account for. The
video page gets a "Fetched windows" card — span, duration, size, requester,
manifest/clip, the reason in their own words, and when it landed — modelled on
the persisted-source panel, which answers the same question for a container.
It also says the thing the card exists to stop somebody assuming: a window is
not a download. The fetch writes no metadata.info.json, so the video's pipeline
state and its absence from the index are unchanged.
A saved-video pointer now renders its `origin` as "Requested by". `keepReason`
cannot carry that — it stays override/pin so the retention prune leaves the
container alone — so "why is this 4 GB file here" needs its own line.
On /jobs, "Fetch window" alone is the least useful row on the page: every
window fetch has that kind. jobSpecDetail reads the requester and clip off the
job's own replay spec, so the row says which tool wanted which clip without a
second place to store it. No per-channel count anywhere: that would be a walk
of every video dir to draw one number.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
8 files changed, 171 insertions(+), 0 deletions(-)
diff --git a/common/jobs/jobDetail.ts b/common/jobs/jobDetail.ts
@@ -0,0 +1,24 @@
+import type { JobSpec } from "./jobSpec";
+
+// The one extra phrase a job row carries beyond its kind.
+//
+// "Fetch window" on its own is the least useful row on /jobs: every fetch-window
+// job has that kind, and the question the operator has is which tool asked and
+// for what. The answer is already on the job — the replay spec's params — so
+// this reads it rather than adding a second place to store it.
+//
+// Pure and total: a kind with nothing to add returns undefined and the row
+// renders exactly as it did.
+export function jobSpecDetail(
+ kind: string | undefined,
+ spec: JobSpec | null | undefined,
+): string | undefined {
+ if (kind !== "fetch-window") return undefined;
+ const p = spec?.params ?? {};
+ const s = (v: unknown): string | null =>
+ typeof v === "string" && v.trim() !== "" ? v.trim() : null;
+ const by = s(p.requestedBy);
+ if (!by) return undefined;
+ const what = [s(p.manifest), s(p.clipId)].filter(Boolean).join("/");
+ return what ? `${by} · ${what}` : by;
+}
diff --git a/common/jobs/listJobs.ts b/common/jobs/listJobs.ts
@@ -7,6 +7,7 @@ import { getRegistry, type JobRecord, type JobStatus } from "./registry";
// One page of job rows, each a couple of small sidecar reads.
const ENTRY_READ_CONCURRENCY = 32;
import { metaPath, readJobMeta } from "./jobMeta";
+import { jobSpecDetail } from "./jobDetail";
import { jobIdTime } from "./ulid";
export type JobListEntry = {
@@ -26,6 +27,9 @@ export type JobListEntry = {
replayable: boolean;
logPath: string;
logSize: number;
+ // A short phrase naming what this particular job is for, when its kind alone
+ // does not say (a fetch-window job's requester and clip). See jobSpecDetail.
+ detail?: string;
};
export type JobsPage = {
@@ -104,6 +108,7 @@ async function buildEntry(
exitCode: live_.exitCode,
inRegistry: true,
replayable: Boolean(live_.spec),
+ detail: jobSpecDetail(live_.kind, live_.spec),
logPath,
logSize,
};
@@ -129,6 +134,7 @@ async function buildEntry(
exitCode: meta.exitCode,
inRegistry: false,
replayable: Boolean(meta.spec),
+ detail: jobSpecDetail(meta.kind, meta.spec),
logPath,
logSize,
};
diff --git a/common/views/jobRowView.ts b/common/views/jobRowView.ts
@@ -89,6 +89,11 @@ export type JobRowView = {
stuck?: { reason: StuckReason; heldMs: number };
pid?: number;
lastLogLine?: string; // stuck rows only — see readLastLogLine
+ // A short phrase naming what THIS job is for, when the kind alone does not
+ // say it — "umtool · elfpire-eva/c03" on a fetch-window row. Built by
+ // jobs/jobDetail.ts from the job's own replay spec; absent for every kind
+ // that has nothing to add, so no existing row changes.
+ detail?: string;
// Which adapter built it. Never rendered; tests and the merge read it.
source: "registry" | "archive" | "slot";
};
diff --git a/common/views/jobRows.ts b/common/views/jobRows.ts
@@ -2,6 +2,7 @@ import { isDrainableKind } from "../jobs/jobKinds";
import { jobIdTime } from "../jobs/ulid";
import type { JobRecord } from "../jobs/registry";
import type { JobListEntry } from "../jobs/listJobs";
+import { jobSpecDetail } from "../jobs/jobDetail";
import type { ChannelStat } from "../controller/channels";
import type { QueueView } from "../jobs/scheduler";
import type { JobRowView, StuckReason } from "./jobRowView";
@@ -251,6 +252,7 @@ export function fromRecord(j: JobRecord, ctx: FromRecordContext): JobRowView {
inRegistry: true,
replayable: Boolean(j.spec),
position: ctx.position >= 0 ? ctx.position : undefined,
+ detail: jobSpecDetail(j.kind, j.spec),
source: "registry",
};
}
@@ -272,6 +274,7 @@ export function fromEntry(e: JobListEntry): JobRowView {
logSize: e.logSize,
inRegistry: e.inRegistry,
replayable: e.replayable,
+ detail: e.detail,
source: "archive",
};
}
diff --git a/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx
@@ -10,6 +10,7 @@ import {
} from "yt-dlp-transcript-common/lib/channelConfig";
import type { DownloadOutcomeRecord } from "yt-dlp-transcript-common/lib/downloadOutcome";
import type { SavedVideoPointer } from "yt-dlp-transcript-common/lib/savedVideo";
+import type { ClipProvenance } from "yt-dlp-transcript-common/lib/clipWindow";
import type { AvailabilityHistoryEntry } from "yt-dlp-transcript-common/lib/availability";
import type { SubtitleProvenance } from "yt-dlp-transcript-common/lib/subtitleProvenance";
import { formatBytes, formatDuration } from "yt-dlp-transcript-common/lib/format";
@@ -80,6 +81,9 @@ type Props = {
// This video's saved-video pointer when its source container is persisted to
// the store, else null. Drives the Source-video persistence card (Phase 5).
savedVideo?: SavedVideoPointer | null;
+ // Windows of this video's source media another tool asked the editor to
+ // fetch, into data/<id>/clips/. Empty for almost every video.
+ clipWindows?: FetchedWindow[];
// Transcript coverage vs. video duration. When `incomplete` the transcript
// covers only a fraction of the runtime (truncated audio download) — surfaced
// as a warning banner with a re-download/re-transcribe action. Absent when
@@ -158,6 +162,7 @@ export function VideoPanel({
doNotClean = false,
excludedFromTruncatedCheck = false,
savedVideo = null,
+ clipWindows = [],
coverage = null,
prevHref,
nextHref,
@@ -424,6 +429,20 @@ export function VideoPanel({
/>
</PipelineStageCard>
+ {clipWindows.length > 0 && (
+ <PipelineStageCard
+ id="fetched-windows"
+ title="Fetched windows"
+ summary={`${clipWindows.length} window${
+ clipWindows.length === 1 ? "" : "s"
+ } of this video's source media, fetched for another tool.`}
+ defaultOpen={false}
+ tone="neutral"
+ >
+ <FetchedWindows windows={clipWindows} />
+ </PipelineStageCard>
+ )}
+
<PipelineStageCard
id="archive-media"
title="Archive media"
@@ -1138,6 +1157,74 @@ function SourceVideoSection({
);
}
+// A window of source media this editor fetched because another tool asked.
+export type FetchedWindow = {
+ file: string;
+ from: number;
+ to: number;
+ bytes: number;
+ provenance: ClipProvenance | null;
+};
+
+// WHO ASKED, FOR WHAT, AND WHY — the three things a `clips/` directory has to
+// be able to answer months later, when the only other record (the job log) has
+// been pruned. Modelled on PersistedSourceVideo below, which answers the same
+// question for a whole container.
+function FetchedWindows({ windows }: { windows: FetchedWindow[] }) {
+ return (
+ <div className="flex flex-col gap-3">
+ <p className="text-sm text-muted-foreground">
+ Seconds of this video's source media, fetched through the managed
+ download path because a tool asked for them, and kept in{" "}
+ <code>clips/</code> so the next tool reuses them instead of paying for
+ the same bytes again.
+ </p>
+ <ul className="flex flex-col gap-2">
+ {windows.map((w) => {
+ const p = w.provenance;
+ const what = [p?.manifest, p?.clipId].filter(Boolean).join("/");
+ return (
+ <li
+ key={w.file}
+ className="flex flex-col gap-0.5 text-sm border-t border-border pt-2 first:border-t-0 first:pt-0"
+ >
+ <span className="font-mono tabular-nums">
+ {w.from.toFixed(2)}–{w.to.toFixed(2)}{" "}
+ <span className="text-muted-foreground">
+ ({formatDuration(w.to - w.from)}) · {formatBytes(w.bytes)}
+ </span>
+ </span>
+ <span className="text-xs text-muted-foreground">
+ {p ? (
+ <>
+ requested by {p.requestedBy}
+ {what ? ` for ${what}` : ""}
+ {p.reason ? ` · \u201c${p.reason}\u201d` : ""}
+ {p.fetchedAt
+ ? ` · ${new Date(p.fetchedAt).toLocaleString()}`
+ : ""}
+ </>
+ ) : (
+ "no provenance sidecar — requester unknown"
+ )}
+ </span>
+ </li>
+ );
+ })}
+ </ul>
+ {/* A WINDOW IS NOT A DOWNLOAD. Thirty seconds of a stream is not the
+ video, and nothing downstream treats it as one: the fetch writes no
+ metadata.info.json, so this video stays exactly as undownloaded (and
+ as absent from the index) as it was. */}
+ <p className="text-xs text-muted-foreground">
+ Fetched windows do not make this video downloaded: they carry no
+ metadata, so the video's pipeline state and its place in the index
+ are unchanged.
+ </p>
+ </div>
+ );
+}
+
// The persisted-details view: a sibling of the run panel above, never a
// replacement for it.
function PersistedSourceVideo({
@@ -1172,6 +1259,27 @@ function PersistedSourceVideo({
<dd>{savedVideo.keepReason}</dd>
</>
)}
+ {/* WHO ASKED. `keepReason` cannot say it — it stays override/pin so the
+ retention prune leaves this container alone — so "why is this 4 GB
+ file here" needs its own line. */}
+ {savedVideo.origin && (
+ <>
+ <dt className="text-muted-foreground">Requested by</dt>
+ <dd>
+ {savedVideo.origin.requestedBy}
+ {[savedVideo.origin.manifest, savedVideo.origin.clipId]
+ .filter(Boolean)
+ .join("/")
+ ? ` for ${[savedVideo.origin.manifest, savedVideo.origin.clipId]
+ .filter(Boolean)
+ .join("/")}`
+ : ""}
+ {savedVideo.origin.reason
+ ? ` \u2014 \u201c${savedVideo.origin.reason}\u201d`
+ : ""}
+ </dd>
+ </>
+ )}
{savedVideo.sha256 && (
<>
<dt className="text-muted-foreground">sha256</dt>
diff --git a/editor/app/channels/[slug]/videos/[id]/page.tsx b/editor/app/channels/[slug]/videos/[id]/page.tsx
@@ -7,6 +7,7 @@ import type { Dirent } from "node:fs";
import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels";
import { loadDownloadOutcome } from "yt-dlp-transcript-common/lib/downloadOutcome-server";
import { loadAvailability } from "yt-dlp-transcript-common/lib/availability-server";
+import { listClipWindows } from "yt-dlp-transcript-common/lib/clipWindow-server";
import { isDoNotClean } from "yt-dlp-transcript-common/lib/doNotClean-server";
import { isExcludedFromTruncatedCheck } from "yt-dlp-transcript-common/lib/excludeTruncatedCheck-server";
import { loadSavedVideo } from "yt-dlp-transcript-common/lib/savedVideo-server";
@@ -119,6 +120,10 @@ export default async function VideoDetailPage({
const excludedFromTruncatedCheck =
await isExcludedFromTruncatedCheck(videoDir);
const savedVideo = await loadSavedVideo(videoDir);
+ // The windows another tool asked this editor to fetch. One readdir of
+ // data/<id>/clips/ plus a stat per file — and no per-CHANNEL count anywhere,
+ // because that would be a walk of every video dir to draw one number.
+ const clipWindows = await listClipWindows(videoDir);
// Where each subtitle track came from, so the panel can say "YouTube
// auto-captions" vs "manual captions" — and offer to replace the former with a
// transcript of our own. One 4 KB head read per VTT (see subtitleProvenance).
@@ -228,6 +233,13 @@ export default async function VideoDetailPage({
doNotClean={doNotClean}
excludedFromTruncatedCheck={excludedFromTruncatedCheck}
savedVideo={savedVideo}
+ clipWindows={clipWindows.map((w) => ({
+ file: w.file,
+ from: w.from,
+ to: w.to,
+ bytes: w.bytes,
+ provenance: w.provenance,
+ }))}
coverage={coverage}
/>
diff --git a/editor/app/jobs/components/JobsTable.tsx b/editor/app/jobs/components/JobsTable.tsx
@@ -416,6 +416,13 @@ export function JobsTable({
<td className="px-3 py-2 align-top" title={j.kind || undefined}>
<span className="flex items-center gap-1.5">
{j.kind ? jobKindLabel(j.kind) : "—"}
+ {/* See RunningJobsList: the kind alone cannot say which
+ tool asked for which clip. */}
+ {j.detail && (
+ <span className="text-xs text-muted-foreground">
+ · {j.detail}
+ </span>
+ )}
{j.background && (
<span
className="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-warning-soft text-warning"
diff --git a/editor/app/jobs/components/RunningJobsList.tsx b/editor/app/jobs/components/RunningJobsList.tsx
@@ -69,6 +69,12 @@ function JobRow({
<span className="text-xs font-medium" title={job.kind}>
{jobKindLabel(job.kind)}
</span>
+ {/* WHO ASKED, beside the kind. "Fetch window" alone is the least
+ useful row on the page: every window fetch has that kind, and the
+ question is which tool wanted which clip. */}
+ {job.detail && (
+ <span className="text-xs text-muted-foreground">· {job.detail}</span>
+ )}
{job.background && (
<span
className="text-[10px] uppercase tracking-wide px-1.5 py-0.5 rounded bg-warning-soft text-warning"