Frontend gains a feature-detected display path for two relationship
rails. Detail responses may now carry also_by_author and in_series.
When present, the detail page mounts Also-by-author followed by
In-series, with the current book highlighted inside the series strip.
When the backend omits them — which is the case today — the rails are
not rendered, so this lands as zero visible change until the backend
ships the data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the narrator as a first-class card with a circular initials
avatar. Rendered only when data.narrator is present, so audiobooks
without narrator metadata don't introduce empty space.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the collapsed-by-default ChapterList disclosure with
ChaptersSection: always-expanded, current-chapter highlighted with a
left-edge accent + 'playing' badge, and a small sort menu (By position
/ Longest first). currentPositionSeconds is plumbed from the player's
seek start when open, otherwise from the resume position, so the
highlight tracks the user's most recent listening point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Progress bar moves above the Resume / Play-from-Start row. Caption
becomes \"Xh Ym listened · NN%\" (was just the listened time). Resume
button now reads \"Resume · <chapter name>\" derived from
findChapterAt so the user sees where they'll land without opening the
player. No progress → no bar, no chapter; the Play button keeps its
existing label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SleepTimerMenu is a new shared player primitive (moon icon, popover
with 5/15/30/45/60-minute presets plus End-of-chapter). Hook state in
useAudiobookPlayback drives both the countdown label and the auto-pause:
duration timers track wall-clock so they survive tab inactivity, and
end-of-chapter watches currentChapter.end_seconds. Both modes pause the
audio element and reset the setting when they fire. Wired into the
right rail of MiniBar and into the utility row of NowListening.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AudiobookPlayer now tracks 'mini' | 'now-listening' locally. The
CoverExpandTile flips to Now Listening, the chevron flips back. The
hidden audio element is owned by the shared parent, so swapping chrome
does not remount it and currentTime advances continuously across the
transition. MiniBar now owns its fixed-bottom positioning so the
AudiobookDetail wrapper can drop. Author + narrator now flow into the
overlay's header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-overlay chrome that mirrors the mini bar's controls scaled up
for a focused listening view: large cover (with view-transition anchor
matching CoverExpandTile), title/author/narrator block, current-chapter
heading, large SeekBar with toggleable remaining-time label, big play
disc, and the same chapter + speed menus. Not yet reachable — wired in
Task 9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tile sits at the far left of the bar; clicking it fires onExpand,
which is a no-op for now (mini ↔ Now Listening swap lands in the next
task). The tile sets viewTransitionName: audiobook-cover-\${contentId}
so the upcoming overlay can morph from it via the View Transitions API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders playback.currentChapter.title between the book title and the
time row. Truncates with a tooltip; absent when the chapter list is
empty or the current position falls outside any chapter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bar chrome moves into MiniBar.tsx; AudiobookPlayer becomes a thin
shell that owns the audio element and renders MiniBar. Same DOM, same
behavior — sets up the NowListening swap (Task 9).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All audio-element wiring, periodic progress reporting, and seek/skip/
toggle/rate logic move into a reusable hook. The player component now
consumes playback.* and switches to the extracted CircleButton
primitive shared with the video player.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure file move. Sets up the directory for the upcoming hook + chrome
split (useAudiobookPlayback, MiniBar, NowListening).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces a shared SpeedMenu component (button trigger + popover with
arrow-key navigation and escape-to-close) used by the audiobook player.
Replaces the previous native <select> with a menu that matches the
in-design dark surface treatment and the planned shared player chrome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third feature-detected rail on the detail page powered by
server-side embedding similarity. Frontend has no embedding logic —
it just renders the ordered list the API returns. Plan gains Task
14b implementing it via an optional subtitle prop on RelatedRail
and an optional similar_audiobooks field on the detail response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles 21 in-progress files spanning scanner, image cache, ABS API
handlers, podcast/audiobook scan, cover enrichment, metadata sync
task, and an interim pass at the audiobook detail page (DetailHero
adoption + fixed-bottom player). Committed as a single baseline so
the upcoming UI redesign (per the 2026-05-24 plan) starts from a
clean working tree. Not split into logical commits because this
captures pre-existing local work, not a curated change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the audiobook detail page and player to parity with the video
player, translated for audio: shared glass-disc primitives, a polished
mini bar with cover-tile expand affordance, and a Now Listening
full-overlay mode. Restructure the detail page so progress, current
chapter, narrator, and cross-book context are foregrounded. Defers
bookmarks, car mode, and cross-book rails to v1.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 'Audiobooks' and 'Podcasts' options to the library-type dropdown
in the admin libraries page so operators can flag a folder as an
audiobook or podcast library. Extends contentLevelsForType() so the
admin UI's downstream filtering treats those types correctly
(audiobook -> ['audiobook'], podcasts -> ['podcast',
'podcast_episode']).
Backend scanner branches for these types were already wired in
sub-plan 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-plan 5 of the audiobooks absorption. Ports the plugin's
podcastfeed.Refresher into internal/audiobooks/podcastfeed and
registers it as a 10-minute scheduled task in silo's task manager.
For each row in podcast_feeds (sub-plan 1 mig 140), the refresher:
- Fetches the RSS feed with a 30-second HTTP timeout
- Parses entries via gofeed (github.com/mmcdole/gofeed)
- UPSERTs new entries into episodes (joined to the podcast's
media_items row via podcast_feeds.media_item_id = series_id)
- Updates podcast_feeds.last_refreshed_at + last_refresh_error
Migration 144 adds podcast_guid and podcast_audio_url columns to
episodes, plus a partial unique index on (series_id, podcast_guid)
WHERE podcast_guid IS NOT NULL so the refresher can ON CONFLICT upsert
without touching non-podcast episode rows.
Enclosure file download is deferred; this stage upserts episode
metadata only. Existing content_ids are reused when a feed re-emits a
known guid so per-user progress rows stay bound to the same FK target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 7 of the ABS-compat port (final). Implements MediaStore, ConfigProvider,
and event/recommender stub adapters, constructs the abs.Handler in
cmd/silo/main.go, and mounts it under /abs and /abs/api on silo's main
router. ABS-compatible Audiobookshelf clients can now hit silo
directly for login, library + items browse, file streaming, progress,
and Socket.io.
End-to-end smoke verified: /login returns 401, /abs/api/login returns
401, /abs/api/me returns 401, /abs/api/libraries returns 401,
/abs/socket.io/ returns 200 on polling handshake.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 6 of the ABS-compat port. Lifts the plugin's abssocket package
into internal/audiobooks/abssocket/, mounted under /abs/socket.io/ on
silo's main HTTP listener. The plugin's standalone-listener
workaround is no longer needed because the host plugin-proxy WS-bridge
constraint disappears once the code runs in-process.
Redis pub/sub adapter behavior is preserved when REDIS_URL is set;
single-replica in-memory fan-out otherwise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 5 of the ABS-compat port. Adds:
- GET /abs/api/me (and /api/me)
- GET /abs/api/libraries (and /api/libraries)
- GET /abs/api/libraries/{id}
- GET /abs/api/libraries/{id}/items
- GET /abs/api/items/{id}
- GET /abs/api/items/{id}/similar
- GET /abs/api/me/items-in-progress (Continue Listening)
- Stub 200 endpoints for authors, series, search, personalized
Extends MediaStore interface: ListAudiobookLibraries and
LibraryID-filtered ListAudiobooks. Adds AudiobookLibrary struct.
Also ports collapse.go (CollapseBySeries) from the plugin — identical
logic, needed for collapseseries=1 query param support.
Author/series detail and filter-data population stubbed (empty lists);
ABS clients fall back to flat item browse when those endpoints return
empty rather than crashing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 4 of the ABS-compat port. Five handlers:
- GET /abs/api/me/progress
- GET /abs/api/me/progress/{libraryItemId}
- POST /abs/api/me/progress/{libraryItemId}
- PATCH /abs/api/session/{sid}
- POST /abs/api/session/{sid}/close
Reads/writes hit silo's user_watch_progress (keyed by user_id,
profile_id, content_id) for resume positions and the new
abs_playback_sessions table (migration 143) for per-session
listening-time tracking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks active /abs/api/items/{id}/play sessions for Audiobookshelf
clients so the /session/{sid}/sync and /session/{sid}/close
endpoints can record per-session listening time. Distinct from
abs_sessions (which tracks auth tokens) and user_watch_progress
(which tracks resume position).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 3 of the ABS-compat port. handleFileStream serves a single
audio file by ino (which we map to silo's media_files.id);
handlePlayStart issues a play-session manifest pointing at silo's
existing /api/v1/direct-download stream URL. Plugin's bookref/
streaming/mediatoken abstractions are replaced with direct
MediaStore lookups against silo's tables.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 2 of the ABS-compat port. Adds the handleLogin handler issuing
JWT access + refresh tokens, a SiloCredValidator adapter that calls
silo's auth.Service directly (in-process — the plugin previously
HTTP-POSTed to /api/v1/auth/login), and an ABSSessionStore adapter
backed by the abs_sessions table from migration 139.
POST /abs/login and POST /login are mounted but not yet reachable
until Stage 7 wires the audiobook service into the main router.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 1 of the audiobookshelf-compatible REST + Socket.io port from
silo-plugin-audiobooks. Lands the package layout, Handler struct with
interface stubs for silo-side dependencies (MediaStore, TokenStore,
ProfileCredentialValidator, ConfigProvider, EventPublisher, Recommender),
and an empty Mount() method. Real route handlers arrive in subsequent stages
(auth, file serving, progress, browse).
Plugin imports of store/backend/streaming/mediatoken/bookref/runtimehost are
replaced with minimal interfaces; real silo implementations come when those
interfaces are needed by specific route handlers.
Also ports filter.go (ABS filter query parsing/matching), access_log.go
(request logging via log/slog), minified.go (minified=1 response shaping),
login_ratelimit.go (per-IP token bucket), jwt.go (JWT mint/parse), and
types.go (shared ABS wire-format types and constants) — all clean of
plugin-specific imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage-based port of silo-plugin-audiobooks' abs/ + abssocket/
packages. Preserves the plugin's file layout but rewrites every SQL
query against silo's existing tables (media_items, media_files,
episodes, user_watch_progress, user_playback_sessions, abs_sessions,
people, item_people) — no new tables. Out-of-scope plugin features
(bookmarks, smart collections, share links, playlists, podcasts/RSS
in this sub-plan) are dropped. 8 stages from support files through
Socket.io, router wiring, and final smoke.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an Audiobooks entry to the sidebar and registers the two new
routes (/audiobooks for the library grid, /audiobooks/book/:id for
detail). The player renders inline inside the detail page; no
dedicated player route is required for MVP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-file audiobook playback for MVP. Multi-file queuing arrives in
a follow-up. Streams via the existing /api/v1/direct-download GET
endpoint. Position is reported to /api/v1/audiobooks/{id}/progress
every 10s while playing plus on pause/seek/end. Skip-30s, playback
rate select, chapter list panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders cover, title, author, narrator, year, and overview alongside a
chapter list. Clicking a chapter opens an inline sticky
AudiobookPlayer at that chapter's start. A "Resume" button restarts
playback at the saved progress position if present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders a paginated grid of audiobook cards using the
useAudiobookLibrary hook. Each card links to /audiobooks/book/{id}.
Cards show poster, title, and year; falls back to a "No cover"
placeholder when the audiobook has no poster_url. Empty state hints
to operators that they need to set a library's type to 'audiobooks'.
Routes themselves are wired in Task 8 (navigation integration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TypeScript types match the JSON shapes from the new
/api/v1/audiobooks endpoints (list, detail, progress). Three hooks:
useAudiobookLibrary (list), useAudiobook (detail), and
useReportAudiobookProgress (mutation that invalidates the detail
query on success so progress updates reflect immediately).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UPSERTs user_watch_progress for the caller's (user_id, profile_id,
content_id). Body carries position_seconds; clients are expected to
post every 5-10s during playback plus on pause/seek (matching silo's
existing video progress cadence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns the media_items row, its media_files (with chapters JSONB),
author/narrator extracted from item_people (kinds 7/8), and the
caller's per-profile listening progress from user_watch_progress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paginated list of media_items with type='audiobook' scoped to the
caller's accessible libraries via the existing access filter.
Mirrors silo's existing list-style handlers for movies and series.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third of six sub-plans. 9 tasks: three REST endpoints (list/detail/
progress), TanStack Query hooks + types, three React pages
(Library/Detail/Player), and navigation integration. Scoped to MVP —
author/series indices, smart collections, share links, and other
nice-to-haves from the spec are deferred. Streaming reuses silo's
existing /api/v1/stream/{session_id}; no new transcode code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ScanPodcastFolder walks a podcasts-typed media folder, treating each
subdirectory as a podcast show and each audio file inside as an
episode. Writes media_items.type='podcast' + episodes rows + media_files
rows. RSS-subscribed feeds (podcast_feeds table) arrive in sub-plan 5;
this task covers filesystem-only ingestion.
ScanFolder dispatches to this path when folder.Type='podcasts'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ScanAudiobookFolder walks an audiobooks-typed media folder and treats
each immediate subdirectory as one audiobook. For each parsed audiobook
it upserts:
- one media_items row with type='audiobook'
- one media_files row per audio file (with chapters JSONB)
- author/narrator links in item_people (kind=7, kind=8)
Adds itemRepo and personRepo to the Scanner struct, wired from
fileRepo.Pool() in NewScanner — no constructor signature change needed.
ScanFolder dispatches to this path when folder.Type='audiobooks',
bypassing the per-file movie/TV pipeline because audiobooks are
folder-scoped entities.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folders containing N audio files (one per chapter/part) get one
parsedAudiobookFile per file; each file's chapter list is synthesized
as a single chapter with title = filename stem. Title/author/series/
year come from the first file's tags.
Also drops the duplicate pickFirstNonEmpty helper added in Task 6 in
favor of the existing firstNonEmpty already in probe.go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
parseAudiobookFolder reads tags + chapters via the existing ProbeFile
(now that Task 5 exposes FormatTags on ProbeData) and produces a
parsedAudiobook struct. Title falls back from "title" tag to "album";
author from "artist" -> "album_artist" -> "composer"; series from
"album" -> "series" -> "mvnm" (Movement Name, used by some MP4 tools).
Year parsed from "date" or "year" tags, tolerating ISO dates and
parenthesized forms.
Single-file case only; multi-file folders (one audio file per chapter)
return a placeholder error and arrive in Task 7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The audiobook scanner needs format-level tags (title, artist, album,
date) for media_items metadata; ffprobe already parses them in
ffprobeFormat.Tags but ProbeData previously discarded them. Add
FormatTags map[string]string to ProbeData, populate it in
convertProbeData via a new normalizeFormatTags helper that lowercases
keys and trims values.
Adds a fixture audiobook .m4b with embedded chapters (Intro/Outro) and
format tags, and a test that verifies ProbeFile() returns both
correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets walkLogicalTree dispatch on multiple library shapes (video, movie,
audiobook, podcast) without proliferating boolean flags. Behavior for
existing video and movie libraries is unchanged; audiobook and podcast
modes will be consumed by the upcoming audiobook.go and podcast.go
parsers in later tasks of this sub-plan.
walkModeFor() derives the mode from a media_folders.type string;
unknown types default to walkModeVideo to preserve prior behavior for
any caller still passing a raw type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
isAudiobookLibraryType and isPodcastLibraryType match singular and
plural forms case-insensitively, mirroring isMovieLibraryType. Used by
upcoming scanner walk branches (Task 4) that filter audio files into
audiobook and podcast libraries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing videoExtensions/SupportsVideoFile pair. Used by
upcoming audiobook and podcast scanner branches to filter directory
walks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovery audit confirmed item_people.kind is unconstrained smallint
with values 1-6 in use. Reserve 7 = Author, 8 = Narrator for audiobook
people-links written by the upcoming scanner branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second of six sub-plans. 10 tasks: PersonKind constants for Author and
Narrator, audio-extension recognizer, library-type helpers, a
walkLogicalTree refactor (movieLibrary bool -> typed walkMode), chapter
extraction via ffprobe, single-file and multi-file audiobook parsers,
scanner write path producing media_items.type='audiobook', and a
filesystem podcast parser (RSS deferred to sub-plan 5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-emptive cleanups flagged by code review before sub-plan 2
copies the patterns:
1. Sort the internal/audiobooks import after internal/adminjob in
cmd/silo/main.go (alphabetical).
2. Drop the redundant "audiobooks: " prefix from the Enabled() error
wrap; matches how every other top-level service package
(watchstate, scanqueue, metadata, etc.) formats errors.
No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>