Comments referencing the now-dropped abs_user_collections,
abs_collection_items, abs_smart_collections, abs_playlists, and
abs_playlist_items tables. Code paths were updated in the prior
commits; only doc strings remained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the endpoints AudioBooth + the official audiobookshelf-app
expect but Silo's ABS-compat layer was missing, plus fixes the recently
shipped playlists/collections/series surfaces that were crashing the
mobile clients.
Browse surfaces (crash fixes):
- Hydrate playlist items with full LibraryItem (mediaType, media,
cover) so PlaylistCover + LazyBookCard stop throwing on undefined
fields.
- Add GET /libraries/{id}/collections paged envelope; hydrate
collection books with full LibraryItem so the cover stack renders.
- Add GET /libraries/{id}/playlists total field so LazyBookshelf
paginates correctly.
- Return real numUserPlaylists count from /libraries/{id} so the
bottom-nav Playlists tab actually appears.
- Extend ListLibrarySeries with up to 4 book previews per series
(window-ranked SQL) so LazySeriesCard renders cover stacks
instead of name-only placeholders.
- Auto-delete a playlist when its last item is removed; fire
playlist_removed so the client routes the user back to the list.
New mobile-blocking endpoints:
- PATCH alias on /me/progress/{id} (AudioBooth's progress writes).
- DELETE /me/progress/{id} backed by new DeleteProgress store method.
- GET /me/stats/year/{year} — year-in-review synthesised from
AggregateStats.
- GET /ping, /healthcheck, /init, /auth-settings — unauthenticated
server discovery on every prefix.
Ebook surface (forward-compat stubs):
- LibraryItemMedia.EbookFile omitempty field + EbookFile type.
- progressBody accepts ebookProgress + ebookLocation (silently
ignored until ebook scanner lands).
- GET /items/{id}/ebook/{fileid} 404 stub, PATCH .../status accept.
- GET /me/ereader-devices empty list, POST
/emails/send-ebook-to-device 503 with clear "not configured".
Podcast stubs (audiobook-only catalog v1):
- POST /podcasts/feed, POST /items/{id}/play/{episodeId},
PATCH /me/progress/{id}/{episodeId}, GET
/libraries/{id}/recent-episodes, GET /search/podcast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ABS mobile create-playlist modal loads existing playlists
BEFORE opening the form, hitting:
GET /api/libraries/{libraryId}/playlists
and reading `data.results` (NOT `data.playlists`). It then iterates
each playlist's `items[]` to render "already in this playlist"
badges. silo's existing `/api/playlists` route emitted the wrong
envelope (`{playlists: [...]}`) and was at the wrong path; the
modal silently treated the response as empty, the user saw an
empty existing-playlist list, and depending on the device-side
UX (no "Create New" CTA when results was undefined) the create
action looked dead.
Add handleListLibraryPlaylists at `/libraries/{libraryId}/playlists`,
emit `{results: [Playlist full-shape]}` so playlist.items is
populated for the membership check. libraryId param accepted but
ignored — silo scopes playlists per (user, profile) globally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires RSSFeedStore into BuildABSHandler. Mounts the ten new
authenticated routes (stats x3 + author x1 + series x1 + continue x2
+ feeds-auth x3) under both /abs/api and /api inside bearerAuth.
Mounts the three public RSS routes (.xml + slug + /file/{ino}) in
the unauth public block. handleAuthorImage wired to CoverResolver.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/feeds/item/{itemId}/open (auto-gen or custom slug),
GET /api/feeds (owner+profile scope, only open feeds),
POST /api/feeds/{id}/close (idempotent, owner-gated).
Slug validation via ^[a-z0-9-]{4,64}$. 409 on collision via pgx
unique-violation substring detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GET /authors/{id} (people.id, kind=7 join over item_people) and
GET /series/{id} (case-insensitive series_name match, ordered by
series_index NULLS LAST). Both return entity + embedded books[].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pgx-backed store + service wiring + six routes registered under both
/abs/api and /api prefixes inside the existing bearerAuth group.
JSONB column written via $9::jsonb cast for query_def.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First handler of the smart collections surface. query_def is
normalized + validated (with allowPersonalized=true) before
marshalling to JSONB bytes for storage. Adds memSmartCollectionStore
harness.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the pgx-backed PlaylistStore impl, wires it into BuildABSHandler
when a Pool is present, and registers all ten playlists routes under
both /abs/api and /api prefixes inside the existing bearerAuth group.
AddPlaylistItem computes position = MAX+1 inside the INSERT (one
round-trip, no read-before-write race); both add and remove run in
transactions so the parent's updated_at bump is atomic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First handler of the playlists surface. Body {name, description?,
cover_item?, isPublic?} returns the created playlist in full-shape
(empty items[]). Fires playlist_added realtime event. Adds the
in-memory test harness (memPlaylistStore) parallel to
memCollectionStore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the pgx-backed CollectionStore impl (parallel to
abs_bookmark_store.go), wires it into BuildABSHandler when a Pool is
present, and registers the seven collections routes under both
/abs/api and /api prefixes inside the existing bearerAuth group.
AddCollectionItem/RemoveCollectionItem run in a transaction so the
parent's updated_at bump is atomic with the item mutation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First handler of the collections surface. Body {name, description?,
isPublic?} returns the created collection in full-shape (empty
books[]). Backed by the new CollectionStore dependency (nil-safe:
handler returns 503 when unwired). Adds the in-memory test harness
(memCollectionStore + dispatchABSWithParams) that the rest of the
collections suite will reuse.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Registers POST/PATCH/DELETE /me/item/{itemId}/bookmark inside the
existing bearerAuth group so real ABS clients hitting either prefix
resolve to the same handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First of three ABS bookmark endpoints. Body { title, time } upserts on
(user, profile, item, time); response is the item's full bookmark
list. Backed by a new BookmarkStore dependency (nil-safe: handler
returns 503 when unwired). Item validation via MediaStore;
realtime user_updated event with reason=bookmark_created on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: the official ABS Android client (PlaybackSession.kt:194-200)
on ABS server v2.22.0+ with DirectPlay builds the streaming URL as
"$serverAddress/public/session/$id/track/$index" WITHOUT a token,
ignoring audioTrack.contentUrl entirely. Silo reports version 2.35.0
and emits playMethod: 0 (DIRECTPLAY) but never mounted this route, so
every play attempt 404'd silently — spinner forever.
Add handlePublicTrack: look up the session by sid (ULID as capability,
matches booklore-ng + continuum-plugin behavior), resolve the track by
1-based index against the session's media files, stream via
playback.ServeDirectPlay (Range + HEAD supported). Mounted OUTSIDE
bearerAuth at both /public/session/... and /abs/public/session/... .
6 unit tests cover serve / HEAD probe / unknown session / closed
session / out-of-range / bad-index paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logout was sitting inside the bearerAuth group, so a client whose access
token had expired — the primary "I want to sign out" UX moment —
received 401 instead of being able to revoke. handleLogout now parses
the bearer locally and ALWAYS returns 204, mirroring continuum-plugin
canonical behavior. Mounted at /logout, /api/logout, /abs/api/logout,
and the legacy /abs/api/auth/logout path; signature is still verified
so an attacker can't revoke a victim JTI by forging the token shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously deps.Items / deps.Files were conditionally constructed into a
MediaStore, leaving it nil when either was missing. Most ABS handlers
deref the store unconditionally on the request hot path, so a
misconfigured deployment would pass /login then panic on the next request.
Two scattered nil-guards (buildFilterData, loginEnvelope) masked the
problem without fixing it.
- BuildABSHandler panics at startup if Items/Files are missing.
- abs.New panics if MediaStore is nil.
- Remove the two nil-guards (production now always has a store).
- noopMediaStore test fake for handlers that don't exercise catalog reads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mounted inside bearerAuth so the JTI is already validated. Revokes the
access JTI in abs_sessions and returns 204. Idempotent: re-calling on an
already-revoked JTI still returns 204. Refresh JTI is intentionally NOT
revoked here — clients that want hard sign-out-everywhere will use the
sessions endpoint added in Phase 3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile clients call refresh every ~22h to avoid the 24h access-token
re-login trap. Accepts the token via x-refresh-token header (real ABS
convention) or {refreshToken} body (legacy). Mints a fresh pair, persists
both new JTIs, then revokes the old refresh JTI atomically — if any step
in 3-4 fails, the old refresh stays valid and the client can retry.
Returns the user{accessToken, refreshToken} object AND top-level token
fields so mainline and 3rd-party clients both find their expected shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the pre-existing cred-validator error log into line with the new
keys added in cd4f629 (all use "err"). Adds "path" to the jwt-secret
fetch error log so it matches its sibling rejection logs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each rejection branch in bearerAuth now emits a slog line so failures
are traceable from journalctl. Login success path emits a debug line
confirming token persistence; this makes "I cant login" debuggable
without a tcpdump.
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>
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>