"use server"; import { revalidatePath } from "next/cache"; import { safeRevalidate } from "../lib/safeRevalidate"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { saveSettings } from "../settings/saveSettings"; import { SYNC_INTERVAL_MAX_MINUTES } from "yt-dlp-transcript-common/lib/channelConfig"; import { formValues, type FormState } from "../lib/formState"; import { backupSavedVideos, verifySavedVideoBackup, } from "yt-dlp-transcript-common/controller/backupSavedVideos"; import { runManagedFunction, type StreamActionResult, } from "yt-dlp-transcript-common/jobs/streamCommand"; // Saved-video backups are a global (not per-channel) concern, so they share a // dedicated queue rather than a channel queue. const SAVED_VIDEOS_QUEUE = "saved-videos"; // Mirror the saved-video store to the configured backup destination and write a // fresh manifest. The destination comes from settings (savedVideoBackup.dest); // an explicit `dest` overrides it for an ad-hoc run. export async function backupSavedVideosAction( dest?: string, queueKey?: string, ): Promise { const paths = getPaths(); const settings = getSettings(); const target = (dest ?? settings.savedVideoBackup.dest).trim(); if (!target) { return { ok: false, error: "No backup destination configured" }; } return runManagedFunction({ kind: "backup-saved-videos", queueKey: queueKey === undefined ? SAVED_VIDEOS_QUEUE : queueKey.trim(), paths, fn: async (onLog, signal) => { const result = await backupSavedVideos({ paths, dest: target, onLog, signal, }); onLog( `Saved-video backup: ${result.backedUp}/${result.entries} container(s), ${result.bytes} bytes.`, ); safeRevalidate(["/saved-videos"]); }, }); } // Verify the backup destination against its manifest and report drift. export async function verifySavedVideoBackupAction( dest?: string, queueKey?: string, ): Promise { const paths = getPaths(); const settings = getSettings(); const target = (dest ?? settings.savedVideoBackup.dest).trim(); if (!target) { return { ok: false, error: "No backup destination configured" }; } return runManagedFunction({ kind: "verify-saved-video-backup", queueKey: queueKey === undefined ? SAVED_VIDEOS_QUEUE : queueKey.trim(), paths, fn: async (onLog, signal) => { const drift = await verifySavedVideoBackup({ paths, dest: target, onLog, signal, }); onLog( drift.ok ? `Backup verified clean (checked ${drift.checked}).` : `Backup drift: ${drift.missing.length} missing, ${drift.sizeMismatch.length} size-mismatch, ${drift.checksumMismatch.length} checksum-mismatch, ${drift.extra.length} extra.`, ); }, }); } // A failure carries what was submitted (lib/formState.ts). export type SaveBackupConfigResult = FormState; // Persist just the saved-video backup block (dest / enabled / interval) from the // Saved Videos page, leaving the rest of SiteSettings untouched. writeSettings // re-sanitizes (blank dest forces enabled off; interval clamped). export async function saveSavedVideoBackupAction( _prev: SaveBackupConfigResult | undefined, formData: FormData, ): Promise { const values = formValues(formData); const dest = String(formData.get("backupDest") ?? "").trim(); const enabled = formData.get("backupEnabled") === "on"; const intervalRaw = String(formData.get("backupIntervalMinutes") ?? "").trim(); const intervalMinutes = Number.parseInt(intervalRaw, 10); if (intervalRaw && (!Number.isFinite(intervalMinutes) || intervalMinutes < 1)) { return { ok: false, error: `Backup interval must be 1–${SYNC_INTERVAL_MAX_MINUTES} minutes`, values, }; } const settings = getSettings(); try { await saveSettings({ savedVideoBackup: { enabled, dest, intervalMinutes: intervalRaw ? intervalMinutes : settings.savedVideoBackup.intervalMinutes, }, }); } catch (e) { return { ok: false, error: (e as Error).message, values }; } revalidatePath("/saved-videos"); return { ok: true }; }