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:
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);
}