docs: navidrome deep-link implementation plan (3 tasks)
This commit is contained in:
105
docs/superpowers/plans/2026-09-04-navidrome-link.md
Normal file
105
docs/superpowers/plans/2026-09-04-navidrome-link.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Navidrome Deep Link Implementation Plan (plan 4)
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the in-app album player with a server-built Navidrome album link on the item page.
|
||||||
|
|
||||||
|
**Architecture:** Deletion-heavy change: the player (context, mini-bar, audio element), stream/tracks/played proxy routes, and their tests are removed. `matchedAlbum` in the item detail gains `webUrl`, built server-side from the user's Subsonic URL. Spec: `docs/superpowers/specs/2026-09-04-navidrome-link-design.md`.
|
||||||
|
|
||||||
|
**Tech Stack:** unchanged. **Prerequisites:** main (post Wave 1), 205/205 tests. Branch: `navidrome-link`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Backend — webUrl in matchedAlbum; delete stream routes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server/src/routes/collectionRoutes.ts`, `server/src/app.ts`
|
||||||
|
- Delete: `server/src/routes/streamRoutes.ts`, `server/test/stream.test.ts`
|
||||||
|
- Test: `server/test/collection.test.ts` (modify), `server/test/lastplayed.test.ts` (modify)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Modify the detail route in `server/src/routes/collectionRoutes.ts`** — build webUrl from the user's Subsonic URL (import `getSettings` from './settingsRoutes.js'):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const matched = findMatchedAlbum(db, request.user.id, row.id)
|
||||||
|
const settings = getSettings(db, request.user.id)
|
||||||
|
const webUrl =
|
||||||
|
matched && settings.subsonic_url
|
||||||
|
? `${settings.subsonic_url.replace(/\/+$/, '')}/app/#/album/${matched.subsonicId}`
|
||||||
|
: null
|
||||||
|
return {
|
||||||
|
...rowToItem(db, row),
|
||||||
|
matchedAlbum: matched ? { ...matched, webUrl } : null,
|
||||||
|
loan: ... // unchanged
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update the matchedAlbum test** in `server/test/collection.test.ts` ('detail includes matchedAlbum') — `webUrl` requires the user's subsonic_url: before the first detail assertion, PUT /api/settings with a subsonic config (reuse the ping-stub pattern from settings.test.ts so the PUT succeeds), seed album `alb-9`, then assert `matchedAlbum` matches `{ subsonicId: 'alb-9', webUrl: 'http://navidrome.local/app/#/album/alb-9' }`. Keep the pre-seed assertion (`matchedAlbum` null).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the stream routes** — remove `server/src/routes/streamRoutes.ts`, `server/test/stream.test.ts`, and the import/registration lines in `server/src/app.ts`. Update `server/test/lastplayed.test.ts`: the 'played endpoint' test is removed (route deleted); keep the sync-stamping test (it exercises sync.ts, which stays).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests** — `npm test && npm run typecheck` — expect ALL PASS (205 − 2 played/stream tests + 0 new = 203; exact count may shift ±1 with the lastplayed restructure — report it).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A && git commit -m "feat: matchedAlbum.webUrl from subsonic settings; remove stream proxy routes"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Web — remove player, add the link
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/src/pages/ItemPage.tsx`, `web/src/App.tsx`, `web/src/api.ts`, `web/src/types.ts`, `web/test/item.test.tsx`, `web/test/api.test.ts`
|
||||||
|
- Delete: `web/src/player/` (PlayerContext.tsx, MiniBar.tsx), `web/test/player.test.tsx`, `web/test/minibar.test.tsx`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update failing tests first in `web/test/item.test.tsx`:**
|
||||||
|
- Remove PlayerProvider from `renderItem`, media prototype mocks, getAlbumTracks/markPlayed/lendItem mock entries where player-related; keep lendItem/returnLoan (loans stay)
|
||||||
|
- The rippedItem fixture keeps `matchedAlbum: matched` — extend `matched` with `webUrl: 'http://navidrome.local/app/#/album/alb-1'`
|
||||||
|
- Replace the 'shows Play…loads the player' test with: ripped+matched → link `[aria-label="Listen in Navidrome"]`... simpler: `getByRole('link', { name: /listen in navidrome/i })` with `getAttribute('href')` = the webUrl and `target` = '_blank'
|
||||||
|
- 'hides Play when unmatched or not ripped' → renamed: link absent when `matchedAlbum: null` (even when ripped)
|
||||||
|
- 'shows last played' unchanged
|
||||||
|
- lend/return tests unchanged
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement `web/src/pages/ItemPage.tsx`:** remove `usePlayer` import + `load` usage + media code; replace the Play button block with:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
{item.matchedAlbum && (
|
||||||
|
<a
|
||||||
|
href={item.matchedAlbum.webUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block w-full rounded-xl bg-emerald-500 py-2.5 text-center font-medium text-neutral-950"
|
||||||
|
>
|
||||||
|
Listen in Navidrome ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Remove the player from the app:** delete `web/src/player/`, `web/test/player.test.tsx`, `web/test/minibar.test.tsx`; revert `web/src/App.tsx` wiring to `<Gate><Shell /></Gate>` (no PlayerProvider/MiniBar imports).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove dead api/types:** delete `getAlbumTracks`, `markPlayed`, `streamUrl` methods from `web/src/api.ts`; delete `Track`, `AlbumTracks` interfaces from `web/src/types.ts` AND add `webUrl: string` to the `MatchedAlbum` interface (the detail route now supplies it); remove their entries + the 'album tracks + played + stream url' test from `web/test/api.test.ts`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests** — `npm test && npm run typecheck` — expect ALL PASS (203 − 3 player tests − 3 minibar tests − 1 api test-block + 1 item rewrite ≈ 197; report exact).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A && git commit -m "feat: navidrome deep link replaces in-app player"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Verification + redeploy
|
||||||
|
|
||||||
|
- [ ] **Step 1:** `npm test && npm run typecheck && npm run build` — all green, both dists.
|
||||||
|
- [ ] **Step 2:** Restart the deployed instance (anchored pkill + setsid nohup pattern; verify in a separate invocation): health ok, SPA title served, `curl -X POST .../api/album/a1/played` → 404 (route gone).
|
||||||
|
- [ ] **Step 3:** No commit expected; report results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification checklist (manual)
|
||||||
|
|
||||||
|
- Item page for a ripped, matched album: 'Listen in Navidrome ↗' opens the correct album in Navidrome (desktop + phone)
|
||||||
|
- 'Last played X ago' still populates after a library sync (Navidrome-provided timestamps)
|
||||||
|
- No mini-bar anywhere; audio nowhere
|
||||||
Reference in New Issue
Block a user