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. */
export async function triggerJob(name: string): Promise<boolean> {
/** Start a job by name without waiting for it to finish. */
export function triggerJob(name: string): "started" | "not_found" | "busy" {
const job = jobs.get(name);
if (!job) return false;
if (job.isBusy()) return false;
await job.trigger();
return true;
if (!job) return "not_found";
if (job.isBusy()) return "busy";
// The scheduled wrapper records the run and catches its errors; don't hold the request open.
void job.trigger();
return "started";
}
// 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 { ensureBackupDir } from "@sofa/core/backup";
import { recoverInterruptedCronRuns } from "@sofa/core/cron";
import { ensureImageDirs, imageCacheEnabled } from "@sofa/core/image-cache";
import { registerJobScheduleProvider } from "@sofa/core/system-health";
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`);
}
// 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).
const clearedPayloads = clearFinishedImportPayloads();
if (clearedPayloads > 0) {
+9 -3
View File
@@ -126,14 +126,20 @@ export const backupsUpdateSchedule = os.admin.backups.updateSchedule
// ─── Jobs ─────────────────────────────────────────────────────
export const triggerJob = os.admin.triggerJob.use(admin).handler(async ({ input }) => {
const triggered = await triggerCronJob(input.name);
if (!triggered) {
export const triggerJob = os.admin.triggerJob.use(admin).handler(({ input }) => {
const outcome = triggerCronJob(input.name);
if (outcome === "not_found") {
throw new ORPCError("NOT_FOUND", {
message: "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 };
});
@@ -34,6 +34,9 @@ import { i18n } from "@sofa/i18n";
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 */
function cronToHuman(pattern: string): string {
const parts = pattern.split(" ");
@@ -316,6 +319,9 @@ function BackgroundJobsCard({
cacheImages: t`Image cache`,
scheduledBackup: t`Backup`,
updateCheck: t`Update check`,
refreshCredits: t`Cast & crew`,
telemetryReport: t`Telemetry`,
optimizeDb: t`Database maintenance`,
};
const triggerJobMutation = useMutation(
@@ -483,36 +489,38 @@ function BackgroundJobsCard({
{/* Trigger button */}
<TableCell className="pe-5 text-end">
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
aria-label={t`Trigger job`}
className="size-6"
disabled={isRunning || job.disabled}
onClick={() =>
triggerJobMutation.mutate({
name: job.jobName as CronJobName,
})
}
/>
}
>
{isRunning ? (
<Spinner className="size-3" />
) : (
<IconPlayerPlay
aria-hidden={true}
className="text-muted-foreground/70 size-3"
/>
)}
</TooltipTrigger>
<TooltipContent>
<Trans>Run now</Trans>
</TooltipContent>
</Tooltip>
{!NOT_TRIGGERABLE.has(job.jobName) && (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="ghost"
size="icon"
aria-label={t`Trigger job`}
className="size-6"
disabled={isRunning || job.disabled}
onClick={() =>
triggerJobMutation.mutate({
name: job.jobName as CronJobName,
})
}
/>
}
>
{isRunning ? (
<Spinner className="size-3" />
) : (
<IconPlayerPlay
aria-hidden={true}
className="text-muted-foreground/70 size-3"
/>
)}
</TooltipTrigger>
<TooltipContent>
<Trans>Run now</Trans>
</TooltipContent>
</Tooltip>
)}
</TableCell>
</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": [
+4
View File
@@ -669,6 +669,10 @@ export const contract = {
message: "Job not found",
data: appErrorData(AppErrorCode.JOB_NOT_FOUND),
},
CONFLICT: {
message: "Job is already running",
data: appErrorData(AppErrorCode.JOB_ALREADY_RUNNING),
},
}),
purgeMetadataCache: oc
.route({
+1
View File
@@ -10,6 +10,7 @@ export const AppErrorCode = {
BACKUP_DELETE_FAILED: "BACKUP_DELETE_FAILED",
BACKUP_RESTORE_FAILED: "BACKUP_RESTORE_FAILED",
JOB_NOT_FOUND: "JOB_NOT_FOUND",
JOB_ALREADY_RUNNING: "JOB_ALREADY_RUNNING",
TMDB_NOT_CONFIGURED: "TMDB_NOT_CONFIGURED",
IMPORT_INVALID_FILE: "IMPORT_INVALID_FILE",
IMPORT_PAYLOAD_TOO_LARGE: "IMPORT_PAYLOAD_TOO_LARGE",
+6
View File
@@ -8,6 +8,7 @@ import {
getTitleIdsCheckedBefore,
getTitleIdsWithStaleSeasons,
insertCronRunReturning,
markInterruptedCronRuns,
updateCronRunError,
updateCronRunSuccess,
} from "@sofa/db/queries/cron";
@@ -55,6 +56,11 @@ export function failCronRun(runId: string, durationMs: number, error: unknown):
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[] {
return queryGetLibraryTitleIds();
}
+6 -1
View File
@@ -8,6 +8,7 @@ import { tmdbApiUrl } from "@sofa/tmdb/config";
import { listBackups } from "./backup";
import { imageCacheEnabled } from "./image-cache";
import { getSetting } from "./settings";
import { isTelemetryEnabled } from "./telemetry";
import { isUpdateCheckEnabled } from "./update-check";
export interface SystemHealthData {
@@ -64,6 +65,9 @@ const JOB_NAMES = [
"cacheImages",
"scheduledBackup",
"updateCheck",
"refreshCredits",
"telemetryReport",
"optimizeDb",
];
function getDatabaseHealth(): SystemHealthData["database"] {
@@ -168,7 +172,8 @@ function getJobsHealth(): SystemHealthData["jobs"] {
const disabled =
(jobName === "scheduledBackup" && getSetting("scheduledBackups") !== "true") ||
(jobName === "updateCheck" && !isUpdateCheckEnabled());
(jobName === "updateCheck" && !isUpdateCheckEnabled()) ||
(jobName === "telemetryReport" && !isTelemetryEnabled());
return {
jobName,
+21
View File
@@ -10,6 +10,7 @@ import {
getStaleLibraryTitles,
getTitleIdsWithStaleSeasons,
libraryRefreshIntervalMs,
recoverInterruptedCronRuns,
runIsolated,
startCronRun,
} 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", () => {
test("continues after a failing item", async () => {
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();
}
/** 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[] {
return db
.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_RESTORE_FAILED: msg`Backup restoration failed`,
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`,
IMPORT_INVALID_FILE: msg`Invalid import file`,
IMPORT_PAYLOAD_TOO_LARGE: msg`Import payload is too large`,
+16
View File
@@ -589,6 +589,10 @@ msgstr "إلغاء التعديل"
msgid "Cast"
msgstr "طاقم التمثيل"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "اللحاق"
@@ -966,6 +970,10 @@ msgstr "قاعدة البيانات"
msgid "Database backups"
msgstr "نسخ احتياطية لقاعدة البيانات"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..."
msgstr "تم استعادة قاعدة البيانات. جارٍ إعادة التحميل..."
@@ -3353,6 +3361,10 @@ msgstr "التبديل إلى {name}"
msgid "Takes effect after restarting the app"
msgstr "يسري بعد إعادة تشغيل التطبيق"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "قد يستغرق هذا بضع دقائق للمكتبات الكبيرة. يرجى عدم إغلاق هذا التبويب."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Bearbeitung abbrechen"
msgid "Cast"
msgstr "Besetzung"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Aufholen"
@@ -966,6 +970,10 @@ msgstr "Datenbank"
msgid "Database 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
msgid "Database restored. Reloading..."
msgstr "Datenbank wiederhergestellt. Lade neu..."
@@ -3353,6 +3361,10 @@ msgstr "Zu {name} wechseln"
msgid "Takes effect after restarting the app"
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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
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"
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
msgid "Catch up"
msgstr ""
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr ""
@@ -3353,6 +3361,10 @@ msgstr "Switch to {name}"
msgid "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
msgid "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"
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
msgid "This may take a few minutes for large libraries. Please don't close this tab."
msgstr ""
+16
View File
@@ -589,6 +589,10 @@ msgstr "Cancelar edición"
msgid "Cast"
msgstr "Reparto"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Ponerse al día"
@@ -966,6 +970,10 @@ msgstr "Base de datos"
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr "Base de datos restaurada. Recargando..."
@@ -3353,6 +3361,10 @@ msgstr "Cambiar a {name}"
msgid "Takes effect after restarting the 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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
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"
msgstr "Distribution"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Rattraper"
@@ -966,6 +970,10 @@ msgstr "Base de données"
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr "Base de données restaurée. Rechargement..."
@@ -3353,6 +3361,10 @@ msgstr "Passer à {name}"
msgid "Takes effect after restarting the app"
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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
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"
msgstr "שחקנים"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "השלם צפייה"
@@ -966,6 +970,10 @@ msgstr "מסד נתונים"
msgid "Database backups"
msgstr "גיבויי מסד נתונים"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..."
msgstr "מסד הנתונים שוחזר. טוען מחדש..."
@@ -3353,6 +3361,10 @@ msgstr "עבור ל-{name}"
msgid "Takes effect after restarting the app"
msgstr "ייכנס לתוקף לאחר הפעלה מחדש של האפליקציה"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "פעולה זו עשויה לקחת מספר דקות עבור ספריות גדולות. אנא אל תסגור לשונית זו."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Annulla modifica"
msgid "Cast"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Recupera episodi"
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr "Database ripristinato. Ricaricamento..."
@@ -3353,6 +3361,10 @@ msgstr "Passa a {name}"
msgid "Takes effect after restarting the 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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "L'operazione potrebbe richiedere alcuni minuti per librerie grandi. Non chiudere questa scheda."
+16
View File
@@ -589,6 +589,10 @@ msgstr "編集をキャンセル"
msgid "Cast"
msgstr "キャスト"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "追いつく"
@@ -966,6 +970,10 @@ msgstr "データベース"
msgid "Database backups"
msgstr "データベースバックアップ"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..."
msgstr "データベースを復元しました。再読み込み中..."
@@ -3353,6 +3361,10 @@ msgstr "{name}に切り替え"
msgid "Takes effect after restarting the app"
msgstr "アプリを再起動後に反映されます"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "大規模なライブラリでは数分かかる場合があります。このタブを閉じないでください。"
+16
View File
@@ -589,6 +589,10 @@ msgstr "편집 취소"
msgid "Cast"
msgstr "출연진"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "따라잡기"
@@ -966,6 +970,10 @@ msgstr "데이터베이스"
msgid "Database backups"
msgstr "데이터베이스 백업"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..."
msgstr "데이터베이스가 복원되었습니다. 다시 로드 중..."
@@ -3353,6 +3361,10 @@ msgstr "{name}(으)로 전환"
msgid "Takes effect after restarting the app"
msgstr "앱을 다시 시작하면 적용됩니다"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "라이브러리가 크면 몇 분 정도 걸릴 수 있습니다. 이 탭을 닫지 마세요."
+16
View File
@@ -589,6 +589,10 @@ msgstr "Bewerken annuleren"
msgid "Cast"
msgstr ""
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Inhalen"
@@ -966,6 +970,10 @@ msgstr ""
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr "Database hersteld. Opnieuw laden..."
@@ -3353,6 +3361,10 @@ msgstr "Overschakelen naar {name}"
msgid "Takes effect after restarting the 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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
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"
msgstr "Elenco"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "Colocar em dia"
@@ -966,6 +970,10 @@ msgstr "Banco de Dados"
msgid "Database backups"
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
msgid "Database restored. Reloading..."
msgstr "Banco de dados restaurado. Recarregando..."
@@ -3353,6 +3361,10 @@ msgstr "Mudar para {name}"
msgid "Takes effect after restarting the app"
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
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
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"
msgstr "演员表"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Cast & crew"
msgstr ""
#: apps/web/src/components/titles/use-title-actions.ts
msgid "Catch up"
msgstr "追上"
@@ -966,6 +970,10 @@ msgstr "数据库"
msgid "Database backups"
msgstr "数据库备份"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Database maintenance"
msgstr ""
#: apps/web/src/components/settings/backup-restore-section.tsx
msgid "Database restored. Reloading..."
msgstr "数据库已还原。正在重新加载..."
@@ -3353,6 +3361,10 @@ msgstr "切换到 {name}"
msgid "Takes effect after restarting the app"
msgstr "重启应用后生效"
#: apps/web/src/components/settings/system-health-section.tsx
msgid "Telemetry"
msgstr ""
#: apps/web/src/components/titles/log-watch-dialog.tsx
msgid "That time is in the future"
msgstr ""
@@ -3385,6 +3397,10 @@ msgstr ""
msgid "This import cannot be cancelled"
msgstr ""
#: packages/i18n/src/errors.ts
msgid "This job is already running"
msgstr ""
#: 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."
msgstr "大型媒体库可能需要几分钟。请勿关闭此标签页。"