import { safeRevalidate } from "../../lib/safeRevalidate"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { relocationQueueKey } from "yt-dlp-transcript-common/lib/queueKeys"; import { runManagedFunction, type StreamActionResult, } from "yt-dlp-transcript-common/jobs/streamCommand"; import { formatBytes } from "yt-dlp-transcript-common/lib/format"; import { relocateSavedVideos } from "yt-dlp-transcript-common/controller/relocateSavedVideos"; import { savedVideosStoreBusyReason } from "./storeBusy"; // ONE ENQUEUE OF THE SAVED-VIDEO STORE MOVE, for the two callers that have one: // the Move control on /storage and its Resume. // // Deliberately NOT in `actions.ts` — that file carries "use server", where // every non-type export is a server action, so a shared helper exported from it // would put an unguarded enqueue on the wire under its own endpoint. Same // reason `lib/repointJob.ts` and `channels/lib/relocationJob.ts` exist. // // THE QUEUE KEY IS THE SHARED RELOCATION ONE. A store move, a channel move and // a re-point all rewrite symlinks under the same roots and all run their space // check when they START; the registry caps a key at concurrency 1, so one at a // time across the three is the whole point of sharing it. // // THE PROGRESS IS THE CHANNEL MOVE'S, for the reason the code is: it is the // same rsync, reported the same way, so /jobs draws the same bar. export async function enqueueSavedVideosRelocation(opts: { // "" moves the store back in place. locationId: string; }): Promise { const paths = getPaths(); const back = opts.locationId.trim() === ""; return runManagedFunction({ kind: "relocate-saved-videos", queueKey: relocationQueueKey(), paths, fn: async (onLog, signal, _setProgress, ctx) => { const taskId = "relocate:saved-videos"; let started = false; const result = await relocateSavedVideos({ paths, locationId: opts.locationId, // Asked again as the job's first step: the action asked at enqueue, // and the move may have waited behind others on the relocation queue. busy: () => savedVideosStoreBusyReason("moving the saved-video store", { runningOnly: true, }), onLog, // Added on the FIRST frame, not at start: the preflight and a resumed // run's verify pass transfer nothing, and a bar sitting at 0 % through // them would be claiming a copy had begun. onProgress: (p) => { if (!started) { started = true; ctx?.addTask({ id: taskId, label: back ? "saved-video store → in place" : `saved-video store → ${opts.locationId}`, kind: "relocate", startedAt: Date.now(), }); } ctx?.updateTask(taskId, { fraction: p.fraction, detail: p.detail }); }, signal, }).finally(() => { if (started) ctx?.removeTask(taskId); }); onLog( `${back ? "Moved back" : "Moved"} ${result.files} file(s) / ` + `${formatBytes(result.bytes)} — ${result.target}` + (result.resumed ? " (resumed an interrupted move)" : "") + (result.retried ? " (a change made during the copy was settled by a second mirror pass)" : ""), ); // No snapshot regen — the kind is in NO_REGEN_KINDS. The move changes // where the store's bytes are, not what any channel has: every count in // every report is identical afterwards. What DOES change is what these // pages read per render. safeRevalidate(["/storage", "/saved-videos"]); }, }); }