mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
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:
24 files changed
+373
-40
No files matched your search
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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 "قد يستغرق هذا بضع دقائق للمكتبات الكبيرة. يرجى عدم إغلاق هذا التبويب."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 "פעולה זו עשויה לקחת מספר דקות עבור ספריות גדולות. אנא אל תסגור לשונית זו."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 "大規模なライブラリでは数分かかる場合があります。このタブを閉じないでください。"
|
||||
|
||||
@@ -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 "라이브러리가 크면 몇 분 정도 걸릴 수 있습니다. 이 탭을 닫지 마세요."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 "大型媒体库可能需要几分钟。请勿关闭此标签页。"
|
||||
|
||||
Reference in new issue
Block a user