From a30a310a17fd7ebbe8ff2ce420c50eb1ed256278 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 16 Jun 2026 22:45:43 +0800 Subject: [PATCH] fix(opds): handle entries with no downloadable format (#4599) (#4611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An OPDS entry with full metadata and a cover image but no acquisition link — e.g. a Calibre book whose file was removed but kept for tracking borrowed/loaned titles — was classified by foliate-js as a navigation item whose href fell back to the cover image link. Tapping it loaded the image, which is neither XML nor JSON, so the OPDS browser crashed with a JSON parse error. - Bump foliate-js to include the getFeed fix that classifies such metadata-only entries as publications instead of navigation. - PublicationView: show "No downloadable format available" when an entry has no acquisition or stream links. - loadOPDS: defense-in-depth — surface a clear message instead of a raw JSON.parse SyntaxError when a response is neither XML nor JSON. - Add tests covering the Calibre no-format entry and a regression guard that a true navigation entry still classifies as navigation; add the two new UI strings across all locales. Closes #4599 Co-authored-by: Claude Opus 4.8 (1M context) --- .../public/locales/ar/translation.json | 4 +- .../public/locales/bn/translation.json | 4 +- .../public/locales/bo/translation.json | 4 +- .../public/locales/de/translation.json | 4 +- .../public/locales/el/translation.json | 4 +- .../public/locales/es/translation.json | 4 +- .../public/locales/fa/translation.json | 4 +- .../public/locales/fr/translation.json | 4 +- .../public/locales/he/translation.json | 4 +- .../public/locales/hi/translation.json | 4 +- .../public/locales/hu/translation.json | 4 +- .../public/locales/id/translation.json | 4 +- .../public/locales/it/translation.json | 4 +- .../public/locales/ja/translation.json | 4 +- .../public/locales/ko/translation.json | 4 +- .../public/locales/ms/translation.json | 4 +- .../public/locales/nl/translation.json | 4 +- .../public/locales/pl/translation.json | 4 +- .../public/locales/pt-BR/translation.json | 4 +- .../public/locales/pt/translation.json | 4 +- .../public/locales/ro/translation.json | 4 +- .../public/locales/ru/translation.json | 4 +- .../public/locales/si/translation.json | 4 +- .../public/locales/sl/translation.json | 4 +- .../public/locales/sv/translation.json | 4 +- .../public/locales/ta/translation.json | 4 +- .../public/locales/th/translation.json | 4 +- .../public/locales/tr/translation.json | 4 +- .../public/locales/uk/translation.json | 4 +- .../public/locales/uz/translation.json | 4 +- .../public/locales/vi/translation.json | 4 +- .../public/locales/zh-CN/translation.json | 4 +- .../public/locales/zh-TW/translation.json | 4 +- .../src/__tests__/utils/opds-feed.test.ts | 70 +++++++++++++++++++ .../app/opds/components/PublicationView.tsx | 4 ++ apps/readest-app/src/app/opds/page.tsx | 11 ++- packages/foliate-js | 2 +- 37 files changed, 184 insertions(+), 35 deletions(-) diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index ff2f7f79..8b849c1a 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1883,5 +1883,7 @@ "Update": "تحديث", "Nightly Builds": "إصدارات ليلية", "Early daily builds": "إصدارات يومية مبكرة", - "Privacy": "الخصوصية" + "Privacy": "الخصوصية", + "No downloadable format available": "لا يتوفر تنسيق قابل للتنزيل", + "Content is neither valid XML nor JSON": "المحتوى ليس بصيغة XML ولا JSON صالحة" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 4f5f38fd..e2a2f7d2 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1751,5 +1751,7 @@ "Update": "আপডেট", "Nightly Builds": "নাইটলি বিল্ড", "Early daily builds": "প্রাথমিক দৈনিক বিল্ড", - "Privacy": "গোপনীয়তা" + "Privacy": "গোপনীয়তা", + "No downloadable format available": "ডাউনলোডযোগ্য কোনো ফরম্যাট নেই", + "Content is neither valid XML nor JSON": "বিষয়বস্তু বৈধ XML বা JSON কোনোটিই নয়" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index c6c8a0b4..2a83b861 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1718,5 +1718,7 @@ "Update": "གསར་སྒྱུར།", "Nightly Builds": "མཚན་མོའི་བཟོ་སྐྲུན།", "Early daily builds": "སྔ་མོའི་ཉིན་རེའི་བཟོ་སྐྲུན།", - "Privacy": "གསང་དོན།" + "Privacy": "གསང་དོན།", + "No downloadable format available": "ཕབ་ལེན་བྱེད་ཆོག་པའི་རྣམ་པ་མི་འདུག", + "Content is neither valid XML nor JSON": "ནང་དོན་ནི་ XML དང་ JSON གང་ཡང་ཚད་ལྡན་མིན།" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index 2fb92b46..107ed740 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1751,5 +1751,7 @@ "Update": "Aktualisierung", "Nightly Builds": "Nightly-Builds", "Early daily builds": "Frühe tägliche Builds", - "Privacy": "Datenschutz" + "Privacy": "Datenschutz", + "No downloadable format available": "Kein herunterladbares Format verfügbar", + "Content is neither valid XML nor JSON": "Inhalt ist weder gültiges XML noch JSON" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index efc57af3..b3857a45 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1751,5 +1751,7 @@ "Update": "Ενημέρωση", "Nightly Builds": "Νυχτερινές εκδόσεις", "Early daily builds": "Πρώιμες καθημερινές εκδόσεις", - "Privacy": "Απόρρητο" + "Privacy": "Απόρρητο", + "No downloadable format available": "Δεν υπάρχει διαθέσιμη μορφή για λήψη", + "Content is neither valid XML nor JSON": "Το περιεχόμενο δεν είναι ούτε έγκυρο XML ούτε JSON" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index d2ff24f9..ac02dcbb 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1784,5 +1784,7 @@ "Update": "Actualización", "Nightly Builds": "Compilaciones nocturnas", "Early daily builds": "Compilaciones diarias anticipadas", - "Privacy": "Privacidad" + "Privacy": "Privacidad", + "No downloadable format available": "No hay ningún formato descargable disponible", + "Content is neither valid XML nor JSON": "El contenido no es XML ni JSON válido" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 07696dff..b3ec742f 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1751,5 +1751,7 @@ "Update": "به‌روزرسانی", "Nightly Builds": "نسخه‌های شبانه", "Early daily builds": "نسخه‌های روزانهٔ آزمایشی", - "Privacy": "حریم خصوصی" + "Privacy": "حریم خصوصی", + "No downloadable format available": "هیچ قالب قابل دانلودی موجود نیست", + "Content is neither valid XML nor JSON": "محتوا نه XML معتبر است و نه JSON" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 4825f646..eb81e92a 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1784,5 +1784,7 @@ "Update": "Mise à jour", "Nightly Builds": "Versions nocturnes", "Early daily builds": "Versions quotidiennes anticipées", - "Privacy": "Confidentialité" + "Privacy": "Confidentialité", + "No downloadable format available": "Aucun format téléchargeable disponible", + "Content is neither valid XML nor JSON": "Le contenu n'est ni du XML ni du JSON valide" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 6a7b0167..62251cf7 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1784,5 +1784,7 @@ "Update": "עדכון", "Nightly Builds": "גרסאות לילה", "Early daily builds": "גרסאות יומיות מוקדמות", - "Privacy": "פרטיות" + "Privacy": "פרטיות", + "No downloadable format available": "אין פורמט להורדה זמין", + "Content is neither valid XML nor JSON": "התוכן אינו XML או JSON תקין" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 80e8c84f..1bb79ca0 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1751,5 +1751,7 @@ "Update": "अपडेट", "Nightly Builds": "नाइटली बिल्ड", "Early daily builds": "प्रारंभिक दैनिक बिल्ड", - "Privacy": "गोपनीयता" + "Privacy": "गोपनीयता", + "No downloadable format available": "कोई डाउनलोड करने योग्य प्रारूप उपलब्ध नहीं है", + "Content is neither valid XML nor JSON": "सामग्री न तो मान्य XML है और न ही JSON" } diff --git a/apps/readest-app/public/locales/hu/translation.json b/apps/readest-app/public/locales/hu/translation.json index 1d263993..bcd6f84c 100644 --- a/apps/readest-app/public/locales/hu/translation.json +++ b/apps/readest-app/public/locales/hu/translation.json @@ -1751,5 +1751,7 @@ "Update": "Frissítés", "Nightly Builds": "Éjszakai buildek", "Early daily builds": "Korai napi buildek", - "Privacy": "Adatvédelem" + "Privacy": "Adatvédelem", + "No downloadable format available": "Nincs letölthető formátum", + "Content is neither valid XML nor JSON": "A tartalom sem érvényes XML, sem JSON" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 74c17aa2..90ea5032 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1718,5 +1718,7 @@ "Update": "Pembaruan", "Nightly Builds": "Build Nightly", "Early daily builds": "Build harian awal", - "Privacy": "Privasi" + "Privacy": "Privasi", + "No downloadable format available": "Tidak ada format yang dapat diunduh", + "Content is neither valid XML nor JSON": "Konten bukan XML maupun JSON yang valid" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 1a693f97..3832f0ce 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1784,5 +1784,7 @@ "Update": "Aggiornamento", "Nightly Builds": "Build notturne", "Early daily builds": "Build giornaliere anticipate", - "Privacy": "Privacy" + "Privacy": "Privacy", + "No downloadable format available": "Nessun formato scaricabile disponibile", + "Content is neither valid XML nor JSON": "Il contenuto non è né XML né JSON valido" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 9c43244b..522c4ffc 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1718,5 +1718,7 @@ "Update": "アップデート", "Nightly Builds": "ナイトリービルド", "Early daily builds": "毎日の先行ビルド", - "Privacy": "プライバシー" + "Privacy": "プライバシー", + "No downloadable format available": "ダウンロード可能な形式がありません", + "Content is neither valid XML nor JSON": "コンテンツが有効な XML でも JSON でもありません" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 4a862375..1e2d3978 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1718,5 +1718,7 @@ "Update": "업데이트", "Nightly Builds": "야간 빌드", "Early daily builds": "매일 제공되는 미리 보기 빌드", - "Privacy": "개인정보 보호" + "Privacy": "개인정보 보호", + "No downloadable format available": "다운로드 가능한 형식이 없습니다", + "Content is neither valid XML nor JSON": "콘텐츠가 유효한 XML 또는 JSON이 아닙니다" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index 18228216..ad1d4153 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1718,5 +1718,7 @@ "Update": "Kemas kini", "Nightly Builds": "Binaan Nightly", "Early daily builds": "Binaan harian awal", - "Privacy": "Privasi" + "Privacy": "Privasi", + "No downloadable format available": "Tiada format yang boleh dimuat turun", + "Content is neither valid XML nor JSON": "Kandungan bukan XML mahupun JSON yang sah" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index 908a3316..bd5516ce 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1751,5 +1751,7 @@ "Update": "Update", "Nightly Builds": "Nightly-builds", "Early daily builds": "Vroege dagelijkse builds", - "Privacy": "Privacy" + "Privacy": "Privacy", + "No downloadable format available": "Geen downloadbaar formaat beschikbaar", + "Content is neither valid XML nor JSON": "Inhoud is geen geldige XML of JSON" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index ae56286e..3994147c 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1817,5 +1817,7 @@ "Update": "Aktualizacja", "Nightly Builds": "Kompilacje nocne", "Early daily builds": "Wczesne kompilacje dzienne", - "Privacy": "Prywatność" + "Privacy": "Prywatność", + "No downloadable format available": "Brak formatu do pobrania", + "Content is neither valid XML nor JSON": "Treść nie jest prawidłowym XML ani JSON" } diff --git a/apps/readest-app/public/locales/pt-BR/translation.json b/apps/readest-app/public/locales/pt-BR/translation.json index 9caf7977..b27fdfd4 100644 --- a/apps/readest-app/public/locales/pt-BR/translation.json +++ b/apps/readest-app/public/locales/pt-BR/translation.json @@ -1784,5 +1784,7 @@ "Update": "Atualização", "Nightly Builds": "Compilações noturnas", "Early daily builds": "Compilações diárias antecipadas", - "Privacy": "Privacidade" + "Privacy": "Privacidade", + "No downloadable format available": "Nenhum formato disponível para download", + "Content is neither valid XML nor JSON": "O conteúdo não é XML nem JSON válido" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index a8d3bd51..9ad7cf0f 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1784,5 +1784,7 @@ "Update": "Atualização", "Nightly Builds": "Compilações noturnas", "Early daily builds": "Compilações diárias antecipadas", - "Privacy": "Privacidade" + "Privacy": "Privacidade", + "No downloadable format available": "Nenhum formato disponível para download", + "Content is neither valid XML nor JSON": "O conteúdo não é XML nem JSON válido" } diff --git a/apps/readest-app/public/locales/ro/translation.json b/apps/readest-app/public/locales/ro/translation.json index f60333e3..98406748 100644 --- a/apps/readest-app/public/locales/ro/translation.json +++ b/apps/readest-app/public/locales/ro/translation.json @@ -1784,5 +1784,7 @@ "Update": "Actualizare", "Nightly Builds": "Versiuni nightly", "Early daily builds": "Versiuni zilnice timpurii", - "Privacy": "Confidențialitate" + "Privacy": "Confidențialitate", + "No downloadable format available": "Niciun format descărcabil disponibil", + "Content is neither valid XML nor JSON": "Conținutul nu este nici XML, nici JSON valid" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index dfde35ba..37f9cd6d 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1817,5 +1817,7 @@ "Update": "Обновление", "Nightly Builds": "Ночные сборки", "Early daily builds": "Ранние ежедневные сборки", - "Privacy": "Конфиденциальность" + "Privacy": "Конфиденциальность", + "No downloadable format available": "Нет доступного формата для загрузки", + "Content is neither valid XML nor JSON": "Содержимое не является ни корректным XML, ни JSON" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index 6f883411..32de95eb 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1751,5 +1751,7 @@ "Update": "යාවත්කාලීන කිරීම", "Nightly Builds": "රාත්‍රී නිකුතු", "Early daily builds": "මුල් දෛනික නිකුතු", - "Privacy": "පෞද්ගලිකත්වය" + "Privacy": "පෞද්ගලිකත්වය", + "No downloadable format available": "බාගත කළ හැකි ආකෘතියක් නොමැත", + "Content is neither valid XML nor JSON": "අන්තර්ගතය වලංගු XML හෝ JSON නොවේ" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index 97fafbbb..258083cd 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1817,5 +1817,7 @@ "Update": "Posodobitev", "Nightly Builds": "Nočne gradnje", "Early daily builds": "Zgodnje dnevne gradnje", - "Privacy": "Zasebnost" + "Privacy": "Zasebnost", + "No downloadable format available": "Na voljo ni nobene oblike za prenos", + "Content is neither valid XML nor JSON": "Vsebina ni veljaven XML niti JSON" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index a741359b..0e774e0f 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1751,5 +1751,7 @@ "Update": "Uppdatering", "Nightly Builds": "Nattliga versioner", "Early daily builds": "Tidiga dagliga versioner", - "Privacy": "Integritet" + "Privacy": "Integritet", + "No downloadable format available": "Inget nedladdningsbart format tillgängligt", + "Content is neither valid XML nor JSON": "Innehållet är varken giltig XML eller JSON" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 0d2af47e..b82f300d 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1751,5 +1751,7 @@ "Update": "புதுப்பிப்பு", "Nightly Builds": "இரவு உருவாக்கங்கள்", "Early daily builds": "ஆரம்ப தினசரி உருவாக்கங்கள்", - "Privacy": "தனியுரிமை" + "Privacy": "தனியுரிமை", + "No downloadable format available": "பதிவிறக்கம் செய்யக்கூடிய வடிவம் எதுவும் இல்லை", + "Content is neither valid XML nor JSON": "உள்ளடக்கம் சரியான XML அல்லது JSON அல்ல" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index 1f45b721..7cda877a 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1718,5 +1718,7 @@ "Update": "อัปเดต", "Nightly Builds": "บิลด์รายคืน", "Early daily builds": "บิลด์รายวันรุ่นแรก", - "Privacy": "ความเป็นส่วนตัว" + "Privacy": "ความเป็นส่วนตัว", + "No downloadable format available": "ไม่มีรูปแบบที่ดาวน์โหลดได้", + "Content is neither valid XML nor JSON": "เนื้อหาไม่ใช่ทั้ง XML และ JSON ที่ถูกต้อง" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 73e47069..715d7b56 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1751,5 +1751,7 @@ "Update": "Güncelleme", "Nightly Builds": "Gecelik sürümler", "Early daily builds": "Erken günlük sürümler", - "Privacy": "Gizlilik" + "Privacy": "Gizlilik", + "No downloadable format available": "İndirilebilir biçim yok", + "Content is neither valid XML nor JSON": "İçerik geçerli XML veya JSON değil" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 6e07dc70..1e791d0d 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1817,5 +1817,7 @@ "Update": "Оновлення", "Nightly Builds": "Нічні збірки", "Early daily builds": "Ранні щоденні збірки", - "Privacy": "Конфіденційність" + "Privacy": "Конфіденційність", + "No downloadable format available": "Немає доступного формату для завантаження", + "Content is neither valid XML nor JSON": "Вміст не є дійсним XML чи JSON" } diff --git a/apps/readest-app/public/locales/uz/translation.json b/apps/readest-app/public/locales/uz/translation.json index 10b83772..0a1a6ddb 100644 --- a/apps/readest-app/public/locales/uz/translation.json +++ b/apps/readest-app/public/locales/uz/translation.json @@ -1751,5 +1751,7 @@ "Update": "Yangilanish", "Nightly Builds": "Tungi buildlar", "Early daily builds": "Erta kunlik buildlar", - "Privacy": "Maxfiylik" + "Privacy": "Maxfiylik", + "No downloadable format available": "Yuklab olinadigan format mavjud emas", + "Content is neither valid XML nor JSON": "Kontent yaroqli XML ham, JSON ham emas" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 128bc945..216a9e65 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1718,5 +1718,7 @@ "Update": "Cập nhật", "Nightly Builds": "Bản dựng hằng đêm", "Early daily builds": "Bản dựng hằng ngày sớm", - "Privacy": "Quyền riêng tư" + "Privacy": "Quyền riêng tư", + "No downloadable format available": "Không có định dạng nào để tải xuống", + "Content is neither valid XML nor JSON": "Nội dung không phải XML hay JSON hợp lệ" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 14e32444..c5542568 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1718,5 +1718,7 @@ "Update": "更新", "Nightly Builds": "每夜构建版", "Early daily builds": "每日抢先构建版", - "Privacy": "隐私" + "Privacy": "隐私", + "No downloadable format available": "没有可下载的格式", + "Content is neither valid XML nor JSON": "内容既不是有效的 XML 也不是 JSON" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 25d84e8d..0b069c1e 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1718,5 +1718,7 @@ "Update": "更新", "Nightly Builds": "每夜建置版", "Early daily builds": "每日搶先建置版", - "Privacy": "隱私" + "Privacy": "隱私", + "No downloadable format available": "沒有可下載的格式", + "Content is neither valid XML nor JSON": "內容既不是有效的 XML 也不是 JSON" } diff --git a/apps/readest-app/src/__tests__/utils/opds-feed.test.ts b/apps/readest-app/src/__tests__/utils/opds-feed.test.ts index c28eb857..528dbe3b 100644 --- a/apps/readest-app/src/__tests__/utils/opds-feed.test.ts +++ b/apps/readest-app/src/__tests__/utils/opds-feed.test.ts @@ -167,6 +167,76 @@ describe('OPDS feed parsing', () => { }); }); + describe('metadata-only entries without an acquisition link (Calibre, #4599)', () => { + // Reproduces https://github.com/readest/readest/issues/4599 + // A Calibre book whose file was removed (e.g. a borrowed/loaned title kept + // for tracking) still emits a full metadata entry with cover/thumbnail + // links but no acquisition link. Such an entry was classified as a + // navigation item whose href fell back to links[0] — the cover image — so + // tapping it tried to load the image as a feed and crashed with a JSON + // parse error. It should be treated as a publication (so its metadata is + // shown) with no downloadable format. + const calibreNoFormatFeed = ` + + Calibre Library + + A Borrowed Book + urn:uuid:1234 + Jane Author + 2026-01-15T10:30:00Z + eng + A book I borrowed; the file is no longer available. + + + +`; + + it('classifies a metadata+cover entry with no acquisition link as a publication', () => { + const doc = parseXML(calibreNoFormatFeed); + const feed = getFeed(doc) as OPDSFeed; + + expect(feed.publications).toBeDefined(); + expect(feed.publications!).toHaveLength(1); + expect(feed.publications![0]!.metadata.title).toBe('A Borrowed Book'); + // The crash path: the entry must NOT become a navigation item pointing at + // the cover image. + expect(feed.navigation).toBeUndefined(); + }); + + it('exposes no acquisition link for a format-less publication', () => { + const doc = parseXML(calibreNoFormatFeed); + const feed = getFeed(doc) as OPDSFeed; + const pub = feed.publications![0]!; + const hasAcquisition = pub.links.some((link) => { + const rels = Array.isArray(link.rel) ? link.rel : [link.rel ?? '']; + return rels.some((rel) => rel.startsWith('http://opds-spec.org/acquisition')); + }); + expect(hasAcquisition).toBe(false); + }); + + it('still classifies a true navigation entry (sub-catalog link, no image) as navigation', () => { + const navFeed = ` + + Root + + By Author + + 2026-03-28T05:37:03Z + +`; + const doc = parseXML(navFeed); + const feed = getFeed(doc) as OPDSFeed; + + expect(feed.publications).toBeUndefined(); + expect(feed.navigation).toBeDefined(); + expect(feed.navigation!).toHaveLength(1); + expect(feed.navigation![0]!.title).toBe('By Author'); + }); + }); + describe('entry id and updated parsing', () => { const shelfFeed = ` + {!downloadedBook && acquisitionLinks.length === 0 && streamLinks.length === 0 && ( +

{_('No downloadable format available')}

+ )} + {(acquisitionLinks.length > 0 || streamLinks.length > 0) && (
{acquisitionLinks.map(({ rel, links }) => { diff --git a/apps/readest-app/src/app/opds/page.tsx b/apps/readest-app/src/app/opds/page.tsx index 1822da35..dae5db59 100644 --- a/apps/readest-app/src/app/opds/page.tsx +++ b/apps/readest-app/src/app/opds/page.tsx @@ -329,7 +329,16 @@ export default function BrowserPage() { } } } else { - const feed = JSON.parse(text); + // Defense-in-depth: a non-feed response (e.g. an image returned for a + // link that isn't actually a feed) is neither XML nor JSON. Surface a + // clear message instead of letting a raw JSON.parse SyntaxError leak + // into the error view (readest issue #4599). + let feed; + try { + feed = JSON.parse(text); + } catch { + throw new Error(_('Content is neither valid XML nor JSON')); + } const newState = { feed, baseURL: responseURL, diff --git a/packages/foliate-js b/packages/foliate-js index 1309db7e..03913b15 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit 1309db7e8ce35d89f23e9da741b2faf3aa59b19c +Subproject commit 03913b157520814c8cfeb623aa413a24e5ed1df1