commit 80680d21b0af285c763a30d869bf180fce426dd4
parent 3a79cb922069221041e9845251396abab112c703
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 13:50:34 -0400
channels: the video page shows the listing scan's metadata for an undownloaded video
loadMeta moves onto readVideoMetadataForDisplay (metadata.info.json, else the
channel's metadata-scan.json entry) and the page's private parser goes. A
scan-sourced header carries "from the listing scan — not downloaded"
(aria-label "metadata source"); a Description <details>, collapsed, shows
whenever either source has one. New e2e video-titles.spec.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 140 insertions(+), 32 deletions(-)
diff --git a/editor/app/channels/[slug]/videos/[id]/page.tsx b/editor/app/channels/[slug]/videos/[id]/page.tsx
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import path from "node:path";
-import { readdir, readFile, stat } from "node:fs/promises";
+import { readdir, stat } from "node:fs/promises";
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";
@@ -13,6 +13,10 @@ import { isExcludedFromTruncatedCheck } from "yt-dlp-transcript-common/lib/exclu
import { loadSavedVideo } from "yt-dlp-transcript-common/lib/savedVideo-server";
import { getPaths } from "yt-dlp-transcript-common/lib/paths";
import {
+ readVideoMetadataForDisplay,
+ type VideoDisplayMetadata,
+} from "yt-dlp-transcript-common/controller/videoTitles";
+import {
isTranscriptVtt,
resolvePrimaryVtt,
} from "yt-dlp-transcript-common/lib/videoStatus";
@@ -38,14 +42,6 @@ import { loadVideoOperationPanels } from "./lib/videoOperationPanels";
export const dynamic = "force-dynamic";
-type VideoMeta = {
- title?: string;
- webpageUrl?: string;
- uploader?: string;
- uploadDate?: string;
- duration?: number;
-};
-
async function loadVideoDir(
slug: string,
videoId: string,
@@ -68,29 +64,11 @@ async function loadVideoDir(
return { files };
}
-async function loadMeta(slug: string, videoId: string): Promise<VideoMeta> {
- const file = path.join(
- getPaths().channelsDir,
- slug,
- "data",
- videoId,
- "metadata.info.json",
- );
- try {
- const raw = await readFile(file, "utf8");
- const j = JSON.parse(raw);
- return {
- title: typeof j?.title === "string" ? j.title : undefined,
- webpageUrl:
- typeof j?.webpage_url === "string" ? j.webpage_url : undefined,
- uploader: typeof j?.uploader === "string" ? j.uploader : undefined,
- uploadDate:
- typeof j?.upload_date === "string" ? j.upload_date : undefined,
- duration: typeof j?.duration === "number" ? j.duration : undefined,
- };
- } catch {
- return {};
- }
+// metadata.info.json when the video was downloaded, else the channel's
+// metadata-scan.json entry for a listed-but-never-downloaded video — so an
+// undownloaded video's page is not a bare id.
+function loadMeta(slug: string, videoId: string): Promise<VideoDisplayMetadata> {
+ return readVideoMetadataForDisplay(getPaths(), slug, videoId);
}
export async function generateMetadata({
@@ -216,7 +194,22 @@ export default async function VideoDetailPage({
{typeof meta.duration === "number" && (
<span>{formatDuration(meta.duration)}</span>
)}
+ {meta.source === "scan" && (
+ <span aria-label="metadata source" className="italic">
+ from the listing scan — not downloaded
+ </span>
+ )}
</div>
+ {meta.description && (
+ <details className="text-sm">
+ <summary className="cursor-pointer text-muted-foreground hover:text-foreground">
+ Description
+ </summary>
+ <p className="mt-2 whitespace-pre-wrap break-words text-muted-foreground max-h-96 overflow-y-auto">
+ {meta.description}
+ </p>
+ </details>
+ )}
</header>
<RunningJobsList jobs={activeJobs} hideChannelSlug hideVideoId />
diff --git a/editor/e2e/video-titles.spec.ts b/editor/e2e/video-titles.spec.ts
@@ -0,0 +1,115 @@
+// The per-channel video list shows what each video is CALLED, and searches by
+// it; an undownloaded video's own page is not a bare id.
+//
+// Three rows, one per title source that can reach a fixture (the transcript
+// index is exercised by common/controller/videoTitles.test.ts):
+// - fake00000001 — downloaded; its title is in data/<id>/metadata.info.json.
+// - fake00000002 — listed, never downloaded; titled by the channel's
+// metadata-scan.json (the listing scan's store).
+// - fake00000003 — listed, never downloaded, never scanned: the bare id.
+
+import { writeFile } from "node:fs/promises";
+import { test, expect } from "@playwright/test";
+import {
+ channelVideos,
+ generateReport,
+ resetData,
+ resolvePath,
+} from "./helpers";
+
+const CHANNEL = "test-youtube";
+const ROOT = `test-transcripts/channels/${CHANNEL}`;
+const DOWNLOADED = "fake00000001";
+const SCANNED = "fake00000002";
+const BARE = "fake00000003";
+const SCANNED_TITLE = "Moonlit Harbor Interview";
+const SCANNED_DESCRIPTION = "A long talk on the pier.\nSecond line of notes.";
+
+async function seed() {
+ await resetData("youtube-with-playlist");
+ // The store's on-disk shape (common/controller/metadataScanStore.ts,
+ // METADATA_SCAN_FILENAME) — the scan writes exactly this, and never a
+ // data/<id>/ directory.
+ await writeFile(
+ resolvePath(`${ROOT}/metadata-scan.json`),
+ JSON.stringify(
+ {
+ version: 1,
+ updatedAt: "2026-09-25T00:00:00.000Z",
+ lastRun: null,
+ entries: {
+ [SCANNED]: {
+ title: SCANNED_TITLE,
+ description: SCANNED_DESCRIPTION,
+ uploadDate: "20240315",
+ duration: 754,
+ scannedAt: "2026-09-25T00:00:00.000Z",
+ },
+ },
+ errors: {},
+ },
+ null,
+ 2,
+ ),
+ );
+}
+
+test("the video list shows titles and searches them", async ({ page }) => {
+ await seed();
+ await generateReport(page, CHANNEL);
+ await page.goto(channelVideos(CHANNEL));
+
+ const list = page.getByLabel("videos", { exact: true });
+ const downloaded = list.getByLabel(`open ${DOWNLOADED}`);
+ const scanned = list.getByLabel(`open ${SCANNED}`);
+ const bare = list.getByLabel(`open ${BARE}`);
+ // Titles where known, the id beneath them; the bare id alone otherwise.
+ await expect(downloaded).toContainText("Synthetic Test Video 1");
+ await expect(downloaded).toContainText(DOWNLOADED);
+ await expect(scanned).toContainText(SCANNED_TITLE);
+ await expect(scanned).toContainText(SCANNED);
+ await expect(bare).toHaveText(BARE);
+
+ // A word from the scan-store title, in the wrong case, leaves that one row.
+ const search = page.getByLabel("search videos");
+ await search.fill("harbor");
+ await expect(scanned).toBeVisible();
+ await expect(list.getByLabel(/^open fake/)).toHaveCount(1);
+
+ // The id still matches, as it always has.
+ await search.fill(BARE);
+ await expect(bare).toBeVisible();
+ await expect(list.getByLabel(/^open fake/)).toHaveCount(1);
+});
+
+test("an undownloaded video's page shows the listing scan's metadata", async ({
+ page,
+}) => {
+ await seed();
+ await page.goto(`/channels/${CHANNEL}/videos/${SCANNED}`);
+
+ await expect(
+ page.getByRole("heading", { name: SCANNED_TITLE }),
+ ).toBeVisible();
+ await expect(page.getByLabel("metadata source")).toHaveText(
+ "from the listing scan — not downloaded",
+ );
+ // Date and duration come from the scan entry too.
+ await expect(page.getByText("2024-03-15")).toBeVisible();
+ await expect(page.getByText("12:34")).toBeVisible();
+
+ // Collapsed by default; opening it shows the description.
+ const summary = page.getByText("Description", { exact: true });
+ await expect(summary).toBeVisible();
+ const body = page.getByText("A long talk on the pier.");
+ await expect(body).toBeHidden();
+ await summary.click();
+ await expect(body).toBeVisible();
+
+ // A downloaded video's page has no scan note.
+ await page.goto(`/channels/${CHANNEL}/videos/${DOWNLOADED}`);
+ await expect(
+ page.getByRole("heading", { name: "Synthetic Test Video 1" }),
+ ).toBeVisible();
+ await expect(page.getByLabel("metadata source")).toHaveCount(0);
+});