feat(scanner): fill sparse sidecars from embedded EPUB metadata
A sparse metadata.opf/metadata.json (title only, no description) left books thin even when the file itself carried rich data: the embedded extractors only ran when no sidecar existed at all. Now mergeMetadata fills blanks from the book's own OPF - title, author, description, publisher, language, ISBN, ASIN, series/number, publish date, tags, contributors - while sidecar values always win and unparseable files skip silently. Also covers .kepub, which the merge previously ignored while the extractor already supported it. Adds TestMergeMetadataEPUBGapFill asserting both directions: sidecar title/author survive, embedded description/publisher/language fill in.
This commit is contained in:
@@ -1161,7 +1161,7 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata
|
|||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
|
||||||
// For EPUB files
|
// For EPUB files
|
||||||
if ext == ".epub" {
|
if ext == ".epub" || ext == ".kepub" {
|
||||||
book, err := epub.ReadBook(path)
|
book, err := epub.ReadBook(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
genreTags := extractGenreTagsFromEPUB(book)
|
genreTags := extractGenreTagsFromEPUB(book)
|
||||||
@@ -1177,6 +1177,48 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata
|
|||||||
metadata.PageCount = int32(pageCount)
|
metadata.PageCount = int32(pageCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gap-fill: sidecar-sourced metadata wins, but blanks are filled from
|
||||||
|
// the book's own OPF so a sparse metadata.opf/metadata.json doesn't
|
||||||
|
// hide data the file carries. Never overwrites sidecar values.
|
||||||
|
if embedded, err := s.extractEPUBMetadata(path); err == nil && embedded != nil {
|
||||||
|
if metadata.Title == "" && embedded.Title != "" {
|
||||||
|
metadata.Title = embedded.Title
|
||||||
|
}
|
||||||
|
if metadata.Author == "" && embedded.Author != "" {
|
||||||
|
metadata.Author = embedded.Author
|
||||||
|
}
|
||||||
|
if metadata.Description == "" && embedded.Description != "" {
|
||||||
|
metadata.Description = embedded.Description
|
||||||
|
}
|
||||||
|
if metadata.Publisher == "" && embedded.Publisher != "" {
|
||||||
|
metadata.Publisher = embedded.Publisher
|
||||||
|
}
|
||||||
|
if metadata.Language == "" && embedded.Language != "" {
|
||||||
|
metadata.Language = embedded.Language
|
||||||
|
}
|
||||||
|
if metadata.ISBN == "" && embedded.ISBN != "" {
|
||||||
|
metadata.ISBN = embedded.ISBN
|
||||||
|
}
|
||||||
|
if metadata.ASIN == "" && embedded.ASIN != "" {
|
||||||
|
metadata.ASIN = embedded.ASIN
|
||||||
|
}
|
||||||
|
if metadata.Series == "" && embedded.Series != "" {
|
||||||
|
metadata.Series = embedded.Series
|
||||||
|
}
|
||||||
|
if metadata.SeriesNumber == 0 && embedded.SeriesNumber != 0 {
|
||||||
|
metadata.SeriesNumber = embedded.SeriesNumber
|
||||||
|
}
|
||||||
|
if metadata.PublishDate.IsZero() && !embedded.PublishDate.IsZero() {
|
||||||
|
metadata.PublishDate = embedded.PublishDate
|
||||||
|
}
|
||||||
|
if len(metadata.Tags) == 0 && len(embedded.Tags) > 0 {
|
||||||
|
metadata.Tags = embedded.Tags
|
||||||
|
}
|
||||||
|
if len(metadata.Contributors) == 0 && len(embedded.Contributors) > 0 {
|
||||||
|
metadata.Contributors = embedded.Contributors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For comic archives, try to extract ComicInfo.xml
|
// For comic archives, try to extract ComicInfo.xml
|
||||||
|
|||||||
@@ -302,3 +302,64 @@ func TestSidecarCoverFallback(t *testing.T) {
|
|||||||
t.Errorf("extracted cover not on disk: %v", err)
|
t.Errorf("extracted cover not on disk: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMergeMetadataEPUBGapFill verifies that a sparse sidecar (metadata.opf
|
||||||
|
// with only a title) gets its blanks filled from the book's own embedded OPF
|
||||||
|
// while sidecar-set fields are never overwritten.
|
||||||
|
func TestMergeMetadataEPUBGapFill(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
epubPath := filepath.Join(dir, "gappy.epub")
|
||||||
|
f, err := os.Create(epubPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
zipWriter := zip.NewWriter(f)
|
||||||
|
mimetype, _ := zipWriter.CreateHeader(&zip.FileHeader{Name: "mimetype", Method: zip.Store})
|
||||||
|
mimetype.Write([]byte("application/epub+zip"))
|
||||||
|
epubFiles := map[string]string{
|
||||||
|
"META-INF/container.xml": `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`,
|
||||||
|
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="u">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:title>Embedded Title</dc:title>
|
||||||
|
<dc:creator>Embedded Author</dc:creator>
|
||||||
|
<dc:description>Embedded description from the book.</dc:description>
|
||||||
|
<dc:publisher>Embedded Publisher</dc:publisher>
|
||||||
|
<dc:language>en</dc:language>
|
||||||
|
<dc:date>2021-06-01</dc:date>
|
||||||
|
</metadata>
|
||||||
|
<manifest/>
|
||||||
|
<spine/>
|
||||||
|
</package>`,
|
||||||
|
}
|
||||||
|
for name, content := range epubFiles {
|
||||||
|
w, err := zipWriter.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write([]byte(content))
|
||||||
|
}
|
||||||
|
if err := zipWriter.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
sparse := &MediaMetadata{Title: "Sidecar Title", Author: "Sidecar Author"}
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
merged, err := s.mergeMetadata(epubPath, sparse)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("mergeMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if merged.Title != "Sidecar Title" || merged.Author != "Sidecar Author" {
|
||||||
|
t.Errorf("sidecar values overwritten: title=%q author=%q", merged.Title, merged.Author)
|
||||||
|
}
|
||||||
|
if merged.Description != "Embedded description from the book." {
|
||||||
|
t.Errorf("Description = %q, want embedded fill", merged.Description)
|
||||||
|
}
|
||||||
|
if merged.Publisher != "Embedded Publisher" {
|
||||||
|
t.Errorf("Publisher = %q, want embedded fill", merged.Publisher)
|
||||||
|
}
|
||||||
|
if merged.Language != "en" {
|
||||||
|
t.Errorf("Language = %q, want embedded fill", merged.Language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user