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
@@ -18,6 +18,10 @@ func registerMediaRoutes(cfg *Config) {
|
||||
// Book download endpoint - same ServeFile flow as /uploads/library-:id/*
|
||||
// (JWT + library-visibility gated), addressed by media item ID.
|
||||
protected.GET("/media-items/:id/download", cfg.MediaHandler.ServeFile)
|
||||
// The format-abstraction chapter list (audio_metadata). Streaming
|
||||
// itself rides the existing /uploads Range-serving path with cookie
|
||||
// auth — no separate endpoint.
|
||||
protected.GET("/media-items/:id/chapters", cfg.MediaHandler.GetAudioChapters)
|
||||
|
||||
// Media rating routes (all authenticated users)
|
||||
protected.POST("/media-items/:id/rating", cfg.MediaHandler.CreateMediaRating)
|
||||
|
||||
@@ -83,7 +83,10 @@ func registerReaderRoutes(cfg *Config) {
|
||||
MediaItemID: mediaUUID.String(),
|
||||
Title: mediaItem.Title,
|
||||
Author: textToString(mediaItem.Author),
|
||||
CoverImagePath: textToString(mediaItem.CoverImagePath),
|
||||
// Sidecar covers store a raw library-relative path; resolve to
|
||||
// the cookie-authed /uploads URL (idempotent for items whose
|
||||
// stored value is already a servable path).
|
||||
CoverImagePath: utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath),
|
||||
LibraryType: mediaItem.FormatGroup,
|
||||
MimeType: textToString(mediaItem.MimeType),
|
||||
FilePath: mediaItem.FilePath,
|
||||
@@ -100,7 +103,13 @@ func registerReaderRoutes(cfg *Config) {
|
||||
}
|
||||
// Render template
|
||||
var buf bytes.Buffer
|
||||
err = templates.Reader(user, metadata).Render(c.Request().Context(), &buf)
|
||||
// Audiobooks get the audio player page — the foliate document
|
||||
// engine is deliberately not involved (handoff item 3 Batch A).
|
||||
if mediaItem.FormatGroup == "audiobook" {
|
||||
err = templates.AudioReader(user, metadata).Render(c.Request().Context(), &buf)
|
||||
} else {
|
||||
err = templates.Reader(user, metadata).Render(c.Request().Context(), &buf)
|
||||
}
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error rendering reader", "render_error")
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -209,6 +210,13 @@ func wantsHTML(header http.Header) bool {
|
||||
|
||||
// RegisterRoutes registers all application routes
|
||||
func RegisterRoutes(cfg *Config) *handlers.Handler {
|
||||
// Go's builtin mime table doesn't know the audiobook containers; the
|
||||
// /uploads file serving and any ServeContent path needs real audio
|
||||
// content types or media elements refuse the stream.
|
||||
mime.AddExtensionType(".m4b", "audio/mp4")
|
||||
mime.AddExtensionType(".m4a", "audio/mp4")
|
||||
mime.AddExtensionType(".mp3", "audio/mpeg")
|
||||
mime.AddExtensionType(".flac", "audio/flac")
|
||||
e := cfg.Echo
|
||||
|
||||
// Set up validator
|
||||
|
||||
Reference in new issue
Block a user