Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 3d273ea14b4d564542222c05c3d327548dfef577
parent 7599fd4db54827e207c0fbeed6e73a9976f76d56
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 27 Apr 2026 23:16:15 -0400

fix(ytdlp): treat exit code 101 as success and update last-sync timestamps

yt-dlp's exit code 101 is its documented "stop iterating" code (emitted by
--break-on-existing / --max-downloads). Previously runChildAndStream and
storePlaylist threw on any non-zero exit, so a Sync that legitimately had
nothing new to fetch was marked failed and touchLastSync/touchLastFullDownload
never ran. Treat 101 the same as 0; lastSyncedAt now updates on no-op syncs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/ytdlp/runYtdlp.ts | 18++++++++++++++++--
Aeditor/cypress/e2e/sync-break-on-existing.cy.ts | 45+++++++++++++++++++++++++++++++++++++++++++++
Meditor/cypress/fixtures/bin/fake-ytdlp.mjs | 7+++++++
Aeditor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/archive | 3+++
Aeditor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/config.json | 6++++++
5 files changed, 77 insertions(+), 2 deletions(-)

diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts @@ -103,9 +103,14 @@ async function storePlaylist(opts: RunYtdlpOpts): Promise<void> { child.stderr?.on("data", (c: Buffer) => opts.onLog(c.toString("utf8"))); const result = await child; - if (result.exitCode !== 0) { + // yt-dlp exit code convention: 101 = "break-on-existing" / "max-downloads" + // (clean stop, not an error). Treat it the same as 0. + if (result.exitCode !== 0 && result.exitCode !== 101) { throw new Error(`yt-dlp exited with code ${result.exitCode}`); } + if (result.exitCode === 101) { + opts.onLog(`yt-dlp stopped on existing entry (exit 101).\n`); + } const stdout = String(result.stdout ?? ""); const urls = stdout @@ -220,9 +225,18 @@ async function runChildAndStream( }); child.all?.on("data", (c: Buffer) => opts.onLog(c.toString("utf8"))); const result = await child; - if (result.exitCode !== 0 && !opts.signal.aborted) { + // yt-dlp exit code convention: 101 = "break-on-existing" / "max-downloads" + // (clean stop, not an error). Treat it the same as 0. + if ( + result.exitCode !== 0 && + result.exitCode !== 101 && + !opts.signal.aborted + ) { throw new Error(`yt-dlp exited with code ${result.exitCode}`); } + if (result.exitCode === 101) { + opts.onLog(`yt-dlp stopped on existing entry (exit 101).\n`); + } } async function touchLastSync(opts: RunYtdlpOpts): Promise<void> { diff --git a/editor/cypress/e2e/sync-break-on-existing.cy.ts b/editor/cypress/e2e/sync-break-on-existing.cy.ts @@ -0,0 +1,45 @@ +describe("Sync with --break-on-existing (yt-dlp exit 101)", () => { + it("treats yt-dlp exit code 101 as success and updates lastSyncedAt", () => { + cy.resetData("sync-already-archived"); + cy.visit("/channels/archived-channel"); + + cy.findByTestId("pipeline-sync").find("button").click(); + cy.findByTestId("pipeline-sync-log", { timeout: 15_000 }).should( + "contain.text", + "exit 101", + ); + + // Run button should re-enable and there's no Cancel button (i.e., not still + // running and not in an error state). + cy.findByTestId("pipeline-sync") + .find("button") + .should("not.be.disabled") + .and("contain.text", "Sync"); + cy.findByTestId("pipeline-sync-cancel").should("not.exist"); + cy.findByTestId("pipeline-sync-error").should("not.exist"); + + // Config now has lastSyncedAt set to a parseable ISO string. + cy.task( + "readJson", + "test-transcripts/channels/archived-channel/config.json", + ).then((config) => { + const c = config as { lastSyncedAt?: string }; + expect(c.lastSyncedAt, "lastSyncedAt").to.be.a("string"); + expect( + Number.isNaN(Date.parse(c.lastSyncedAt!)), + "lastSyncedAt parses", + ).to.equal(false); + }); + + // The job lands as `done`, not `failed`. + cy.visit("/jobs"); + cy.findByTestId("jobs-table").within(() => { + cy.contains("sync"); + }); + cy.get('[data-testid^="job-row-"]') + .first() + .within(() => { + cy.contains(/^done$/i); + }); + }); +}); diff --git a/editor/cypress/fixtures/bin/fake-ytdlp.mjs b/editor/cypress/fixtures/bin/fake-ytdlp.mjs @@ -133,6 +133,13 @@ async function main() { await sleep(30_000); } + // Emit a friendly message and exit 101 so the editor's exit-code handling + // can be tested against the "nothing new to download" case. + if (has("--cy-break-on-existing")) { + process.stderr.write(`[fake-ytdlp] all entries already in archive\n`); + process.exit(101); + } + if ( has("--flat-playlist") && has("--skip-download") && diff --git a/editor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/archive b/editor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/archive @@ -0,0 +1,3 @@ +youtube oldvid000001 +youtube oldvid000002 +youtube oldvid000003 diff --git a/editor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/config.json b/editor/cypress/fixtures/test-transcripts/sync-already-archived/channels/archived-channel/config.json @@ -0,0 +1,6 @@ +{ + "handling": "youtube", + "name": "Archived Channel", + "url": "https://www.youtube.com/@archived/videos", + "ytdlpExtraArgs": ["--cy-break-on-existing"] +}