Archilyzer · Source

archilyzer

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

commit 956eb67bf450a99ec72f1fe6a868ae990a8e85f5
parent feef1dd454f68e27d7702271bc2d19fd8779394a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 26 Jun 2026 14:55:45 -0400

Phase 2: collapse the runJobSpec switch into a replay-handler lookup

Move each per-kind case body (and the idsForBucket helper + str/bool/num
coercers) out of the runJobSpec switch into a data-driven table,
editor/app/jobs/jobReplayRegistry.ts (JOB_REPLAY_HANDLERS keyed by kind).
runJobSpec is now a thin dispatcher: look up the handler, or return the same
"Cannot re-run job kind" error on a miss. All 18 kinds moved verbatim; outputs
identical. This is the seam the Phase 8 retry actions reuse.

Server-only module; typecheck (editor) clean; bookmarks + jobs e2e green
(incl. the Run-again re-derive and retry-partial replay tests).

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

Diffstat:
Aeditor/app/jobs/jobReplayRegistry.ts | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/jobs/runJobSpec.ts | 147++++++-------------------------------------------------------------------------
2 files changed, 198 insertions(+), 136 deletions(-)

diff --git a/editor/app/jobs/jobReplayRegistry.ts b/editor/app/jobs/jobReplayRegistry.ts @@ -0,0 +1,187 @@ +// The data-driven replay table: maps a stored JobSpec's `kind` to the server +// action that re-runs it. Previously this was a hard-coded switch in +// runJobSpec.ts; collapsing it to a lookup means adding a bookmarkable kind is +// one colocated entry here (plus its jobKinds.ts metadata) instead of a new +// switch case. runJobSpec.ts is now a thin dispatcher over this table, and the +// retry actions (Phase 8) reuse the same seam. +// +// Server-only: reached through bookmarkActions ("use server"), so it is never +// bundled to the client. +import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { readChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; +import type { JobSpec, ReplayBucket } from "yt-dlp-transcript-common/jobs/jobSpec"; +import type { StreamActionResult } from "yt-dlp-transcript-common/jobs/streamCommand"; +import type { AudioFormat } from "yt-dlp-transcript-common/lib/channelConfig"; +import { + downloadAction, + downloadMissingAction, + downloadMissingSubsAction, + retryBucketAction, + storePlaylistAction, + syncAction, +} from "../channels/[slug]/pipelineActions"; +import { + checkKeptDeletedAction, + cleanAudioAction, + cleanExtraAudioFormatsAction, + clearFailedTranscodingsAction, + clearFailedTranscriptionsAction, + removeFailedTranscodingsAction, + removeWrongFormatAudioAction, + transcodeFailuresAction, + transcodeUntranscodedAction, + transcribeBucketAction, + transcribeMissingAction, +} from "../channels/[slug]/whisperActions"; +import { persistKeptAction } from "../channels/[slug]/persistActions"; + +export type ReplayHandler = (spec: JobSpec) => Promise<StreamActionResult>; + +// Bucket jobs re-derive their work from the channel's CURRENT snapshot (not a +// frozen id list), so re-running "retry partial downloads" always acts on +// whatever is partial right now. +async function idsForBucket( + slug: string, + bucket: ReplayBucket, +): Promise<string[]> { + const snap = await readChannelSnapshot(getPaths(), slug); + const ids = snap?.buckets?.[bucket]; + return Array.isArray(ids) ? ids : []; +} + +const str = (v: unknown): string | undefined => + typeof v === "string" ? v : undefined; +const bool = (v: unknown): boolean | undefined => + typeof v === "boolean" ? v : undefined; +const num = (v: unknown): number | undefined => + typeof v === "number" ? v : undefined; + +// Flag-style params + the captured queueKey for a spec. +function params(spec: JobSpec): { + p: Record<string, unknown>; + queueKey: string | undefined; +} { + const p = spec.params ?? {}; + return { p, queueKey: str(p.queueKey) }; +} + +export const JOB_REPLAY_HANDLERS: Record<string, ReplayHandler> = { + "whisper-all": (spec) => { + const { p, queueKey } = params(spec); + return transcribeMissingAction( + spec.slug, + queueKey, + bool(p.reverse), + str(p.audioFormat) as AudioFormat | undefined, + bool(p.strictAudioFormat), + num(p.shardTotal), + num(p.shardIndex), + ); + }, + "whisper-bucket-downloaded-no-transcript": async (spec) => { + const { p, queueKey } = params(spec); + if (!spec.bucket) return { ok: false, error: "Bookmark is missing its bucket." }; + const ids = await idsForBucket(spec.slug, spec.bucket); + if (ids.length === 0) { + return { ok: false, error: "Nothing to transcribe right now.", info: true }; + } + return transcribeBucketAction( + spec.slug, + ids, + queueKey, + str(p.audioFormat) as AudioFormat | undefined, + bool(p.strictAudioFormat), + spec.bucket, + ); + }, + "retry-bucket": async (spec) => { + const { p, queueKey } = params(spec); + if (!spec.bucket) return { ok: false, error: "Bookmark is missing its bucket." }; + const ids = await idsForBucket(spec.slug, spec.bucket); + if (ids.length === 0) { + return { ok: false, error: "Nothing to retry right now.", info: true }; + } + return retryBucketAction( + spec.slug, + ids, + queueKey, + bool(p.abortOnError), + str(p.handlingOverride), + spec.bucket, + ); + }, + "download-from-playlist": (spec) => { + const { p, queueKey } = params(spec); + return downloadAction(spec.slug, queueKey, bool(p.abortOnError)); + }, + "download-missing": (spec) => { + const { p, queueKey } = params(spec); + return downloadMissingAction( + spec.slug, + queueKey, + bool(p.ignoreArchive), + bool(p.abortOnError), + num(p.shardTotal), + num(p.shardIndex), + bool(p.keepSourceVideoOverride), + bool(p.extractImmediately), + str(p.audioFormatOverride) as AudioFormat | undefined, + ); + }, + sync: (spec) => { + const { queueKey } = params(spec); + return syncAction(spec.slug, queueKey); + }, + "download-missing-subs": (spec) => { + const { p, queueKey } = params(spec); + return downloadMissingSubsAction(spec.slug, queueKey, bool(p.abortOnError)); + }, + "store-playlist": (spec) => { + const { queueKey } = params(spec); + return storePlaylistAction(spec.slug, queueKey); + }, + "clear-failed-transcriptions": (spec) => { + const { queueKey } = params(spec); + return clearFailedTranscriptionsAction(spec.slug, queueKey); + }, + "transcode-failures": (spec) => { + const { p, queueKey } = params(spec); + return transcodeFailuresAction( + spec.slug, + str(p.targetFormat) as AudioFormat, + queueKey, + ); + }, + "transcode-untranscoded": (spec) => { + const { queueKey } = params(spec); + return transcodeUntranscodedAction(spec.slug, queueKey); + }, + "remove-failed-transcodings": (spec) => { + const { queueKey } = params(spec); + return removeFailedTranscodingsAction(spec.slug, queueKey); + }, + "clear-failed-transcodings": (spec) => { + const { queueKey } = params(spec); + return clearFailedTranscodingsAction(spec.slug, queueKey); + }, + "clean-extra-audio-formats": (spec) => { + const { queueKey } = params(spec); + return cleanExtraAudioFormatsAction(spec.slug, queueKey); + }, + "remove-wrong-format-audio": (spec) => { + const { queueKey } = params(spec); + return removeWrongFormatAudioAction(spec.slug, queueKey); + }, + "clean-audio-transcribed": (spec) => { + const { queueKey } = params(spec); + return cleanAudioAction(spec.slug, queueKey); + }, + "check-kept-deleted": (spec) => { + const { queueKey } = params(spec); + return checkKeptDeletedAction(spec.slug, queueKey); + }, + "persist-kept": (spec) => { + const { queueKey } = params(spec); + return persistKeptAction(spec.slug, queueKey); + }, +}; diff --git a/editor/app/jobs/runJobSpec.ts b/editor/app/jobs/runJobSpec.ts @@ -1,143 +1,18 @@ // Server-only dispatcher: reached only through bookmarkActions ("use server"), // so it is never bundled to the client. -import { getPaths } from "yt-dlp-transcript-common/lib/paths"; -import { readChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; -import type { JobSpec, ReplayBucket } from "yt-dlp-transcript-common/jobs/jobSpec"; +// +// The per-kind replay logic lives in jobReplayRegistry.ts (a data-driven +// lookup); this is just the dispatch over it. Re-running RE-DERIVES the work +// from the channel's CURRENT state rather than replaying a frozen list — see +// jobReplayRegistry.ts and jobSpec.ts. +import type { JobSpec } from "yt-dlp-transcript-common/jobs/jobSpec"; import type { StreamActionResult } from "yt-dlp-transcript-common/jobs/streamCommand"; -import type { AudioFormat } from "yt-dlp-transcript-common/lib/channelConfig"; -import { - downloadAction, - downloadMissingAction, - downloadMissingSubsAction, - retryBucketAction, - storePlaylistAction, - syncAction, -} from "../channels/[slug]/pipelineActions"; -import { - checkKeptDeletedAction, - cleanAudioAction, - cleanExtraAudioFormatsAction, - clearFailedTranscodingsAction, - clearFailedTranscriptionsAction, - removeFailedTranscodingsAction, - removeWrongFormatAudioAction, - transcodeFailuresAction, - transcodeUntranscodedAction, - transcribeBucketAction, - transcribeMissingAction, -} from "../channels/[slug]/whisperActions"; -import { persistKeptAction } from "../channels/[slug]/persistActions"; - -// The single place that maps a stored JobSpec back to the server action that -// runs it. Bucket jobs re-derive their work from the channel's CURRENT snapshot -// (not a frozen id list) via idsForBucket, so re-running "retry partial -// downloads" always acts on whatever is partial right now. - -async function idsForBucket( - slug: string, - bucket: ReplayBucket, -): Promise<string[]> { - const snap = await readChannelSnapshot(getPaths(), slug); - const ids = snap?.buckets?.[bucket]; - return Array.isArray(ids) ? ids : []; -} - -const str = (v: unknown): string | undefined => - typeof v === "string" ? v : undefined; -const bool = (v: unknown): boolean | undefined => - typeof v === "boolean" ? v : undefined; -const num = (v: unknown): number | undefined => - typeof v === "number" ? v : undefined; +import { JOB_REPLAY_HANDLERS } from "./jobReplayRegistry"; export async function runJobSpec(spec: JobSpec): Promise<StreamActionResult> { - const p = spec.params ?? {}; - const queueKey = str(p.queueKey); - switch (spec.kind) { - case "whisper-all": - return transcribeMissingAction( - spec.slug, - queueKey, - bool(p.reverse), - str(p.audioFormat) as AudioFormat | undefined, - bool(p.strictAudioFormat), - num(p.shardTotal), - num(p.shardIndex), - ); - case "whisper-bucket-downloaded-no-transcript": { - if (!spec.bucket) return { ok: false, error: "Bookmark is missing its bucket." }; - const ids = await idsForBucket(spec.slug, spec.bucket); - if (ids.length === 0) { - return { ok: false, error: "Nothing to transcribe right now.", info: true }; - } - return transcribeBucketAction( - spec.slug, - ids, - queueKey, - str(p.audioFormat) as AudioFormat | undefined, - bool(p.strictAudioFormat), - spec.bucket, - ); - } - case "retry-bucket": { - if (!spec.bucket) return { ok: false, error: "Bookmark is missing its bucket." }; - const ids = await idsForBucket(spec.slug, spec.bucket); - if (ids.length === 0) { - return { ok: false, error: "Nothing to retry right now.", info: true }; - } - return retryBucketAction( - spec.slug, - ids, - queueKey, - bool(p.abortOnError), - str(p.handlingOverride), - spec.bucket, - ); - } - case "download-from-playlist": - return downloadAction(spec.slug, queueKey, bool(p.abortOnError)); - case "download-missing": - return downloadMissingAction( - spec.slug, - queueKey, - bool(p.ignoreArchive), - bool(p.abortOnError), - num(p.shardTotal), - num(p.shardIndex), - bool(p.keepSourceVideoOverride), - bool(p.extractImmediately), - str(p.audioFormatOverride) as AudioFormat | undefined, - ); - case "sync": - return syncAction(spec.slug, queueKey); - case "download-missing-subs": - return downloadMissingSubsAction(spec.slug, queueKey, bool(p.abortOnError)); - case "store-playlist": - return storePlaylistAction(spec.slug, queueKey); - case "clear-failed-transcriptions": - return clearFailedTranscriptionsAction(spec.slug, queueKey); - case "transcode-failures": - return transcodeFailuresAction( - spec.slug, - str(p.targetFormat) as AudioFormat, - queueKey, - ); - case "transcode-untranscoded": - return transcodeUntranscodedAction(spec.slug, queueKey); - case "remove-failed-transcodings": - return removeFailedTranscodingsAction(spec.slug, queueKey); - case "clear-failed-transcodings": - return clearFailedTranscodingsAction(spec.slug, queueKey); - case "clean-extra-audio-formats": - return cleanExtraAudioFormatsAction(spec.slug, queueKey); - case "remove-wrong-format-audio": - return removeWrongFormatAudioAction(spec.slug, queueKey); - case "clean-audio-transcribed": - return cleanAudioAction(spec.slug, queueKey); - case "check-kept-deleted": - return checkKeptDeletedAction(spec.slug, queueKey); - case "persist-kept": - return persistKeptAction(spec.slug, queueKey); - default: - return { ok: false, error: `Cannot re-run job kind: ${spec.kind}` }; + const handler = JOB_REPLAY_HANDLERS[spec.kind]; + if (!handler) { + return { ok: false, error: `Cannot re-run job kind: ${spec.kind}` }; } + return handler(spec); }