154 adds hide_from_continue to user_watch_progress (backs the
ABS remove/readd-to-continue-listening endpoints). 155 creates
abs_rss_feeds for the upcoming RSS surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combined spec for the four remaining Phase 1 surfaces. Migrations 154
(hide_from_continue column on user_watch_progress) and 155
(abs_rss_feeds). RSS scoped to item-feed minimum; series/collection
feed variants and RSS cover/track endpoints deferred to Phase 2.
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>
Items handler evaluates the stored query_def against the audiobook
catalog. Per-user state hydrated in 2 batched calls (progress list +
bookmark counts) when caller is the owner; non-owner viewing public
sees personalized rules silently dropped. Results paginated post-eval.
siloItemToSmartcollItem adapter maps silo's MediaItem onto the
audiobook-domain Item shape; author/narrator/series/publisher/
duration_seconds left as zero-values for v1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four CRUD handlers + tests. Anti-enumeration 404 on non-owner
private. List envelope is {"items": [...]}. PATCH re-validates
query_def when present.
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>
Defines the storage contract and wire-shape serialiser. queryDef is
emitted as a nested JSON object on the wire (decoded from the JSONB
bytes once at serialisation time).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In-memory rule evaluator over Candidate{Item, IsFinished, ProgressPct,
CurrentSeconds, LastPlayedAt, BookmarkCount, PlayCount}. Covers all
15 fields + 7 operators + 9 sort keys including deterministic
seeded random. Personalized rules drop to false (silent) when
opts.AllowPersonalized is false. Decoupled from silo's catalog
model via the local Item struct — handler adapts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 153 backs the upcoming smart-collections surface.
BookmarkStore.CountByUser returns per-item counts in one SQL pass —
used by the smart-collection items evaluator to hydrate the
bookmark_count personalized rule without N+1 queries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 TDD-ordered tasks. DSL package ported from continuum with audiobook
catalog. CRUD handlers + items eval with batched per-user state
hydration. Author/narrator/series/duration hydration deferred to a
follow-up — rules referencing those fields evaluate false in v1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final review flagged two small gaps in the delete tests:
- TestCollection_Delete_Owner_204 now seeds a book and asserts the
items table is empty after delete (proves FK CASCADE works).
- TestPlaylist_Delete_Owner_FiresRemovedEvent now also asserts the
post-delete GET returns 404 (symmetry with the collection test).
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>
POST /playlists/{id}/batch/add and /batch/remove accept arrays of
{libraryItemId, episodeId?} tuples. Per-item failures are tolerated
silently (matching continuum); only a whole-body decode error
surfaces as 400. One playlist_updated event fires for the whole
batch regardless of per-item outcomes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two route variants share the same body via removePlaylistItemImpl:
the bare libraryItemId form removes the item with empty episode_id;
the libraryItemId+episodeId form removes only that episode-keyed
entry, leaving other entries with the same libraryItemId intact.
Idempotent; fires playlist_updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Append a new (libraryItemId, episodeId) tuple to the end of the
playlist (positions start at 1 and increment). Audiobook items
validated against MediaStore (404 on unknown); episode items
accept-and-echo per spec §7.1 (podcast hydration is a future
sub-project). Idempotent on the tuple. Fires playlist_updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner-gated mutation with partial-body PATCH semantics. Non-owner gets
404 (anti-enumeration). Both handlers fire realtime events
(playlist_updated, playlist_removed) — clients re-render from the
response (the event payloads carry only the id).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
List wraps the result in {"playlists": [...]} and emits list-shape
(no items[]). Detail handler returns full-shape for owner or for any
caller when isPublic=true; otherwise 404 matching the bookmarks
anti-enumeration pattern.
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>
Defines the storage contract and wire-shape serialiser the playlists
handlers will consume. Envelope test asserts the eight (or nine with
coverPath) top-level keys including description always round-tripped
correctly and coverPath omitted when empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrations 151 + 152 back the upcoming ABS playlist endpoints.
Schema rationale documented in
docs/superpowers/specs/2026-05-26-abs-collections-playlists-design.md
§5.3, §5.4, §5.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace string-compare on "no rows in result set" with the canonical
errors.Is(err, pgx.ErrNoRows) — matches every other store in
internal/audiobooks/ (abs_session_store, abs_playback_session_store,
abs_progress_store) and is robust against pgx error-message changes.
Also wrap rows.Err() returns with the contextual fmt.Errorf prefix
that the bookmark store uses.
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>
POST /collections/{id}/book/{bookId} validates the item against
MediaStore (404 on unknown) and is idempotent on the store side. The
DELETE variant is unconditional idempotent (returns the current
membership state regardless of whether the row existed). Both 404
when non-owner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner-gated mutation with partial-body PATCH semantics (only fields
present in the body are updated). Non-owner attempts return 404
matching the bookmarks anti-enumeration pattern. DELETE cascades to
abs_collection_items via FK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner sees their own collection in full-shape (with books[]).
Non-owner sees it only when isPublic=true; otherwise 404 with the same
body as a genuine not-found (anti-enumeration pattern from the
bookmarks sub-project).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the result in {"collections": [...]} matching continuum/real-ABS
clients. Owner-scope only (other users' collections never leaked).
Profile-scoped (collections under a different profile excluded).
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>
Defines the storage contract and wire-shape serialiser the collections
handlers will consume. Envelope test asserts the seven required keys
including description (which the continuum reference always emits as
empty regardless of stored value — this round-trips it correctly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrations 149 + 150 back the upcoming ABS collection endpoints.
Schema rationale documented in
docs/superpowers/specs/2026-05-26-abs-collections-playlists-design.md
§5.1, §5.2, §5.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 TDD-ordered tasks covering the four migrations (149-152), two
store interfaces with in-memory fakes, seven collection handlers +
ten playlist handlers, pgx-backed concrete stores, service wiring,
route registration, and the full-verification gauntlet. Total ~3000
lines; each task is bite-sized (2-5 minute steps) and produces a
green commit. Each spec §8 test maps to a task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Approved design for the second sub-project of Phase 1: manual user
collections (named groupings of audiobooks) and ordered playlists
(queue with cover image). Both profile-scoped, with cross-user
public visibility. Includes migrations 149-152, two REST surfaces
sharing a uniform CRUD shape, in-memory test fakes parallel to the
bookmark sub-project's harness, and the same anti-enumeration 404
security pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plan that drove the now-completed bookmarks sub-project. Belongs
on the branch alongside its sibling spec (committed in 73b9aca) per
the project convention of checking in both spec and plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop dead pgx.ErrNoRows guard in ABSBookmarkStore.Delete (Pool.Exec
never returns it; the prior guard was misleading dead code).
- Reject ±Infinity in parseBookmarkTime so a DELETE /…/bookmark/Inf
URL path is consistent with the POST/PATCH body path (which JSON
itself excludes — JSON has no Infinity literal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When deps.Pool is non-nil, construct the pgx-backed store and pass
it through to the ABS handler. Mirrors the other store wirings in
BuildABSHandler; when no pool is available (tests, minimal fixtures),
BookmarkStore stays nil and the handlers respond 503.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements abs.BookmarkStore against abs_bookmarks (migration 148).
COALESCE-to-sentinel-UUID matches the table's unique index for
profile NULL collapsing. Upsert is one round-trip via INSERT ... ON
CONFLICT ... DO UPDATE RETURNING.
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>
Covers the three reason discriminators (bookmark_created /
bookmark_updated / bookmark_deleted) documented in
docs/superpowers/specs/2026-05-26-abs-bookmarks-design.md §4. Asserts
event count, scope (event userID), event name, and payload shape.
DELETE event uses the pre-delete snapshot so clients keep the title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Idempotent: returns 200 with the caller's current bookmark list
regardless of whether the row existed. Skips item validation so
bookmarks remain removable even after the underlying item is deleted.
Realtime user_updated event with reason=bookmark_deleted fires only
when a row actually existed (carries the pre-delete title).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the same handleUpsertBookmark with reason="bookmark_updated",
asserts the (user, profile, item, time) tuple is unique (PATCH updates
title in place, never duplicates) and that the ULID is preserved
across upsert.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helper was added in advance of the DELETE handler (next task in
the plan) but is unused in this commit, which trips golangci-lint's
unused check. Reintroduce it together with its first caller.
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>
Defines the storage contract and wire-shape serialiser the bookmarks
handlers will consume. Envelope test asserts the six required keys
(id, libraryItemId, time, title, createdAt, updatedAt) and the
JS-epoch-millis timestamp shape ABS Android pattern-matches on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the convention used by 143_abs_playback_sessions and
147_abs_sessions. Functionally a no-op (DROP TABLE cascade-drops owned
indexes), but reads as complete next to its sibling migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backs the upcoming ABS-compatible bookmark endpoints. Schema and
rationale documented in
docs/superpowers/specs/2026-05-26-abs-bookmarks-design.md §5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-endpoint, per-(user,profile)-scoped bookmark surface for the
Audiobookshelf-compatibility API. Mirrors the canonical continuum
plugin's "key by (item, time)" model, with silo's profile-isolation
overlay and the existing nil-safe socket publish wrapper for
realtime user_updated events. One migration (148_abs_bookmarks),
one new store, one new handlers file. Ready for the implementation
plan once the user signs off.
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>