feat(library): add batch refresh metadata option, closes #3294 (#3573)

Add a "Refresh Metadata" option under Advanced Settings that re-parses
book files to update metadata (series info, language, etc.) for books
imported before these fields were supported.

- Add refreshBookMetadata() in bookService that opens a book file,
  re-parses with DocumentLoader, and updates metadata/metaHash/series
  without touching updatedAt or user-edited fields
- Add menu item in SettingsMenu with progress display
- Add unit tests for single-book metadata refresh
- Translate new i18n strings across all 29 locales

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-03-21 02:32:09 +08:00
committed by GitHub
parent e41bcfbac9
commit 7445d5befe
34 changed files with 291 additions and 30 deletions
@@ -1145,5 +1145,14 @@
"Restore completed successfully!": "تمت الاستعادة بنجاح!",
"Your library has been saved to the selected location.": "تم حفظ مكتبتك في الموقع المحدد.",
"{{added}} books added, {{updated}} books updated.": "تمت إضافة {{added}} كتب، وتحديث {{updated}} كتب.",
"Operation failed": "فشلت العملية"
"Operation failed": "فشلت العملية",
"Loading library...": "جارٍ تحميل المكتبة...",
"{{count}} books refreshed_zero": "لم يتم تحديث أي كتاب",
"{{count}} books refreshed_one": "تم تحديث كتاب واحد",
"{{count}} books refreshed_two": "تم تحديث كتابين",
"{{count}} books refreshed_few": "تم تحديث {{count}} كتب",
"{{count}} books refreshed_many": "تم تحديث {{count}} كتابًا",
"{{count}} books refreshed_other": "تم تحديث {{count}} كتاب",
"Failed to refresh metadata": "فشل تحديث البيانات الوصفية",
"Refresh Metadata": "تحديث البيانات الوصفية"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "পুনরুদ্ধার সফলভাবে সম্পন্ন!",
"Your library has been saved to the selected location.": "আপনার লাইব্রেরি নির্বাচিত স্থানে সংরক্ষিত হয়েছে।",
"{{added}} books added, {{updated}} books updated.": "{{added}}টি বই যোগ হয়েছে, {{updated}}টি বই আপডেট হয়েছে।",
"Operation failed": "অপারেশন ব্যর্থ"
"Operation failed": "অপারেশন ব্যর্থ",
"Loading library...": "লাইব্রেরি লোড হচ্ছে...",
"{{count}} books refreshed_one": "{{count}}টি বইয়ের মেটাডেটা রিফ্রেশ হয়েছে",
"{{count}} books refreshed_other": "{{count}}টি বইয়ের মেটাডেটা রিফ্রেশ হয়েছে",
"Failed to refresh metadata": "মেটাডেটা রিফ্রেশ ব্যর্থ",
"Refresh Metadata": "মেটাডেটা রিফ্রেশ"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "སླར་གསོ་ལེགས་གྲུབ་བྱུང་!",
"Your library has been saved to the selected location.": "ཁྱོད་ཀྱི་དཔེ་མཛོད་འདེམས་ས་དེར་ཉར་ཟིན།",
"{{added}} books added, {{updated}} books updated.": "དཔེ་དེབ {{added}} བསྣན་ཟིན, {{updated}} གསར་བསྒྱུར་བྱས་ཟིན།",
"Operation failed": "བཀོལ་སྤྱོད་བྱེད་མ་ཐུབ"
"Operation failed": "བཀོལ་སྤྱོད་བྱེད་མ་ཐུབ",
"Loading library...": "དཔེ་མཛོད་འཇུག་བཞིན་པ...",
"{{count}} books refreshed_other": "تم تحديث {{count}} كتب",
"Failed to refresh metadata": "མེ་ཊ་ཌེ་ཊ་གསར་བསྒྱུར་བྱེད་མ་ཐུབ",
"Refresh Metadata": "མེ་ཊ་ཌེ་ཊ་གསར་བསྒྱུར"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "Wiederherstellung erfolgreich abgeschlossen!",
"Your library has been saved to the selected location.": "Ihre Bibliothek wurde am ausgewählten Ort gespeichert.",
"{{added}} books added, {{updated}} books updated.": "{{added}} Bücher hinzugefügt, {{updated}} Bücher aktualisiert.",
"Operation failed": "Vorgang fehlgeschlagen"
"Operation failed": "Vorgang fehlgeschlagen",
"Loading library...": "Bibliothek wird geladen...",
"{{count}} books refreshed_one": "{{count}} Buch aktualisiert",
"{{count}} books refreshed_other": "{{count}} Bücher aktualisiert",
"Failed to refresh metadata": "Metadaten-Aktualisierung fehlgeschlagen",
"Refresh Metadata": "Metadaten aktualisieren"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "Η επαναφορά ολοκληρώθηκε!",
"Your library has been saved to the selected location.": "Η βιβλιοθήκη σας αποθηκεύτηκε στην επιλεγμένη τοποθεσία.",
"{{added}} books added, {{updated}} books updated.": "{{added}} βιβλία προστέθηκαν, {{updated}} βιβλία ενημερώθηκαν.",
"Operation failed": "Η λειτουργία απέτυχε"
"Operation failed": "Η λειτουργία απέτυχε",
"Loading library...": "Φόρτωση βιβλιοθήκης...",
"{{count}} books refreshed_one": "{{count}} βιβλίο ανανεώθηκε",
"{{count}} books refreshed_other": "{{count}} βιβλία ανανεώθηκαν",
"Failed to refresh metadata": "Αποτυχία ανανέωσης μεταδεδομένων",
"Refresh Metadata": "Ανανέωση μεταδεδομένων"
}
@@ -1109,5 +1109,11 @@
"Restore completed successfully!": "¡Restauración completada!",
"Your library has been saved to the selected location.": "Tu biblioteca se ha guardado en la ubicación seleccionada.",
"{{added}} books added, {{updated}} books updated.": "{{added}} libros añadidos, {{updated}} libros actualizados.",
"Operation failed": "La operación falló"
"Operation failed": "La operación falló",
"Loading library...": "Cargando biblioteca...",
"{{count}} books refreshed_one": "{{count}} libro actualizado",
"{{count}} books refreshed_many": "{{count}} libros actualizados",
"{{count}} books refreshed_other": "{{count}} libros actualizados",
"Failed to refresh metadata": "Error al actualizar metadatos",
"Refresh Metadata": "Actualizar metadatos"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "بازیابی با موفقیت انجام شد!",
"Your library has been saved to the selected location.": "کتابخانه شما در مکان انتخاب‌شده ذخیره شد.",
"{{added}} books added, {{updated}} books updated.": "{{added}} کتاب اضافه شد، {{updated}} کتاب به‌روزرسانی شد.",
"Operation failed": "عملیات ناموفق"
"Operation failed": "عملیات ناموفق",
"Loading library...": "در حال بارگذاری کتابخانه...",
"{{count}} books refreshed_one": "{{count}} کتاب به‌روزرسانی شد",
"{{count}} books refreshed_other": "{{count}} کتاب به‌روزرسانی شد",
"Failed to refresh metadata": "به‌روزرسانی فراداده ناموفق",
"Refresh Metadata": "به‌روزرسانی فراداده"
}
@@ -1109,5 +1109,11 @@
"Restore completed successfully!": "Restauration terminée avec succès !",
"Your library has been saved to the selected location.": "Votre bibliothèque a été enregistrée à l'emplacement sélectionné.",
"{{added}} books added, {{updated}} books updated.": "{{added}} livres ajoutés, {{updated}} livres mis à jour.",
"Operation failed": "L'opération a échoué"
"Operation failed": "L'opération a échoué",
"Loading library...": "Chargement de la bibliothèque...",
"{{count}} books refreshed_one": "{{count}} livre rafraîchi",
"{{count}} books refreshed_many": "{{count}} livres rafraîchis",
"{{count}} books refreshed_other": "{{count}} livres rafraîchis",
"Failed to refresh metadata": "Échec du rafraîchissement des métadonnées",
"Refresh Metadata": "Rafraîchir les métadonnées"
}
@@ -1109,5 +1109,11 @@
"Restore completed successfully!": "השחזור הושלם בהצלחה!",
"Your library has been saved to the selected location.": "הספרייה שלך נשמרה במיקום שנבחר.",
"{{added}} books added, {{updated}} books updated.": "{{added}} ספרים נוספו, {{updated}} ספרים עודכנו.",
"Operation failed": "הפעולה נכשלה"
"Operation failed": "הפעולה נכשלה",
"Loading library...": "טוען ספרייה...",
"{{count}} books refreshed_one": "ספר {{count}} רוענן",
"{{count}} books refreshed_two": "{{count}} ספרים רועננו",
"{{count}} books refreshed_other": "{{count}} ספרים רועננו",
"Failed to refresh metadata": "רענון המטא-נתונים נכשל",
"Refresh Metadata": "רענון מטא-נתונים"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "पुनर्स्थापना सफलतापूर्वक पूर्ण!",
"Your library has been saved to the selected location.": "आपकी लाइब्रेरी चयनित स्थान पर सहेजी गई है।",
"{{added}} books added, {{updated}} books updated.": "{{added}} पुस्तकें जोड़ी गईं, {{updated}} पुस्तकें अपडेट की गईं।",
"Operation failed": "ऑपरेशन विफल"
"Operation failed": "ऑपरेशन विफल",
"Loading library...": "लाइब्रेरी लोड हो रही है...",
"{{count}} books refreshed_one": "{{count}} पुस्तक रिफ्रेश हुई",
"{{count}} books refreshed_other": "{{count}} पुस्तकें रिफ्रेश हुईं",
"Failed to refresh metadata": "मेटाडेटा रिफ्रेश विफल",
"Refresh Metadata": "मेटाडेटा रिफ्रेश करें"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "Pemulihan berhasil!",
"Your library has been saved to the selected location.": "Perpustakaan Anda telah disimpan di lokasi yang dipilih.",
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambahkan, {{updated}} buku diperbarui.",
"Operation failed": "Operasi gagal"
"Operation failed": "Operasi gagal",
"Loading library...": "Memuat perpustakaan...",
"{{count}} books refreshed_other": "{{count}} buku diperbarui",
"Failed to refresh metadata": "Gagal memperbarui metadata",
"Refresh Metadata": "Perbarui Metadata"
}
@@ -1109,5 +1109,11 @@
"Restore completed successfully!": "Ripristino completato con successo!",
"Your library has been saved to the selected location.": "La tua libreria è stata salvata nella posizione selezionata.",
"{{added}} books added, {{updated}} books updated.": "{{added}} libri aggiunti, {{updated}} libri aggiornati.",
"Operation failed": "Operazione fallita"
"Operation failed": "Operazione fallita",
"Loading library...": "Caricamento libreria...",
"{{count}} books refreshed_one": "{{count}} libro aggiornato",
"{{count}} books refreshed_many": "{{count}} libri aggiornati",
"{{count}} books refreshed_other": "{{count}} libri aggiornati",
"Failed to refresh metadata": "Aggiornamento metadati fallito",
"Refresh Metadata": "Aggiorna metadati"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "復元が完了しました!",
"Your library has been saved to the selected location.": "ライブラリが選択した場所に保存されました。",
"{{added}} books added, {{updated}} books updated.": "{{added}}冊追加、{{updated}}冊更新されました。",
"Operation failed": "操作に失敗しました"
"Operation failed": "操作に失敗しました",
"Loading library...": "ライブラリを読み込み中...",
"{{count}} books refreshed_other": "{{count}}冊のメタデータを更新しました",
"Failed to refresh metadata": "メタデータの更新に失敗しました",
"Refresh Metadata": "メタデータを更新"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "복원이 완료되었습니다!",
"Your library has been saved to the selected location.": "라이브러리가 선택한 위치에 저장되었습니다.",
"{{added}} books added, {{updated}} books updated.": "{{added}}권 추가, {{updated}}권 업데이트되었습니다.",
"Operation failed": "작업 실패"
"Operation failed": "작업 실패",
"Loading library...": "라이브러리 로딩 중...",
"{{count}} books refreshed_other": "{{count}}권 새로고침됨",
"Failed to refresh metadata": "메타데이터 새로고침 실패",
"Refresh Metadata": "메타데이터 새로고침"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "Pemulihan berjaya!",
"Your library has been saved to the selected location.": "Perpustakaan anda telah disimpan di lokasi yang dipilih.",
"{{added}} books added, {{updated}} books updated.": "{{added}} buku ditambah, {{updated}} buku dikemas kini.",
"Operation failed": "Operasi gagal"
"Operation failed": "Operasi gagal",
"Loading library...": "Memuatkan perpustakaan...",
"{{count}} books refreshed_other": "{{count}} buku dimuat semula",
"Failed to refresh metadata": "Gagal memuat semula metadata",
"Refresh Metadata": "Muat Semula Metadata"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "Herstel succesvol voltooid!",
"Your library has been saved to the selected location.": "Uw bibliotheek is opgeslagen op de geselecteerde locatie.",
"{{added}} books added, {{updated}} books updated.": "{{added}} boeken toegevoegd, {{updated}} boeken bijgewerkt.",
"Operation failed": "Bewerking mislukt"
"Operation failed": "Bewerking mislukt",
"Loading library...": "Bibliotheek laden...",
"{{count}} books refreshed_one": "{{count}} boek vernieuwd",
"{{count}} books refreshed_other": "{{count}} boeken vernieuwd",
"Failed to refresh metadata": "Metadata vernieuwen mislukt",
"Refresh Metadata": "Metadata vernieuwen"
}
@@ -1121,5 +1121,12 @@
"Restore completed successfully!": "Przywracanie ukończone!",
"Your library has been saved to the selected location.": "Biblioteka została zapisana w wybranej lokalizacji.",
"{{added}} books added, {{updated}} books updated.": "Dodano {{added}} książek, zaktualizowano {{updated}} książek.",
"Operation failed": "Operacja nie powiodła się"
"Operation failed": "Operacja nie powiodła się",
"Loading library...": "Wczytywanie biblioteki...",
"{{count}} books refreshed_one": "Odświeżono {{count}} książkę",
"{{count}} books refreshed_few": "Odświeżono {{count}} książki",
"{{count}} books refreshed_many": "Odświeżono {{count}} książek",
"{{count}} books refreshed_other": "Odświeżono {{count}} książek",
"Failed to refresh metadata": "Nie udało się odświeżyć metadanych",
"Refresh Metadata": "Odśwież metadane"
}
@@ -1109,5 +1109,11 @@
"Restore completed successfully!": "Restauração concluída com sucesso!",
"Your library has been saved to the selected location.": "Sua biblioteca foi salva no local selecionado.",
"{{added}} books added, {{updated}} books updated.": "{{added}} livros adicionados, {{updated}} livros atualizados.",
"Operation failed": "Operação falhou"
"Operation failed": "Operação falhou",
"Loading library...": "Carregando biblioteca...",
"{{count}} books refreshed_one": "{{count}} livro atualizado",
"{{count}} books refreshed_many": "{{count}} livros atualizados",
"{{count}} books refreshed_other": "{{count}} livros atualizados",
"Failed to refresh metadata": "Falha ao atualizar metadados",
"Refresh Metadata": "Atualizar metadados"
}
@@ -1121,5 +1121,12 @@
"Restore completed successfully!": "Восстановление завершено!",
"Your library has been saved to the selected location.": "Ваша библиотека сохранена в выбранном месте.",
"{{added}} books added, {{updated}} books updated.": "Добавлено {{added}} книг, обновлено {{updated}} книг.",
"Operation failed": "Операция не удалась"
"Operation failed": "Операция не удалась",
"Loading library...": "Загрузка библиотеки...",
"{{count}} books refreshed_one": "Обновлена {{count}} книга",
"{{count}} books refreshed_few": "Обновлено {{count}} книги",
"{{count}} books refreshed_many": "Обновлено {{count}} книг",
"{{count}} books refreshed_other": "Обновлено {{count}} книг",
"Failed to refresh metadata": "Не удалось обновить метаданные",
"Refresh Metadata": "Обновить метаданные"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "ප්‍රතිසාධනය සාර්ථකව සම්පූර්ණයි!",
"Your library has been saved to the selected location.": "ඔබේ පුස්තකාලය තෝරාගත් ස්ථානයේ සුරැකිණි.",
"{{added}} books added, {{updated}} books updated.": "පොත් {{added}}ක් එකතු විය, {{updated}}ක් යාවත්කාලීන විය.",
"Operation failed": "මෙහෙයුම අසාර්ථකයි"
"Operation failed": "මෙහෙයුම අසාර්ථකයි",
"Loading library...": "පුස්තකාලය පූරණය වෙමින්...",
"{{count}} books refreshed_one": "පොත් {{count}}ක් යාවත්කාලීන විය",
"{{count}} books refreshed_other": "පොත් {{count}}ක් යාවත්කාලීන විය",
"Failed to refresh metadata": "පාරදත්ත යාවත්කාලීන කිරීම අසාර්ථකයි",
"Refresh Metadata": "පාරදත්ත යාවත්කාලීන කරන්න"
}
@@ -1121,5 +1121,12 @@
"Restore completed successfully!": "Obnovitev uspešno zaključena!",
"Your library has been saved to the selected location.": "Vaša knjižnica je shranjena na izbrani lokaciji.",
"{{added}} books added, {{updated}} books updated.": "{{added}} knjig dodanih, {{updated}} knjig posodobljenih.",
"Operation failed": "Operacija ni uspela"
"Operation failed": "Operacija ni uspela",
"Loading library...": "Nalaganje knjižnice...",
"{{count}} books refreshed_one": "Osvežena {{count}} knjiga",
"{{count}} books refreshed_two": "Osveženi {{count}} knjigi",
"{{count}} books refreshed_few": "Osvežene {{count}} knjige",
"{{count}} books refreshed_other": "Osveženih {{count}} knjig",
"Failed to refresh metadata": "Osveževanje metapodatkov ni uspelo",
"Refresh Metadata": "Osveži metapodatke"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "Återställningen slutförd!",
"Your library has been saved to the selected location.": "Ditt bibliotek har sparats på den valda platsen.",
"{{added}} books added, {{updated}} books updated.": "{{added}} böcker tillagda, {{updated}} böcker uppdaterade.",
"Operation failed": "Åtgärden misslyckades"
"Operation failed": "Åtgärden misslyckades",
"Loading library...": "Laddar bibliotek...",
"{{count}} books refreshed_one": "{{count}} bok uppdaterad",
"{{count}} books refreshed_other": "{{count}} böcker uppdaterade",
"Failed to refresh metadata": "Kunde inte uppdatera metadata",
"Refresh Metadata": "Uppdatera metadata"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "மீட்டமைப்பு வெற்றிகரமாக முடிந்தது!",
"Your library has been saved to the selected location.": "உங்கள் நூலகம் தேர்ந்தெடுக்கப்பட்ட இடத்தில் சேமிக்கப்பட்டது.",
"{{added}} books added, {{updated}} books updated.": "{{added}} புத்தகங்கள் சேர்க்கப்பட்டன, {{updated}} புத்தகங்கள் புதுப்பிக்கப்பட்டன.",
"Operation failed": "செயல்பாடு தோல்வி"
"Operation failed": "செயல்பாடு தோல்வி",
"Loading library...": "நூலகம் ஏற்றப்படுகிறது...",
"{{count}} books refreshed_one": "{{count}} புத்தகம் புதுப்பிக்கப்பட்டது",
"{{count}} books refreshed_other": "{{count}} புத்தகங்கள் புதுப்பிக்கப்பட்டன",
"Failed to refresh metadata": "மெட்டாடேட்டா புதுப்பிப்பு தோல்வி",
"Refresh Metadata": "மெட்டாடேட்டா புதுப்பிக்கவும்"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "กู้คืนสำเร็จ!",
"Your library has been saved to the selected location.": "ห้องสมุดของคุณถูกบันทึกไปยังตำแหน่งที่เลือก",
"{{added}} books added, {{updated}} books updated.": "เพิ่ม {{added}} เล่ม, อัปเดต {{updated}} เล่ม",
"Operation failed": "การดำเนินการล้มเหลว"
"Operation failed": "การดำเนินการล้มเหลว",
"Loading library...": "กำลังโหลดห้องสมุด...",
"{{count}} books refreshed_other": "รีเฟรชแล้ว {{count}} เล่ม",
"Failed to refresh metadata": "รีเฟรชข้อมูลเมตาล้มเหลว",
"Refresh Metadata": "รีเฟรชข้อมูลเมตา"
}
@@ -1097,5 +1097,10 @@
"Restore completed successfully!": "Geri yükleme başarıyla tamamlandı!",
"Your library has been saved to the selected location.": "Kütüphaneniz seçilen konuma kaydedildi.",
"{{added}} books added, {{updated}} books updated.": "{{added}} kitap eklendi, {{updated}} kitap güncellendi.",
"Operation failed": "İşlem başarısız"
"Operation failed": "İşlem başarısız",
"Loading library...": "Kütüphane yükleniyor...",
"{{count}} books refreshed_one": "{{count}} kitap yenilendi",
"{{count}} books refreshed_other": "{{count}} kitap yenilendi",
"Failed to refresh metadata": "Meta veri yenileme başarısız",
"Refresh Metadata": "Meta Verileri Yenile"
}
@@ -1121,5 +1121,12 @@
"Restore completed successfully!": "Відновлення завершено!",
"Your library has been saved to the selected location.": "Вашу бібліотеку збережено у вибраному місці.",
"{{added}} books added, {{updated}} books updated.": "Додано {{added}} книг, оновлено {{updated}} книг.",
"Operation failed": "Операція не вдалася"
"Operation failed": "Операція не вдалася",
"Loading library...": "Завантаження бібліотеки...",
"{{count}} books refreshed_one": "Оновлено {{count}} книгу",
"{{count}} books refreshed_few": "Оновлено {{count}} книги",
"{{count}} books refreshed_many": "Оновлено {{count}} книг",
"{{count}} books refreshed_other": "Оновлено {{count}} книг",
"Failed to refresh metadata": "Не вдалося оновити метадані",
"Refresh Metadata": "Оновити метадані"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "Khôi phục thành công!",
"Your library has been saved to the selected location.": "Thư viện của bạn đã được lưu tại vị trí đã chọn.",
"{{added}} books added, {{updated}} books updated.": "Đã thêm {{added}} sách, cập nhật {{updated}} sách.",
"Operation failed": "Thao tác thất bại"
"Operation failed": "Thao tác thất bại",
"Loading library...": "Đang tải thư viện...",
"{{count}} books refreshed_other": "Đã làm mới {{count}} sách",
"Failed to refresh metadata": "Không thể làm mới siêu dữ liệu",
"Refresh Metadata": "Làm mới siêu dữ liệu"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "恢复成功!",
"Your library has been saved to the selected location.": "您的书库已保存到所选位置。",
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本书,更新 {{updated}} 本书。",
"Operation failed": "操作失败"
"Operation failed": "操作失败",
"Loading library...": "正在加载书库...",
"{{count}} books refreshed_other": "已刷新 {{count}} 本书",
"Failed to refresh metadata": "刷新元数据失败",
"Refresh Metadata": "刷新元数据"
}
@@ -1085,5 +1085,9 @@
"Restore completed successfully!": "還原成功!",
"Your library has been saved to the selected location.": "您的書庫已儲存至所選位置。",
"{{added}} books added, {{updated}} books updated.": "新增 {{added}} 本書,更新 {{updated}} 本書。",
"Operation failed": "操作失敗"
"Operation failed": "操作失敗",
"Loading library...": "正在載入書庫...",
"{{count}} books refreshed_other": "已重新整理 {{count}} 本書",
"Failed to refresh metadata": "重新整理中繼資料失敗",
"Refresh Metadata": "重新整理中繼資料"
}
@@ -213,4 +213,32 @@ export function bookTests(
expect(loaded.progress).toEqual([10, 200]);
});
});
describe('Refresh metadata', () => {
it('should refresh metadata for a single book', async () => {
const service = getService();
const books: Book[] = [];
const book = await service.importBook(await getBookFile('sample-alice.epub'), books);
expect(book).not.toBeNull();
// Clear metadata to simulate a book imported before metadata parsing was added
book!.metadata = undefined;
book!.primaryLanguage = undefined;
const result = await service.refreshBookMetadata(book!);
expect(result).toBe(true);
expect(book!.metadata).toBeDefined();
});
it('should not update updatedAt', async () => {
const service = getService();
const books: Book[] = [];
const book = await service.importBook(await getBookFile('sample-alice.epub'), books);
expect(book).not.toBeNull();
const oldUpdatedAt = book!.updatedAt;
await service.refreshBookMetadata(book!);
expect(book!.updatedAt).toBe(oldUpdatedAt);
});
});
}
@@ -67,7 +67,9 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
);
const iconSize = useResponsiveSize(16);
const { isSyncing } = useLibraryStore();
const [isRefreshingMetadata, setIsRefreshingMetadata] = useState(false);
const [refreshMetadataProgress, setRefreshMetadataProgress] = useState('');
const { isSyncing, setLibrary } = useLibraryStore();
const { stats, hasActiveTransfers, setIsTransferQueueOpen } = useTransferQueue();
const openTransferQueue = () => {
@@ -189,6 +191,42 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
setBackupDialogVisible(true);
};
const handleRefreshMetadata = async () => {
if (!appService || isRefreshingMetadata) return;
setIsRefreshingMetadata(true);
setRefreshMetadataProgress(_('Loading library...'));
try {
const books = await appService.loadLibraryBooks();
const activeBooks = books.filter((b) => !b.deletedAt);
let refreshed = 0;
for (let i = 0; i < activeBooks.length; i++) {
setRefreshMetadataProgress(`${i + 1} / ${activeBooks.length}`);
try {
if (await appService.refreshBookMetadata(activeBooks[i]!)) {
refreshed++;
}
} catch {
// Skip books whose files can't be opened
}
}
setLibrary(books);
await appService.saveLibraryBooks(books);
setRefreshMetadataProgress(_('{{count}} books refreshed', { count: refreshed }));
onPullLibrary(true);
setTimeout(() => {
setIsRefreshingMetadata(false);
setRefreshMetadataProgress('');
}, 2000);
} catch (error) {
console.error('Failed to refresh metadata:', error);
setRefreshMetadataProgress(_('Failed to refresh metadata'));
setTimeout(() => {
setIsRefreshingMetadata(false);
setRefreshMetadataProgress('');
}, 2000);
}
};
const openSettingsDialog = () => {
setIsDropdownOpen?.(false);
setSettingsDialogOpen(true);
@@ -381,6 +419,12 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
<MenuItem label={_('Change Data Location')} onClick={handleSetRootDir} />
)}
<MenuItem label={_('Backup & Restore')} onClick={handleBackupRestore} />
<MenuItem
label={_('Refresh Metadata')}
description={refreshMetadataProgress}
onClick={handleRefreshMetadata}
disabled={isRefreshingMetadata}
/>
{appService?.isAndroidApp && appService?.distChannel !== 'playstore' && (
<MenuItem
label={_('Save Book Cover')}
@@ -301,6 +301,10 @@ export abstract class BaseAppService implements AppService {
);
}
async refreshBookMetadata(book: Book): Promise<boolean> {
return BookSvc.refreshBookMetadata(this.fs, book);
}
async isBookAvailable(book: Book): Promise<boolean> {
return BookSvc.isBookAvailable(this.fs, book);
}
@@ -527,6 +527,37 @@ export async function fetchBookDetails(
return bookDoc.metadata;
}
/**
* Refresh metadata for a single book by re-opening and re-parsing its file.
* Updates series info, language, and other metadata fields without modifying
* user-edited titles or reading progress.
* Returns true if the metadata was successfully refreshed.
*/
export async function refreshBookMetadata(fs: FileSystem, book: Book): Promise<boolean> {
const { file } = await loadBookContent(fs, book);
const { book: bookDoc } = await new DocumentLoader(file).open();
if (!bookDoc) return false;
book.metadata = bookDoc.metadata;
book.metaHash = getMetadataHash(bookDoc.metadata);
const primaryLanguage = getPrimaryLanguage(bookDoc.metadata.language);
if (primaryLanguage) {
book.primaryLanguage = primaryLanguage;
}
// Update series info from metadata
if (book.metadata?.belongsTo?.series) {
const belongsTo = book.metadata.belongsTo.series;
const series = Array.isArray(belongsTo) ? belongsTo[0] : belongsTo;
if (series) {
book.metadata.series = formatTitle(series.name);
book.metadata.seriesIndex = parseFloat(series.position || '0');
}
}
return true;
}
export async function exportBook(
fs: FileSystem,
book: Book,
+1
View File
@@ -133,6 +133,7 @@ export interface AppService {
overwrite?: boolean,
transient?: boolean,
): Promise<Book | null>;
refreshBookMetadata(book: Book): Promise<boolean>;
deleteBook(book: Book, deleteAction: DeleteAction): Promise<void>;
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
downloadBook(