1
0
Files
record-shop/docs/superpowers/plans/2026-09-04-navidrome-link.md

6.0 KiB
Raw Blame History

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'):

    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 testsnpm 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"

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:

      {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 testsnpm 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