6.0 KiB
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 (importgetSettingsfrom './settingsRoutes.js'):
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') —webUrlrequires 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 albumalb-9, then assertmatchedAlbummatches{ subsonicId: 'alb-9', webUrl: 'http://navidrome.local/app/#/album/alb-9' }. Keep the pre-seed assertion (matchedAlbumnull). -
Step 3: Delete the stream routes — remove
server/src/routes/streamRoutes.ts,server/test/stream.test.ts, and the import/registration lines inserver/src/app.ts. Updateserver/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
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— extendmatchedwithwebUrl: '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 })withgetAttribute('href')= the webUrl andtarget= '_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: removeusePlayerimport +loadusage + media code; replace the Play button block with:
{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; revertweb/src/App.tsxwiring to<Gate><Shell /></Gate>(no PlayerProvider/MiniBar imports). -
Step 4: Remove dead api/types: delete
getAlbumTracks,markPlayed,streamUrlmethods fromweb/src/api.ts; deleteTrack,AlbumTracksinterfaces fromweb/src/types.tsAND addwebUrl: stringto theMatchedAlbuminterface (the detail route now supplies it); remove their entries + the 'album tracks + played + stream url' test fromweb/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
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