docs(audiobooks): implementation plan sub-plan 4 (ABS-compat)

Stage-based port of silo-plugin-audiobooks' abs/ + abssocket/
packages. Preserves the plugin's file layout but rewrites every SQL
query against silo's existing tables (media_items, media_files,
episodes, user_watch_progress, user_playback_sessions, abs_sessions,
people, item_people) — no new tables. Out-of-scope plugin features
(bookmarks, smart collections, share links, playlists, podcasts/RSS
in this sub-plan) are dropped. 8 stages from support files through
Socket.io, router wiring, and final smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
RXWatcher
2026-05-24 15:06:09 +02:00
co-authored by Claude Opus 4.7
parent 9ed283d2ef
commit 7ea7609931
@@ -0,0 +1,148 @@
# Audiobooks Absorption — Sub-plan 4: ABS-compat REST + Socket.io
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax.
**Goal:** Port the audiobookshelf-compatible REST + Socket.io surface from `silo-plugin-audiobooks/internal/abs/` and `silo-plugin-audiobooks/internal/abssocket/` into `internal/audiobooks/abs/` and `internal/audiobooks/abssocket/`. **Preserve the plugin's file layout** but **rewrite every SQL query** against silo's existing tables (`media_items`, `media_files`, `episodes`, `user_watch_progress`, `user_playback_sessions`, `abs_sessions`, `people`, `item_people`). No new tables.
**Architecture:** Stage-based port. Support files first, then auth, then file serving, then progress, then author/series, then Socket.io, then router wiring. Out-of-scope features (bookmarks, smart collections, share links, playlists, podcasts/RSS in this sub-plan) are dropped — files containing them are not copied, or are copied with the unsupported handlers stubbed to 501 Not Implemented.
**Source plugin:** `/opt/silo_plugins/silo-plugin-audiobooks/internal/{abs,abssocket}/`
---
## Stage 1: Support files
**Files to copy and adapt:**
- `mount.go` — package mount + route registration shape
- `handler.go` — top-level handler type + dependency wiring
- `filter.go` — generic helpers
- `access_log.go` — request-log helper
- `minified.go` — generic helpers (likely small)
**Adapt steps:**
1. Replace import paths: `github.com/RXWatcher/silo-plugin-audiobooks/...` → `github.com/Silo-Server/silo-server/internal/audiobooks/...`
2. Replace plugin SDK references with direct silo equivalents:
- Plugin's `*pgx.Pool` field on Handler → silo's `*pgxpool.Pool` (same type via different import alias)
- Plugin's `Logger` → silo's `log/slog` package-level functions
- Plugin's config types → struct fields on `audiobooks.Service` (extended)
3. Remove any code that talks to the plugin SDK runtime (plugin proxy callbacks etc.)
4. Get `go build ./internal/audiobooks/abs/...` to compile cleanly even if no routes are wired yet.
Commit: `feat(audiobooks): port ABS handler scaffolding (mount/handler/filter)`.
---
## Stage 2: Auth + JWT
**Files:**
- `jwt.go` — token generation + validation
- `login_internal_test.go` (keep as test) + `login_ratelimit.go`
- Whatever the actual `login.go` file is called (look for the login handler — likely inside `handler.go` or a dedicated file)
**Adapt:**
- The plugin's login handler posts credentials to silo's `/api/v1/auth/login` over HTTP. **In silo's in-process port, replace this HTTP round-trip with a direct call to silo's auth service** (find via `grep -nE 'auth.*Authenticate|auth.*Login' /opt/silo-server/internal/auth/*.go`). The plugin's network call is no longer needed because the code is now in-process.
- ABS sessions are stored in `abs_sessions` (migration 139 from sub-plan 1).
- JWT signing secret: store in `server_settings` under key `audiobooks.abs.jwt_secret` (auto-generated on first request if missing).
Commit: `feat(audiobooks): port ABS auth (JWT + login bridge)`.
---
## Stage 3: File serving + play session
**Files:**
- `file_handler.go` — handles ABS GET requests for audio file streams
- `play_response.go` — issues a play session to the client (URL + token)
**Adapt:**
- Plugin's `bookref` lookup table (linking ABS-style IDs to backend file IDs) → use silo's `media_files.id` directly. ABS-side item ID = silo's `content_id`; ABS-side file ID = silo's `media_files.id`.
- The stream URL ABS clients expect must point back at silo. Issue them a `/api/v1/direct-download?file_id={id}` URL signed with silo's existing media-token signer (grep `mediatoken` in `internal/`).
Commit: `feat(audiobooks): port ABS file handler + play session`.
---
## Stage 4: Progress tracking
**Files:**
- `progress_internal_test.go` (test, port)
- Whatever file owns the `/api/me/progress` ABS endpoint family — look in `handler.go` or grep ABS progress route paths
- `continue_listening.go`
**Adapt:**
- All progress reads/writes hit silo's `user_watch_progress` keyed on `(user_id, profile_id, content_id)`. Sub-plan 3's progress handler did this already — copy the patterns.
Commit: `feat(audiobooks): port ABS progress + continue listening`.
---
## Stage 5: Author / Series / Browse
**Files:**
- `author_series_handler.go`
- `collapse.go`
**Adapt:**
- Author rows come from silo's `people` + `item_people` (kind=7).
- Series grouping: if silo doesn't have an explicit "audiobook series" concept, use `media_items.title` prefix matching (e.g., books named "Foundation #1", "Foundation #2") for MVP. Document the heuristic.
Commit: `feat(audiobooks): port ABS author/series browse`.
---
## Stage 6: Socket.io
**Files:**
- `abssocket/server.go` + `abssocket/server_test.go`
**Adapt:**
- Reuse silo's `gorilla/websocket` upgrader. The plugin's standalone-listener escape hatch is no longer needed — mount under `/abs/socket.io/` on silo's main listener.
- Redis adapter behavior preserved if `REDIS_URL` is set (silo always has one in the docker setup).
Commit: `feat(audiobooks): port ABS Socket.io endpoint`.
---
## Stage 7: Router wiring
**Files:**
- Modify `internal/api/router.go` — mount `audiobookABSHandler` under `/abs/*` and the small set of `/api/*` legacy paths ABS clients hardcode
- Modify `internal/audiobooks/service.go` — add `ABSHandler` field, wire it from `audiobooks.New(...)`
- Modify `cmd/silo/main.go` — construct the ABS handler and pass it into the audiobooks service
**Routes to expose** (the minimum ABS-required set):
- `POST /abs/login` (login flow)
- `GET /abs/api/me` (profile)
- `GET /abs/api/libraries` (library list)
- `GET /abs/api/libraries/{id}/items` (browse)
- `GET /abs/api/items/{id}` (item detail)
- `GET /abs/api/items/{id}/file/{ino}` (stream)
- `POST /abs/api/me/progress/{libraryItemId}` (progress)
- `GET /abs/socket.io/` (Socket.io)
For ABS-legacy hard-coded paths that don't fit under `/abs/`: register them explicitly with the ABS auth middleware so they don't collide with silo's existing `/api/v1/*`.
Build + smoke:
```bash
sudo docker build -t silo:latest /opt/silo-server
sudo docker compose -p silo-prod up -d --force-recreate silo
curl -sS -o /dev/null -w "POST /abs/login -> %{http_code}\n" -X POST -H 'Content-Type: application/json' -d '{}' http://localhost:8090/abs/login
```
Expected: 400 (bad request body) or 401 (validation failed), NOT 404.
Commit: `feat(audiobooks): wire ABS routes into main router`.
---
## Stage 8: Build + smoke + lint sweep
Single final task. Same shape as sub-plan 2 Task 10.
---
## Risks
- The plugin's code expects a specific database schema. Some plugin queries may have no clean silo-table equivalent (e.g., the plugin's `bookref` mapping table). In that case, stub the handler to return 501 + log + flag in PR notes — don't add new tables.
- ABS clients have undocumented expectations (CORS headers, specific error shapes, etc.). Smoke testing against a real ABS client is the only way to find these. Defer to sub-plan 6's manual verification.
- Each stage commits independently and produces partial functionality (e.g., after stage 2 you can log in but can't list libraries). That's intentional — clients won't connect end-to-end until stage 7.