Mounts the Audiobookshelf-compatible API on its own http.Server so
discovery probes (/ping, /healthcheck, /status, /login, /socket.io)
own the URL space at the root without colliding with silo's SPA
fallback. Mirrors the Jellyfin compat pattern at :8096.
Also adds the ABSRecommender adapter, wired into ABSHandlerDeps via
recommendations.NewRepo + catalog.DetailService. The recommender file
was previously untracked; service.go has already been referencing
ABSRecommender, so HEAD did not build without it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Detail page:
- collapse Chapters section behind header toggle (73-chapter pages no
longer push content below the fold)
- square cover frames throughout (audiobook covers are Audible-style 1:1,
not 2:3 book portrait)
- narrator picker dropdown when multiple narrations of the same book exist
- clickable genre badges (route to /audiobooks?genre=X)
- reordered so credits/rails sit above the chapter list
- Play-from-Start button forces remount via playToken counter (was a
no-op when player was already at position 0)
- regression test for the Play-from-Start fix
Mini bar / Now Listening:
- mini bar respects --app-sidebar-offset so it stops getting covered by
the desktop sidebar
- Now Listening adds overflow scroll + a labeled "Back to player" button
so controls are never inaccessible on short viewports
Library page:
- infinite scroll (replaces Previous/Next pagination)
- genre filter chip with X-to-clear
Scanner:
- 8-worker parallel reconcile (env SILO_AUDIOBOOK_SCAN_WORKERS to override)
- file-path-first dedup so cleaned titles don't collapse separate
narrations
- title cleanup at write time (strips "Read by X" / "(unabridged)"
suffixes; original tag preserved in original_title)
- audiobook_series upsert from tag-derived series_name/series_position
- secondary dedup check (author + narrator + year + duration ±0.5% +
title-prefix) so two folders of the same book attach to one row
Backend detail handler:
- new fetchAlsoByAuthor, fetchInSeries (with series row > 1 entry guard),
fetchSimilar (embedding-first with shared-genre fallback),
fetchOtherNarrations (regex-strips narrator suffix to group siblings)
- audiobookDetailResponse gained also_by_author, in_series,
similar_audiobooks, other_narrations fields
- list endpoint accepts a genre query param
Embeddings:
- BuildEmbeddingText branches on item.Type == "audiobook" to use
author/narrator credits instead of cast/director/writer
- mediaTypeLabel helper centralizes movie / "TV series" / audiobook
- ListEmbeddingTextCandidates SQL mirrors the Go branching exactly
- ItemsNeedingEmbedding and TotalMediaItemCount loosen status='matched'
gate to also include audiobooks (which don't go through TMDB match)
- FindSimilar gains a mediaType filter so cross-type results never appear
- callers in similar.go and personal.go pass the source item's type
Collections:
- MediaAudiobook MediaKind + audiobook(s) case in templateEligibleForLibrary
(stops offering broken movie/TV templates to audiobook libraries)
- useAddItemToCollection hook (user + admin/library endpoints)
- AddToCollectionDialog wired into audiobook detail and movie/series
ActionBar overflow menu
- ManualCollectionItemsEditor gained a search-and-add panel with
debounced live results
- QueryDefinition.media_scope, QuerySortRelevanceScope, ALL_MEDIA_SCOPES
extended to include "audiobook"
- CatalogFilterBar gained an Audiobooks media scope option
- parseCatalogMediaScope (backend) accepts "audiobook"
Migrations:
- 145_audiobook_series: per-book series_name/series_index with a
best-effort title-pattern backfill for the existing corpus
- 146_audiobook_title_cleanup: strips narrator suffix / (unabridged)
noise from existing titles, preserving raw in original_title
Scripts:
- scripts/dedup_audiobooks.py: one-shot merge for "Title" vs
"Title: Subtitle" duplicates, file-path-stable, dry-run by default
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- GetExternalIDs now uses the dedicated /movie/{id}/external_ids and
/tv/{id}/external_ids endpoints instead of fetching the full detail
with append_to_response=external_ids. The dedicated payload is
one or two orders of magnitude smaller for the same fields.
- Document PosterPath/BackdropPath on MediaResult as raw TMDB path
fragments that callers must prefix with the image base URL.
- normalizeCast switches from inline insertion sort to sort.SliceStable.
The output is identical; the new form is one line and O(n log n).
- normalizeIntegration no longer reuses integration.Tags' backing
array via Tags[:0]; the slice is callable code, so reusing the
array would silently corrupt the caller's slice if it kept a
reference. Allocate a fresh slice instead.
- HandleGet now requires a profile, matching the rest of the
/requests user-group handlers. Router middleware enforces this
already, but the inline check is defense-in-depth for any future
remount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The radarr and sonarr clients carried byte-identical copies of
rootFolderResource, qualityProfileResource, tagResource (and the
corresponding list helpers) plus acceptedWithoutResponse and
statusFromQueueEvaluation. Move the shared wire types and helpers
into the arrclient package and update the callers to use the
exported helpers. No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owners can now POST /requests/{id}/cancel to withdraw a pending
request; admins can cancel any active request that has not entered
the fulfillment pipeline. The route is mounted on both the user
group (with profile required) and the admin group. The cancelled
outcome was already reserved in the migration's CHECK constraint
but was unreachable from any handler.
Decline now also rejects approved requests — between Approve setting
StatusApproved and the reconciler picking the request up, an admin
could declare the request declined while submission was about to
fire. The reconciler's outcome filter would skip the request, but
the narrow window meant external state could diverge from Silo's
view. Refuse decline once a request is approved; callers should
wait for completion or use the failed/retry path.
Reconcile now emits a slog.WarnContext at the per-request failure
site with request id, media type, tmdb id, status, and integration
kind. Aggregated counters in ReconcileResult are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Batch integration upserts in a single transaction
- Treat radarr/sonarr lookup results as arrays and require exact matches
- Prefer queue failures over downloading state when evaluating arr queues
- Allow retrying queued/downloading requests and block declines once fulfillment started
- Fall back to pending when auto-approval integration check fails
- Rename requests query hooks file and fix discover card request affordance
Wire curated TMDB-backed studios/networks/genres discovery into the requests service and UI, replacing on-demand logo fetches with fixed duotone logos and adding browse routes plus tests.
Co-authored-by: Cursor <cursoragent@cursor.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>
- Add GetMediaDetail TMDB client returning normalized detail with cast, crew, recommendations, and certifications
- Add /api/requests/detail/{media_type}/{tmdb_id} endpoint overlaying availability and request state
- Add RequestDetail page and link poster cards to it
- Treat empty/truncated Radarr/Sonarr POST responses as accepted; drop pre-submit existence lookups
- Add request domain, repository, service, and reconcile task
- Add Radarr/Sonarr fulfillment adapters and TMDB discovery
- Expose user and admin request APIs with quota and approval rules
- Add web UI for browsing, requesting, and admin queue management
- Migration 139 introduces media_requests and related tables
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>
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>
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>
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>
- 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
- 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
- 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
- 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
- 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
- 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