import { rm, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, pathExists, readJson, resetData, resolvePath, } from "./helpers"; test("store playlist writes a playlist file with 5 URLs", async ({ page }) => { await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); await page.getByRole("button", { name: "Store playlist" }).click(); await expect(page.getByLabel("Store playlist output")).toContainText( "Wrote 5 URLs", { timeout: 20_000 }, ); expect( await pathExists("test-transcripts/channels/test-pipeline/playlist"), ).toBe(true); }); test("download from playlist fetches all 5 URLs and writes the archive", async ({ page, }) => { test.setTimeout(120_000); await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); await page.getByRole("button", { name: "Store playlist" }).click(); await expect(page.getByLabel("Store playlist output")).toContainText( "Wrote 5 URLs", { timeout: 20_000 }, ); // Store playlist lives on the Playlist stage and the download on the // Download one; one panel is open at a time, so crossing between them is a // navigation. await page.goto(channelStage("test-pipeline", "download")); await page.getByRole("button", { name: "Download from playlist" }).click(); const downloadLog = page.getByLabel("Download from playlist output"); await expect(downloadLog).toContainText("5 new, 0 already archived", { timeout: 30_000, }); await expect(downloadLog).toContainText("Managed download complete", { timeout: 60_000, }); for (let i = 1; i <= 5; i++) { expect( await pathExists( `test-transcripts/channels/test-pipeline/data/fake0000000${i}/metadata.info.json`, ), ).toBe(true); } // Each managed download writes a per-video log alongside the data. expect( await pathExists( "test-transcripts/channels/test-pipeline/data/fake00000001/download.log", ), ).toBe(true); const config = await readJson<{ lastFullDownloadAt?: string }>( "test-transcripts/channels/test-pipeline/config.json", ); expect(typeof config.lastFullDownloadAt).toBe("string"); }); test("re-running download skips already-archived entries", async ({ page }) => { test.setTimeout(120_000); await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); await page.getByRole("button", { name: "Store playlist" }).click(); await expect(page.getByLabel("Store playlist output")).toContainText( "Wrote 5 URLs", { timeout: 20_000 }, ); await page.goto(channelStage("test-pipeline", "download")); await page.getByRole("button", { name: "Download from playlist" }).click(); const downloadLog = page.getByLabel("Download from playlist output"); await expect(downloadLog).toContainText("Managed download complete", { timeout: 60_000, }); await page.getByRole("button", { name: "Download from playlist" }).click(); await expect(downloadLog).toContainText("0 new, 5 already archived", { timeout: 20_000, }); await expect(downloadLog).toContainText("Nothing to fetch"); }); test("sync enumerates the channel, downloads new entries, writes lastSyncedAt", async ({ page, }) => { test.setTimeout(120_000); await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); await page.getByRole("button", { name: "Sync" }).click(); // Sync now pages the flat-playlist (fake emits 5 entries) and downloads the // archive diff per page via the managed per-URL path. await expect(page.getByLabel("Sync output")).toContainText("Sync complete", { timeout: 60_000, }); for (let i = 1; i <= 5; i++) { expect( await pathExists( `test-transcripts/channels/test-pipeline/data/fake0000000${i}/metadata.info.json`, ), ).toBe(true); } const config = await readJson<{ lastSyncedAt?: string }>( "test-transcripts/channels/test-pipeline/config.json", ); expect(typeof config.lastSyncedAt).toBe("string"); }); test("download videos skips a transcribed video even when its audio was cleaned up", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); const channelRoot = "test-transcripts/channels/test-transcribe"; // vidA: transcribed, audio cleaned up — should be skipped via transcript.json. await writeFile( resolvePath(`${channelRoot}/data/vidA/transcript.json`), JSON.stringify({ result: { language: "en" }, transcription: [] }), ); await rm(resolvePath(`${channelRoot}/data/vidA/audio.m4a`)); // vidB: still has audio.m4a — exercises the existing skip-via-audio path. // vidC: genuinely missing — clear its audio so it is the only fetch target. await rm(resolvePath(`${channelRoot}/data/vidC/audio.m4a`)); await writeFile( resolvePath(`${channelRoot}/playlist`), [ "https://www.youtube.com/watch?v=vidA", "https://www.youtube.com/watch?v=vidB", "https://www.youtube.com/watch?v=vidC", ].join("\n") + "\n", ); await generateReport(page, "test-transcribe"); await page.goto(channelStage("test-transcribe", "download")); await page.getByRole("button", { name: "Download videos" }).click(); const log = page.getByLabel("Download videos output"); await expect(log).toContainText( "Prefilter: 1 missing destination files, 2 already complete (of 3 total)", { timeout: 30_000 }, ); // vidA's transcript survives and its audio is not re-fetched. expect( await pathExists(`${channelRoot}/data/vidA/transcript.json`), ).toBe(true); expect(await pathExists(`${channelRoot}/data/vidA/audio.m4a`)).toBe(false); }); test("channel list sync button runs the platform-queue sync", async ({ page, }) => { await resetData("test-pipeline"); await page.goto("/channels"); await page.getByLabel("sync test-pipeline").click(); await expect(page.getByLabel("sync test-pipeline done")).toBeVisible({ timeout: 30_000, }); const config = await readJson<{ lastSyncedAt?: string }>( "test-transcripts/channels/test-pipeline/config.json", ); expect(typeof config.lastSyncedAt).toBe("string"); }); test("blocks pipeline buttons when channel has no url", async ({ page }) => { await resetData("empty"); await generateReport(page, "new"); await page.goto("/channels/new"); await page.getByLabel(/^name/i).fill("No URL Channel"); await page.getByLabel(/^slug/i).fill("no-url"); await page.getByRole("button", { name: /create channel/i }).click(); await page.waitForURL("**/channels/no-url", { timeout: 10_000 }); // Creation lands on the channel overview; the no-URL notice and the buttons // it blocks both live on the Playlist stage. await page.goto(channelStage("no-url", "playlist")); await expect(page.getByText(/set a/i)).toBeVisible(); await expect( page.getByRole("button", { name: "Store playlist" }), ).not.toBeVisible(); });