Skip to content

Implement getNowPlaying + live Streams on Activity page (#237) - #263

Merged
benders merged 2 commits into
mainfrom
feature/237-now-playing
Jul 16, 2026
Merged

Implement getNowPlaying + live Streams on Activity page (#237)#263
benders merged 2 commits into
mainfrom
feature/237-now-playing

Conversation

@benders

@benders benders commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Closes #237.

What

  • getNowPlaying implemented (was a stub). Fed by scrobble?submission=false now-playing pings, held in a new in-memory NowPlayingService — one entry per (user, client) player, stable playerId, expiring 5 minutes after the last ping. Entries are full Song objects plus username, minutesAgo, playerId, playerName, with starred/playCount annotations.
  • Per-user by design (per Nic's comment on the issue): a caller sees only their own active players. The cross-user view is admin-only.
  • Activity page: GET /api/admin/hub/activity/active now returns nowPlaying[]; the Streams list shows live playback rows (green pulsing "Playing" badge, user, client, minutes-ago) above the in-flight transfer rows — so a track playing from the SPA, a 3rd-party client, or a Sonos cast is visible for the duration of the listen, not just while bytes move.
  • SPA pings: PlayerBar sends scrobble(id, false) on play/resume/track-change and every 2 minutes while playing, on both local and Sonos-cast sinks.
  • Bug found by the federation suite: submission was parsed with === "false", but py-sonic sends submission=False (capital F) — the ping was being recorded as a durable play. Now parsed case-insensitively like Java's Boolean.parseBoolean. Added to docs/pitfalls.md.

Not in scope

The broader Activity-page overhaul from the issue comment (restructuring history into local "plays" vs peer "fetches") — this PR adds the live now-playing surface; the history-view rework is a follow-up if wanted.

Testing

  • pnpm verify: hub 815, frontend 150 — green
  • pnpm lint: zero output
  • pnpm test:federation: 84/84 py-sonic compat (incl. upgraded test_get_now_playing exercising the real ping→read flow) + eviction/re-admission acts — green

🤖 Generated with Claude Code

https://claude.ai/code/session_01SwwPcrohddTUN7kmQVedQC

…age (closes #237)

- New in-memory NowPlayingService (5-min TTL, one entry per user+client,
  stable playerId) fed by scrobble?submission=false pings; nothing durable.
- getNowPlaying implemented per-user (owner decision on #237): callers see
  only their own players. Entries carry the full Song + username,
  minutesAgo, playerId, playerName; starred/playCount annotated.
- scrobble submission param now parsed case-insensitively (py-sonic sends
  'False'; matches Java Boolean.parseBoolean) — caught by the federation
  compat suite; pitfall documented.
- Admin GET /api/admin/hub/activity/active gains nowPlaying[] (cross-user);
  Activity page renders live now-playing rows atop the Streams list.
- SPA PlayerBar pings now-playing on play/resume/track-change and every
  2 min while playing, for both local and Sonos-cast sinks.
- Docs: opensubsonic.md (getNowPlaying Stub→Implemented, scrobble note),
  pitfalls.md boolean-param row. Compat suite stub test upgraded to
  exercise the real flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwwPcrohddTUN7kmQVedQC
@benders
benders marked this pull request as ready for review July 16, 2026 05:39
@benders

benders commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

FROM @claude:

Working through review feedback on this branch:

  1. Vibrdrome missing from Now Playing — confirmed client issue: hub logs show Vibrdrome calls stream/savePlayQueue/createBookmark but never scrobble, so it sends no now-playing pings.
  2. Hung transfersstream.ts pipes with .pipe() + manual events; client abort never fires end/error, entry stuck in Active map forever (confirmed: rows with NULL finished_at in live DB). Fix: pipeline() like federation.ts, + boot-time recovery for orphaned rows. Also splitting Active Streams UI into Now Playing / Transfers.
  3. Peer name + format/bitrate for Now Playing — info is available (preferred source lookup); enriching now-playing entries with source/format/bitrate.
  4. Duplicate History rows — real duplicate HTTP requests per play, all recording FULL file size because hub slurps upstream even after client abort. Same pipeline fix makes bytes honest; also coalescing consecutive same-track rows in History UI.
  5. Track name → album link — adding albumId to stream ops + now-playing entries, linking titles to /albums/:id.

@benders

benders commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

FROM @claude:

All five feedback items addressed and deployed to the live hub for testing (not yet committed — awaiting approval):

  1. Vibrdrome — client limitation confirmed: hub logs show it never calls scrobble (only stream/savePlayQueue/createBookmark), so it can't appear in Now Playing.
  2. Hung transfers — root cause: stream.ts used bare .pipe(); a client abort fired neither end nor error, leaking the active entry forever AND recording full-file byte counts for aborted requests. Now uses stream/promises pipeline() (same as federation.ts). Boot-time recovery closed 155 orphaned rows on the live DB (error='interrupted'). Active section is now split into Now Playing and Transfers.
  3. Peer/format/bitrate on Now Playing — yes, available: entries now carry a preferred-source snapshot (source, format, bitrate) resolved at ping time; rendered on the Activity page.
  4. Duplicate History rows — clients genuinely issue several HTTP requests per listen; the pipeline fix makes their byte counts honest, and the History list now coalesces consecutive same-listen rows into one ×N row with summed bytes.
  5. Track → album link — track titles on Now Playing, Transfers, and History rows link to the album page (album_id added to stream_operations via additive migration; now-playing entries carry it too).

pnpm verify, frontend build, full lint, and pnpm test:federation all green. Docs updated (pitfalls.md, hub-internals.md).

…ks (PR #263 review, refs #237)

- stream.ts: pipe with stream/promises pipeline() instead of bare .pipe().
  A client abort now cancels the upstream fetch and finish()es the op with
  the real byte count; before, aborted requests neither ended nor errored —
  entries leaked in the Active list forever and recorded full-file sizes
  (the "identical duplicate rows" symptom). Pitfall documented.
- StreamTrackingService: close out rows left unfinished by a previous
  process at boot (error='interrupted'); finish() is idempotent.
- Now-playing entries carry a preferred-source snapshot (sourceKind/
  sourcePeerId/format/bitrate) + albumId, resolved at ping time.
- stream_operations gains album_id (additive migration); Subsonic and
  federation stream starts record it.
- Activity page: Active split into "Now Playing" and "Transfers"
  subsections; History coalesces consecutive same-listen transfers into
  one ×N row with summed bytes; track titles link to their album.
- Vibrdrome absence from Now Playing confirmed as a client limitation:
  it never calls scrobble (no submission=false pings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@benders
benders merged commit b9dded1 into main Jul 16, 2026
2 checks passed
@benders
benders deleted the feature/237-now-playing branch July 16, 2026 06:37
@benders benders mentioned this pull request Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement getNowPlaying (currently a stub returning empty)

2 participants