import { readdir, readFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, pathExists, resetData, resolvePath, } from "./helpers"; // The Playlist stage's "Import single video" panel fetches one off-playlist // video by URL into the channel using downloadOneManaged (the same primitive // the per-video Download button uses), appending it to the archive. test("import single video by URL fetches it and appends to the archive", async ({ page, }) => { test.setTimeout(60_000); await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); const importBtn = page.getByRole("button", { name: "Import video" }); // The button is gated until a URL is entered. await expect(importBtn).toBeDisabled(); await page .getByLabel("Video URL to import") .fill("https://www.youtube.com/watch?v=oneoff00001"); await expect(importBtn).toBeEnabled(); await importBtn.click(); const log = page.getByLabel("Import video output"); await expect(log).toContainText("oneoff00001", { timeout: 30_000 }); // The video lands in the canonical data// dir, with metadata + transcript // for a youtube-handling channel. The log above matches as soon as the // command echo (which contains the URL) streams, which can be before the // prefetch has written the file — so poll for the artifact rather than // reading it once. await expect .poll( () => pathExists( "test-transcripts/channels/test-pipeline/data/oneoff00001/metadata.info.json", ), { timeout: 30_000 }, ) .toBe(true); // And it's recorded in the channel archive so a later sync won't re-fetch it. await expect(async () => { const archive = await readFile( resolvePath("test-transcripts/channels/test-pipeline/archive"), "utf8", ); expect(archive).toContain("youtube oneoff00001"); }).toPass({ timeout: 10_000 }); }); test("import rejects a non-URL input", async ({ page }) => { await resetData("test-pipeline"); await generateReport(page, "test-pipeline"); await page.goto(channelStage("test-pipeline", "playlist")); await page.getByLabel("Video URL to import").fill("not a url"); await page.getByRole("button", { name: "Import video" }).click(); await expect(page.getByLabel("Import video error")).toContainText( "valid video URL", { timeout: 10_000 }, ); }); // A BitChute video imported into an Odysee (transcribe) channel runs the way BitChute's own // downloads do: its canonical page, BitChute's queue (one transfer at a time) // and yt-dlp args — the queue control's default (the channel's queue) is no // choice — the record labelled from yt-dlp's BitChute extractor, and a second // import of it refused without asking BitChute again. test("a BitChute import runs on BitChute's queue and pace, and is never fetched twice", async ({ page, }) => { test.setTimeout(90_000); await resetData("one-transcribe-channel"); await generateReport(page, "test-transcribe"); await page.goto(channelStage("test-transcribe", "playlist")); const id = "bcOneoff0001"; await page .getByLabel("Video URL to import") .fill(`https://old.bitchute.com/video/${id}/`); const importBtn = page.getByRole("button", { name: "Import video" }); await importBtn.click(); const log = page.getByLabel("Import video output"); await expect(log).toContainText(`https://www.bitchute.com/video/${id}/`, { timeout: 30_000, }); await expect(log).toContainText("--sleep-requests 3"); const metaPath = `test-transcripts/channels/test-transcribe/data/${id}/metadata.info.json`; await expect.poll(() => pathExists(metaPath), { timeout: 30_000 }).toBe(true); const meta = JSON.parse(await readFile(resolvePath(metaPath), "utf8")); expect(meta.extractor_key).toBe("BitChute"); // The job ran on BitChute's queue, not the channel's. await expect(async () => { const dir = resolvePath("test-transcripts/.jobs"); const queues: string[] = []; for (const name of await readdir(dir)) { if (!name.endsWith(".meta.json")) continue; const m = JSON.parse(await readFile(`${dir}/${name}`, "utf8")); if (m.kind === "import-one" && m.videoId === id) queues.push(m.queueKey); } expect(queues).toEqual(["platform:bitchute"]); }).toPass({ timeout: 15_000 }); // Imported again: refused as already downloaded, nothing fetched. The first // run's end refreshes the page, and a click that lands during that refresh // never reaches the button's handler — so click until the notice shows. An // import that was NOT refused never shows it, and still fails here. await expect(importBtn).toBeEnabled({ timeout: 30_000 }); await expect(async () => { await importBtn.click(); await expect(page.getByLabel("Import video notice")).toContainText( "Already downloaded", { timeout: 3_000 }, ); }).toPass({ timeout: 30_000 }); // However many clicks that took, BitChute was asked once. const imports: string[] = []; for (const name of await readdir(resolvePath("test-transcripts/.jobs"))) { if (!name.endsWith(".meta.json")) continue; const m = JSON.parse(await readFile(resolvePath(`test-transcripts/.jobs/${name}`), "utf8")); if (m.kind === "import-one" && m.videoId === id) imports.push(m.id); } expect(imports).toHaveLength(1); });