feat: add metadata refresh job to re-download metadata for linked series

Adds a new job type that refreshes metadata from external providers for
all series already linked via approved external_metadata_links. Tracks
and displays per-field diffs (series and book level), respects locked
fields, and provides a detailed change report in the job detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 09:09:10 +01:00
parent 818bd82e0f
commit 163dc3698c
17 changed files with 1170 additions and 56 deletions

View File

@@ -803,6 +803,49 @@ export async function startMetadataBatch(libraryId: string) {
});
}
export async function startMetadataRefresh(libraryId: string) {
return apiFetch<{ id: string; status: string }>("/metadata/refresh", {
method: "POST",
body: JSON.stringify({ library_id: libraryId }),
});
}
export type RefreshFieldDiff = {
field: string;
old?: unknown;
new?: unknown;
};
export type RefreshBookDiff = {
book_id: string;
title: string;
volume: number | null;
changes: RefreshFieldDiff[];
};
export type RefreshSeriesResult = {
series_name: string;
provider: string;
status: string; // "updated" | "unchanged" | "error"
series_changes: RefreshFieldDiff[];
book_changes: RefreshBookDiff[];
error?: string;
};
export type MetadataRefreshReportDto = {
job_id: string;
status: string;
total_links: number;
refreshed: number;
unchanged: number;
errors: number;
changes: RefreshSeriesResult[];
};
export async function getMetadataRefreshReport(jobId: string) {
return apiFetch<MetadataRefreshReportDto>(`/metadata/refresh/${jobId}/report`);
}
export async function getMetadataBatchReport(jobId: string) {
return apiFetch<MetadataBatchReportDto>(`/metadata/batch/${jobId}/report`);
}

View File

