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>
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>
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>
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>
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)
- reload introdb API key on setting updates
- support recap/preview markers in playback and next-episode flow
- add profile defaults for recap and preview auto-play settings
- Rename actor-oriented API, storage, and webhook fields to user/profile terminology
- Switch dev compose helpers to use docker-compose.yml
- Update frontend types and webhook sync settings for the new endpoints
- Wire introdb marker fetching into playback and Jellyfin compat
- Persist and expose recap/preview markers alongside intro and credits
- Add new playback/profile settings for recap and preview behavior
- Add admin job support for collection defaults applies
- Keep preview synchronous while real applies run via the job queue
- Show collection apply job status in the admin UI
- Store bundled template posters in public S3 when available
- Presign imported user collection posters in API responses
- Thread frontend assets into router and collection handlers
Agent F's code review surfaced a critical gap: every Phase 1-3 template
ships a non-zero `DefaultSortOrder` in the documented PMM-style bands
(1000s charts, 2000s best-of-year, 3000s awards, 4000s streaming,
5000s popular by genre, 6000s top rated by genre, 7000s franchises,
8000s seasonal, 9000s misc/kids) and the test suite pins those values,
but none of the four `createCollectionFromTemplate` dispatch paths
forwarded the rank into `catalog.CreateLibraryCollectionInput.SortOrder`.
Applying any template — bundled or one-off — landed the new collection at
SortOrder 0, defeating the band scheme entirely.
Fix: add `SortOrder int` to each request struct
(`importMDBListRequest`, `importTMDBRequest`, `importTMDBFranchiseRequest`,
`importTMDBDiscoverRequest`) and forward `tmpl.DefaultSortOrder` →
`req.SortOrder` → `CreateLibraryCollectionInput.SortOrder` end-to-end.
The pre-existing `SourceTrakt` path is unchanged — no Trakt template
currently sets a non-zero `DefaultSortOrder` so the gap is dormant
there; revisit if/when a Trakt template needs ordering.
No new unit test: the fix is a per-struct field forward whose
end-to-end exercise requires the live repository. Adding a fake repo
to test the propagation would be a refactor beyond this fix's scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase-3 of the collection-templates expansion. Adds ten curated movie
franchises plus a generic placeholder template, all backed by Agent B's
tmdb_collection source. Templates land in the 7000s sort-order band.
Verified TMDB collection IDs (via api.themoviedb.org/3/collection/{id}):
- 10 Star Wars Collection (9 films)
- 645 James Bond Collection (25 films)
- 1241 Harry Potter Collection (8 films) — used for "Wizarding World"
- 9485 The Fast and the Furious Collection (12 films)
- 119 The Lord of the Rings Collection (3 films)
- 121938 The Hobbit Collection (3 films)
- 328 Jurassic Park Collection (7 films)
- 295 Pirates of the Caribbean Collection (5 films)
- 87359 Mission: Impossible Collection (8 films)
- 535313 Godzilla Collection (3 films) — used for "MonsterVerse"
Plan corrections / skips:
- MCU (plan ID 86311) resolves to "The Avengers Collection" (6 films),
not the full Marvel Cinematic Universe. TMDB has no umbrella MCU
collection. Skipped per the no-invent-IDs rule. Sort slot 7001 left
open.
- DC Extended Universe (plan ID 573436) resolves to "Spider-Man:
Spider-Verse Collection" — unrelated. No DCEU umbrella exists on
TMDB. Skipped. Sort slot 7005 left open.
- Wizarding World (plan ID 1241) is TMDB's Harry Potter octalogy;
Fantastic Beasts lives in a separate collection (435259). Used as-is
with description noting the scope.
- MonsterVerse (plan ID 535313) is labelled "Godzilla Collection" but
contains the three Legendary MonsterVerse Godzilla films, which is
the closest TMDB representation. Used as-is.
The placeholder template ships with CollectionID=0 and an empty sync
schedule so it can be applied without a valid franchise ID; admins edit
the resulting collection's source_config before the first sync.
Append the Phase-2 catalog from the collection-templates expansion plan:
- 18 Popular by Genre (sort_order 5001..5018, daily 0 4 * * *,
popularity.desc with vote_count_gte=300)
- 18 Top Rated by Genre (sort_order 6001..6018, weekly 0 5 * * 0,
vote_average.desc with vote_count_gte=1000)
- 1 Kids Movies (sort_order 9050, weekly, Family genre with
certification_lte=PG)
Adds TestPhase2DiscoverTemplatesUseExpectedBands to pin the band ranges
and the TMDB query parameters that define each section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>