diff --git a/static/css/app.css b/static/css/app.css index a2f0264..8242c99 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1684,8 +1684,10 @@ body.dnd-mode .timer-display { border: 1px solid var(--border); border-radius: var(--radius); background: none; + color: var(--fg); text-align: left; cursor: pointer; + font: inherit; } .book-recent-item-title { font-size: 13px; @@ -1717,8 +1719,10 @@ body.dnd-mode .timer-display { border: 1px solid var(--border); border-radius: var(--radius); background: none; + color: var(--fg); cursor: pointer; text-align: center; + font: inherit; } .book-folder-tile-icon { font-size: 22px; diff --git a/static/js/app.js b/static/js/app.js index dacfef4..30c3917 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3618,6 +3618,35 @@ async function assignBookFolder(bookId) { } } +// Fallback for books uploaded before ISBN extraction existed (or repaired/re-encrypted +// without it): pulls the already-accessible encrypted book bytes, decrypts and unzips just +// far enough to read the OPF identifiers — same source openBook() itself parses — without +// doing a full reader render. EPUB only; PDFs never carried a reliable ISBN via pdf.js. +async function _extractIsbnFromBookFile(bookId) { + try { + const key = await getOrCreateEncKey(); + let data_ct, data_iv; + const cached = await _getCachedBook(bookId); + if (cached) { + ({data_ct, data_iv} = cached); + } else { + const res = await fetch(`/books/${bookId}/data/`); + ({data_ct, data_iv} = await res.json()); + } + const plain = await decryptBytes(key, data_iv, data_ct); + const zip = await JSZip.loadAsync(plain); + const containerXml = await zip.file('META-INF/container.xml').async('text'); + const opfPath = new DOMParser() + .parseFromString(containerXml, 'application/xml') + .querySelector('rootfile')?.getAttribute('full-path'); + if (!opfPath) return ''; + const opfDoc = new DOMParser().parseFromString(await zip.file(opfPath).async('text'), 'application/xml'); + return _extractIsbnFromOpf(opfDoc); + } catch (e) { + return ''; + } +} + // Manually triggered (never automatic) — looks up which library shelf/DDC category this // book falls under via the server-side DNB→Open Library proxy, purely as an organizational // hint (shown as a badge). See books/views.py:lookup_book_metadata for why this one call is @@ -3625,20 +3654,34 @@ async function assignBookFolder(bookId) { async function lookupBookMetadata(bookId) { const book = _lastBookListData.find(b => b.id === bookId); if (!book) return; - const isbn = (bookMetaCache[bookId] || {}).isbn || book.isbn || ''; + const cached = bookMetaCache[bookId] || {}; + let isbn = cached.isbn || book.isbn || ''; + const isPdf = (cached.type || book.type) === 'pdf'; + if (!isbn) { - await customAlert('Keine ISBN in den Metadaten dieses Buchs gefunden (nur bei neueren Uploads automatisch erkannt).'); - return; + if (isPdf) { + await customAlert('Keine ISBN gespeichert — bei PDFs wird sie nicht automatisch erkannt.'); + return; + } + // Older upload without a stored ISBN — extract it from the book file itself (may take + // a moment for large books, since it downloads/decrypts the full file if not cached). + isbn = await _extractIsbnFromBookFile(bookId); + if (!isbn) { + await customAlert('Keine ISBN in diesem Buch gefunden.'); + return; + } } + try { const res = await fetch(`/books/metadata-lookup/?isbn=${encodeURIComponent(isbn)}`); const data = await res.json(); - if (!data.label) { - await customAlert('Keine Regal-Kategorie gefunden (weder bei der DNB noch bei Open Library).'); - return; - } - await _updateBookMeta(bookId, {shelfTag: data.label}); + // Persist the (possibly newly-found) ISBN either way, so a repeat lookup never needs + // to re-download/decrypt the book file again. + await _updateBookMeta(bookId, data.label ? {isbn, shelfTag: data.label} : {isbn}); renderBookList(_lastBookListData); + if (!data.label) { + await customAlert('Keine Regal-Kategorie gefunden (weder bei der DNB noch bei Open Library). Die ISBN wurde für spätere Versuche gespeichert.'); + } } catch (e) { await customAlert('Metadaten-Abruf fehlgeschlagen: ' + e.message); } diff --git a/static/js/sw.js b/static/js/sw.js index 5017b17..a683b55 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v37'; +const CACHE = 'diora-v38'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css',