# Calibre metadata.opf Support - Implementation Plan ## Overview This document outlines the implementation of **Calibre metadata.opf sidecar file support** for Bookhoard's media scanner. The scanner will use a **sidecar-first approach** - if a Calibre `metadata.opf` file exists alongside a book file, it will take precedence over embedded metadata. ## Motivation Calibre is the most popular ebook management software, with millions of users. When users manage their libraries in Calibre: 1. **They curate metadata** - Adding series information, tags, ratings, custom covers 2. **They fetch from online sources** - Google Books, Amazon, Goodreads (better than publisher metadata) 3. **They convert/edit books** - The sidecar reflects the current, correct state 4. **They organize in folders** - Often using Calibre's folder structure conventions By supporting Calibre `metadata.opf` sidecar files, Bookhoard can: - Respect users' curation work - Import richer, more accurate metadata - Provide seamless integration with existing Calibre libraries - Support all file types (EPUB, PDF, comics, etc.) that Calibre manages ## Requirements ### Functional Requirements - [ ] Scanner detects `metadata.opf` files in the same directory as book files - [ ] Parses Dublin Core metadata (`dc:*` namespace) and Calibre-specific fields (`meta name="calibre:*"`) - [ ] Uses sidecar metadata **before** embedded metadata (sidecar-first approach) - [ ] Works for all library types (ebook, audiobook, comic, etc.) - [ ] Works for all file types (EPUB, PDF, CBZ, CBR, MP3, M4B, etc.) - [ ] Handles missing or malformed sidecar files gracefully - [ ] Maps all OPF fields to existing `media_items` database schema - [ ] Maintains backward compatibility (no sidecar = existing behavior) ### Non-Functional Requirements - [ ] No breaking changes to existing scanner functionality - [ ] No database schema changes required (all fields already exist) - [ ] Follows existing code patterns and style - [ ] Comprehensive test coverage - [ ] Performance: Minimal impact on scan performance - [ ] Error handling: Graceful degradation on malformed XML ## Database Schema Mapping All required fields **already exist** in the `media_items` table. No schema changes needed. ### Dublin Core Fields (dc: namespace) | OPF Field | Database Column | Type | Notes | |-----------|-----------------|------|-------| | `dc:title` | `title` | VARCHAR(255) | Required | | `dc:creator` | `author` | VARCHAR(255) | First author only | | `dc:subject` | `tags` | TEXT[] | **Tags/keywords** (multiple) | | `dc:description` | `description` | TEXT | Book summary | | `dc:publisher` | `publisher` | VARCHAR(255) | Publisher name | | `dc:date` | `date_published` | DATE | Publication date | | `dc:language` | `language` | VARCHAR(10) | ISO 639-1 code (en, es, fr) | | `dc:identifier` (ISBN) | `isbn` | VARCHAR(13) | Check `opf:scheme="ISBN"` | | `dc:identifier` (ASIN) | `asin` | VARCHAR(20) | Check `opf:scheme="ASIN"` | | `dc:contributor` | `contributors` | TEXT[] | Multiple contributors | | `dc:rights` | *(not stored)* | - | Copyright info (ignored) | | `dc:source` | *(not stored)* | - | Source publication (ignored) | ### Calibre-Specific Fields (meta name="calibre:*") | OPF Field | Database Column | Type | Notes | |-----------|-----------------|------|-------| | `calibre:series` | `series` | VARCHAR(255) | Series name | | `calibre:series_index` | `series_number` | INTEGER | Position in series | | `calibre:rating` | *(use media_ratings)* | INTEGER | 0-5 scale, stored per-user | | `calibre:title_sort` | *(not stored)* | - | Sortable title (ignored) | | `calibre:author_sort` | *(not stored)* | - | Sortable author (ignored) | | `calibre:timestamp` | `created_at` | TIMESTAMP | When added to Calibre | ### Special Cases 1. **Multiple authors**: Store first author in `author`, suggest adding `authors[]` array in future 2. **Multiple identifiers**: Extract ISBN and ASIN if present, ignore others 3. **Calibre rating**: Not imported (ratings are per-user in Bookhoard) 4. **Sort fields**: Ignored (Bookhoard has its own sorting logic) 5. **Timestamp**: Use for `created_at` if more recent than file modification time ## OPF Format Reference ### Standard Dublin Core Elements ```xml Book Title Author Name Fantasy Adventure Book summary... Publisher Name 2024-01-15 en 978-0-123456-78-9 B08XXXXX Illustrator Name ``` ### Calibre-Specific Meta Tags ```xml ``` ### Example Complete metadata.opf ```xml The Fellowship of the Ring J.R.R. Tolkien 978-0-618-00222-0 12345678-1234-1234-1234-123456789012 en 1954-07-29 HarperCollins In a sleepy village in the Shire... Fantasy Adventure Classics Alan Lee (illustrator) ``` ## Implementation Design ### Architecture ``` Scanner Pipeline (EXISTING) ↓ 1. Detect book file 2. Check if already in database 3. Extract metadata from file (EPUB/PDF/Comic) 4. Extract folder structure metadata 5. Normalize and store ``` **New Step 3a**: Check for Calibre sidecar **before** embedded extraction ``` Scanner Pipeline (NEW) ↓ 1. Detect book file 2. Check if already in database 3. **NEW** Extract metadata from Calibre metadata.opf (if exists) - If sidecar found → use sidecar metadata - If no sidecar → use embedded metadata (existing) 4. Extract folder structure metadata (existing) 5. Normalize and store (existing) ``` ### Code Changes #### File: `internal/services/media_scanner.go` **New Functions:** 1. `extractMetadataFromCalibreSidecar(path string) (*MediaMetadata, error)` - Checks for `metadata.opf` in same directory as book file - Calls `parseCalibreMetadataOPF()` if found - Returns nil if no sidecar exists (not an error) 2. `parseCalibreMetadataOPF(opfPath string) (*MediaMetadata, error)` - Opens and parses XML file - Extracts Dublin Core fields (`dc:*`) - Extracts Calibre-specific fields (`meta name="calibre:*"`) - Maps to `MediaMetadata` struct - Handles errors gracefully (malformed XML, missing fields) **Modified Function:** 1. `processMediaFile(ctx context.Context, path string) (bool, error)` - Add sidecar extraction check at line ~544 (before embedded extraction) - Flow: sidecar → embedded → folder → filename #### New Struct: `CalibreOPFMetadata` ```go // Intermediate struct for parsed OPF data type CalibreOPFMetadata struct { Title string Authors []string Tags []string Description string Publisher string PublishDate *time.Time Language string ISBN string ASIN string UUID string Contributors []string Series string SeriesIndex *float64 Rating *int32 Timestamp *time.Time } ``` ### Metadata Priority (Sidecar-First) ``` Priority Order: 1. Calibre metadata.opf sidecar ← NEW (highest priority) 2. Embedded file metadata (EPUB OPF, PDF info, ComicInfo.xml) 3. Folder structure parsing 4. Filename parsing (last resort) ``` **Rationale**: If a user has a `metadata.opf` file, they're actively managing their library in Calibre. The sidecar represents their curated "source of truth." ### Error Handling 1. **Sidecar file not found** → Return nil, continue to embedded metadata 2. **Malformed XML** → Log warning, continue to embedded metadata 3. **Missing required fields** → Use available fields, continue 4. **Invalid date format** → Log warning, skip field 5. **File permission errors** → Log error, continue to embedded metadata **Principle**: Graceful degradation - never fail a scan due to sidecar issues ### Testing Strategy #### Unit Tests File: `internal/services/media_scanner_calibre_test.go` - Test parsing complete metadata.opf with all fields - Test parsing minimal metadata.opf (title only) - Test parsing malformed XML (graceful failure) - Test missing metadata.opf (returns nil, not error) - Test multiple `` tags (tags array) - Test multiple `` tags (extract ISBN/ASIN) - Test Calibre-specific fields (series, series_index) - Test invalid date formats - Test empty/whitespace values #### Integration Tests File: `cmd/server/tests/calibre_integration_test.go` - Test scanning directory with Calibre library structure - Test scanning directory without sidecar files (existing behavior) - Test mixed: some books with sidecar, some without - Test force rescan updates from sidecar changes - Test all file types: EPUB, PDF, CBZ, MP3, M4B - Test all library types: ebook, audiobook, comic #### Test Data Create `testdata/calibre/` with sample files: - `Complete Book/metadata.opf` - All fields populated - `Minimal Book/metadata.opf` - Only required fields - `Malformed Book/metadata.opf` - Invalid XML - No sidecar files (rely on existing test data) ## Implementation Steps ### Phase 1: Core Functionality (REQUIRED) - [ ] **Step 1.1**: Add `CalibreOPFMetadata` struct - [ ] **Step 1.2**: Implement `parseCalibreMetadataOPF()` function - [ ] **Step 1.3**: Implement `extractMetadataFromCalibreSidecar()` function - [ ] **Step 1.4**: Modify `processMediaFile()` to call sidecar extraction - [ ] **Step 1.5**: Add unit tests for OPF parsing - [ ] **Step 1.6**: Test with real Calibre library ### Phase 2: Documentation (REQUIRED) - [ ] **Step 2.1**: Create user documentation (`docs/user/calibre-support.md`) - [ ] **Step 2.2**: Update developer documentation (`docs/development/calibre-opf-implementation.md`) - [ ] **Step 2.3**: Update scanner API docs if needed - [ ] **Step 2.4**: Add examples to documentation ### Phase 3: Testing & Polish (REQUIRED) - [ ] **Step 3.1**: Add integration tests - [ ] **Step 3.2**: Test with various Calibre library configurations - [ ] **Step 3.3**: Performance testing (ensure no regression) - [ ] **Step 3.4**: Error handling review - [ ] **Step 3.5**: Code review and refinement ### Phase 4: Future Enhancements (OPTIONAL) - [ ] Support Calibre `author_link` metadata - [ ] Support Calibre custom columns - [ ] Import Calibre ratings as user's initial rating - [ ] Detect Calibre library structure patterns - [ ] Configuration option for merge strategy (sidecar-first vs. smart-merge) ## Backward Compatibility **No breaking changes.** Existing behavior is preserved: - Libraries without `metadata.opf` files → Existing behavior (embedded metadata) - Scanning logic unchanged → Only adds sidecar check before embedded extraction - Database unchanged → All fields already exist - API unchanged → No new endpoints or response fields ## Success Criteria - [ ] All existing tests pass - [ ] New unit tests pass (90%+ coverage) - [ ] New integration tests pass - [ ] Scans real Calibre library successfully - [ ] Scans non-Calibre library without errors - [ ] No performance regression (scan time +5% max) - [ ] Documentation complete and accurate - [ ] Code review approved ## Open Questions 1. **Q**: Should we import Calibre ratings? **A**: No - ratings are per-user in Bookhoard. Calibre rating is the owner's opinion, not necessarily the current user's. 2. **Q**: Should we support Calibre custom columns? **A**: Not in initial implementation. Custom columns are user-defined and would require dynamic schema or JSON storage. Future enhancement. 3. **Q**: How to handle multiple authors? **A**: Store first author in `author` field (existing behavior). Future enhancement could add `authors[]` array. 4. **Q**: Should we follow Calibre folder structure? **A**: Already implemented! `extractFolderStructureMetadata()` handles Calibre patterns. ## References - [EPUB Publications 3.0 Specification](https://www.idpf.org/epub/30/spec/epub30-publications.html) - [Dublin Core Metadata Element Set](https://www.dublincore.org/specifications/dces/1998-08-13/) - [Calibre Manual - Editing Metadata](https://manual.calibre-ebook.com/metadata.html) - [Calibre Source Code](https://github.com/kovidgoyal/calibre) ## Appendix: Decision Log ### Decision 1: Sidecar-First Approach **Date**: 2026-03-26 **Decision**: Use sidecar-first (not smart-merge) **Rationale**: - Users with Calibre sidecars actively curate their metadata - Sidecar metadata is typically richer and more accurate - Simple implementation, predictable behavior - Easy to test and debug - Future enhancement can add configurable merge strategy ### Decision 2: No Database Schema Changes **Date**: 2026-03-26 **Decision**: Use existing schema only **Rationale**: - All required fields already exist in `media_items` table - Pre-production application (no legacy data to migrate) - Simpler implementation - No backward compatibility concerns ### Decision 3: All Library Types Supported **Date**: 2026-03-26 **Decision**: Support OPF for all libraries, not just ebooks **Rationale**: - Calibre supports all file types - Users may organize audiobooks/comics in Calibre - Consistent behavior across library types - No additional complexity