docs: add Calibre metadata.opf implementation plan
Add comprehensive implementation plan for Calibre metadata.opf sidecar file support in the media scanner. Key features: - Sidecar-first approach: Calibre metadata.opf takes precedence over embedded metadata - Complete database schema mapping (no schema changes required - all fields exist) - Dublin Core and Calibre-specific field support - Simplified implementation: modify existing extractMetadata() instead of wrapper pattern - Works for all library types and file types - Comprehensive testing strategy Implementation details: - ~150 lines of new code (2 new functions + 1 modification) - No call site changes required - Graceful degradation on malformed XML - Performance target: <5% scan time increase This plan reflects simplified approach based on user feedback to directly modify extractMetadata() rather than creating wrapper functions. Related: User guide and developer docs added in separate commits
This commit is contained in:
@@ -157,30 +157,37 @@ Scanner Pipeline (EXISTING)
|
|||||||
5. Normalize and store
|
5. Normalize and store
|
||||||
```
|
```
|
||||||
|
|
||||||
**New Step 3a**: Check for Calibre sidecar **before** embedded extraction
|
**Simplified Approach**: Modify existing `extractMetadata()` to check for Calibre sidecar first
|
||||||
|
|
||||||
```
|
```
|
||||||
Scanner Pipeline (NEW)
|
Scanner Pipeline (NEW)
|
||||||
↓
|
↓
|
||||||
1. Detect book file
|
1. Detect book file
|
||||||
2. Check if already in database
|
2. Check if already in database
|
||||||
3. **NEW** Extract metadata from Calibre metadata.opf (if exists)
|
3. Extract metadata (MODIFIED FUNCTION)
|
||||||
- If sidecar found → use sidecar metadata
|
├── Check for Calibre metadata.opf sidecar ← NEW
|
||||||
- If no sidecar → use embedded metadata (existing)
|
│ └─ If found → use sidecar metadata
|
||||||
|
└── Fallback to embedded metadata (existing)
|
||||||
|
├── EPUB → extractEPUBMetadata()
|
||||||
|
├── PDF → extractPDFMetadata()
|
||||||
|
└── Comic → extractComicMetadata()
|
||||||
4. Extract folder structure metadata (existing)
|
4. Extract folder structure metadata (existing)
|
||||||
5. Normalize and store (existing)
|
5. Normalize and store (existing)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code Changes
|
### Code Changes
|
||||||
|
|
||||||
|
**SIMPLIFIED APPROACH** (thanks to user feedback): Modify existing `extractMetadata()` function instead of creating wrapper.
|
||||||
|
|
||||||
#### File: `internal/services/media_scanner.go`
|
#### File: `internal/services/media_scanner.go`
|
||||||
|
|
||||||
**New Functions:**
|
**New Functions (to add):**
|
||||||
|
|
||||||
1. `extractMetadataFromCalibreSidecar(path string) (*MediaMetadata, error)`
|
1. `extractCalibreSidecar(path string) *MediaMetadata`
|
||||||
- Checks for `metadata.opf` in same directory as book file
|
- Checks for `metadata.opf` in same directory as book file
|
||||||
- Calls `parseCalibreMetadataOPF()` if found
|
- Calls `parseCalibreMetadataOPF()` if found
|
||||||
- Returns nil if no sidecar exists (not an error)
|
- Returns nil if no sidecar exists (not an error)
|
||||||
|
- Returns nil on parse errors (graceful fallback)
|
||||||
|
|
||||||
2. `parseCalibreMetadataOPF(opfPath string) (*MediaMetadata, error)`
|
2. `parseCalibreMetadataOPF(opfPath string) (*MediaMetadata, error)`
|
||||||
- Opens and parses XML file
|
- Opens and parses XML file
|
||||||
@@ -191,9 +198,11 @@ Scanner Pipeline (NEW)
|
|||||||
|
|
||||||
**Modified Function:**
|
**Modified Function:**
|
||||||
|
|
||||||
1. `processMediaFile(ctx context.Context, path string) (bool, error)`
|
1. `extractMetadata(path string) (*MediaMetadata, error)` at line 712
|
||||||
- Add sidecar extraction check at line ~544 (before embedded extraction)
|
- Add sidecar check at very beginning (before switch statement)
|
||||||
- Flow: sidecar → embedded → folder → filename
|
- If sidecar found → return sidecar metadata immediately
|
||||||
|
- Otherwise → continue to existing embedded extraction logic
|
||||||
|
- **No call site changes needed** - all existing code continues to work
|
||||||
|
|
||||||
#### New Struct: `CalibreOPFMetadata`
|
#### New Struct: `CalibreOPFMetadata`
|
||||||
|
|
||||||
@@ -279,13 +288,18 @@ Create `testdata/calibre/` with sample files:
|
|||||||
|
|
||||||
### Phase 1: Core Functionality (REQUIRED)
|
### Phase 1: Core Functionality (REQUIRED)
|
||||||
|
|
||||||
- [ ] **Step 1.1**: Add `CalibreOPFMetadata` struct
|
- [ ] **Step 1.1**: Add `CalibreOPFMetadata` struct (intermediate parsing struct)
|
||||||
- [ ] **Step 1.2**: Implement `parseCalibreMetadataOPF()` function
|
- [ ] **Step 1.2**: Implement `parseCalibreMetadataOPF()` function (~120 lines)
|
||||||
- [ ] **Step 1.3**: Implement `extractMetadataFromCalibreSidecar()` function
|
- [ ] **Step 1.3**: Implement `extractCalibreSidecar()` function (~20 lines)
|
||||||
- [ ] **Step 1.4**: Modify `processMediaFile()` to call sidecar extraction
|
- [ ] **Step 1.4**: Modify `extractMetadata()` function (~10 lines)
|
||||||
|
- Add sidecar check at beginning of function (line ~712)
|
||||||
|
- Return sidecar metadata if found
|
||||||
|
- Otherwise continue to existing switch statement
|
||||||
- [ ] **Step 1.5**: Add unit tests for OPF parsing
|
- [ ] **Step 1.5**: Add unit tests for OPF parsing
|
||||||
- [ ] **Step 1.6**: Test with real Calibre library
|
- [ ] **Step 1.6**: Test with real Calibre library
|
||||||
|
|
||||||
|
**Total new code: ~150 lines** (simplified from original ~250 lines thanks to direct modification of `extractMetadata()`)
|
||||||
|
|
||||||
### Phase 2: Documentation (REQUIRED)
|
### Phase 2: Documentation (REQUIRED)
|
||||||
|
|
||||||
- [ ] **Step 2.1**: Create user documentation (`docs/user/calibre-support.md`)
|
- [ ] **Step 2.1**: Create user documentation (`docs/user/calibre-support.md`)
|
||||||
@@ -314,9 +328,10 @@ Create `testdata/calibre/` with sample files:
|
|||||||
**No breaking changes.** Existing behavior is preserved:
|
**No breaking changes.** Existing behavior is preserved:
|
||||||
|
|
||||||
- Libraries without `metadata.opf` files → Existing behavior (embedded metadata)
|
- Libraries without `metadata.opf` files → Existing behavior (embedded metadata)
|
||||||
- Scanning logic unchanged → Only adds sidecar check before embedded extraction
|
- Scanning logic unchanged → Single function modification, no call site changes
|
||||||
- Database unchanged → All fields already exist
|
- Database unchanged → All fields already exist
|
||||||
- API unchanged → No new endpoints or response fields
|
- API unchanged → No new endpoints or response fields
|
||||||
|
- **No wrapper function needed** → Simpler, cleaner implementation
|
||||||
|
|
||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
@@ -379,3 +394,34 @@ Create `testdata/calibre/` with sample files:
|
|||||||
- Users may organize audiobooks/comics in Calibre
|
- Users may organize audiobooks/comics in Calibre
|
||||||
- Consistent behavior across library types
|
- Consistent behavior across library types
|
||||||
- No additional complexity
|
- No additional complexity
|
||||||
|
|
||||||
|
### Decision 4: Simplified Implementation (REVISED)
|
||||||
|
**Date**: 2026-03-26
|
||||||
|
**Decision**: Modify `extractMetadata()` directly instead of creating wrapper function
|
||||||
|
**Rationale**:
|
||||||
|
- **User feedback**: Suggested simpler approach with if statement check
|
||||||
|
- **Less code**: ~150 lines vs. ~250 lines
|
||||||
|
- **No call site changes**: All existing code continues to work
|
||||||
|
- **Clearer flow**: Single entry point for metadata extraction
|
||||||
|
- **Better testability**: Test `extractMetadata()` with/without sidecar
|
||||||
|
- **Easier to maintain**: All metadata logic in one place
|
||||||
|
|
||||||
|
**Original plan**:
|
||||||
|
```go
|
||||||
|
// Wrapper function approach
|
||||||
|
extractMetadataWithSidecar() {
|
||||||
|
if sidecar exists → return sidecar
|
||||||
|
return extractMetadata()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Revised plan (SIMPLER)**:
|
||||||
|
```go
|
||||||
|
// Direct modification
|
||||||
|
extractMetadata() {
|
||||||
|
if sidecar exists → return sidecar // ← NEW: Just add this at top
|
||||||
|
// existing switch statement continues...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This simplification reduces code complexity and makes the implementation cleaner and easier to understand.
|
||||||
|
|||||||
Reference in New Issue
Block a user