# Library Thumbnail Caching **Status:** ✅ Implemented | Cache library covers during scans, serve in BookDate ## Overview Caches book covers from Plex/Audiobookshelf during library scans. Stores cached files in `/app/cache/library/` with SHA-256 hashed filenames. Dramatically improves BookDate user experience by showing real covers instead of placeholders. ## Key Details ### Caching Strategy - **When:** During full scans (scan-plex.processor.ts) and recently-added scans (plex-recently-added.processor.ts) - **Where:** `/app/cache/library/` directory - **Filename:** SHA-256 hash (first 16 chars) of plexGuid + extension (e.g., `a3f5e9d2c1b4.jpg`) - **Smart caching:** Checks if file exists before downloading (subsequent scans are fast) ### Database Schema - **Field:** `PlexLibrary.cachedLibraryCoverPath` (nullable TEXT) - **Stores:** Full path like `/app/cache/library/{hash}.jpg` - **Migration:** `20260120000000_add_cached_library_cover_path` ### URL Construction (Backend-Specific) - **Plex:** `{serverUrl}{thumbUrl}?X-Plex-Token={token}` - **Audiobookshelf:** `{serverUrl}{coverPath}` with `Authorization: Bearer {token}` header ### Cover Priority (BookDate Library Picker) 1. **Library cached cover** (`cachedLibraryCoverPath`) → `/api/cache/library/{filename}` 2. **Audible cache** (if book has ASIN) → from `AudibleCache.coverArtUrl` 3. **Null** (show placeholder 📚) ## API Endpoints ### GET /api/cache/library/[filename] Serves cached library covers (24-hour browser cache). **Path validation:** Prevents directory traversal (rejects `..` and `/`). **Content types:** jpg, jpeg, png, gif, webp → image/*, else application/octet-stream ### GET /api/bookdate/library Returns library books with cover URLs. **Response:** ```json { "books": [ { "id": "uuid", "title": "Book Title", "author": "Author Name", "coverUrl": "/api/cache/library/a3f5e9d2c1b4.jpg" // or Audible URL, or null } ] } ``` ## Service Layer ### ThumbnailCacheService Located: `src/lib/services/thumbnail-cache.service.ts` **Methods:** - `cacheLibraryThumbnail(plexGuid, coverUrl, backendBaseUrl, authToken, backendMode)` → Returns cached path or null - `cleanupLibraryThumbnails(plexGuidToHashMap)` → Returns deleted count **Safeguards:** - 10s timeout per download - 5MB max file size - Content-type validation (must be image/*) - Graceful degradation (logs warning, returns null on failure) ### Library Services Located: `src/lib/services/library/` **Both PlexLibraryService and AudiobookshelfLibraryService provide:** - `getCoverCachingParams()` → Returns `{ backendBaseUrl, authToken, backendMode }` ## Performance ### First Full Scan (1000 books) - Database: ~30 seconds - Downloads: ~1-5 minutes (network-dependent) - **Total: ~1.5-5.5 minutes** (one-time cost) ### Subsequent Scans (1000 books) - Database: ~30 seconds - Downloads: **~0 seconds** (skipped, files exist) - **Total: ~30 seconds** (same as before caching) ### BookDate Library Load - **Before:** Mostly placeholder covers - **After:** Real covers for all books with valid thumbUrl - **Performance:** No change (local file serving is fast) ## Error Handling - Download fails → log warning, store null, continue scan - Invalid content-type → reject, store null - File system errors → log, store null - Missing backend config → throw (scan fails early with clear error) ## Cleanup (Future Enhancement) **Manual or Scheduled:** - Builds hash-to-plexGuid reverse map from database - Deletes cached files for plexGuids no longer in library - Returns count of deleted files **Trigger:** Admin endpoint or weekly scheduled job ## Docker Configuration **Volume mount required:** ```yaml volumes: - ./cache/library:/app/cache/library ``` Ensures cached covers persist across container restarts. ## Related - documentation/backend/database.md (PlexLibrary schema) - documentation/features/bookdate.md (cover loading logic) - documentation/integrations/audible.md (Audible thumbnail caching pattern) - documentation/backend/services/jobs.md (scan processors)