// A video whose only audio file is `audio..part` should surface in a // "Partial downloads" bucket on the channel page, with a Resume action that // kicks off the standard retry-bucket flow. Resume relies on yt-dlp's // implicit `-c` behavior to pick up where the prior attempt left off. import { rename, writeFile } from "node:fs/promises"; import { test, expect } from "@playwright/test"; import { channelStage, generateReport, pathExists, resetData, resolvePath, } from "./helpers"; const CHANNEL = "test-transcribe"; const CHANNEL_ROOT = `test-transcripts/channels/${CHANNEL}`; test("partial download surfaces in a bucket on the channel page", async ({ page, }) => { await resetData("one-transcribe-channel-with-audio"); // Strand vidA mid-download: only a .part file, no completed audio. await rename( resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a`), resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a.part`), ); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "download")); await expect( page.getByRole("heading", { name: /Partial downloads \(1\)/ }), ).toBeVisible(); await expect( page.getByLabel("partial downloads list").getByLabel("partial download vidA"), ).toBeVisible(); }); test("resume action re-downloads the partial video", async ({ page }) => { test.setTimeout(60_000); await resetData("one-transcribe-channel-with-audio"); await rename( resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a`), resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a.part`), ); // retry-bucket reads the channel playlist file to know which URL to fetch. // The slug-style URL keeps extractVideoId() === "vidA" without needing an // Odysee-style claim id. await writeFile( resolvePath(`${CHANNEL_ROOT}/playlist`), "https://www.youtube.com/watch?v=vidA\n", ); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "download")); await page .getByLabel("retry resume partial downloads bucket") .getByRole("button", { name: /^Retry \(1\)$/ }) .click(); const log = page.getByLabel("Retry resume partial downloads output"); await expect(log).toContainText("Retry allowlist: kept 1 of 1", { timeout: 30_000, }); await expect(log).toContainText("Managed download complete", { timeout: 30_000, }); expect( await pathExists(`${CHANNEL_ROOT}/data/vidA/audio.m4a`), ).toBe(true); }); test("resume retries a partial that carries an audio-check snapshot", async ({ page, }) => { // Regression: a genuine audio..part alongside an audio-check snapshot // (audio..part.good) used to be prefiltered out as "already complete" // because destinationExists() matched any audio.* not ending in .part. The // prefilter must agree with the bucket and keep the partial. test.setTimeout(60_000); await resetData("one-transcribe-channel-with-audio"); await rename( resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a`), resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a.part`), ); // The leftover audio-check snapshot that previously fooled the prefilter. await writeFile( resolvePath(`${CHANNEL_ROOT}/data/vidA/audio.m4a.part.good`), "snapshot\n", ); await writeFile( resolvePath(`${CHANNEL_ROOT}/playlist`), "https://www.youtube.com/watch?v=vidA\n", ); await generateReport(page, CHANNEL); await page.goto(channelStage(CHANNEL, "download")); await page .getByLabel("retry resume partial downloads bucket") .getByRole("button", { name: /^Retry \(1\)$/ }) .click(); const log = page.getByLabel("Retry resume partial downloads output"); await expect(log).toContainText("Prefilter: 1 missing destination files", { timeout: 30_000, }); await expect(log).not.toContainText("Nothing to fetch."); });