feat(audiobooks): Batch A — foundation, m4b ingest, playback sync, web player
Phase 1 (foundation): 'audiobooks' library type seeded (schema + DB);
audio_metadata JSONB on media_items (the kobo_metadata precedent —
only audio paths read it); reading_progress.audio_offset_seconds for
the honest {chapter, offset} locator; extensions/mimes registered for
ALL breadth formats (m4b/m4a/mp3/flac — deliberate per the DOC/LIT
doctrine, m4b scans richly first); classifyFormatGroup 'audiobook'
case (gotcha 39 recomputes on rescan); Go mime table extended at boot
so media elements accept the streams; ffmpeg/ffprobe in the image
(8.1.2 — also the xHE-AAC identification tripwire).
Phase 2 (ingest): sidecar-first (Libation/ABS metadata.json —
narrators/ASIN/curated chapters), ffprobe fallback/filler (duration,
bitrate, codec/profile, embedded chapters). Fixed three real
parser brittlenesses the Libation sidecar exposed: series accepts
string OR array-of-objects, publishedYear accepts number OR string,
ffprobe chapter timings accept number OR string (flexFloat). Folder-
structure fallback now runs on the UPDATE path too (rescans were
clearing sidecar-absent series/author); audio facts re-derive on
every rescan (gotcha 39).
Phase 3 (playback + sync): GET /api/media-items/:id/chapters (the
format-abstraction list); streaming rides the existing cookie-authed
Range /uploads path; PUT progress carries audio_offset_seconds +
elapsed_seconds (honest wall-clock playing time) — the reading-
history capture prefers player-reported elapsed over the inter-push
delta, same 2h ceiling; reading_mode='audio' marks the row.
Phase 4 (web player): dedicated audio reader page (foliate not
involved) — cover, chapter title, whole-book scrubber, skip ± (chips
1/5/10/15/30s, default 10s), speed (0.75×–2×), chapter list with
jump-to, Media Session lockscreen/notification transport, periodic
playing-only progress pushes, position restore on open.
Verified live: ingest of the Libation sample (Gardens of the Moon —
title/author/series/cover/date from sidecar+folders, 34 chapters,
26h duration, aac/LC); chapters endpoint; the reader route serving
the player; playback with position restore (0:57 round-trip);
progress rows in audio mode; wall-clock session rows in
reading_history.
This commit is contained in:
1 parent
a093529b80
commit
902cbe5915
20 files changed
+1425
-212
No files matched your search
@@ -83,36 +83,36 @@ func TestContinueSeriesRowToMediaItems_AllFieldsMapped(t *testing.T) {
|
||||
libUUID := uuid.New()
|
||||
|
||||
row := database.GetContinueSeriesItemsRow{
|
||||
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
||||
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||
Title: "Full Book",
|
||||
Author: pgtype.Text{String: "Author", Valid: true},
|
||||
Isbn: pgtype.Text{String: "ISBN", Valid: true},
|
||||
Description: pgtype.Text{String: "Desc", Valid: true},
|
||||
FilePath: "/book.epub",
|
||||
FileSize: pgtype.Int8{Int64: 2048, Valid: true},
|
||||
MimeType: pgtype.Text{String: "epub", Valid: true},
|
||||
CoverImagePath: pgtype.Text{String: "/cover.jpg", Valid: true},
|
||||
Series: pgtype.Text{String: "Series", Valid: true},
|
||||
SeriesNumber: pgtype.Int4{Int32: 1, Valid: true},
|
||||
Tags: []string{"tag1"},
|
||||
Asin: pgtype.Text{String: "ASIN", Valid: true},
|
||||
Publisher: pgtype.Text{String: "Pub", Valid: true},
|
||||
Language: pgtype.Text{String: "en", Valid: true},
|
||||
Edition: pgtype.Text{String: "1st", Valid: true},
|
||||
Genre: pgtype.Text{String: "Fiction", Valid: true},
|
||||
FormatGroup: "epub",
|
||||
FormatMimetype: pgtype.Text{String: "application/epub+zip", Valid: true},
|
||||
IsReflowable: pgtype.Bool{Bool: true, Valid: true},
|
||||
HasFixedLayout: pgtype.Bool{Bool: false, Valid: true},
|
||||
TotalCharacters: pgtype.Int8{Int64: 500000, Valid: true},
|
||||
ChapterCount: pgtype.Int4{Int32: 20, Valid: true},
|
||||
MangaType: pgtype.Text{String: "manga", Valid: true},
|
||||
ReadingDirection: pgtype.Text{String: "rtl", Valid: true},
|
||||
SeriesCount: pgtype.Int4{Int32: 10, Valid: true},
|
||||
Volume: pgtype.Int4{Int32: 1, Valid: true},
|
||||
LibraryTypeName: pgtype.Text{String: "ebooks", Valid: true},
|
||||
FileSha256: pgtype.Text{String: "abc123", Valid: true},
|
||||
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
||||
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
||||
Title: "Full Book",
|
||||
Author: pgtype.Text{String: "Author", Valid: true},
|
||||
Isbn: pgtype.Text{String: "ISBN", Valid: true},
|
||||
Description: pgtype.Text{String: "Desc", Valid: true},
|
||||
FilePath: "/book.epub",
|
||||
FileSize: pgtype.Int8{Int64: 2048, Valid: true},
|
||||
MimeType: pgtype.Text{String: "epub", Valid: true},
|
||||
CoverImagePath: pgtype.Text{String: "/cover.jpg", Valid: true},
|
||||
Series: pgtype.Text{String: "Series", Valid: true},
|
||||
SeriesNumber: pgtype.Int4{Int32: 1, Valid: true},
|
||||
Tags: []string{"tag1"},
|
||||
Asin: pgtype.Text{String: "ASIN", Valid: true},
|
||||
Publisher: pgtype.Text{String: "Pub", Valid: true},
|
||||
Language: pgtype.Text{String: "en", Valid: true},
|
||||
Edition: pgtype.Text{String: "1st", Valid: true},
|
||||
Genre: pgtype.Text{String: "Fiction", Valid: true},
|
||||
FormatGroup: "epub",
|
||||
FormatMimetype: pgtype.Text{String: "application/epub+zip", Valid: true},
|
||||
IsReflowable: pgtype.Bool{Bool: true, Valid: true},
|
||||
HasFixedLayout: pgtype.Bool{Bool: false, Valid: true},
|
||||
TotalCharacters: pgtype.Int8{Int64: 500000, Valid: true},
|
||||
ChapterCount: pgtype.Int4{Int32: 20, Valid: true},
|
||||
MangaType: pgtype.Text{String: "manga", Valid: true},
|
||||
ReadingDirection: pgtype.Text{String: "rtl", Valid: true},
|
||||
SeriesCount: pgtype.Int4{Int32: 10, Valid: true},
|
||||
Volume: pgtype.Int4{Int32: 1, Valid: true},
|
||||
LibraryTypeName: pgtype.Text{String: "ebooks", Valid: true},
|
||||
FileSha256: pgtype.Text{String: "abc123", Valid: true},
|
||||
}
|
||||
|
||||
result := continueSeriesRowToMediaItems(row)
|
||||
|
||||
Reference in new issue
Block a user