feat(scanner): store EPUB reading direction at scan time
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:
John O'Keefe
2026-09-13 13:13:25 -04:00
parent e944f11415
commit 3ab294bf81
3 changed files with 101 additions and 1 deletions
+9
View File
@@ -1209,6 +1209,9 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata
if metadata.SeriesNumber == 0 && embedded.SeriesNumber != 0 {
metadata.SeriesNumber = embedded.SeriesNumber
}
if metadata.ReadingDirection == "" && embedded.ReadingDirection != "" {
metadata.ReadingDirection = embedded.ReadingDirection
}
if metadata.PublishDate.IsZero() && !embedded.PublishDate.IsZero() {
metadata.PublishDate = embedded.PublishDate
}
@@ -1866,6 +1869,12 @@ func parseOPFContent(content []byte) (*MediaMetadata, error) {
if title := opf.selectTitle(); title != "" {
metadata.Title = title
}
// Reading direction from the OPF spine's page-progression-direction.
// Sidecar OPFs are metadata-only documents without a spine, so this is a
// no-op for them and only fires on real books.
if dir := opf.pageProgressionDirection(); dir != "" {
metadata.ReadingDirection = dir
}
// Author (first creator)
if len(md.Creators) > 0 {
metadata.Author = md.Creators[0]