fix(cron): start manual jobs in the background and report busy jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 22:23:28 -04:00
1 parent 085b681d8a
commit a6d642616b
24 files changed
+373 -40

No files matched your search

+7 -6
View File
@@ -89,13 +89,14 @@ export function getJobSchedules(): {
})); }));
} }
/** Manually trigger a job by name. Returns false if job not found or already running. */ /** Start a job by name without waiting for it to finish. */
export async function triggerJob(name: string): Promise<boolean> { export function triggerJob(name: string): "started" | "not_found" | "busy" {
const job = jobs.get(name); const job = jobs.get(name);
if (!job) return false; if (!job) return "not_found";
if (job.isBusy()) return false; if (job.isBusy()) return "busy";
await job.trigger(); // The scheduled wrapper records the run and catches its errors; don't hold the request open.
return true; void job.trigger();
return "started";
} }
// Refresh titles where lastFetchedAt is stale // Refresh titles where lastFetchedAt is stale
+7
View File
@@ -5,6 +5,7 @@ import { cors } from "hono/cors";
import { CACHE_DIR } from "@sofa/config"; import { CACHE_DIR } from "@sofa/config";
import { ensureBackupDir } from "@sofa/core/backup"; import { ensureBackupDir } from "@sofa/core/backup";
import { recoverInterruptedCronRuns } from "@sofa/core/cron";
import { ensureImageDirs, imageCacheEnabled } from "@sofa/core/image-cache"; import { ensureImageDirs, imageCacheEnabled } from "@sofa/core/image-cache";
import { registerJobScheduleProvider } from "@sofa/core/system-health"; import { registerJobScheduleProvider } from "@sofa/core/system-health";
import { closeDatabase, isDatabaseAccessBlocked } from "@sofa/db/client"; import { closeDatabase, isDatabaseAccessBlocked } from "@sofa/db/client";
@@ -48,6 +49,12 @@ if (recoveredJobs > 0) {
log.warn(`Recovered ${recoveredJobs} stale import job(s) from previous shutdown`); log.warn(`Recovered ${recoveredJobs} stale import job(s) from previous shutdown`);
} }
// Background job runs left "running" by a previous crash would otherwise look active forever
const interruptedRuns = recoverInterruptedCronRuns();
if (interruptedRuns > 0) {
log.warn(`Marked ${interruptedRuns} interrupted background job run(s) as failed`);
}
// Payloads of finished imports are never read again; reclaim the space (and keep backups small). // Payloads of finished imports are never read again; reclaim the space (and keep backups small).
const clearedPayloads = clearFinishedImportPayloads(); const clearedPayloads = clearFinishedImportPayloads();
if (clearedPayloads > 0) { if (clearedPayloads > 0) {
+9 -3
View File
@@ -126,14 +126,20 @@ export const backupsUpdateSchedule = os.admin.backups.updateSchedule
// ─── Jobs ───────────────────────────────────────────────────── // ─── Jobs ─────────────────────────────────────────────────────
export const triggerJob = os.admin.triggerJob.use(admin).handler(async ({ input }) => { export const triggerJob = os.admin.triggerJob.use(admin).handler(({ input }) => {
const triggered = await triggerCronJob(input.name); const outcome = triggerCronJob(input.name);
if (!triggered) { if (outcome === "not_found") {
throw new ORPCError("NOT_FOUND", { throw new ORPCError("NOT_FOUND", {
message: "Job not found", message: "Job not found",
data: { code: AppErrorCode.JOB_NOT_FOUND }, data: { code: AppErrorCode.JOB_NOT_FOUND },
}); });
} }
if (outcome === "busy") {
throw new ORPCError("CONFLICT", {
message: "Job is already running",
data: { code: AppErrorCode.JOB_ALREADY_RUNNING },
});
}
return { ok: true as const }; return { ok: true as const };
}); });
@@ -34,6 +34,9 @@ import { i18n } from "@sofa/i18n";
const DIGITS_ONLY_RE = /^\d+$/; const DIGITS_ONLY_RE = /^\d+$/;
/** Scheduled jobs the API does not allow triggering manually. */
const NOT_TRIGGERABLE = new Set(["optimizeDb"]);
/** Convert a cron pattern to a short human-readable string */ /** Convert a cron pattern to a short human-readable string */
function cronToHuman(pattern: string): string { function cronToHuman(pattern: string): string {
const parts = pattern.split(" "); const parts = pattern.split(" ");
@@ -316,6 +319,9 @@ function BackgroundJobsCard({
cacheImages: t`Image cache`, cacheImages: t`Image cache`,
scheduledBackup: t`Backup`, scheduledBackup: t`Backup`,
updateCheck: t`Update check`, updateCheck: t`Update check`,
refreshCredits: t`Cast & crew`,
telemetryReport: t`Telemetry`,
optimizeDb: t`Database maintenance`,
}; };
const triggerJobMutation = useMutation( const triggerJobMutation = useMutation(
@@ -483,36 +489,38 @@ function BackgroundJobsCard({
{/* Trigger button */} {/* Trigger button */}
<TableCell className="pe-5 text-end"> <TableCell className="pe-5 text-end">
<Tooltip> {!NOT_TRIGGERABLE.has(job.jobName) && (
<TooltipTrigger <Tooltip>
render={ <TooltipTrigger
<Button render={
variant="ghost" <Button
size="icon" variant="ghost"
aria-label={t`Trigger job`} size="icon"
className="size-6" aria-label={t`Trigger job`}
disabled={isRunning || job.disabled} className="size-6"
onClick={() => disabled={isRunning || job.disabled}
triggerJobMutation.mutate({ onClick={() =>
name: job.jobName as CronJobName, triggerJobMutation.mutate({
}) name: job.jobName as CronJobName,
} })
/> }
} />
> }
{isRunning ? ( >
<Spinner className="size-3" /> {isRunning ? (
) : ( <Spinner className="size-3" />
<IconPlayerPlay ) : (
aria-hidden={true} <IconPlayerPlay
className="text-muted-foreground/70 size-3" aria-hidden={true}
/> className="text-muted-foreground/70 size-3"
)} />
</TooltipTrigger> )}
<TooltipContent> </TooltipTrigger>
<Trans>Run now</Trans> <TooltipContent>
</TooltipContent> <Trans>Run now</Trans>
</Tooltip> </TooltipContent>
</Tooltip>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
+71
View File
@@ -6635,6 +6635,77 @@
} }
} }
} }
},
"409": {
"description": "409",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"properties": {
"defined": {
"const": true
},
"code": {
"const": "CONFLICT"
},
"status": {
"const": 409
},
"message": {
"type": "string",
"default": "Job is already running"
},
"data": {
"type": "object",
"properties": {
"code": {
"const": "JOB_ALREADY_RUNNING"
}
},
"required": [
"code"
]
}
},
"required": [
"defined",
"code",
"status",
"message",
"data"
]
},
{
"type": "object",
"properties": {
"defined": {
"const": false
},
"code": {
"type": "string"
},
"status": {
"type": "number"
},
"message": {
"type": "string"
},
"data": {}
},
"required": [
"defined",
"code",
"status",
"message"
]
}
]
}
}
}
} }
}, },
"security": [ "security": [
+4
View File
@@ -669,6 +669,10 @@ export const contract = {
message: "Job not found", message: "Job not found",
data: appErrorData(AppErrorCode.JOB_NOT_FOUND), data: appErrorData(AppErrorCode.JOB_NOT_FOUND),
}, },
CONFLICT: {
message: "Job is already running",
data: appErrorData(AppErrorCode.JOB_ALREADY_RUNNING),
},
}), }),
purgeMetadataCache: oc purgeMetadataCache: oc
.route({ .route({
+1
View File
@@ -10,6 +10,7 @@ export const AppErrorCode = {
BACKUP_DELETE_FAILED: "BACKUP_DELETE_FAILED", BACKUP_DELETE_FAILED: "BACKUP_DELETE_FAILED",
BACKUP_RESTORE_FAILED: "BACKUP_RESTORE_FAILED", BACKUP_RESTORE_FAILED: "BACKUP_RESTORE_FAILED",
JOB_NOT_FOUND: "JOB_NOT_FOUND", JOB_NOT_FOUND: "JOB_NOT_FOUND",
JOB_ALREADY_RUNNING: "JOB_ALREADY_RUNNING",
TMDB_NOT_CONFIGURED: "TMDB_NOT_CONFIGURED", TMDB_NOT_CONFIGURED: "TMDB_NOT_CONFIGURED",
IMPORT_INVALID_FILE: "IMPORT_INVALID_FILE", IMPORT_INVALID_FILE: "IMPORT_INVALID_FILE",
IMPORT_PAYLOAD_TOO_LARGE: "IMPORT_PAYLOAD_TOO_LARGE", IMPORT_PAYLOAD_TOO_LARGE: "IMPORT_PAYLOAD_TOO_LARGE",
+6
View File
@@ -8,6 +8,7 @@ import {
getTitleIdsCheckedBefore, getTitleIdsCheckedBefore,
getTitleIdsWithStaleSeasons, getTitleIdsWithStaleSeasons,
insertCronRunReturning, insertCronRunReturning,
markInterruptedCronRuns,
updateCronRunError, updateCronRunError,
updateCronRunSuccess, updateCronRunSuccess,
} from "@sofa/db/queries/cron"; } from "@sofa/db/queries/cron";
@@ -55,6 +56,11 @@ export function failCronRun(runId: string, durationMs: number, error: unknown):
updateCronRunError(runId, durationMs, errorMessage); updateCronRunError(runId, durationMs, errorMessage);
} }
/** Mark runs left "running" by a previous process as failed. Returns how many were recovered. */
export function recoverInterruptedCronRuns(): number {
return markInterruptedCronRuns();
}
export function getLibraryTitleIds(): string[] { export function getLibraryTitleIds(): string[] {
return queryGetLibraryTitleIds(); return queryGetLibraryTitleIds();
} }
+6 -1
View File
@@ -8,6 +8,7 @@ import { tmdbApiUrl } from "@sofa/tmdb/config";
import { listBackups } from "./backup"; import { listBackups } from "./backup";
import { imageCacheEnabled } from "./image-cache"; import { imageCacheEnabled } from "./image-cache";
import { getSetting } from "./settings"; import { getSetting } from "./settings";
import { isTelemetryEnabled } from "./telemetry";
import { isUpdateCheckEnabled } from "./update-check"; import { isUpdateCheckEnabled } from "./update-check";
export interface SystemHealthData { export interface SystemHealthData {
@@ -64,6 +65,9 @@ const JOB_NAMES = [
"cacheImages", "cacheImages",
"scheduledBackup", "scheduledBackup",
"updateCheck", "updateCheck",
"refreshCredits",
"telemetryReport",
"optimizeDb",
]; ];
function getDatabaseHealth(): SystemHealthData["database"] { function getDatabaseHealth(): SystemHealthData["database"] {
@@ -168,7 +172,8 @@ function getJobsHealth(): SystemHealthData["jobs"] {
const disabled = const disabled =
(jobName === "scheduledBackup" && getSetting("scheduledBackups") !== "true") || (jobName === "scheduledBackup" && getSetting("scheduledBackups") !== "true") ||
(jobName === "updateCheck" && !isUpdateCheckEnabled()); (jobName === "updateCheck" && !isUpdateCheckEnabled()) ||
(jobName === "telemetryReport" && !isTelemetryEnabled());
return { return {
jobName, jobName,
+21
View File
@@ -10,6 +10,7 @@ import {
getStaleLibraryTitles, getStaleLibraryTitles,
getTitleIdsWithStaleSeasons, getTitleIdsWithStaleSeasons,
libraryRefreshIntervalMs, libraryRefreshIntervalMs,
recoverInterruptedCronRuns,
runIsolated, runIsolated,
startCronRun, startCronRun,
} from "../src/cron"; } from "../src/cron";
@@ -61,6 +62,26 @@ describe("failCronRun", () => {
}); });
}); });
describe("recoverInterruptedCronRuns", () => {
test("marks only still-running runs as interrupted errors", () => {
const open = startCronRun("job-a");
const done = startCronRun("job-b");
completeCronRun(done.id, 100);
expect(recoverInterruptedCronRuns()).toBe(1);
const openRow = testDb.select().from(cronRuns).where(eq(cronRuns.id, open.id)).get();
expect(openRow?.status).toBe("error");
expect(openRow?.errorMessage).toBe("Interrupted by server restart");
expect(openRow?.finishedAt).toBeInstanceOf(Date);
const doneRow = testDb.select().from(cronRuns).where(eq(cronRuns.id, done.id)).get();
expect(doneRow?.status).toBe("success");
expect(doneRow?.durationMs).toBe(100);
expect(doneRow?.errorMessage).toBeNull();
});
});
describe("runIsolated", () => { describe("runIsolated", () => {
test("continues after a failing item", async () => { test("continues after a failing item", async () => {
const fn = vi.fn<(item: number) => Promise<void>>(async (item) => { const fn = vi.fn<(item: number) => Promise<void>>(async (item) => {
+10
View File
@@ -25,6 +25,16 @@ export function updateCronRunError(id: string, durationMs: number, errorMessage:
.run(); .run();
} }
/** Mark runs left "running" by a previous process as errors (called once at startup). */
export function markInterruptedCronRuns(): number {
return db
.update(cronRuns)
.set({ status: "error", finishedAt: new Date(), errorMessage: "Interrupted by server restart" })
.where(eq(cronRuns.status, "running"))
.returning({ id: cronRuns.id })
.all().length;
}
export function getLibraryTitleIds(): string[] { export function getLibraryTitleIds(): string[] {
return db return db
.select({ titleId: userTitleStatus.titleId }) .select({ titleId: userTitleStatus.titleId })
+1
View File
@@ -22,6 +22,7 @@ const APP_ERROR_MESSAGES: Record<AppErrorCode, MessageDescriptor> = {
BACKUP_DELETE_FAILED: msg`Failed to delete backup`, BACKUP_DELETE_FAILED: msg`Failed to delete backup`,
BACKUP_RESTORE_FAILED: msg`Backup restoration failed`, BACKUP_RESTORE_FAILED: msg`Backup restoration failed`,
JOB_NOT_FOUND: msg`Job not found`, JOB_NOT_FOUND: msg`Job not found`,
JOB_ALREADY_RUNNING: msg`This job is already running`,
TMDB_NOT_CONFIGURED: msg`TMDB API key is not configured`, TMDB_NOT_CONFIGURED: msg`TMDB API key is not configured`,
IMPORT_INVALID_FILE: msg`Invalid import file`, IMPORT_INVALID_FILE: msg`Invalid import file`,
IMPORT_PAYLOAD_TOO_LARGE: msg`Import payload is too large`, IMPORT_PAYLOAD_TOO_LARGE: msg`Import payload is too large`,
+16
View File
@@ -589,6 +589,10 @@ msgstr "إلغاء التعديل"
msgid "Cast" msgid "Cast"
msgstr "طاقم التمثيل" msgstr "طاقم التمثيل"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "اللحاق" msgstr "اللحاق"
@@ -966,6 +970,10 @@ msgstr "قاعدة البيانات"
msgid "Database backups" msgid "Database backups"
msgstr "نسخ احتياطية لقاعدة البيانات" msgstr "نسخ احتياطية لقاعدة البيانات"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "تم استعادة قاعدة البيانات. جارٍ إعادة التحميل..." msgstr "تم استعادة قاعدة البيانات. جارٍ إعادة التحميل..."
@@ -3353,6 +3361,10 @@ msgstr "التبديل إلى {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "يسري بعد إعادة تشغيل التطبيق" msgstr "يسري بعد إعادة تشغيل التطبيق"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "قد يستغرق هذا بضع دقائق للمكتبات الكبيرة. يرجى عدم إغلاق هذا التبويب." msgstr "قد يستغرق هذا بضع دقائق للمكتبات الكبيرة. يرجى عدم إغلاق هذا التبويب."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Bearbeitung abbrechen"
msgid "Cast" msgid "Cast"
msgstr "Besetzung" msgstr "Besetzung"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Aufholen" msgstr "Aufholen"
@@ -966,6 +970,10 @@ msgstr "Datenbank"
msgid "Database backups" msgid "Database backups"
msgstr "Datenbank-Backups" msgstr "Datenbank-Backups"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Datenbank wiederhergestellt. Lade neu..." msgstr "Datenbank wiederhergestellt. Lade neu..."
@@ -3353,6 +3361,10 @@ msgstr "Zu {name} wechseln"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Tritt nach einem Neustart der App in Kraft" msgstr "Tritt nach einem Neustart der App in Kraft"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "Dies kann bei großen Bibliotheken einige Minuten dauern. Bitte schließe diesen Tab nicht." msgstr "Dies kann bei großen Bibliotheken einige Minuten dauern. Bitte schließe diesen Tab nicht."
+16
View File
@@ -589,6 +589,10 @@ msgstr ""
msgid "Cast" msgid "Cast"
msgstr "" msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr "Cast & crew"
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "" msgstr ""
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups" msgid "Database backups"
msgstr "" msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr "Database maintenance"
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "" msgstr ""
@@ -3353,6 +3361,10 @@ msgstr "Switch to {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Takes effect after restarting the app" msgstr "Takes effect after restarting the app"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr "Telemetry"
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "That time is in the future" msgstr "That time is in the future"
@@ -3385,6 +3397,10 @@ msgstr "This doesn't appear to be a Sofa server. Double-check the URL."
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "This import cannot be cancelled" msgstr "This import cannot be cancelled"
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr "This job is already running"
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "" msgstr ""
+16
View File
@@ -589,6 +589,10 @@ msgstr "Cancelar edición"
msgid "Cast" msgid "Cast"
msgstr "Reparto" msgstr "Reparto"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Ponerse al día" msgstr "Ponerse al día"
@@ -966,6 +970,10 @@ msgstr "Base de datos"
msgid "Database backups" msgid "Database backups"
msgstr "Copias de seguridad de la base de datos" msgstr "Copias de seguridad de la base de datos"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Base de datos restaurada. Recargando..." msgstr "Base de datos restaurada. Recargando..."
@@ -3353,6 +3361,10 @@ msgstr "Cambiar a {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Tendrá efecto tras reiniciar la app" msgstr "Tendrá efecto tras reiniciar la app"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "Esto puede tardar unos minutos para bibliotecas grandes. Por favor, no cierres esta pestaña." msgstr "Esto puede tardar unos minutos para bibliotecas grandes. Por favor, no cierres esta pestaña."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Annuler la modification"
msgid "Cast" msgid "Cast"
msgstr "Distribution" msgstr "Distribution"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Rattraper" msgstr "Rattraper"
@@ -966,6 +970,10 @@ msgstr "Base de données"
msgid "Database backups" msgid "Database backups"
msgstr "Sauvegardes de la base de données" msgstr "Sauvegardes de la base de données"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Base de données restaurée. Rechargement..." msgstr "Base de données restaurée. Rechargement..."
@@ -3353,6 +3361,10 @@ msgstr "Passer à {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Prend effet après le redémarrage de l'application" msgstr "Prend effet après le redémarrage de l'application"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "Cela peut prendre quelques minutes pour les grandes bibliothèques. Ne fermez pas cet onglet." msgstr "Cela peut prendre quelques minutes pour les grandes bibliothèques. Ne fermez pas cet onglet."
+16
View File
@@ -589,6 +589,10 @@ msgstr "ביטול עריכה"
msgid "Cast" msgid "Cast"
msgstr "שחקנים" msgstr "שחקנים"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "השלם צפייה" msgstr "השלם צפייה"
@@ -966,6 +970,10 @@ msgstr "מסד נתונים"
msgid "Database backups" msgid "Database backups"
msgstr "גיבויי מסד נתונים" msgstr "גיבויי מסד נתונים"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "מסד הנתונים שוחזר. טוען מחדש..." msgstr "מסד הנתונים שוחזר. טוען מחדש..."
@@ -3353,6 +3361,10 @@ msgstr "עבור ל-{name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "ייכנס לתוקף לאחר הפעלה מחדש של האפליקציה" msgstr "ייכנס לתוקף לאחר הפעלה מחדש של האפליקציה"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "פעולה זו עשויה לקחת מספר דקות עבור ספריות גדולות. אנא אל תסגור לשונית זו." msgstr "פעולה זו עשויה לקחת מספר דקות עבור ספריות גדולות. אנא אל תסגור לשונית זו."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Annulla modifica"
msgid "Cast" msgid "Cast"
msgstr "" msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Recupera episodi" msgstr "Recupera episodi"
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups" msgid "Database backups"
msgstr "Backup del database" msgstr "Backup del database"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Database ripristinato. Ricaricamento..." msgstr "Database ripristinato. Ricaricamento..."
@@ -3353,6 +3361,10 @@ msgstr "Passa a {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Avrà effetto dopo il riavvio dell'app" msgstr "Avrà effetto dopo il riavvio dell'app"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "L'operazione potrebbe richiedere alcuni minuti per librerie grandi. Non chiudere questa scheda." msgstr "L'operazione potrebbe richiedere alcuni minuti per librerie grandi. Non chiudere questa scheda."
+16
View File
@@ -589,6 +589,10 @@ msgstr "編集をキャンセル"
msgid "Cast" msgid "Cast"
msgstr "キャスト" msgstr "キャスト"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "追いつく" msgstr "追いつく"
@@ -966,6 +970,10 @@ msgstr "データベース"
msgid "Database backups" msgid "Database backups"
msgstr "データベースバックアップ" msgstr "データベースバックアップ"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "データベースを復元しました。再読み込み中..." msgstr "データベースを復元しました。再読み込み中..."
@@ -3353,6 +3361,10 @@ msgstr "{name}に切り替え"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "アプリを再起動後に反映されます" msgstr "アプリを再起動後に反映されます"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "大規模なライブラリでは数分かかる場合があります。このタブを閉じないでください。" msgstr "大規模なライブラリでは数分かかる場合があります。このタブを閉じないでください。"
+16
View File
@@ -589,6 +589,10 @@ msgstr "편집 취소"
msgid "Cast" msgid "Cast"
msgstr "출연진" msgstr "출연진"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "따라잡기" msgstr "따라잡기"
@@ -966,6 +970,10 @@ msgstr "데이터베이스"
msgid "Database backups" msgid "Database backups"
msgstr "데이터베이스 백업" msgstr "데이터베이스 백업"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "데이터베이스가 복원되었습니다. 다시 로드 중..." msgstr "데이터베이스가 복원되었습니다. 다시 로드 중..."
@@ -3353,6 +3361,10 @@ msgstr "{name}(으)로 전환"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "앱을 다시 시작하면 적용됩니다" msgstr "앱을 다시 시작하면 적용됩니다"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "라이브러리가 크면 몇 분 정도 걸릴 수 있습니다. 이 탭을 닫지 마세요." msgstr "라이브러리가 크면 몇 분 정도 걸릴 수 있습니다. 이 탭을 닫지 마세요."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Bewerken annuleren"
msgid "Cast" msgid "Cast"
msgstr "" msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Inhalen" msgstr "Inhalen"
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups" msgid "Database backups"
msgstr "Database back-ups" msgstr "Database back-ups"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Database hersteld. Opnieuw laden..." msgstr "Database hersteld. Opnieuw laden..."
@@ -3353,6 +3361,10 @@ msgstr "Overschakelen naar {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Van kracht na het herstarten van de app" msgstr "Van kracht na het herstarten van de app"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "Dit kan een paar minuten duren voor grote bibliotheken. Sluit dit tabblad niet." msgstr "Dit kan een paar minuten duren voor grote bibliotheken. Sluit dit tabblad niet."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Cancelar edição"
msgid "Cast" msgid "Cast"
msgstr "Elenco" msgstr "Elenco"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "Colocar em dia" msgstr "Colocar em dia"
@@ -966,6 +970,10 @@ msgstr "Banco de Dados"
msgid "Database backups" msgid "Database backups"
msgstr "Backups do banco de dados" msgstr "Backups do banco de dados"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "Banco de dados restaurado. Recarregando..." msgstr "Banco de dados restaurado. Recarregando..."
@@ -3353,6 +3361,10 @@ msgstr "Mudar para {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "Entra em vigor após reiniciar a aplicação" msgstr "Entra em vigor após reiniciar a aplicação"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "Isso pode levar alguns minutos para bibliotecas grandes. Por favor, não feche esta aba." msgstr "Isso pode levar alguns minutos para bibliotecas grandes. Por favor, não feche esta aba."
+16
View File
@@ -589,6 +589,10 @@ msgstr "取消编辑"
msgid "Cast" msgid "Cast"
msgstr "演员表" msgstr "演员表"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts #: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up" msgid "Catch up"
msgstr "追上" msgstr "追上"
@@ -966,6 +970,10 @@ msgstr "数据库"
msgid "Database backups" msgid "Database backups"
msgstr "数据库备份" msgstr "数据库备份"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx #: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..." msgid "Database restored. Reloading..."
msgstr "数据库已还原。正在重新加载..." msgstr "数据库已还原。正在重新加载..."
@@ -3353,6 +3361,10 @@ msgstr "切换到 {name}"
msgid "Takes effect after restarting the app" msgid "Takes effect after restarting the app"
msgstr "重启应用后生效" msgstr "重启应用后生效"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx #: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future" msgid "That time is in the future"
msgstr "" msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled" msgid "This import cannot be cancelled"
msgstr "" msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: apps/web/src/components/settings/import-dialog-parts.tsx #: apps/web/src/components/settings/import-dialog-parts.tsx
msgid "This may take a few minutes for large libraries. Please don't close this tab." msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr "大型媒体库可能需要几分钟。请勿关闭此标签页。" msgstr "大型媒体库可能需要几分钟。请勿关闭此标签页。"