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>
Empty-but-compiling Service that reads the audiobooks.enabled feature
flag from server_settings. Wired into cmd/silo so the package is
referenced from the binary; no routes mounted, no scheduled tasks
registered, no DB writes. Subsequent sub-plans hang scanner branches,
ABS handlers, Socket.io, podcast refresher, and SPA pages off this
Service.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-settings row that gates the absorbed audiobooks feature.
Defaults to 'false' so sub-plan 1 lands as a strict no-op; subsequent
sub-plans branch on this flag and operators flip it to 'true' at
cutover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sub-plan 1 originally reserved migration 141 to add a 'kind' column to
media_libraries discriminating audiobook/podcast libraries. Discovery
audit (sub-plan 1 Task 1) found that the actual table is media_folders
and it already has a type text NOT NULL column with no CHECK constraint
or enum, so 'audiobooks' and 'podcasts' can be added as future values
without DDL.
Landing this migration as a documented no-op preserves the version
numbering audit trail and pins the decision in git history. The
matching down migration is also a no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the codebase convention (type keywords lowercase,
constraint keywords uppercase) established in migration 139's
post-style-fix form. Cosmetic only — running schema is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Side table on media_items for RSS-subscribed podcasts. Holds feed URL,
ETag/Last-Modified for conditional fetches, last-refresh timestamp, and
the per-feed refresh interval consumed by the upcoming
podcastfeed.Refresher scheduled task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lowercases type keywords in the abs_sessions CREATE TABLE body to
match neighboring migrations, fixes the client_version column
alignment, and replaces the misleading "parallel to
jellycompat_sessions" header comment with a more accurate
description of the table's role.
Cosmetic only — the running schema is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parallel of jellycompat_sessions for Audiobookshelf-compatible clients.
Lets ABS mobile/desktop apps maintain a device-bound session that
silo's audiobooks/abs handlers will validate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks schema/code decisions for migrations 139-142 and downstream
sub-plans. Resolves open Risk questions from the absorption design spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First of six sub-plans for the absorption. Six tasks: a discovery
audit that resolves the spec's Risk questions, four idempotent SQL
migrations (abs_sessions, podcast_feeds, media_libraries.kind,
audiobooks.enabled feature flag), and an empty-but-compiling
internal/audiobooks package scaffolded into cmd/silo. Lands as a
strict no-op for users (feature flag defaults to false).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan to absorb silo-plugin-audiobooks into silo-server as a first-party
feature. Audiobooks land in silo's existing SPA; ABS clients connect
directly. Hard constraints: reuse existing tables (media_items,
media_files, user_watch_progress, user_playback_sessions, people,
item_people, library_collections); only two new tables (abs_sessions,
podcast_feeds) and at most one column add (media_libraries.kind);
silo's main :8080 listener handles ABS Socket.io natively. Out of
scope: audiobook requests flow, smart collections, share links,
external recommender, custom metadata providers, separate audiobook
SPA.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Firefox (and some download managers) issue a HEAD request before
starting a download. The route only registered GET, so HEAD returned
405 Method Not Allowed and the browser aborted the download.
Mirrors the pattern already used by /stream/{session_id}, which
registers both GET and HEAD on the same handler. ServeDirect is built
on http.ServeContent / ServeFile, which natively handle HEAD by
writing headers without a body, so no handler changes are needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Map number words and ordinals (e.g. "Two"/"2nd") to digit tokens in both SQL normalize_search_text and the Go mirror so "Dune: Part Two" and "Dune Part 2" match
- Rebuild title_normalized generated column and title FTS GIN indexes (migration 138)