These two files came in with this branch but were missed by prettier
formatting passes — long single-prop lines were wrapped where they
would have fit on one line under the project's 100-col width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sessions are persisted with an inet column for ip_address; passing
an empty string failed the inet input parser (SQLSTATE 22P02). The
in-process ABS-compat login validates creds without a real *http.Request
to read RemoteAddr from, so the IP is genuinely unknown there. Pass
NULL instead of "" when the caller couldn't determine a client IP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/me returns the minimal {id, username, defaultLibraryId} envelope
emitted by continuum-plugin-audiobooks. ABS clients already have the
rich user object from /login and /authorize; /me is a session-resume
probe in real-ABS, and the previous rich envelope (mediaProgress,
librariesAccessible, permissions) could confuse clients that pattern-
match on the minimal shape.
Library map also trims folders / displayOrder / icon / settings /
createdAt / lastUpdate down to {id, name, mediaType} — real ABS
clients ignore the extra fields and emitting them risks behavior
drift from the plugin reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Documents which envelope tests guard each ABS endpoint's wire shape,
the manual diff procedure for live verification, and the explicit
zero-emit decisions (CoverItem, Color, IsPinned per spec section 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites ABSSmartCollectionStore methods to read/write
user_personal_collections (collection_type='smart'). The rule DSL
goes into query_definition (formerly the abs_smart_collections.query_def
column).
color and is_pinned have no canonical analog; emitted as zero values
on every read. See spec §6 for the deferred decision on those columns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites ABSPlaylistStore methods to read/write user_personal_collections
(collection_type='playlist') and user_personal_collection_items
(sub_item_id replaces episode_id). DeletePlaylist runs in a transaction
since the items table has no FK cascade. Drops the coverArg helper —
the canonical schema has no cover_item column, so CoverItem is always
emitted as zero value per spec §6 deferral.
In-memory abs.Playlist / abs.PlaylistItem struct shape unchanged;
playlistToABS() emitter preserves the wire-shape contract.
Note: user_personal_collection_items PRIMARY KEY is (user_id,
collection_id, media_item_id) — sub_item_id is NOT part of it. The
old abs_playlist_items PK included episode_id, so a playlist that
referenced multiple episodes of the same library item had distinct
rows. Under the canonical PK the second insert silently collides;
prod baseline has zero playlist items so no live data is affected.
Multi-episode-of-same-book inside one playlist would require a schema
change (extend PK / add unique index) and is out of scope here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites ABSCollectionStore's 8 methods to read/write
user_personal_collections (collection_type='manual') instead of the
dropped abs_user_collections / abs_collection_items tables. The
in-memory abs.Collection / abs.CollectionItem struct shape is
unchanged; ToABS() emitter helpers in internal/audiobooks/abs/
preserve the wire-shape contract.
Notable mappings:
- abs.Collection.IsPublic ↔ user_personal_collections.is_shared.
- profile_id is now text NOT NULL (defaults to ''), so we bind the
empty string for primary-profile rows rather than SQL NULL.
- user_personal_collection_items.user_id is NOT NULL with no FK
cascade from the parent — AddCollectionItem uses an INSERT ... SELECT
to copy user_id from the parent collection, and DeleteCollection
explicitly drops items before the parent within a single tx.
- ListCollectionItems filters sub_item_id='' to avoid leaking the
playlist podcast-episode rows that share the items table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds sub_item_id to user_personal_collection_items, widens
collection_type to include 'playlist' and 'smart', adds media_types
filter to page_sections, moves the existing abs_playlists row into
the canonical store, and drops the abs_* collection tables.
Lays the storage foundation for the ABS adapter rewrites (collections
unify sub-project 3) to wire the application against.
Decomposes the unified-audiobook-collections design into 4 independently
shippable plans:
1. Schema migration (156) - foundation
2. Smartcoll engine lift to internal/smartcoll/
3. ABS store adapter rewrites
4. Section recipes media-type parameterization + audiobook recipes
Sub-project 1 blocks the others; 2-4 land in any order.
Captures the decision to fold abs_playlists / abs_user_collections /
abs_smart_collections into the canonical user_personal_collections
with a discriminating collection_type column, lift the smartcoll
engine to internal/smartcoll/, and make page_sections recipes
media-type-parameterized so audiobooks land in homepage rails.
Was: every scan ran DELETE+INSERT on item_people for every audiobook.
Now: compare existing vs desired credit set first; skip the write when
identical. Compares case-insensitively to absorb tag casing drift.
Was: serial loop over claimed batch of 50.
Now: N workers fan out over the batch (default 4, configurable via
SILO_AUDIOBOOK_ENRICH_WORKERS).
Adds a runBatch seam used by Run so tests can stub the per-item
enrich function and assert worker fan-out without hitting providers.
Hooks the audiobook enricher into the existing lazy image-cacher
install block so the deferred local-cover fallback has the bits it
needs when S3Public is configured.
Drops imageCacher + ffmpegPath from NewEnricher and exposes setters
to mirror MetadataService / Scanner. Lets main.go install them in the
same lazy image-cacher block as the other consumers.
Was: fallback only fired on the success path, leaving the
no-providers and no-metadata branches with a stamped last_refreshed
and no chance to extract the embedded cover.
Now: defer at the top so every return path runs the fallback. The
fallback is idempotent — no-ops when poster_path is already set.
Replaces the inline cover extraction removed from the scan path.
Pulls the embedded cover from the primary audio file via ffmpeg when
the provider chain didn't supply a poster_path.
Transient DB errors from the skip-check fell through silently to the
full reconcile path. The fall-through is still correct, but operators
need visibility when it happens - a 240K-folder rescan that quietly
loses the fast path is a 12h elapsed_sec, not 12s.
Bypasses ffprobe + DB writes for folders whose on-disk audio files
match existing media_files rows by size and mtime. Stable libraries
now rescan in minutes instead of hours.
Introduces audiobookDiskFile + audiobookFolderUnchanged to detect when a
folder's on-disk audio files match existing media_files rows by path,
size, and mtime. Sets up the upcoming scan-loop skip path that bypasses
ffprobe + DB writes for stable folders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
{"subject":"fix(jellycompat): stabilize signed image tags across restarts","body":"- Sign library poster and episode parent series image tags from canonical paths/thumbhashes instead of presigned URLs so tags survive restarts\n- Accept signed canonical tags in the image handler without a session and fall back to legacy URL-derived cache tags\n- Always fetch series detail for episodes to build stable parent image tags"}
- Add "Now Viewing" badge with pulsing indicator on current episode
- Replace border with primary-color ring for current episode card
- Set aria-current="page" on links to the current episode
- Add `floating` prop to pin PageBack to viewport on lg+ screens
- Switch styling from glass-subtle to glass with shadow for better contrast
- Use floating variant on SettingsLayout
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>
Replace the eight inconsistent back affordances across user-facing pages
with a single absolute-positioned chevron pill, so the control lives in
the same screen position regardless of title length or hero content.
DetailBreadcrumb keeps its textual hierarchy path but no longer owns the
back chevron; PageBack does. DetailHero gains a topNav slot consumed by
Movie/Series/Season/Episode/Request detail pages. Non-hero pages drop
their bespoke back buttons and add PageBack inside a relative wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Score candidate metadata completeness and auto-match the richer duplicate when title/year/type tie
- Enrich near-duplicate candidates via the provider chain before initial match selection
- Seed both movie and series match queues for mixed-type libraries and wait for TV queue settle
- Add taskmanager worker test coverage and a plan doc for the tie-breaker work
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>