diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index dc5f9303..65cf1ddb 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1923,5 +1923,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "يؤدي هذا إلى مسح تقدم القراءة والملاحظات والإشارات المرجعية نهائيًا. لا يمكن التراجع عن هذا الإجراء.", "Also erase reading progress, notes, and bookmarks.": "يمسح أيضًا تقدم القراءة والملاحظات والإشارات المرجعية.", "Background Image (Library)": "صورة الخلفية (المكتبة)", - "Background Image (Reader)": "صورة الخلفية (واجهة القراءة)" + "Background Image (Reader)": "صورة الخلفية (واجهة القراءة)", + "updated metadata for {{n}} book(s)": "تم تحديث البيانات الوصفية لـ {{n}} كتاب", + "{{n}} metadata": "{{n}} بيانات وصفية", + "Metadata updated": "تم تحديث البيانات الوصفية" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 68fe177d..946cfef5 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "এটি স্থায়ীভাবে পড়ার অগ্রগতি, নোট এবং বুকমার্ক মুছে ফেলে। এটি পূর্বাবস্থায় ফেরানো যাবে না।", "Also erase reading progress, notes, and bookmarks.": "পড়ার অগ্রগতি, নোট এবং বুকমার্কও মুছে ফেলে।", "Background Image (Library)": "ব্যাকগ্রাউন্ড ছবি (লাইব্রেরি)", - "Background Image (Reader)": "ব্যাকগ্রাউন্ড ছবি (রিডার)" + "Background Image (Reader)": "ব্যাকগ্রাউন্ড ছবি (রিডার)", + "updated metadata for {{n}} book(s)": "{{n}}টি বইয়ের মেটাডেটা আপডেট হয়েছে", + "{{n}} metadata": "{{n}} মেটাডেটা", + "Metadata updated": "মেটাডেটা আপডেট হয়েছে" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index 934aea2f..3e6fa4c9 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "འདིས་ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་གཏན་དུ་སུབ་ངེས་ཡིན། ཕྱིར་ལྡོག་བྱེད་མི་ཐུབ།", "Also erase reading progress, notes, and bookmarks.": "ཀློག་པའི་ཡར་འཕེལ་དང་། ཟིན་བྲིས། དཔེ་རྟགས་བཅས་ཀྱང་སུབ་པ།", "Background Image (Library)": "གནས་སྟངས་ཀྱི་རི་མོ། (དེབ་མཛོད།)", - "Background Image (Reader)": "གནས་སྟངས་ཀྱི་རི་མོ། (ཀློག་ངོས།)" + "Background Image (Reader)": "གནས་སྟངས་ཀྱི་རི་མོ། (ཀློག་ངོས།)", + "updated metadata for {{n}} book(s)": "དཔེ་ཆ་{{n}}་ཡི་གནད་སྨིན་གོ་དོན་གསར་བསྒྱུར་བྱས།", + "{{n}} metadata": "{{n}} གནད་སྨིན་གོ་དོན།", + "Metadata updated": "གནད་སྨིན་གོ་དོན་གསར་བསྒྱུར་བྱས།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 4acadf9e..055c4520 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Dadurch werden Lesefortschritt, Notizen und Lesezeichen dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.", "Also erase reading progress, notes, and bookmarks.": "Löscht auch Lesefortschritt, Notizen und Lesezeichen.", "Background Image (Library)": "Hintergrundbild (Bibliothek)", - "Background Image (Reader)": "Hintergrundbild (Leseansicht)" + "Background Image (Reader)": "Hintergrundbild (Leseansicht)", + "updated metadata for {{n}} book(s)": "Metadaten für {{n}} Buch/Bücher aktualisiert", + "{{n}} metadata": "{{n}} Metadaten", + "Metadata updated": "Metadaten aktualisiert" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index c066b538..0c5f936c 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Αυτό διαγράφει οριστικά την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες. Δεν είναι δυνατή η αναίρεση.", "Also erase reading progress, notes, and bookmarks.": "Διαγράφει επίσης την πρόοδο ανάγνωσης, τις σημειώσεις και τους σελιδοδείκτες.", "Background Image (Library)": "Εικόνα φόντου (Βιβλιοθήκη)", - "Background Image (Reader)": "Εικόνα φόντου (Προβολή ανάγνωσης)" + "Background Image (Reader)": "Εικόνα φόντου (Προβολή ανάγνωσης)", + "updated metadata for {{n}} book(s)": "ενημερώθηκαν τα μεταδεδομένα για {{n}} βιβλίο/α", + "{{n}} metadata": "{{n}} μεταδεδομένα", + "Metadata updated": "Ενημερωμένα μεταδεδομένα" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 33a67d6d..d4097ee2 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Esto borra permanentemente el progreso de lectura, las notas y los marcadores. Esta acción no se puede deshacer.", "Also erase reading progress, notes, and bookmarks.": "También borra el progreso de lectura, las notas y los marcadores.", "Background Image (Library)": "Imagen de fondo (Biblioteca)", - "Background Image (Reader)": "Imagen de fondo (Lector)" + "Background Image (Reader)": "Imagen de fondo (Lector)", + "updated metadata for {{n}} book(s)": "metadatos actualizados de {{n}} libro(s)", + "{{n}} metadata": "{{n}} metadatos", + "Metadata updated": "Metadatos actualizados" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 3bc18b5d..825f3134 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "این کار وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را برای همیشه پاک می‌کند. این عمل قابل بازگشت نیست.", "Also erase reading progress, notes, and bookmarks.": "همچنین وضعیت مطالعه، یادداشت‌ها و نشانک‌ها را پاک می‌کند.", "Background Image (Library)": "تصویر پس‌زمینه (کتابخانه)", - "Background Image (Reader)": "تصویر پس‌زمینه (صفحه مطالعه)" + "Background Image (Reader)": "تصویر پس‌زمینه (صفحه مطالعه)", + "updated metadata for {{n}} book(s)": "متادیتای {{n}} کتاب به‌روزرسانی شد", + "{{n}} metadata": "{{n}} متادیتا", + "Metadata updated": "متادیتا به‌روزرسانی شد" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 29027650..22d4bbe8 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Cela efface définitivement la progression de lecture, les notes et les signets. Cette action est irréversible.", "Also erase reading progress, notes, and bookmarks.": "Efface aussi la progression de lecture, les notes et les signets.", "Background Image (Library)": "Image de Fond (Bibliothèque)", - "Background Image (Reader)": "Image de Fond (Vue de lecture)" + "Background Image (Reader)": "Image de Fond (Vue de lecture)", + "updated metadata for {{n}} book(s)": "métadonnées mises à jour pour {{n}} livre(s)", + "{{n}} metadata": "{{n}} métadonnées", + "Metadata updated": "Métadonnées mises à jour" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 968507e9..dffeb5ae 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "פעולה זו מוחקת לצמיתות את התקדמות הקריאה, ההערות והסימניות. לא ניתן לבטל פעולה זו.", "Also erase reading progress, notes, and bookmarks.": "מוחק גם את התקדמות הקריאה, ההערות והסימניות.", "Background Image (Library)": "תמונת רקע (ספרייה)", - "Background Image (Reader)": "תמונת רקע (מצב קריאה)" + "Background Image (Reader)": "תמונת רקע (מצב קריאה)", + "updated metadata for {{n}} book(s)": "המטא-נתונים עודכנו עבור {{n}} ספרים", + "{{n}} metadata": "{{n}} מטא-נתונים", + "Metadata updated": "המטא-נתונים עודכנו" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 31531b9d..49f37909 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "यह पढ़ने की प्रगति, नोट्स और बुकमार्क को स्थायी रूप से मिटा देता है। इसे पूर्ववत नहीं किया जा सकता।", "Also erase reading progress, notes, and bookmarks.": "पढ़ने की प्रगति, नोट्स और बुकमार्क भी मिटा देता है।", "Background Image (Library)": "पृष्ठभूमि छवि (लाइब्रेरी)", - "Background Image (Reader)": "पृष्ठभूमि छवि (रीडर)" + "Background Image (Reader)": "पृष्ठभूमि छवि (रीडर)", + "updated metadata for {{n}} book(s)": "{{n}} पुस्तक(ों) के मेटाडेटा अपडेट किए गए", + "{{n}} metadata": "{{n}} मेटाडेटा", + "Metadata updated": "मेटाडेटा अपडेट किया गया" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 2922bbc0..cd934cd3 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ez véglegesen törli az olvasási haladást, a jegyzeteket és a könyvjelzőket. A művelet nem vonható vissza.", "Also erase reading progress, notes, and bookmarks.": "Az olvasási haladást, jegyzeteket és könyvjelzőket is törli.", "Background Image (Library)": "Háttérkép (Könyvtár)", - "Background Image (Reader)": "Háttérkép (Olvasónézet)" + "Background Image (Reader)": "Háttérkép (Olvasónézet)", + "updated metadata for {{n}} book(s)": "{{n}} könyv metaadatai frissítve", + "{{n}} metadata": "{{n}} metaadat", + "Metadata updated": "Metaadatok frissítve" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 50168e1e..d5aa72c7 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini menghapus permanen progres membaca, catatan, dan penanda. Tindakan ini tidak dapat dibatalkan.", "Also erase reading progress, notes, and bookmarks.": "Juga menghapus progres membaca, catatan, dan penanda.", "Background Image (Library)": "Gambar Latar Belakang (Perpustakaan)", - "Background Image (Reader)": "Gambar Latar Belakang (Tampilan Baca)" + "Background Image (Reader)": "Gambar Latar Belakang (Tampilan Baca)", + "updated metadata for {{n}} book(s)": "metadata diperbarui untuk {{n}} buku", + "{{n}} metadata": "{{n}} metadata", + "Metadata updated": "Metadata diperbarui" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index d835f298..b676b933 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Questa operazione cancella definitivamente il progresso di lettura, le note e i segnalibri. Non può essere annullata.", "Also erase reading progress, notes, and bookmarks.": "Cancella anche il progresso di lettura, le note e i segnalibri.", "Background Image (Library)": "Immagine di sfondo (Libreria)", - "Background Image (Reader)": "Immagine di sfondo (Vista di lettura)" + "Background Image (Reader)": "Immagine di sfondo (Vista di lettura)", + "updated metadata for {{n}} book(s)": "metadati aggiornati per {{n}} libro/i", + "{{n}} metadata": "{{n}} metadati", + "Metadata updated": "Metadati aggiornati" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index f591c979..96da46d9 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "読書進捗、メモ、ブックマークが完全に消去されます。この操作は元に戻せません。", "Also erase reading progress, notes, and bookmarks.": "読書進捗、メモ、ブックマークも消去します。", "Background Image (Library)": "背景画像(ライブラリ)", - "Background Image (Reader)": "背景画像(リーダー)" + "Background Image (Reader)": "背景画像(リーダー)", + "updated metadata for {{n}} book(s)": "{{n}} 冊分のメタデータを更新", + "{{n}} metadata": "{{n}} 件メタデータ", + "Metadata updated": "更新したメタデータ" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 125664b2..cedba22f 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "읽기 진행 상황, 메모, 북마크가 영구적으로 지워집니다. 이 작업은 되돌릴 수 없습니다.", "Also erase reading progress, notes, and bookmarks.": "읽기 진행 상황, 메모, 북마크도 함께 지웁니다.", "Background Image (Library)": "배경 이미지 (라이브러리)", - "Background Image (Reader)": "배경 이미지 (리더)" + "Background Image (Reader)": "배경 이미지 (리더)", + "updated metadata for {{n}} book(s)": "{{n}}권의 메타데이터 업데이트됨", + "{{n}} metadata": "{{n}} 메타데이터", + "Metadata updated": "메타데이터 업데이트됨" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 11c51d5f..366b6ac4 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Ini memadam kemajuan membaca, nota dan penanda buku secara kekal. Tindakan ini tidak boleh dibatalkan.", "Also erase reading progress, notes, and bookmarks.": "Turut memadam kemajuan membaca, nota dan penanda buku.", "Background Image (Library)": "Imej Latar Belakang (Perpustakaan)", - "Background Image (Reader)": "Imej Latar Belakang (Paparan Bacaan)" + "Background Image (Reader)": "Imej Latar Belakang (Paparan Bacaan)", + "updated metadata for {{n}} book(s)": "metadata dikemas kini untuk {{n}} buku", + "{{n}} metadata": "{{n}} metadata", + "Metadata updated": "Metadata dikemas kini" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 3adab7a6..bdea4d29 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Hiermee worden leesvoortgang, notities en bladwijzers permanent gewist. Dit kan niet ongedaan worden gemaakt.", "Also erase reading progress, notes, and bookmarks.": "Wist ook leesvoortgang, notities en bladwijzers.", "Background Image (Library)": "Achtergrondafbeelding (Bibliotheek)", - "Background Image (Reader)": "Achtergrondafbeelding (Leesweergave)" + "Background Image (Reader)": "Achtergrondafbeelding (Leesweergave)", + "updated metadata for {{n}} book(s)": "metadata bijgewerkt voor {{n}} boek(en)", + "{{n}} metadata": "{{n}} metadata", + "Metadata updated": "Metadata bijgewerkt" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 21777d17..098c4b52 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1857,5 +1857,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Spowoduje to trwałe usunięcie postępu czytania, notatek i zakładek. Tej operacji nie można cofnąć.", "Also erase reading progress, notes, and bookmarks.": "Usuwa również postęp czytania, notatki i zakładki.", "Background Image (Library)": "Obraz tła (Biblioteka)", - "Background Image (Reader)": "Obraz tła (Widok czytania)" + "Background Image (Reader)": "Obraz tła (Widok czytania)", + "updated metadata for {{n}} book(s)": "zaktualizowano metadane dla {{n}} książek", + "{{n}} metadata": "{{n}} metadane", + "Metadata updated": "Zaktualizowane metadane" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 353dd54e..04c1465a 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isso apaga permanentemente o progresso de leitura, as notas e os marcadores. Esta ação não pode ser desfeita.", "Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, as notas e os marcadores.", "Background Image (Library)": "Imagem de fundo (Biblioteca)", - "Background Image (Reader)": "Imagem de fundo (Vista de leitura)" + "Background Image (Reader)": "Imagem de fundo (Vista de leitura)", + "updated metadata for {{n}} book(s)": "metadados atualizados de {{n}} livro(s)", + "{{n}} metadata": "{{n}} metadados", + "Metadata updated": "Metadados atualizados" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index 94bc809b..a1f420b2 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Isto apaga permanentemente o progresso de leitura, notas e marcadores. Esta ação não pode ser desfeita.", "Also erase reading progress, notes, and bookmarks.": "Também apaga o progresso de leitura, notas e marcadores.", "Background Image (Library)": "Imagem de Fundo (Biblioteca)", - "Background Image (Reader)": "Imagem de Fundo (Vista de leitura)" + "Background Image (Reader)": "Imagem de Fundo (Vista de leitura)", + "updated metadata for {{n}} book(s)": "metadados atualizados de {{n}} livro(s)", + "{{n}} metadata": "{{n}} metadados", + "Metadata updated": "Metadados atualizados" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index dcd05cae..05c6222a 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1824,5 +1824,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Aceasta șterge definitiv progresul citirii, notele și marcajele. Această acțiune nu poate fi anulată.", "Also erase reading progress, notes, and bookmarks.": "Șterge de asemenea progresul citirii, notele și marcajele.", "Background Image (Library)": "Imagine de fundal (Bibliotecă)", - "Background Image (Reader)": "Imagine de fundal (Vizualizare de lectură)" + "Background Image (Reader)": "Imagine de fundal (Vizualizare de lectură)", + "updated metadata for {{n}} book(s)": "metadate actualizate pentru {{n}} carte/cărți", + "{{n}} metadata": "{{n}} metadate", + "Metadata updated": "Metadate actualizate" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index d11145b4..611041a1 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1857,5 +1857,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Это безвозвратно удалит прогресс чтения, заметки и закладки. Это действие нельзя отменить.", "Also erase reading progress, notes, and bookmarks.": "Также удаляет прогресс чтения, заметки и закладки.", "Background Image (Library)": "Фоновое изображение (Библиотека)", - "Background Image (Reader)": "Фоновое изображение (Режим чтения)" + "Background Image (Reader)": "Фоновое изображение (Режим чтения)", + "updated metadata for {{n}} book(s)": "обновлены метаданные для {{n}} книг", + "{{n}} metadata": "{{n}} метаданные", + "Metadata updated": "Метаданные обновлены" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 46b4d0b5..7e3a7bb3 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "මෙය කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ස්ථිරවම මකා දමයි. මෙය අහෝසි කළ නොහැක.", "Also erase reading progress, notes, and bookmarks.": "කියවීමේ ප්‍රගතිය, සටහන් සහ පොත් සලකුණු ද මකා දමයි.", "Background Image (Library)": "පසුබිම් රූපය (පුස්තකාලය)", - "Background Image (Reader)": "පසුබිම් රූපය (කියවීමේ දසුන)" + "Background Image (Reader)": "පසුබිම් රූපය (කියවීමේ දසුන)", + "updated metadata for {{n}} book(s)": "පොත් {{n}}ක මෙටාඩේටා යාවත්කාලීන කරන ලදී", + "{{n}} metadata": "{{n}} මෙටාඩේටා", + "Metadata updated": "මෙටාඩේටා යාවත්කාලීන කරන ලදී" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index df274a9c..fb449824 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1857,5 +1857,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "To trajno izbriše napredek branja, opombe in zaznamke. Tega ni mogoče razveljaviti.", "Also erase reading progress, notes, and bookmarks.": "Izbriše tudi napredek branja, opombe in zaznamke.", "Background Image (Library)": "Slikovno ozadje (Knjižnica)", - "Background Image (Reader)": "Slikovno ozadje (Bralni pogled)" + "Background Image (Reader)": "Slikovno ozadje (Bralni pogled)", + "updated metadata for {{n}} book(s)": "posodobljeni metapodatki za {{n}} knjig", + "{{n}} metadata": "{{n}} metapodatki", + "Metadata updated": "Metapodatki posodobljeni" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index a62607c6..3c5ea89b 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Detta raderar permanent läsframsteg, anteckningar och bokmärken. Detta kan inte ångras.", "Also erase reading progress, notes, and bookmarks.": "Raderar även läsframsteg, anteckningar och bokmärken.", "Background Image (Library)": "Bakgrundsbild (Bibliotek)", - "Background Image (Reader)": "Bakgrundsbild (Läsvy)" + "Background Image (Reader)": "Bakgrundsbild (Läsvy)", + "updated metadata for {{n}} book(s)": "uppdaterade metadata för {{n}} bok/böcker", + "{{n}} metadata": "{{n}} metadata", + "Metadata updated": "Metadata uppdaterad" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 7a053aa7..b761dc0b 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "இது வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளை நிரந்தரமாக அழிக்கும். இதை மீட்டெடுக்க முடியாது.", "Also erase reading progress, notes, and bookmarks.": "வாசிப்பு முன்னேற்றம், குறிப்புகள் மற்றும் புக்மார்க்குகளையும் அழிக்கும்.", "Background Image (Library)": "பின்னணி படம் (நூலகம்)", - "Background Image (Reader)": "பின்னணி படம் (வாசிப்புப் பக்கம்)" + "Background Image (Reader)": "பின்னணி படம் (வாசிப்புப் பக்கம்)", + "updated metadata for {{n}} book(s)": "{{n}} புத்தகத்தின் மெட்டாடேட்டா புதுப்பிக்கப்பட்டது", + "{{n}} metadata": "{{n}} மெட்டாடேட்டா", + "Metadata updated": "மெட்டாடேட்டா புதுப்பிக்கப்பட்டது" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 44edd147..aa50894b 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "การดำเนินการนี้จะลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กอย่างถาวร ไม่สามารถเลิกทำได้", "Also erase reading progress, notes, and bookmarks.": "ลบความคืบหน้าในการอ่าน บันทึก และบุ๊กมาร์กด้วย", "Background Image (Library)": "ภาพพื้นหลัง (คลังหนังสือ)", - "Background Image (Reader)": "ภาพพื้นหลัง (มุมมองการอ่าน)" + "Background Image (Reader)": "ภาพพื้นหลัง (มุมมองการอ่าน)", + "updated metadata for {{n}} book(s)": "อัปเดตเมทาดาตาสำหรับ {{n}} เล่ม", + "{{n}} metadata": "{{n}} เมทาดาตา", + "Metadata updated": "อัปเดตเมทาดาตาแล้ว" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 5ecfbb9b..de8b2566 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu, okuma ilerlemesini, notları ve yer işaretlerini kalıcı olarak siler. Bu işlem geri alınamaz.", "Also erase reading progress, notes, and bookmarks.": "Okuma ilerlemesi, notlar ve yer işaretlerini de siler.", "Background Image (Library)": "Arka Plan Görüntüsü (Kütüphane)", - "Background Image (Reader)": "Arka Plan Görüntüsü (Okuma Görünümü)" + "Background Image (Reader)": "Arka Plan Görüntüsü (Okuma Görünümü)", + "updated metadata for {{n}} book(s)": "{{n}} kitabın meta verileri güncellendi", + "{{n}} metadata": "{{n}} meta veri", + "Metadata updated": "Meta veriler güncellendi" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 71f98620..606ed5e5 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1857,5 +1857,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Це назавжди видалить проґрес читання, нотатки та закладки. Цю дію не можна скасувати.", "Also erase reading progress, notes, and bookmarks.": "Також видаляє проґрес читання, нотатки та закладки.", "Background Image (Library)": "Фонове зображення (Бібліотека)", - "Background Image (Reader)": "Фонове зображення (Режим читання)" + "Background Image (Reader)": "Фонове зображення (Режим читання)", + "updated metadata for {{n}} book(s)": "оновлено метадані для {{n}} книг", + "{{n}} metadata": "{{n}} метадані", + "Metadata updated": "Метадані оновлено" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index 4054d10f..db9cde80 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -1791,5 +1791,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Bu oʻqish jarayoni, eslatmalar va xatchoʻplarni butunlay oʻchiradi. Buni qaytarib boʻlmaydi.", "Also erase reading progress, notes, and bookmarks.": "Shuningdek, oʻqish jarayoni, eslatmalar va xatchoʻplarni ham oʻchiradi.", "Background Image (Library)": "Fon rasmi (Kutubxona)", - "Background Image (Reader)": "Fon rasmi (Oʻqish koʻrinishi)" + "Background Image (Reader)": "Fon rasmi (Oʻqish koʻrinishi)", + "updated metadata for {{n}} book(s)": "{{n}} ta kitob metamaʼlumotlari yangilandi", + "{{n}} metadata": "{{n}} metamaʼlumot", + "Metadata updated": "Metamaʼlumotlar yangilandi" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index bcbc17a2..81b6f9eb 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "Thao tác này xóa vĩnh viễn tiến độ đọc, ghi chú và dấu trang. Không thể hoàn tác.", "Also erase reading progress, notes, and bookmarks.": "Đồng thời xóa tiến độ đọc, ghi chú và dấu trang.", "Background Image (Library)": "Ảnh nền (Thư viện)", - "Background Image (Reader)": "Ảnh nền (Trình đọc)" + "Background Image (Reader)": "Ảnh nền (Trình đọc)", + "updated metadata for {{n}} book(s)": "đã cập nhật siêu dữ liệu cho {{n}} sách", + "{{n}} metadata": "{{n}} siêu dữ liệu", + "Metadata updated": "Đã cập nhật siêu dữ liệu" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 69baf40b..dc5f87cd 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "这将永久清除阅读进度、笔记和书签,且无法撤销。", "Also erase reading progress, notes, and bookmarks.": "同时清除阅读进度、笔记和书签。", "Background Image (Library)": "背景图片(书库)", - "Background Image (Reader)": "背景图片(阅读界面)" + "Background Image (Reader)": "背景图片(阅读界面)", + "updated metadata for {{n}} book(s)": "已更新 {{n}} 本书的元数据", + "{{n}} metadata": "{{n}} 元数据", + "Metadata updated": "已更新元数据" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index ea425542..cf287825 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1758,5 +1758,8 @@ "This permanently erases reading progress, notes, and bookmarks. This cannot be undone.": "這將永久清除閱讀進度、筆記和書籤,且無法復原。", "Also erase reading progress, notes, and bookmarks.": "同時清除閱讀進度、筆記和書籤。", "Background Image (Library)": "背景圖片(書庫)", - "Background Image (Reader)": "背景圖片(閱讀介面)" + "Background Image (Reader)": "背景圖片(閱讀介面)", + "updated metadata for {{n}} book(s)": "已更新 {{n}} 本書的中繼資料", + "{{n}} metadata": "{{n}} 中繼資料", + "Metadata updated": "已更新中繼資料" } diff --git a/apps/readest-app/src/__tests__/services/webdav-metadata-sync.test.ts b/apps/readest-app/src/__tests__/services/webdav-metadata-sync.test.ts new file mode 100644 index 00000000..a6f9b51e --- /dev/null +++ b/apps/readest-app/src/__tests__/services/webdav-metadata-sync.test.ts @@ -0,0 +1,277 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +import type { WebDAVSettings } from '@/types/settings'; +import type { Book, BookConfig, BookNote } from '@/types/book'; + +/** + * Regression tests for issue #4756: once a device already has a book in its + * local library, the manual "Sync now" path never pulled the book's metadata + * (title / author / cover) back from the shared `library.json`, and the final + * index re-push clobbered the remote with the device's stale copy. + * + * These tests mock the WebDAVClient I/O primitives so we can drive + * `syncLibrary` deterministically without a real server, and assert the + * last-writer-wins reconciliation on `book.updatedAt`. + */ + +vi.mock('@/services/webdav/WebDAVClient', async (importActual) => { + const actual = await importActual(); + return { + ...actual, + getFile: vi.fn(), + getFileBinary: vi.fn(), + headFile: vi.fn(), + listDirectory: vi.fn(), + putFile: vi.fn(), + putFileBinary: vi.fn(), + ensureDirectory: vi.fn(), + deleteDirectory: vi.fn(), + }; +}); + +import { + getFile, + getFileBinary, + headFile, + listDirectory, + putFile, + putFileBinary, + ensureDirectory, +} from '@/services/webdav/WebDAVClient'; +import { + syncLibrary, + type RemoteLibraryIndex, + type RemoteBookConfig, +} from '@/services/webdav/WebDAVSync'; + +const settings: WebDAVSettings = { + enabled: true, + serverUrl: 'https://dav.example.com', + username: 'alice', + password: 'secret', + rootPath: '/', +}; + +const makeLocalBook = (overrides: Partial = {}): Book => ({ + hash: 'h1', + format: 'EPUB', + title: 'Old Title', + sourceTitle: 'Old Title', + author: 'Old Author', + createdAt: 1, + updatedAt: 100, + ...overrides, +}); + +const makeRemoteIndex = (book: Book, updatedAt = book.updatedAt): RemoteLibraryIndex => ({ + schemaVersion: 1, + updatedAt, + books: [book], +}); + +/** + * Route `getFile` by path: library.json → index JSON, config.json → the + * supplied remote envelope (or null when none). + */ +const wireGetFile = ( + index: RemoteLibraryIndex | null, + remoteConfig: RemoteBookConfig | null = null, +) => { + (getFile as ReturnType).mockImplementation(async (_client, path: string) => { + if (path.endsWith('library.json')) return index ? JSON.stringify(index) : null; + if (path.endsWith('config.json')) return remoteConfig ? JSON.stringify(remoteConfig) : null; + return null; + }); +}; + +/** Capture the library index that was re-pushed at the end of the sync. */ +const capturePushedIndex = (): { value: RemoteLibraryIndex | null } => { + const captured: { value: RemoteLibraryIndex | null } = { value: null }; + (putFile as ReturnType).mockImplementation(async (_client, path: string, body) => { + if (path.endsWith('library.json')) captured.value = JSON.parse(body as string); + }); + return captured; +}; + +/** Capture the per-book config envelope pushed to /config.json. */ +const capturePushedConfig = (): { value: RemoteBookConfig | null } => { + const captured: { value: RemoteBookConfig | null } = { value: null }; + (putFile as ReturnType).mockImplementation(async (_client, path: string, body) => { + if (path.endsWith('config.json')) captured.value = JSON.parse(body as string); + }); + return captured; +}; + +const makeNote = (id: string, updatedAt: number): BookNote => ({ + id, + type: 'annotation', + cfi: `cfi-${id}`, + note: '', + createdAt: updatedAt, + updatedAt, +}); + +const makeRemoteConfig = (overrides: Partial = {}): RemoteBookConfig => ({ + schemaVersion: 1, + bookHash: 'h1', + config: { updatedAt: 100 }, + booknotes: [], + writerDeviceId: 'mobile', + writerVersion: 'readest-webdav-1', + updatedAt: 100, + ...overrides, +}); + +beforeEach(() => { + vi.clearAllMocks(); + (listDirectory as ReturnType).mockResolvedValue([]); + (getFileBinary as ReturnType).mockResolvedValue(new ArrayBuffer(8)); + (headFile as ReturnType).mockResolvedValue(null); + (putFile as ReturnType).mockResolvedValue(undefined); + (putFileBinary as ReturnType).mockResolvedValue(undefined); + (ensureDirectory as ReturnType).mockResolvedValue(undefined); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +const baseOptions = () => ({ + strategy: 'silent' as const, + syncBooks: false, + deviceId: 'pc-device', + loadConfig: async (): Promise => ({ updatedAt: 50, booknotes: [] }), + loadBookFile: async () => null, + loadBookCover: async () => null, + onProgress: () => {}, +}); + +describe('syncLibrary metadata reconciliation (#4756)', () => { + test('pulls newer remote metadata for a book the device already has', async () => { + const local = makeLocalBook({ updatedAt: 100 }); + const remote = makeLocalBook({ + title: 'New Title', + author: 'New Author', + updatedAt: 200, + }); + wireGetFile(makeRemoteIndex(remote, 200)); + const pushedIndex = capturePushedIndex(); + + const updateBookMetadata = vi.fn(async (_book: Book) => {}); + const saveBookCover = vi.fn(async (_book: Book, _bytes: ArrayBuffer) => {}); + + const result = await syncLibrary(settings, [local], { + ...baseOptions(), + updateBookMetadata, + saveBookCover, + }); + + // The device must learn the remote title/author. + expect(updateBookMetadata).toHaveBeenCalledTimes(1); + const merged = updateBookMetadata.mock.calls[0]![0]; + expect(merged.title).toBe('New Title'); + expect(merged.author).toBe('New Author'); + expect(result.metadataUpdated).toBe(1); + + // The cover must be re-pulled so a changed cover propagates. + expect(saveBookCover).toHaveBeenCalledTimes(1); + + // The re-pushed index must carry the newer metadata, not clobber it. + expect(pushedIndex.value).not.toBeNull(); + const indexedBook = pushedIndex.value!.books.find((b) => b.hash === 'h1')!; + expect(indexedBook.title).toBe('New Title'); + }); + + test('does not overwrite local metadata when the local copy is newer', async () => { + const local = makeLocalBook({ title: 'Local Newer', updatedAt: 300 }); + const remote = makeLocalBook({ title: 'Remote Older', updatedAt: 200 }); + wireGetFile(makeRemoteIndex(remote, 200)); + const pushedIndex = capturePushedIndex(); + + const updateBookMetadata = vi.fn(async (_book: Book) => {}); + + const result = await syncLibrary(settings, [local], { + ...baseOptions(), + updateBookMetadata, + }); + + expect(updateBookMetadata).not.toHaveBeenCalled(); + expect(result.metadataUpdated).toBe(0); + + // Local wins: the re-pushed index keeps the local title. + const indexedBook = pushedIndex.value!.books.find((b) => b.hash === 'h1')!; + expect(indexedBook.title).toBe('Local Newer'); + }); +}); + +describe('syncLibrary config merge before push (Sync now must not blind-overwrite)', () => { + test('unions remote booknotes into the pushed config instead of clobbering them', async () => { + const local = makeLocalBook({ updatedAt: 100 }); + // Same updatedAt in the index so the metadata pass is a no-op — this + // isolates the config/notes path. + wireGetFile( + makeRemoteIndex(makeLocalBook({ updatedAt: 100 }), 100), + makeRemoteConfig({ config: { updatedAt: 100 }, booknotes: [makeNote('remote-note', 100)] }), + ); + const pushedConfig = capturePushedConfig(); + + await syncLibrary(settings, [local], { + ...baseOptions(), + loadConfig: async (): Promise => ({ + updatedAt: 50, + booknotes: [makeNote('local-note', 50)], + }), + }); + + // The pushed envelope must carry BOTH notes — a blind push would have + // dropped the peer's note that this device never pulled. + expect(pushedConfig.value).not.toBeNull(); + const ids = pushedConfig.value!.booknotes.map((n) => n.id).sort(); + expect(ids).toEqual(['local-note', 'remote-note']); + }); + + test('does not regress newer remote progress with an older local push', async () => { + const local = makeLocalBook({ updatedAt: 100 }); + wireGetFile( + makeRemoteIndex(makeLocalBook({ updatedAt: 100 }), 100), + makeRemoteConfig({ config: { updatedAt: 200, progress: [50, 100] }, booknotes: [] }), + ); + const pushedConfig = capturePushedConfig(); + + await syncLibrary(settings, [local], { + ...baseOptions(), + loadConfig: async (): Promise => ({ + updatedAt: 50, + progress: [10, 100], + booknotes: [], + }), + }); + + // Remote progress is newer (updatedAt 200 > 50): the LWW merge must push + // the remote's page, never regress it back to the local page. + expect(pushedConfig.value!.config.progress).toEqual([50, 100]); + }); + + test('send strategy keeps the blind push (local authoritative, no pull-merge)', async () => { + const local = makeLocalBook({ updatedAt: 100 }); + wireGetFile( + null, + makeRemoteConfig({ config: { updatedAt: 200 }, booknotes: [makeNote('remote-note', 200)] }), + ); + const pushedConfig = capturePushedConfig(); + + await syncLibrary(settings, [local], { + ...baseOptions(), + strategy: 'send', + loadConfig: async (): Promise => ({ + updatedAt: 50, + booknotes: [makeNote('local-note', 50)], + }), + }); + + // 'send' deliberately treats local as the source of truth — it must not + // pull the remote note in. + const ids = pushedConfig.value!.booknotes.map((n) => n.id); + expect(ids).toEqual(['local-note']); + }); +}); diff --git a/apps/readest-app/src/components/settings/integrations/WebDAVForm.tsx b/apps/readest-app/src/components/settings/integrations/WebDAVForm.tsx index cb6545d9..70d61f25 100644 --- a/apps/readest-app/src/components/settings/integrations/WebDAVForm.tsx +++ b/apps/readest-app/src/components/settings/integrations/WebDAVForm.tsx @@ -410,6 +410,20 @@ const WebDAVForm: React.FC = ({ onBack }) => { // library that's already persisted on disk. setLibrary(newLibrary); }, + updateBookMetadata: async (book) => { + if (!appService) return; + // The cover bytes were just refreshed via saveBookCover, so + // regenerate the device-local blob URL the bookshelf renders. + try { + book.coverImageUrl = await appService.generateCoverImageUrl(book); + } catch (e) { + console.warn('WD library sync: cover URL generation failed', book.hash, e); + } + book.syncedAt = Date.now(); + // updateBook persists via saveLibraryBooks and refreshes the store, + // so the new title / author / cover show up without a reload. + await useLibraryStore.getState().updateBook(envConfig, book); + }, onProgress: ({ book, index, total, action }) => { const actionStr = action === 'downloading' ? _('Downloading') : _('Uploading'); updateProgress( @@ -431,6 +445,9 @@ const WebDAVForm: React.FC = ({ onBack }) => { if (result.configsDownloaded > 0) { parts.push(_('pulled progress for {{n}} book(s)', { n: result.configsDownloaded })); } + if (result.metadataUpdated > 0) { + parts.push(_('updated metadata for {{n}} book(s)', { n: result.metadataUpdated })); + } if (result.configsUploaded > 0) { parts.push(_('pushed {{n}} config(s)', { n: result.configsUploaded })); } diff --git a/apps/readest-app/src/services/webdav/WebDAVSync.ts b/apps/readest-app/src/services/webdav/WebDAVSync.ts index 1c753ec7..d64c9f4d 100644 --- a/apps/readest-app/src/services/webdav/WebDAVSync.ts +++ b/apps/readest-app/src/services/webdav/WebDAVSync.ts @@ -140,6 +140,28 @@ const mergeNotes = (local: BookNote[], remote: BookNote[]): BookNote[] => { return Array.from(byId.values()); }; +/** + * Overlay the user-facing metadata of `remote` onto `local`, preserving every + * device-local / file-system field: `filePath`, `sourceTitle` (which names the + * on-disk file), `coverImageUrl` (a device-local blob URL the caller + * regenerates), reading progress, reading status, group membership, `hash`, + * `format`, `createdAt`, etc. + * + * Only the fields a metadata edit actually changes travel — this list mirrors + * `getBookWithUpdatedMetadata` in `utils/book.ts`, which is the local side of + * the same operation. The cover image is replicated separately as cover.png + * bytes (see the reconciliation pass in `syncLibrary`), so it is intentionally + * absent here. + */ +const mergeRemoteBookMetadata = (local: Book, remote: Book): Book => ({ + ...local, + title: remote.title, + author: remote.author, + metadata: remote.metadata ?? local.metadata, + primaryLanguage: remote.primaryLanguage ?? local.primaryLanguage, + updatedAt: remote.updatedAt, +}); + export interface PullResult { /** True when the remote had a config and we merged something into local. */ applied: boolean; @@ -572,6 +594,11 @@ export interface SyncLibraryResult { filesAlreadyInSync: number; coversUploaded: number; booksDownloaded: number; + /** + * Number of already-local books whose metadata (title / author / cover) + * was refreshed from a newer copy in the shared library.json (#4756). + */ + metadataUpdated: number; failures: number; /** * Per-book failure breakdown for the diagnostic log surfaced in the @@ -641,6 +668,15 @@ export interface SyncLibraryOptions { */ saveBookConfig?: (book: Book, config: BookConfig) => Promise; addBookToLibrary?: (book: Book) => Promise; + /** + * Persist refreshed metadata for a book that already exists in the local + * library. Called once per book whose copy in the shared library.json is + * strictly newer than the local one (last-writer-wins on `book.updatedAt`), + * so a peer's title / author / cover edit propagates to devices that + * already hold the book. Distinct from `addBookToLibrary`, which only + * inserts brand-new rows and no-ops on an existing hash. + */ + updateBookMetadata?: (book: Book) => Promise; /** Stable per-device id; written into every config envelope. */ deviceId: string; /** @@ -721,6 +757,7 @@ export const syncLibrary = async ( filesAlreadyInSync: 0, coversUploaded: 0, booksDownloaded: 0, + metadataUpdated: 0, failures: 0, failedBooks: [], }; @@ -751,6 +788,45 @@ export const syncLibrary = async ( // listing the hash dir. const explicitRemotePaths = new Map(); + // Metadata reconciliation for books present BOTH locally and in the shared + // library.json (#4756). Last-writer-wins on `book.updatedAt`: when a peer's + // indexed copy is strictly newer, pull its title / author / cover down to + // this device. Without this, a device that already holds the book never + // learns about a peer's metadata edit, AND the index re-push at the end of + // syncLibrary would clobber the peer's newer metadata with this device's + // stale copy (allBooksMap still pointed at the local book). Updating + // allBooksMap with the merged copy fixes both directions at once. + if (canPull && remoteIndex && remoteIndex.books && options.updateBookMetadata) { + for (const rb of remoteIndex.books) { + if (rb.deletedAt) continue; + const local = allBooksMap.get(rb.hash); + if (!local || local.deletedAt) continue; + if ((rb.updatedAt ?? 0) <= (local.updatedAt ?? 0)) continue; + const merged = mergeRemoteBookMetadata(local, rb); + // Re-pull the cover so a changed cover travels with the metadata. The + // cover is best-effort: a missing remote cover.png simply leaves the + // existing local cover in place. The subsequent push-side pushBookCover + // HEAD/size short-circuit then matches (local now equals remote), so we + // never bounce the freshly-pulled cover back up. + if (options.saveBookCover) { + try { + const coverBytes = await pullBookCover(settings, rb.hash); + if (coverBytes) await options.saveBookCover(merged, coverBytes); + } catch (e) { + console.warn('WD library sync: metadata cover pull failed', rb.hash, e); + } + } + try { + await options.updateBookMetadata(merged); + // Keep the merged metadata authoritative for the index re-push below. + allBooksMap.set(rb.hash, merged); + result.metadataUpdated += 1; + } catch (e) { + console.warn('WD library sync: metadata update failed', rb.hash, e); + } + } + } + if (canPull) { const client = toClientConfig(settings); const candidateHashes = new Set(); @@ -938,7 +1014,30 @@ export const syncLibrary = async ( try { const config = await options.loadConfig(book); if (config) { - await pushBookConfig(settings, book, config, options.deviceId); + // Mirror the reader hook's pull-merge-push discipline so a manual + // "Sync now" can't blind-overwrite state this device hasn't pulled + // yet: a peer's booknotes (element-set CRDT) or newer progress + // (per-config LWW). Only in two-way ('silent') mode — 'send' + // deliberately treats the local copy as authoritative and keeps the + // blind push. A failed pull-merge falls back to the local config so + // a flaky GET never blocks the book's sync. + let configToPush = config; + if (canPull) { + try { + const pull = await pullBookConfig(settings, book, config); + if (pull.applied && pull.mergedConfig) { + configToPush = pull.mergedConfig; + // Persist the merged superset locally so this device converges + // too, not just the remote. + if (options.saveBookConfig) { + await options.saveBookConfig(book, pull.mergedConfig); + } + } + } catch (e) { + console.warn('WD library sync: config pull-merge failed', book.hash, e); + } + } + await pushBookConfig(settings, book, configToPush, options.deviceId); result.configsUploaded += 1; } // Covers ride along with the config-level sync, NOT with diff --git a/packages/foliate-js b/packages/foliate-js index 23ec1c79..982f168c 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 23ec1c7957af6a5dcbd7d340314fe5238ef4e665 +Subproject commit 982f168c668dedcf38b2304bec788337712d60aa