feat(subsonic): wire /rest/scrobble into playevents.Writer

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.
This commit is contained in:
2026-04-26 00:23:20 -04:00
parent f4e73b81b4
commit 599a19f780
7 changed files with 189 additions and 94 deletions
+4 -11
View File
@@ -6,26 +6,19 @@ package api
import (
"log/slog"
"time"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
"git.fabledsword.com/bvandeusen/minstrel/internal/config"
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
)
// Mount attaches /api/* handlers to r. Public endpoints (login) are outside
// RequireUser; everything else is gated by the middleware.
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, cfg config.EventsConfig) {
w := playevents.NewWriter(
pool, logger,
time.Duration(cfg.SessionTimeoutMinutes)*time.Minute,
cfg.SkipMaxCompletionRatio,
cfg.SkipMaxDurationPlayedMs,
)
h := &handlers{pool: pool, logger: logger, events: w}
// RequireUser; everything else is gated by the middleware. The events writer
// is shared with the Subsonic mount so /rest/scrobble feeds the same store.
func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playevents.Writer) {
h := &handlers{pool: pool, logger: logger, events: events}
r.Route("/api", func(api chi.Router) {
api.Post("/auth/login", h.handleLogin)
+7 -6
View File
@@ -2,13 +2,16 @@ package api
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"git.fabledsword.com/bvandeusen/minstrel/internal/config"
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
)
// newLibraryRouter builds a test-only chi router with the library handlers
@@ -435,11 +438,9 @@ func TestHandleListArtists_EmptyDB(t *testing.T) {
func TestRoutesRegisteredInMount(t *testing.T) {
h, _ := testHandlers(t)
r := chi.NewRouter()
Mount(r, h.pool, h.logger, config.EventsConfig{
SessionTimeoutMinutes: 30,
SkipMaxCompletionRatio: 0.5,
SkipMaxDurationPlayedMs: 30000,
})
w := playevents.NewWriter(h.pool, slog.New(slog.NewTextHandler(io.Discard, nil)),
30*time.Minute, 0.5, 30000)
Mount(r, h.pool, h.logger, w)
paths := []string{
"/api/artists",