# 010 — Open Library metadata on import **Status:** Implemented **Created:** 2026-06-03 ## Objective After a user uploads an EPUB/PDF (`POST /api/books/ebooks/`), enrich the `EBook` with metadata and a cover from [Open Library](https://openlibrary.org/developers/api), using only **title** and **author** from the upload form. Prefer **Spanish** editions when available; fall back to English/any. Upload must **never fail** if Open Library is down or no match is found. ## Trigger - **Automatic:** `EBookUploadSerializer.create()` calls `enrich_ebook_metadata(ebook)` after file save. - **Manual:** `POST /api/books/ebooks/{id}/enrich-metadata/` re-runs enrichment (owner only). ## Open Library usage ### Search `GET https://openlibrary.org/search.json` | Param | Value | |-------|--------| | `title` | User-provided title | | `author` | User-provided author | | `lang` | `es` (primary) or `en` (fallback) | | `limit` | `5` | | `fields` | `key,title,author_name,cover_i,first_publish_year,subject,language,edition_key,number_of_pages_median,publisher` | Primary pass also uses query filter `language:spa`. Fallback omits language filter. ### Covers `GET https://covers.openlibrary.org/b/id/{cover_i}-L.jpg` — downloaded and stored on `EBook.cover_image`. ## Match scoring | Score | Behavior | |-------|----------| | ≥ 0.8 | Apply OL title/author + cover + full metadata | | 0.6 – 0.8 | Metadata + cover only; keep user title/author | | < 0.6 | `match_status: not_found`; no field changes except metadata stub | Author overlap + title similarity (normalized strings, `difflib.SequenceMatcher`). Prefer hits with `cover_i`. ## Data model No migration. Uses existing fields on `EBook`: - `metadata_json` — full enrichment payload (see below) - `cover_image` — downloaded cover file - `title` / `author` — updated when match score ≥ 0.8 ### `metadata_json` shape ```json { "source": "openlibrary", "matched_at": "2026-06-03T12:00:00+00:00", "match_language": "es", "match_score": 0.92, "match_status": "matched", "user_input": { "title": "...", "author": "..." }, "openlibrary": { "work_key": "/works/OL...", "edition_key": "...", "title": "...", "authors": ["..."], "cover_id": 12345, "cover_url": "https://covers.openlibrary.org/b/id/12345-L.jpg", "first_publish_year": 1605, "subjects": ["..."], "languages": ["spa"], "publishers": ["..."], "number_of_pages_median": 320 } } ``` ## API changes ### Upload response (unchanged path) `POST /api/books/ebooks/` — response may include populated `cover_image` and updated `title`/`author` after sync enrichment. ### Detail `GET /api/books/ebooks/{id}/` — adds read-only `metadata` (alias of `metadata_json`). ### Manual refresh `POST /api/books/ebooks/{id}/enrich-metadata/` — returns updated `EBookDetailSerializer` payload. ## Configuration | Env var | Default | Description | |---------|---------|-------------| | `OPENLIBRARY_ENABLED` | `true` | Kill switch | | `OPENLIBRARY_PREFERRED_LANG` | `es` | Primary `lang` param | | `OPENLIBRARY_FALLBACK_LANG` | `en` | Fallback `lang` param | | `OPENLIBRARY_TIMEOUT_SECONDS` | `5` | HTTP timeout | | `OPENLIBRARY_USER_AGENT` | `CloudReader/1.0` | User-Agent header | ## Code layout ``` backend/apps/books/services/ ├── openlibrary.py # Search, scoring, cover download └── metadata.py # enrich_ebook_metadata orchestrator ``` ## Verification 1. Upload with title `Don Quijote`, author `Cervantes` → cover + Spanish-friendly metadata. 2. Upload with nonsense title/author → 201, no cover, `match_status: not_found`. 3. `POST .../enrich-metadata/` on existing ebook refreshes metadata.