feat(scanner): Calibre-aligned OPF metadata extraction
Release / build-and-push (push) Successful in 3m5s
Release / build-and-push (push) Successful in 3m5s
Adopt Calibre's reading conventions for the Dublin Core metadata that
parseOPFContent now pulls from the structured OPF parse:
- Titles: EPUB3 title-type selection (prefer 'main', join a distinct
subtitle with ': ' exactly as Calibre stores it). There is no separate
subtitle column by design - Calibre-sidecar books arrive pre-joined,
so a column would stay empty for most libraries and force every client
to reimplement concatenation.
- Genre: first dc:subject, mirroring the existing processGenresAndTags
behavior of the Calibre-sidecar path; the embedded path never
populated Genre before. Subjects stay one-element-one-tag - Library
of Congress headings legitimately contain commas ("Holmes, Sherlock
(Fictitious character) -- Fiction") and must not be split.
- Identifiers: urn:isbn:/urn:asin: prefixed values parse in addition to
opf:scheme attributes, and the scheme-less fallback now requires an
ISBN-shaped value (10/13 digits, optional separators/trailing X) so
URIs like the Gutenberg identifiers cannot masquerade as ISBNs -
observed live on 'A Study in Scarlet'.
- Series: EPUB3 belongs-to-collection with collection-type=series and
group-position refines, ahead of the classic calibre:series metas.
- Audiobookshelf metadata.json sidecars join their subtitle field into
the title the same way.
Tests cover title-type main+subtitle joining, belongs-to-collection
series with fractional group-position, urn:isbn extraction, genre/tag
parity, and comma preservation inside subject headings.
This commit is contained in:
@@ -145,6 +145,65 @@ func TestFindCoverInOPFImageFirstSpine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseOPFContentTitleTypeAndSeries covers EPUB3 refines-based title
|
||||
// selection (main + subtitle joined Calibre-style) and belongs-to-collection
|
||||
// series with collection-type and group-position refines.
|
||||
func TestParseOPFContentTitleTypeAndSeries(t *testing.T) {
|
||||
opf := `<?xml version="1.0"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:title id="t1">The Main Title</dc:title>
|
||||
<dc:title id="t2">The Subtitle</dc:title>
|
||||
<meta refines="#t1" property="title-type">main</meta>
|
||||
<meta refines="#t2" property="title-type">subtitle</meta>
|
||||
<dc:subject>Programming</dc:subject>
|
||||
<dc:subject>Algorithms</dc:subject>
|
||||
<dc:identifier>urn:isbn:978-3-16-148410-0</dc:identifier>
|
||||
<meta id="coll1" property="belongs-to-collection">Great Series</meta>
|
||||
<meta refines="#coll1" property="collection-type">series</meta>
|
||||
<meta refines="#coll1" property="group-position">4.5</meta>
|
||||
</metadata>
|
||||
</package>`
|
||||
metadata, err := parseOPFContent([]byte(opf))
|
||||
if err != nil {
|
||||
t.Fatalf("parseOPFContent() error: %v", err)
|
||||
}
|
||||
if want := "The Main Title: The Subtitle"; metadata.Title != want {
|
||||
t.Errorf("Title = %q, want %q", metadata.Title, want)
|
||||
}
|
||||
if metadata.Series != "Great Series" || metadata.SeriesNumber != 4 {
|
||||
t.Errorf("Series = %q/%d, want Great Series/4", metadata.Series, metadata.SeriesNumber)
|
||||
}
|
||||
if metadata.ISBN == "" {
|
||||
t.Error("urn:isbn: identifier not extracted")
|
||||
}
|
||||
if metadata.Genre != "Programming" {
|
||||
t.Errorf("Genre = %q, want first subject %q", metadata.Genre, "Programming")
|
||||
}
|
||||
if len(metadata.Tags) != 2 {
|
||||
t.Errorf("Tags = %v, want both subjects", metadata.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseOPFContentSubjectsWithCommas verifies subject headings keep their
|
||||
// embedded commas as single tags (Library of Congress style headings).
|
||||
func TestParseOPFContentSubjectsWithCommas(t *testing.T) {
|
||||
opf := `<?xml version="1.0"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="2.0">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:title>A Study in Scarlet</dc:title>
|
||||
<dc:subject>Holmes, Sherlock (Fictitious character) -- Fiction</dc:subject>
|
||||
</metadata>
|
||||
</package>`
|
||||
metadata, err := parseOPFContent([]byte(opf))
|
||||
if err != nil {
|
||||
t.Fatalf("parseOPFContent() error: %v", err)
|
||||
}
|
||||
if len(metadata.Tags) != 1 {
|
||||
t.Errorf("Tags = %v, want exactly 1 unsplit subject heading", metadata.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveOPFPath checks URL decoding and posix normalization of
|
||||
// OPF-relative hrefs.
|
||||
func TestResolveOPFPath(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user