Add GET /api/v1/statistics/soundtracks with every soundtrack and its songs
The other soundtrack statistics are capped at 100, so a client that shows every soundtrack (the MusicPlayer statistics tab) had no way to get them all. This one has no limit and is sorted by name.
This commit is contained in:
1 parent
fc7d787014
commit
080bacae80
9 files changed
+147
-1
No files matched your search
@@ -70,6 +70,22 @@ func TestLibraryAndStatisticsWithPostgres(t *testing.T) {
|
||||
assert.Equal(t, song.Uuid, songs[0].SongUuid)
|
||||
assert.Equal(t, bird.Uuid, songs[0].SoundtrackUuid)
|
||||
|
||||
all, err := stats.AllSoundtracks(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, all)
|
||||
for i := 1; i < len(all); i++ {
|
||||
assert.LessOrEqual(t, all[i-1].SoundtrackName, all[i].SoundtrackName, "sorted by name")
|
||||
}
|
||||
for _, st := range all {
|
||||
if st.SoundtrackUuid == bird.Uuid {
|
||||
assert.Equal(t, int32(1), st.SoundtrackPlayed)
|
||||
assert.Len(t, st.Songs, 6)
|
||||
}
|
||||
if st.SoundtrackName == "Empty Soundtrack" {
|
||||
assert.Empty(t, st.Songs, "a soundtrack without songs has an empty song list")
|
||||
}
|
||||
}
|
||||
|
||||
never, err := stats.NeverPlayedSoundtracks(ctx)
|
||||
require.NoError(t, err)
|
||||
for _, st := range never {
|
||||
|
||||
@@ -59,7 +59,8 @@ type soundtrackRow interface {
|
||||
repository.GetMostPlayedSoundtracksWithSongsRow |
|
||||
repository.GetLeastPlayedSoundtracksWithSongsRow |
|
||||
repository.GetLastPlayedSoundtracksRow |
|
||||
repository.GetOldestPlayedSoundtracksRow
|
||||
repository.GetOldestPlayedSoundtracksRow |
|
||||
repository.GetAllSoundtracksWithSongsRow
|
||||
}
|
||||
|
||||
func toSoundtracks[R soundtrackRow](rows []R, err error) ([]SoundtrackWithSongs, error) {
|
||||
@@ -106,6 +107,11 @@ func parseSongs(data []byte) []SongInfoForStats {
|
||||
return songs
|
||||
}
|
||||
|
||||
// AllSoundtracks returns every soundtrack with its songs, sorted by name
|
||||
func (s *StatisticsService) AllSoundtracks(ctx context.Context) ([]SoundtrackWithSongs, error) {
|
||||
return toSoundtracks(s.q.GetAllSoundtracksWithSongs(ctx))
|
||||
}
|
||||
|
||||
// MostPlayedSoundtracks returns the top N most played soundtracks with their songs
|
||||
func (s *StatisticsService) MostPlayedSoundtracks(ctx context.Context, limit int32) ([]SoundtrackWithSongs, error) {
|
||||
return toSoundtracks(s.q.GetMostPlayedSoundtracksWithSongs(ctx, limit))
|
||||
|
||||
@@ -180,3 +180,30 @@ SELECT
|
||||
COALESCE(MIN(times_played), 0)::bigint as min_soundtrack_plays
|
||||
FROM soundtrack
|
||||
WHERE deleted IS NULL;
|
||||
|
||||
-- Every soundtrack with its songs, by name
|
||||
-- name: GetAllSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.uuid as soundtrack_uuid,
|
||||
g.soundtrack_name,
|
||||
g.times_played as soundtrack_played,
|
||||
g.last_played as soundtrack_last_played,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'song_uuid', s.uuid,
|
||||
'soundtrack_uuid', g.uuid,
|
||||
'song_name', s.song_name,
|
||||
'path', s.path,
|
||||
'times_played', s.times_played,
|
||||
'file_name', s.file_name
|
||||
)
|
||||
) FILTER (WHERE s.uuid IS NOT NULL),
|
||||
'[]'
|
||||
)::json as songs
|
||||
FROM soundtrack g
|
||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||
WHERE g.deleted IS NULL
|
||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||
ORDER BY g.soundtrack_name;
|
||||
@@ -31,6 +31,8 @@ type Querier interface {
|
||||
FindSongsFromSoundtrack(ctx context.Context, soundtrackID int32) ([]Song, error)
|
||||
GetActiveSongByUUID(ctx context.Context, argUuid uuid.UUID) (GetActiveSongByUUIDRow, error)
|
||||
GetAllSoundtracksIncludingDeleted(ctx context.Context) ([]Soundtrack, error)
|
||||
// Every soundtrack with its songs, by name
|
||||
GetAllSoundtracksWithSongs(ctx context.Context) ([]GetAllSoundtracksWithSongsRow, error)
|
||||
GetIdBySoundtrackName(ctx context.Context, soundtrackName string) (int32, error)
|
||||
// Last played soundtracks (most recently played)
|
||||
GetLastPlayedSoundtracks(ctx context.Context, limit int32) ([]GetLastPlayedSoundtracksRow, error)
|
||||
|
||||
@@ -12,6 +12,70 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getAllSoundtracksWithSongs = `-- name: GetAllSoundtracksWithSongs :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
g.uuid as soundtrack_uuid,
|
||||
g.soundtrack_name,
|
||||
g.times_played as soundtrack_played,
|
||||
g.last_played as soundtrack_last_played,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'song_uuid', s.uuid,
|
||||
'soundtrack_uuid', g.uuid,
|
||||
'song_name', s.song_name,
|
||||
'path', s.path,
|
||||
'times_played', s.times_played,
|
||||
'file_name', s.file_name
|
||||
)
|
||||
) FILTER (WHERE s.uuid IS NOT NULL),
|
||||
'[]'
|
||||
)::json as songs
|
||||
FROM soundtrack g
|
||||
LEFT JOIN song s ON g.id = s.soundtrack_id
|
||||
WHERE g.deleted IS NULL
|
||||
GROUP BY g.id, g.soundtrack_name, g.times_played, g.last_played
|
||||
ORDER BY g.soundtrack_name
|
||||
`
|
||||
|
||||
type GetAllSoundtracksWithSongsRow struct {
|
||||
SoundtrackID int32 `json:"soundtrack_id"`
|
||||
SoundtrackUuid uuid.UUID `json:"soundtrack_uuid"`
|
||||
SoundtrackName string `json:"soundtrack_name"`
|
||||
SoundtrackPlayed int32 `json:"soundtrack_played"`
|
||||
SoundtrackLastPlayed *time.Time `json:"soundtrack_last_played"`
|
||||
Songs []byte `json:"songs"`
|
||||
}
|
||||
|
||||
// Every soundtrack with its songs, by name
|
||||
func (q *Queries) GetAllSoundtracksWithSongs(ctx context.Context) ([]GetAllSoundtracksWithSongsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getAllSoundtracksWithSongs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetAllSoundtracksWithSongsRow
|
||||
for rows.Next() {
|
||||
var i GetAllSoundtracksWithSongsRow
|
||||
if err := rows.Scan(
|
||||
&i.SoundtrackID,
|
||||
&i.SoundtrackUuid,
|
||||
&i.SoundtrackName,
|
||||
&i.SoundtrackPlayed,
|
||||
&i.SoundtrackLastPlayed,
|
||||
&i.Songs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLastPlayedSoundtracks = `-- name: GetLastPlayedSoundtracks :many
|
||||
SELECT
|
||||
g.id as soundtrack_id,
|
||||
|
||||
@@ -260,6 +260,17 @@ func (s statsStore) GetMostPlayedSoundtracksWithSongs(_ context.Context, limit i
|
||||
}}, s.err
|
||||
}
|
||||
|
||||
func (s statsStore) GetAllSoundtracksWithSongs(context.Context) ([]repository.GetAllSoundtracksWithSongsRow, error) {
|
||||
return []repository.GetAllSoundtracksWithSongsRow{{
|
||||
SoundtrackUuid: statsSoundtrackUUID,
|
||||
SoundtrackName: "Game A",
|
||||
SoundtrackPlayed: 4,
|
||||
Songs: []byte(`[{"song_uuid":"` + statsSongUUID.String() + `","song_name":"Song","times_played":3}]`),
|
||||
}, {
|
||||
SoundtrackName: "Game B",
|
||||
}}, s.err
|
||||
}
|
||||
|
||||
func (s statsStore) GetStatisticsSummary(context.Context) (repository.GetStatisticsSummaryRow, error) {
|
||||
return repository.GetStatisticsSummaryRow{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, s.err
|
||||
}
|
||||
@@ -288,6 +299,13 @@ func TestStatisticsEndpoints(t *testing.T) {
|
||||
assert.Equal(t, http.StatusUnprocessableEntity, e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played?limit="+limit).Code, limit)
|
||||
}
|
||||
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks").Body.Bytes(), &soundtracks))
|
||||
require.Len(t, soundtracks, 2, "every soundtrack, no limit")
|
||||
assert.Equal(t, "Game A", soundtracks[0].SoundtrackName)
|
||||
assert.Equal(t, int32(4), soundtracks[0].SoundtrackPlayed)
|
||||
assert.Equal(t, "Song", soundtracks[0].Songs[0].SongName)
|
||||
assert.Empty(t, soundtracks[1].Songs, "no songs is an empty list")
|
||||
|
||||
var summary backend.StatisticsSummary
|
||||
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/summary").Body.Bytes(), &summary))
|
||||
assert.Equal(t, backend.StatisticsSummary{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, summary)
|
||||
@@ -295,4 +313,5 @@ func TestStatisticsEndpoints(t *testing.T) {
|
||||
e.server.stats = backend.NewStatisticsService(statsStore{err: errors.New("db down")})
|
||||
e.handler = e.server.RegisterRoutes()
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/statistics/summary").Code)
|
||||
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/statistics/soundtracks").Code)
|
||||
}
|
||||
@@ -103,6 +103,7 @@ func TestOpenAPIDocumentsTheV1API(t *testing.T) {
|
||||
"GET /api/v1/soundtracks/{uuid}",
|
||||
"GET /api/v1/statistics/songs/least-played",
|
||||
"GET /api/v1/statistics/songs/most-played",
|
||||
"GET /api/v1/statistics/soundtracks",
|
||||
"GET /api/v1/statistics/soundtracks/last-played",
|
||||
"GET /api/v1/statistics/soundtracks/least-played",
|
||||
"GET /api/v1/statistics/soundtracks/most-played",
|
||||
@@ -225,6 +226,7 @@ func TestResponsesMatchOpenAPI(t *testing.T) {
|
||||
v.check(http.MethodGet, "/api/v1/statistics"+path+"?limit=5", "", http.StatusOK)
|
||||
}
|
||||
v.check(http.MethodGet, "/api/v1/statistics/soundtracks/never-played", "", http.StatusOK)
|
||||
v.check(http.MethodGet, "/api/v1/statistics/soundtracks", "", http.StatusOK)
|
||||
v.check(http.MethodGet, "/api/v1/statistics/summary", "", http.StatusOK)
|
||||
|
||||
// Sync, including the locked state and the result after it finishes.
|
||||
@@ -254,6 +256,7 @@ func TestResponsesMatchOpenAPI(t *testing.T) {
|
||||
stats.handler = stats.server.RegisterRoutes()
|
||||
vs := newSpecValidator(t, stats)
|
||||
vs.check(http.MethodGet, "/api/v1/statistics/soundtracks/most-played", "", http.StatusOK)
|
||||
vs.check(http.MethodGet, "/api/v1/statistics/soundtracks", "", http.StatusOK)
|
||||
vs.check(http.MethodGet, "/api/v1/statistics/summary", "", http.StatusOK)
|
||||
|
||||
// Every documented operation was checked at least once (statistics and tokens use other environments).
|
||||
|
||||
@@ -59,6 +59,11 @@ func (s *Server) registerStatistics(a apis) {
|
||||
songs("get-least-played-songs", "/songs/least-played", "Least played songs",
|
||||
"Returns the least played songs with their soundtrack.", st.LeastPlayedSongs)
|
||||
|
||||
register(s, a, op("get-all-soundtrack-statistics", "/soundtracks", "All soundtracks with statistics",
|
||||
"Returns every soundtrack with its songs and play counts, sorted by name."),
|
||||
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.SoundtrackWithSongs], error) {
|
||||
return ok(st.AllSoundtracks(ctx))
|
||||
})
|
||||
register(s, a, op("get-never-played-soundtracks", "/soundtracks/never-played", "Never played soundtracks",
|
||||
"Returns all soundtracks that have never been played, with their songs."),
|
||||
func(ctx context.Context, _ *struct{}) (*jsonBody[[]backend.SoundtrackWithSongs], error) {
|
||||
|
||||
@@ -391,6 +391,10 @@ func (s *Store) GetOldestPlayedSoundtracks(context.Context, int32) ([]repository
|
||||
return nil, s.Err
|
||||
}
|
||||
|
||||
func (s *Store) GetAllSoundtracksWithSongs(context.Context) ([]repository.GetAllSoundtracksWithSongsRow, error) {
|
||||
return nil, s.Err
|
||||
}
|
||||
|
||||
func (s *Store) GetNeverPlayedSoundtracks(context.Context) ([]repository.GetNeverPlayedSoundtracksRow, error) {
|
||||
return nil, s.Err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user