Bücher: Titel/Autor-Fallback + manuelle ISBN-Eingabe für Regal-Lookup (SW v39)
Bisher scheiterte "Metadaten abrufen" komplett, wenn im EPUB keine ISBN steckte (oder bei PDFs, die nie eine liefern). Der Server versucht jetzt bei fehlendem/ erfolglosem ISBN-Treffer automatisch eine DNB- bzw. Open-Library-Textsuche nach Titel+Autor (weniger präzise, daher als "dnb-title"/"openlibrary-title" markiert). Bleibt auch das ohne Treffer, fragt der Client einmalig nach einer manuell eingetippten ISBN (z.B. vom Buchrücken) und versucht es damit erneut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011j4LcoeddwFt6Rw8Wyaknj
This commit is contained in:
parent
e6b3ee620e
commit
635fd7ef1c
3 changed files with 137 additions and 28 deletions
103
books/views.py
103
books/views.py
|
|
@ -120,29 +120,112 @@ def _lookup_openlibrary_shelf(isbn):
|
|||
return None
|
||||
|
||||
|
||||
def _cql_phrase(value):
|
||||
# CQL string literals are quoted; strip embedded quotes rather than escaping them
|
||||
# (this only feeds a lookup heuristic, not a stored/displayed value).
|
||||
return value.replace('"', ' ').strip()
|
||||
|
||||
|
||||
def _lookup_dnb_shelf_by_title(title, author):
|
||||
url = 'https://services.dnb.de/sru/dnb'
|
||||
query = f'dnb.tit="{_cql_phrase(title)}"'
|
||||
if author:
|
||||
query += f' and dnb.per="{_cql_phrase(author)}"'
|
||||
params = {
|
||||
'version': '1.1',
|
||||
'operation': 'searchRetrieve',
|
||||
'query': query,
|
||||
'recordSchema': 'MARC21-xml',
|
||||
'maximumRecords': '1',
|
||||
}
|
||||
resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6))
|
||||
resp.raise_for_status()
|
||||
root = ET.fromstring(resp.content)
|
||||
for datafield in root.iter(f'{_MARC_NS}datafield'):
|
||||
if datafield.get('tag') != '082':
|
||||
continue
|
||||
for subfield in datafield.findall(f'{_MARC_NS}subfield'):
|
||||
if subfield.get('code') == 'a' and subfield.text:
|
||||
label = _ddc_label(subfield.text)
|
||||
if label:
|
||||
return label
|
||||
return None
|
||||
|
||||
|
||||
def _lookup_openlibrary_shelf_by_title(title, author):
|
||||
url = 'https://openlibrary.org/search.json'
|
||||
params = {'title': title, 'limit': 1, 'fields': 'ddc,subject'}
|
||||
if author:
|
||||
params['author'] = author
|
||||
resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6))
|
||||
resp.raise_for_status()
|
||||
docs = resp.json().get('docs') or []
|
||||
if not docs:
|
||||
return None
|
||||
doc = docs[0]
|
||||
ddc = doc.get('ddc') or []
|
||||
if ddc:
|
||||
label = _ddc_label(ddc[0])
|
||||
if label:
|
||||
return label
|
||||
subjects = doc.get('subject') or []
|
||||
if subjects:
|
||||
return subjects[0]
|
||||
return None
|
||||
|
||||
|
||||
@require_http_methods(['GET'])
|
||||
def lookup_book_metadata(request):
|
||||
err = _require_auth(request)
|
||||
if err:
|
||||
return err
|
||||
|
||||
isbn = re.sub(r'[^0-9Xx]', '', request.GET.get('isbn', ''))
|
||||
if len(isbn) not in (10, 13):
|
||||
isbn_raw = request.GET.get('isbn', '').strip()
|
||||
title = request.GET.get('title', '').strip()
|
||||
author = request.GET.get('author', '').strip()
|
||||
isbn = re.sub(r'[^0-9Xx]', '', isbn_raw)
|
||||
|
||||
if isbn_raw and len(isbn) not in (10, 13):
|
||||
return JsonResponse({'error': 'invalid ISBN'}, status=400)
|
||||
if not isbn and not title:
|
||||
return JsonResponse({'error': 'isbn or title required'}, status=400)
|
||||
|
||||
label = None
|
||||
source = None
|
||||
try:
|
||||
label = _lookup_dnb_shelf(isbn)
|
||||
source = 'dnb' if label else None
|
||||
except Exception:
|
||||
pass
|
||||
if not label:
|
||||
|
||||
# ISBN is the precise path — try it first when we have one.
|
||||
if isbn:
|
||||
try:
|
||||
label = _lookup_openlibrary_shelf(isbn)
|
||||
source = 'openlibrary' if label else None
|
||||
label = _lookup_dnb_shelf(isbn)
|
||||
if label:
|
||||
source = 'dnb'
|
||||
except Exception:
|
||||
pass
|
||||
if not label:
|
||||
try:
|
||||
label = _lookup_openlibrary_shelf(isbn)
|
||||
if label:
|
||||
source = 'openlibrary'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# No ISBN (or it drew a blank) — fall back to a title/author text search. Less
|
||||
# precise (wrong edition/translation is possible), so the source is tagged
|
||||
# distinctly for the client to hint at that if it wants to.
|
||||
if not label and title:
|
||||
try:
|
||||
label = _lookup_dnb_shelf_by_title(title, author)
|
||||
if label:
|
||||
source = 'dnb-title'
|
||||
except Exception:
|
||||
pass
|
||||
if not label:
|
||||
try:
|
||||
label = _lookup_openlibrary_shelf_by_title(title, author)
|
||||
if label:
|
||||
source = 'openlibrary-title'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return JsonResponse({'label': label, 'source': source})
|
||||
|
||||
|
|
|
|||
|
|
@ -3647,40 +3647,66 @@ async function _extractIsbnFromBookFile(bookId) {
|
|||
}
|
||||
}
|
||||
|
||||
async function _fetchShelfLookup(isbn, title, author) {
|
||||
const params = new URLSearchParams();
|
||||
if (isbn) params.set('isbn', isbn);
|
||||
if (title) params.set('title', title);
|
||||
if (author) params.set('author', author);
|
||||
const res = await fetch(`/books/metadata-lookup/?${params.toString()}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 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
|
||||
// allowed to briefly touch the server with a plaintext ISBN.
|
||||
// allowed to briefly touch the server with a plaintext ISBN/title/author.
|
||||
//
|
||||
// Fallback chain when there's no ISBN (older upload, or a book that never had one — e.g.
|
||||
// every PDF, since pdf.js doesn't expose one): (1) extract ISBN from the file itself, for
|
||||
// EPUBs, (2) server-side title/author text search (less precise — could match the wrong
|
||||
// edition/translation), (3) if that also comes up empty, ask the user to type in an ISBN
|
||||
// by hand (from the book's cover/an online store) and retry once.
|
||||
async function lookupBookMetadata(bookId) {
|
||||
const book = _lastBookListData.find(b => b.id === bookId);
|
||||
if (!book) return;
|
||||
const cached = bookMetaCache[bookId] || {};
|
||||
let isbn = cached.isbn || book.isbn || '';
|
||||
const isPdf = (cached.type || book.type) === 'pdf';
|
||||
const title = cached.title || book.title || '';
|
||||
const author = cached.author || book.author || '';
|
||||
|
||||
if (!isbn) {
|
||||
if (isPdf) {
|
||||
await customAlert('Keine ISBN gespeichert — bei PDFs wird sie nicht automatisch erkannt.');
|
||||
return;
|
||||
}
|
||||
if (!isbn && !isPdf) {
|
||||
// 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();
|
||||
// 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);
|
||||
let data = await _fetchShelfLookup(isbn, title, author);
|
||||
|
||||
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.');
|
||||
const manual = await customPrompt(
|
||||
'Keine Regal-Kategorie per ISBN/Titel/Autor gefunden. ISBN manuell eingeben (z. B. vom Buchrücken oder Online-Shop), oder leer lassen zum Abbrechen:', ''
|
||||
);
|
||||
const manualIsbn = (manual || '').replace(/[^0-9Xx]/g, '');
|
||||
if (manualIsbn) {
|
||||
isbn = manualIsbn;
|
||||
data = await _fetchShelfLookup(isbn, title, author);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist whatever we ended up with either way, so a repeat lookup never needs to
|
||||
// re-extract/re-search from scratch.
|
||||
const overrides = {};
|
||||
if (isbn) overrides.isbn = isbn;
|
||||
if (data.label) overrides.shelfTag = data.label;
|
||||
if (Object.keys(overrides).length) {
|
||||
await _updateBookMeta(bookId, overrides);
|
||||
renderBookList(_lastBookListData);
|
||||
}
|
||||
|
||||
if (!data.label) {
|
||||
await customAlert('Keine Regal-Kategorie gefunden (weder DNB noch Open Library, per ISBN oder Titel/Autor).');
|
||||
}
|
||||
} catch (e) {
|
||||
await customAlert('Metadaten-Abruf fehlgeschlagen: ' + e.message);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v38';
|
||||
const CACHE = 'diora-v39';
|
||||
const PODCAST_CACHE = 'diora-podcast-v1';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue