Validates seed_track + optional limit (default cfg.Recommendation.RadioSize,
clamped to RadioSizeMax). Calls recommendation.LoadCandidates +
recommendation.Shuffle. Prepends seed to result. Server seeds a
math/rand source at startup; handlers package threads that as a
func() float64 so tests inject deterministic RNGs.
Mount + server.New gain a RecommendationConfig parameter.
submission=false → play_started (replaces in-memory nowPlayingMap).
submission=true (default) → synthetic completed play. The nowPlayingMap
struct + factory + the nowPlaying field on mediaHandlers are deleted;
play_events WHERE ended_at IS NULL is now the source of truth for
'currently playing,' reachable from M3+ work.
api.Mount now takes the shared *playevents.Writer instead of cfg so
the same writer instance feeds both surfaces.
Discriminated-union JSON over three event types: play_started,
play_ended, play_skipped. Auth via existing RequireUser. play_started
returns play_event_id + session_id; the other two return { ok: true }.
Validates ownership: play_ended/play_skipped on another user's row
returns 403. Mount signature now takes EventsConfig and constructs
the playevents.Writer; server.New propagates it through.
- NotFound wrapper routes unknown /api/* and /rest/* to a JSON 404
- Everything else falls through to the embedded web handler, which
resolves static assets or returns index.html for SPA deep links
Wires the native JSON surface alongside the existing /api/admin
and /rest/* routes. Subsonic compatibility remains untouched.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>