feat(scanner): store EPUB reading direction at scan time
Release / build-and-push (push) Successful in 2m33s
Release / build-and-push (push) Successful in 2m33s
Fixed-layout EPUBs lean on the reading_direction column (the web reader forces book.dir = rtl from it when the file didn't set direction itself), but the scanner never populated it for EPUBs - only ComicInfo fed it. Meanwhile real Japanese EPUBs declare page-progression-direction on the OPF spine, which foliate reads client-side but nothing stored. Read the spine attribute in the structured OPF parser and map it into ReadingDirection in parseOPFContent (EPUB2/3, case-insensitive, plus 'right-to-left'/'left-to-right' spellings); undeclared stays empty rather than forcing ltr, preserving the editor's Auto default. Sidecar OPFs are metadata-only documents without spines, so the Calibre path is a no-op. The merge gap-fill copies an embedded-only direction into a blank sidecar field, and hand-set values keep winning through the existing OverrideReadingDirection protection. Tests: declared rtl/RTL/ltr, undeclared and unknown values staying empty, plus sidecar-wins vs embedded-fills merge cases. Existing manga EPUBs declaring rtl (verified live in-library) pick the value up on their next scan, feeding the API and reader config mobile clients consume.
This commit is contained in:
@@ -221,3 +221,77 @@ func TestResolveOPFPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseOPFContentPageProgressionDirection verifies the EPUB spine's
|
||||
// page-progression-direction feeds ReadingDirection, and that an undeclared
|
||||
// direction stays empty rather than forcing left-to-right.
|
||||
func TestParseOPFContentPageProgressionDirection(t *testing.T) {
|
||||
makeOPF := func(spineAttrs string) string {
|
||||
return `<?xml version="1.0"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>Dir Test</dc:title></metadata>
|
||||
<manifest><item id="p1" href="p1.xhtml" media-type="application/xhtml+xml"/></manifest>
|
||||
<spine` + spineAttrs + `><itemref idref="p1"/></spine>
|
||||
</package>`
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
spineAttrs string
|
||||
want string
|
||||
}{
|
||||
{"rtl declared lowercase", ` page-progression-direction="rtl"`, "rtl"},
|
||||
{"rtl declared uppercase", ` page-progression-direction="RTL"`, "rtl"},
|
||||
{"ltr declared", ` page-progression-direction="ltr"`, "ltr"},
|
||||
{"undeclared stays empty", ``, ""},
|
||||
{"unknown value stays empty", ` page-progression-direction="sideways"`, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
metadata, err := parseOPFContent([]byte(makeOPF(tt.spineAttrs)))
|
||||
if err != nil {
|
||||
t.Fatalf("parseOPFContent() error: %v", err)
|
||||
}
|
||||
if metadata.ReadingDirection != tt.want {
|
||||
t.Errorf("ReadingDirection = %q, want %q", metadata.ReadingDirection, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMergeMetadataReadingDirectionGapFill verifies the sidecar wins when it
|
||||
// declares a direction, while an embedded-only direction fills the blank.
|
||||
func TestMergeMetadataReadingDirectionGapFill(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
epubPath := filepath.Join(dir, "dir.epub")
|
||||
files := map[string]string{
|
||||
"META-INF/container.xml": containerXML,
|
||||
"OEBPS/package.opf": `<?xml version="1.0"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>RTL Book</dc:title></metadata>
|
||||
<manifest><item id="p1" href="p1.xhtml" media-type="application/xhtml+xml"/></manifest>
|
||||
<spine page-progression-direction="rtl"><itemref idref="p1"/></spine>
|
||||
</package>`,
|
||||
"OEBPS/p1.xhtml": `<html><body><p>hi</p></body></html>`,
|
||||
}
|
||||
writeEPUB(t, epubPath, files)
|
||||
|
||||
s := NewMediaScanner(nil)
|
||||
|
||||
// Sidecar blank -> embedded rtl fills it
|
||||
merged, err := s.mergeMetadata(epubPath, &MediaMetadata{Title: "Sidecar"})
|
||||
if err != nil {
|
||||
t.Fatalf("mergeMetadata() error: %v", err)
|
||||
}
|
||||
if merged.ReadingDirection != "rtl" {
|
||||
t.Errorf("ReadingDirection = %q, want embedded rtl fill", merged.ReadingDirection)
|
||||
}
|
||||
|
||||
// Sidecar ltr wins over embedded rtl
|
||||
merged, err = s.mergeMetadata(epubPath, &MediaMetadata{Title: "Sidecar", ReadingDirection: "ltr"})
|
||||
if err != nil {
|
||||
t.Fatalf("mergeMetadata() error: %v", err)
|
||||
}
|
||||
if merged.ReadingDirection != "ltr" {
|
||||
t.Errorf("ReadingDirection = %q, want sidecar ltr preserved", merged.ReadingDirection)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user