fix(scanner): extract embedded covers for sidecar-managed books
A full rescan wiped cover_image_path for every PDF/EPUB living in a
metadata.json (or metadata.opf-less) folder with no cover.jpg next to
the book: the sidecar branches returned early after findSidecarCover
missed, and mergeMetadata has no PDF/EPUB cover logic of its own. Four
books lost their thumbnails while their {file}.cover.jpg files still sat
on disk - most visibly the Audiobookshelf-managed No Starch titles.
Both sidecar branches now fall through to an embedded-cover fallback
(PDF via extractPDFCover, EPUB/KEPUB via extractEPUBCover) whenever no
sidecar cover file exists. Comics are untouched: mergeMetadata already
extracts their covers from the archive.
Adds TestSidecarCoverFallback with a hand-built one-page PDF carrying a
JPEG XObject plus a metadata.json sidecar, asserting the sidecar title
wins while the cover still comes from the file. Verified live: rescans
restored all four dereferenced covers with no leftover rows.
This commit is contained in:
@@ -1007,6 +1007,27 @@ func (s *MediaScanner) extractCalibreSidecar(path string) *MediaMetadata {
|
||||
return metadata
|
||||
}
|
||||
|
||||
// applyEmbeddedCoverFallback extracts a cover from the media file itself when
|
||||
// a metadata sidecar (metadata.opf / metadata.json) supplied the metadata but
|
||||
// no sidecar cover (cover.jpg etc.) exists. Without this, a full rescan would
|
||||
// clear cover_image_path for sidecar-managed PDFs and EPUBs - mergeMetadata
|
||||
// has no PDF/EPUB cover logic of its own.
|
||||
func (s *MediaScanner) applyEmbeddedCoverFallback(path string, metadata *MediaMetadata) {
|
||||
if metadata.CoverPath != "" {
|
||||
return
|
||||
}
|
||||
switch strings.ToLower(filepath.Ext(path)) {
|
||||
case ".pdf":
|
||||
if coverPath, err := s.extractPDFCover(path); err == nil && coverPath != "" {
|
||||
metadata.CoverPath = s.getRelativePath(coverPath)
|
||||
}
|
||||
case ".epub", ".kepub":
|
||||
if coverPath, err := s.extractEPUBCover(path); err == nil && coverPath != "" {
|
||||
metadata.CoverPath = s.getRelativePath(coverPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractAudiobookshelfSidecar checks for and parses an Audiobookshelf-style
|
||||
// metadata.json sidecar next to the media file. Only fields with a matching
|
||||
// media_items column are mapped; narrators, subtitle, explicit, abridged and
|
||||
@@ -1414,6 +1435,7 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
||||
if coverPath != "" {
|
||||
calibreMetadata.CoverPath = s.getRelativePath(coverPath)
|
||||
}
|
||||
s.applyEmbeddedCoverFallback(path, calibreMetadata)
|
||||
|
||||
return s.mergeMetadata(path, calibreMetadata)
|
||||
}
|
||||
@@ -1428,6 +1450,7 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
||||
if coverPath != "" {
|
||||
abMetadata.CoverPath = s.getRelativePath(coverPath)
|
||||
}
|
||||
s.applyEmbeddedCoverFallback(path, abMetadata)
|
||||
|
||||
return s.mergeMetadata(path, abMetadata)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user