# 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.