@@ -173,6 +173,8 @@ const en: Record<TranslationKey, string> = {
"jobs.generateThumbnails": "Generate thumbnails",
"jobs.regenerateThumbnails": "Regenerate thumbnails",
"jobs.batchMetadata": "Batch metadata",
"jobs.refreshMetadata": "Refresh metadata",
"jobs.refreshMetadataDescription": "Refreshes metadata for all series already linked to an external provider. Re-downloads information from the provider and updates series and books in the database (respecting locked fields). Series without an approved link are ignored. <strong>Requires a specific library</strong> (does not work on \"All libraries\").",
"jobs.referenceTitle": "Job types reference",
"jobs.rebuildDescription": "Incremental scan: detects files added, modified, or deleted since the last scan, indexes them, and generates missing thumbnails. Existing unmodified data is preserved. This is the most common and fastest action.",
"jobs.fullRebuildDescription": "Deletes all indexed data (books, series, thumbnails) then performs a full scan from scratch. Useful if the database is out of sync or corrupted. Long and destructive operation: reading statuses and manual metadata will be lost.",
@@ -185,8 +187,7 @@ const en: Record<TranslationKey, string> = {
"jobsList.library": "Library",
"jobsList.type": "Type",
"jobsList.status": "Status",
"jobsList.files": "Files",
"jobsList.thumbnails": "Thumbnails",
"jobsList.stats": "Stats",
"jobsList.duration": "Duration",
"jobsList.created": "Created",
"jobsList.actions": "Actions",
@@ -195,6 +196,12 @@ const en: Record<TranslationKey, string> = {
"jobRow.showProgress": "Show progress",
"jobRow.hideProgress": "Hide progress",
"jobRow.scanned": "{{count}} scanned",
"jobRow.filesIndexed": "{{count}} files indexed",
"jobRow.filesRemoved": "{{count}} files removed",
"jobRow.thumbnailsGenerated": "{{count}} thumbnails generated",
"jobRow.metadataProcessed": "{{count}} series processed",
"jobRow.metadataRefreshed": "{{count}} series refreshed",
"jobRow.errors": "{{count}} errors",
"jobRow.view": "View",
// Job progress
@@ -234,6 +241,14 @@ const en: Record<TranslationKey, string> = {
"jobDetail.phase2b": "Phase 2b — Thumbnail generation",
"jobDetail.metadataSearch": "Metadata search",
"jobDetail.metadataSearchDesc": "Searching external providers for each series",
"jobDetail.metadataRefresh": "Metadata refresh",
"jobDetail.metadataRefreshDesc": "Re-downloading metadata from providers for already linked series",
"jobDetail.refreshReport": "Refresh report",
"jobDetail.refreshReportDesc": "{{count}} linked series processed",
"jobDetail.refreshed": "Refreshed",
"jobDetail.unchanged": "Unchanged",
"jobDetail.refreshChanges": "Changes detail",
"jobDetail.refreshChangesDesc": "{{count}} series with changes",
"jobDetail.phase1Desc": "Scanning and indexing library files",
"jobDetail.phase2aDesc": "Extracting the first page of each archive (page count + raw image)",
"jobDetail.phase2bDesc": "Generating thumbnails for scanned books",
@@ -273,6 +288,7 @@ const en: Record<TranslationKey, string> = {
"jobType.thumbnail_regenerate": "Regen. thumbnails",
"jobType.cbr_to_cbz": "CBR → CBZ",
"jobType.metadata_batch": "Batch metadata",
"jobType.metadata_refresh": "Refresh meta.",
"jobType.rebuildLabel": "Incremental indexing",
"jobType.rebuildDesc": "Scans new/modified files, analyzes them, and generates missing thumbnails.",
"jobType.full_rebuildLabel": "Full reindexing",
@@ -285,6 +301,8 @@ const en: Record<TranslationKey, string> = {
"jobType.cbr_to_cbzDesc": "Converts a CBR archive to the open CBZ format.",
"jobType.metadata_batchLabel": "Batch metadata",
"jobType.metadata_batchDesc": "Searches external metadata providers for all series in the library and automatically applies 100% confidence matches.",
"jobType.metadata_refreshLabel": "Metadata refresh",
"jobType.metadata_refreshDesc": "Re-downloads and updates metadata for all series already linked to an external provider.",
// Status badges
"statusBadge.extracting_pages": "Extracting pages",

View File

@@ -171,6 +171,8 @@ const fr = {
"jobs.generateThumbnails": "Générer les miniatures",
"jobs.regenerateThumbnails": "Regénérer les miniatures",
"jobs.batchMetadata": "Métadonnées en lot",
"jobs.refreshMetadata": "Rafraîchir métadonnées",
"jobs.refreshMetadataDescription": "Rafraîchit les métadonnées de toutes les séries déjà liées à un fournisseur externe. Re-télécharge les informations depuis le fournisseur et met à jour les séries et livres en base (en respectant les champs verrouillés). Les séries sans lien approuvé sont ignorées. <strong>Requiert une bibliothèque spécifique</strong> (ne fonctionne pas sur \u00ab Toutes les bibliothèques \u00bb).",
"jobs.referenceTitle": "Référence des types de tâches",
"jobs.rebuildDescription": "Scan incrémental : détecte les fichiers ajoutés, modifiés ou supprimés depuis le dernier scan, les indexe et génère les miniatures manquantes. Les données existantes non modifiées sont conservées. C'est l'action la plus courante et la plus rapide.",
"jobs.fullRebuildDescription": "Supprime toutes les données indexées (livres, séries, miniatures) puis effectue un scan complet depuis zéro. Utile si la base de données est désynchronisée ou corrompue. Opération longue et destructive : les statuts de lecture et les métadonnées manuelles seront perdus.",
@@ -183,8 +185,7 @@ const fr = {
"jobsList.library": "Bibliothèque",
"jobsList.type": "Type",
"jobsList.status": "Statut",
"jobsList.files": "Fichiers",
"jobsList.thumbnails": "Miniatures",
"jobsList.stats": "Stats",
"jobsList.duration": "Durée",
"jobsList.created": "Créé",
"jobsList.actions": "Actions",
@@ -193,6 +194,12 @@ const fr = {
"jobRow.showProgress": "Afficher la progression",
"jobRow.hideProgress": "Masquer la progression",
"jobRow.scanned": "{{count}} analysés",
"jobRow.filesIndexed": "{{count}} fichiers indexés",
"jobRow.filesRemoved": "{{count}} fichiers supprimés",
"jobRow.thumbnailsGenerated": "{{count}} miniatures générées",
"jobRow.metadataProcessed": "{{count}} séries traitées",
"jobRow.metadataRefreshed": "{{count}} séries rafraîchies",
"jobRow.errors": "{{count}} erreurs",
"jobRow.view": "Voir",
// Job progress
@@ -232,6 +239,14 @@ const fr = {
"jobDetail.phase2b": "Phase 2b — Génération des miniatures",
"jobDetail.metadataSearch": "Recherche de métadonnées",
"jobDetail.metadataSearchDesc": "Recherche auprès des fournisseurs externes pour chaque série",
"jobDetail.metadataRefresh": "Rafraîchissement des métadonnées",
"jobDetail.metadataRefreshDesc": "Re-téléchargement des métadonnées depuis les fournisseurs pour les séries déjà liées",
"jobDetail.refreshReport": "Rapport de rafraîchissement",
"jobDetail.refreshReportDesc": "{{count}} séries liées traitées",
"jobDetail.refreshed": "Rafraîchies",
"jobDetail.unchanged": "Inchangées",
"jobDetail.refreshChanges": "Détail des changements",
"jobDetail.refreshChangesDesc": "{{count}} séries avec des modifications",
"jobDetail.phase1Desc": "Scan et indexation des fichiers de la bibliothèque",
"jobDetail.phase2aDesc": "Extraction de la première page de chaque archive (nombre de pages + image brute)",
"jobDetail.phase2bDesc": "Génération des miniatures pour les livres analysés",
@@ -271,6 +286,7 @@ const fr = {
"jobType.thumbnail_regenerate": "Régén. miniatures",
"jobType.cbr_to_cbz": "CBR → CBZ",
"jobType.metadata_batch": "Métadonnées en lot",
"jobType.metadata_refresh": "Rafraîchir méta.",
"jobType.rebuildLabel": "Indexation incrémentale",
"jobType.rebuildDesc": "Scanne les fichiers nouveaux/modifiés, les analyse et génère les miniatures manquantes.",
"jobType.full_rebuildLabel": "Réindexation complète",
@@ -283,6 +299,8 @@ const fr = {
"jobType.cbr_to_cbzDesc": "Convertit une archive CBR au format ouvert CBZ.",
"jobType.metadata_batchLabel": "Métadonnées en lot",
"jobType.metadata_batchDesc": "Recherche les métadonnées auprès des fournisseurs externes pour toutes les séries de la bibliothèque et applique automatiquement les correspondances à 100% de confiance.",
"jobType.metadata_refreshLabel": "Rafraîchissement métadonnées",
"jobType.metadata_refreshDesc": "Re-télécharge et met à jour les métadonnées pour toutes les séries déjà liées à un fournisseur externe.",
// Status badges
"statusBadge.extracting_pages": "Extraction des pages",