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>
Audit of the ABS Android Kotlin data classes against silo's emitted
JSON found six non-nullable LibraryItem fields silo wasn't emitting
at all (ino, path, relPath, mtimeMs, ctimeMs, birthtimeMs) plus a
non-nullable BookMetadata.explicit. jackson-module-kotlin's behaviour
around missing required parameters varies by config — some flows are
lenient enough that the library list loads with the omissions, but
the strict download path (apiHandler.getLibraryItemWithProgress)
hits a MissingKotlinParameterException and silently resolves cb(null)
which kills the AbsDownloader without a user-visible toast.
Emit safe defaults: ino = ContentID (stable item-level identifier
matching the real-ABS shape), path/relPath = "", mtime/ctime/
birthtimeMs = AddedAt, explicit = false. Costs almost nothing on the
wire and forecloses future Jackson-strictness surprises.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ABS Android client's Kotlin Book model declares
var tags: List<String> // non-nullable
and Jackson throws MissingKotlinParameterException when the field is
absent from /api/items/{id} responses. ApiHandler.getLibraryItem
WithProgress catches the exception and resolves cb(null), at which
point AbsDownloader silently aborts — the user sees no error, the
download just never starts.
Add Tags to LibraryItemMedia and initialise it to []string{} at
construction. Verified end-to-end: download URL the Android client
builds (/api/items/{id}/file/{ino}/download) returns HTTP 206 with
audio/mpeg bytes; the parent /api/items/{id} response now carries
"tags": [] so Jackson deserialises cleanly.
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>
- loginEnvelope now takes the caller's time.Now() rather than computing
its own. completeLogin and handleABSAuthorize each pass the same
instant they use elsewhere, so the user.lastSeen/createdAt timestamps
share a single moment with the token ExpiresAt the caller persisted —
no more two-call drift in the same response.
- buildFilterData: add a comment explaining why the parallel author/series
blocks aren't extracted into a helper (different types, different store
methods — a generic version costs more LoC than it saves).
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>
Final review follow-up for ABS Phase 0. Five fixes:
1. loginEnvelope and handlePersonalized now nil-guard h.deps.MediaStore
so a partially-wired deployment doesnt panic on /login or /personalized.
2. handleLibraryAuthors and handleLibrarySeries respect ABS limit=0
("return all") instead of returning an empty slice.
3. buildSiloAudioTracks now sets MetaTags to map[string]string{} so the
"spinner forever" failure mode types.go warns about cannot bite the
play-session response (item-detail path already sets it; play path
silently omitted the key).
4. slog key normalised from "error" to "err" in the play-session
persist-failure log so log parsers find it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cap shadowed the Go builtin. fetchCap matches the naming used elsewhere
in the file for the same kind of over-fetch ceiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleLibraryDetail now populates filterdata.authors and filterdata.series
from MediaStore so the iOS filter sheet has real options. Narrators,
genres, publishers, languages, tags stay empty arrays for now (Phase 1
will index those aggregations); empty arrays are gracefully handled by
the client.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review follow-up to c0e9229. handleLibraryAuthors and handleLibrarySeries
fetched the page slice and reported len(results) as total, hiding the
next-page button for libraries with > 50 authors / > 25 series. Both
now over-fetch (cap 5000) and paginate locally so total is the real DB
row count. Also adds the "tags" key (and confirms "genres") on the play
session mediaMetadata map so strict 3rd-party clients dont crash on
undefined; this completes the empty-array guarantee Task 4 began on the
browse/detail surface. Strengthens the series slug test to pin actual
slugify output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3rd-party ABS clients (Plappa, AudioBookShelfFully) require id on every
authors[] and series[] entry to encode filter selections; missing IDs
made author/series chips dead-end. Also ensures genres and tags are
always non-nil arrays so clients that branch on .length don't crash.
Tags is empty for now (silo has no item-tag concept); shape is stable so
future tag work won't break clients.
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>