feat(subsonic): add getStarred and getStarred2 handlers

Both return user's starred artists/albums/songs sorted liked_at DESC.
Cap at 500 entries per category for v1 (Subsonic spec doesn't define
pagination on these endpoints; M3+ can revisit if needed).
This commit is contained in:
2026-04-26 16:45:04 -04:00
parent 32fb3fec20
commit c7f4adbcc3
4 changed files with 194 additions and 0 deletions
+59
View File
@@ -2,6 +2,7 @@ package subsonic
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
@@ -190,3 +191,61 @@ func contains(s, substr string) bool {
}
return false
}
func TestHandleGetStarred2_ReturnsAllThreeArrays(t *testing.T) {
pool, user, track, album, artist := testStarPool(t)
m := newStarHandlers(pool)
q := dbq.New(pool)
// Star one of each.
_ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: user.ID, TrackID: track.ID})
_ = q.LikeAlbum(context.Background(), dbq.LikeAlbumParams{UserID: user.ID, AlbumID: album.ID})
_ = q.LikeArtist(context.Background(), dbq.LikeArtistParams{UserID: user.ID, ArtistID: artist.ID})
req := httptest.NewRequest(http.MethodGet, "/rest/getStarred2?f=json", nil)
ctx := context.WithValue(req.Context(), userCtxKey, user)
w := httptest.NewRecorder()
m.handleGetStarred2(w, req.WithContext(ctx))
if w.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", w.Code, w.Body.String())
}
// Subsonic JSON envelope: {"subsonic-response":{...,"starred2":{...}}}
var raw map[string]any
_ = json.Unmarshal(w.Body.Bytes(), &raw)
resp, _ := raw["subsonic-response"].(map[string]any)
starred, _ := resp["starred2"].(map[string]any)
if starred == nil {
t.Fatalf("missing starred2 envelope: %v", raw)
}
songs, _ := starred["song"].([]any)
albums, _ := starred["album"].([]any)
artists, _ := starred["artist"].([]any)
if len(songs) != 1 || len(albums) != 1 || len(artists) != 1 {
t.Errorf("counts: songs=%d albums=%d artists=%d", len(songs), len(albums), len(artists))
}
}
func TestHandleGetStarred2_CrossUserIsolation(t *testing.T) {
pool, alice, track, _, _ := testStarPool(t)
q := dbq.New(pool)
bob, _ := q.CreateUser(context.Background(), dbq.CreateUserParams{
Username: "bob", PasswordHash: "x", ApiToken: "y", IsAdmin: false,
})
_ = q.LikeTrack(context.Background(), dbq.LikeTrackParams{UserID: alice.ID, TrackID: track.ID})
m := newStarHandlers(pool)
req := httptest.NewRequest(http.MethodGet, "/rest/getStarred2?f=json", nil)
ctx := context.WithValue(req.Context(), userCtxKey, bob)
w := httptest.NewRecorder()
m.handleGetStarred2(w, req.WithContext(ctx))
var raw map[string]any
_ = json.Unmarshal(w.Body.Bytes(), &raw)
resp, _ := raw["subsonic-response"].(map[string]any)
starred, _ := resp["starred2"].(map[string]any)
songs, _ := starred["song"].([]any)
if len(songs) != 0 {
t.Errorf("bob's starred songs should be empty, got %d", len(songs))
}
}