# 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 && ( Listen in Navidrome ↗ )} ``` - [ ] **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 `` (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