Commit Graph
152 Commits
Author SHA1 Message Date
RXWatcherandClaude Opus 4.7 0ab06e5242 feat(audiobooks): wire ABSSmartCollectionStore + mount routes
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>
2026-05-26 20:39:49 +02:00
RXWatcherandClaude Opus 4.7 7061110ee5 feat(audiobooks): GET /me/smart-collections/{id}/items — eval + page
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>
2026-05-26 20:37:57 +02:00
RXWatcherandClaude Opus 4.7 e2670b3055 feat(audiobooks): list/get/patch/delete smart collections
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>
2026-05-26 20:34:21 +02:00
RXWatcherandClaude Opus 4.7 e8b9b1bebf feat(audiobooks): POST /me/smart-collections — create + DSL validation
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>
2026-05-26 20:31:58 +02:00
RXWatcherandClaude Opus 4.7 d11326875f feat(audiobooks): SmartCollectionStore interface + envelope helper
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>
2026-05-26 20:29:11 +02:00
RXWatcherandClaude Opus 4.7 a65852ab9b feat(audiobooks): smartcoll DSL evaluator + tests
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>
2026-05-26 20:27:49 +02:00
RXWatcherandClaude Opus 4.7 dba55e770d feat(audiobooks): smartcoll DSL — types + Normalize + Validate
Ports continuum's QueryDefinition / QueryGroup / QueryRule / QuerySort
types and the audiobook-domain field/sort catalogs to a new
internal/audiobooks/smartcoll/ package. Covers Normalize (alias +
default + dedupe) and Validate (unknown fields, invalid ops, sort,
personalization scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:24:34 +02:00
RXWatcherandClaude Opus 4.7 502f0d10d6 feat(audiobooks): migration 153 + BookmarkStore.CountByUser extension
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>
2026-05-26 20:22:04 +02:00
RXWatcherandClaude Opus 4.7 7a8749739a test(audiobooks): tighten delete tests with cascade + unreachability checks
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>
2026-05-26 19:59:38 +02:00
RXWatcherandClaude Opus 4.7 0339eba225 feat(audiobooks): wire ABSPlaylistStore + mount playlists routes
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>
2026-05-26 19:23:22 +02:00
RXWatcherandClaude Opus 4.7 1632e9a481 feat(audiobooks): batch add/remove playlist items
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>
2026-05-26 19:20:25 +02:00
RXWatcherandClaude Opus 4.7 ba8e5fdf96 feat(audiobooks): DELETE /playlists/{id}/item/{libraryItemId}[/{episodeId}]
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>
2026-05-26 19:17:35 +02:00
RXWatcherandClaude Opus 4.7 f661659194 feat(audiobooks): POST /playlists/{id}/item — add single item
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>
2026-05-26 19:15:10 +02:00
RXWatcherandClaude Opus 4.7 7da951021e feat(audiobooks): PATCH + DELETE /playlists/{id}
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>
2026-05-26 19:12:40 +02:00
RXWatcherandClaude Opus 4.7 7cdbfbb637 feat(audiobooks): GET /playlists + GET /playlists/{id}
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>
2026-05-26 19:10:28 +02:00
RXWatcherandClaude Opus 4.7 f5dd7f79ed feat(audiobooks): POST /playlists — ABS playlist create + event
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>
2026-05-26 19:06:37 +02:00
RXWatcherandClaude Opus 4.7 2be46146f2 feat(audiobooks): add PlaylistStore interface + ABS envelope helper
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>
2026-05-26 19:03:14 +02:00
RXWatcherandClaude Opus 4.7 509dff8839 chore(audiobooks): align ABSCollectionStore with sibling-store patterns
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>
2026-05-26 18:52:42 +02:00
RXWatcherandClaude Opus 4.7 78732ca6fd feat(audiobooks): wire ABSCollectionStore + mount collections routes
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>
2026-05-26 18:50:16 +02:00
RXWatcherandClaude Opus 4.7 e1e5b151ca feat(audiobooks): add/remove collection items
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>
2026-05-26 18:47:22 +02:00
RXWatcherandClaude Opus 4.7 da21cab37d feat(audiobooks): PATCH + DELETE /collections/{id}
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>
2026-05-26 18:44:25 +02:00
RXWatcherandClaude Opus 4.7 aedf545481 feat(audiobooks): GET /collections/{id} — owner and public access
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>
2026-05-26 18:41:58 +02:00
RXWatcherandClaude Opus 4.7 d9ea7be3fc feat(audiobooks): GET /collections — list caller's collections
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>
2026-05-26 18:25:30 +02:00
RXWatcherandClaude Opus 4.7 7bdff1141c feat(audiobooks): POST /collections — ABS collection create
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>
2026-05-26 18:22:41 +02:00
RXWatcherandClaude Opus 4.7 d89f6197f5 feat(audiobooks): add CollectionStore interface + ABS envelope helper
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>
2026-05-26 18:18:57 +02:00
RXWatcherandClaude Opus 4.7 0fb8323503 chore(audiobooks): tighten bookmark store + URL-time parsing
- 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>
2026-05-26 15:21:06 +02:00
RXWatcherandClaude Opus 4.7 21257035d3 feat(audiobooks): wire ABSBookmarkStore into BuildABSHandler
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>
2026-05-26 14:16:03 +02:00
RXWatcherandClaude Opus 4.7 ddab7be474 feat(audiobooks): ABSBookmarkStore — pgx-backed concrete store
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>
2026-05-26 14:11:48 +02:00
RXWatcherandClaude Opus 4.7 a0dcc860b4 feat(audiobooks): mount ABS bookmark routes at /abs/api and /api
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>
2026-05-26 14:09:36 +02:00
RXWatcherandClaude Opus 4.7 2736688538 test(audiobooks): assert realtime user_updated events on bookmark ops
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>
2026-05-26 14:07:44 +02:00
RXWatcherandClaude Opus 4.7 316ca5005a test(audiobooks): cover ABS bookmark edge cases
Adds ordering, per-profile isolation, cross-user no-op (existence
leak guard), missing-item 404, malformed-body 400, and missing-time
400 to the bookmark handler suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:06:02 +02:00
RXWatcherandClaude Opus 4.7 c23e10a184 feat(audiobooks): DELETE /me/item/{id}/bookmark/{time} — ABS delete
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>
2026-05-26 14:03:35 +02:00
RXWatcherandClaude Opus 4.7 462ce40ef7 test(audiobooks): cover PATCH /me/item/{id}/bookmark upsert semantics
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>
2026-05-26 14:01:02 +02:00
RXWatcherandClaude Opus 4.7 29c40f448c chore(audiobooks): drop unused parseBookmarkTime from bookmark handler
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>
2026-05-26 14:00:01 +02:00
RXWatcherandClaude Opus 4.7 815f207d6c feat(audiobooks): POST /me/item/{id}/bookmark — ABS bookmark create
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>
2026-05-26 13:41:03 +02:00
RXWatcherandClaude Opus 4.7 6ae492be84 feat(audiobooks): add BookmarkStore interface + ABS envelope helper
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>
2026-05-26 13:36:13 +02:00
RXWatcherandClaude Opus 4.7 a902055acf fix(audiobooks): mount /public/session/{sid}/track/{idx} for ABS DirectPlay
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>
2026-05-26 10:56:48 +02:00
RXWatcherandClaude Opus 4.7 64bb15de84 refactor(audiobooks): unify login timestamp + document filterdata shape
- 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>
2026-05-26 10:34:25 +02:00
RXWatcherandClaude Opus 4.7 460d4fd4d5 refactor(audiobooks): minor review-cleanup pass
- loginEnvelope: add a comment explaining the displayName→userID fallback.
- handleRefresh: stop leaking internal err detail in 500 responses; log
  via slog and return a sanitized "token mint/persist/rotation failed".
- handlePlayStart: document why no "progress" field on playbackSession
  (canonical omits it; spec was over-specified).
- Extract resolveDefaultLibrary helper to dedupe the
  "first-audiobook-lib-else-virtual" snippet from three handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:29:48 +02:00
RXWatcherandClaude Opus 4.7 ca4ebc65a9 test(audiobooks): cover refresh race + revoke-failure semantics
The plan documented both behaviors but neither was exercised:

- Concurrent rotation: two clients presenting the same refresh token whose
  GetTokenByJTI lookups both complete before either revoke must both
  succeed with distinct new pairs. Uses a barrierStore that gates the
  first Revoke so the test is deterministic instead of relying on the
  scheduler.
- Revoke failure: if RevokeTokenByJTI errors after the new pair is
  persisted, /auth/refresh returns 500 and the OLD JTI stays valid so
  the client can retry without losing access.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:24:55 +02:00
RXWatcherandClaude Opus 4.7 2415e6b570 test(audiobooks): assert /login + /authorize envelope shape stays stable
Tasks 2 and 3 enriched the login envelope to match the real ABS shape but
shipped without tests; a regression dropping seriesHideFromContinueListening,
itemTagsAccessible, or any of the permissions/serverSettings keys would
silently land and only surface on a live device. Cover the marshaled JSON
shape (top-level keys + user + permissions + serverSettings), the
x-return-tokens opt-in path, and the displayName fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:22:50 +02:00
RXWatcherandClaude Opus 4.7 38cc9f1f0c fix(audiobooks): mount /logout outside bearerAuth + add root path
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>
2026-05-26 10:21:57 +02:00
RXWatcherandClaude Opus 4.7 9c870b3749 fix(audiobooks): make MediaStore mandatory at construction
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>
2026-05-26 10:20:03 +02:00
RXWatcherandClaude Opus 4.7 be69e92b74 fix(audiobooks): nil-guard MediaStore + MetaTags + limit=0 + slog key
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>
2026-05-26 07:48:12 +02:00
RXWatcherandClaude Opus 4.7 8a780fbe45 feat(audiobooks): add POST /logout for ABS sign-out
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>
2026-05-26 07:30:58 +02:00
RXWatcherandClaude Opus 4.7 0e71f31aa1 feat(audiobooks): add POST /auth/refresh for ABS token rotation
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>
2026-05-26 07:27:56 +02:00
RXWatcherandClaude Opus 4.7 4bbab4ccf0 fix(audiobooks): seed currentTime from ProgressStore so resume works
handlePlayStart now looks up the persisted progress row and emits the
saved currentTime in the playback session manifest. Without this every
play start began at 0, breaking cross-device resume which is one of the
core ABS-app value props.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 07:24:18 +02:00
RXWatcherandClaude Opus 4.7 4b97908d92 chore(audiobooks): rename const cap to fetchCap in buildFilterData
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>
2026-05-26 07:22:22 +02:00
RXWatcherandClaude Opus 4.7 18898f9f7f fix(audiobooks): hydrate filterdata authors and series in library detail
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>
2026-05-26 07:20:53 +02:00
RXWatcherandClaude Opus 4.7 c91daa72c4 fix(audiobooks): proper pagination total + tags key in play session
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>
2026-05-26 07:19:10 +02:00