fix: use sql rank (#148)

This commit is contained in:
Gabe Farrell 2026-01-15 21:08:30 -05:00 committed by GitHub
parent aa7fddd518
commit d2d6924e05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 386 additions and 270 deletions

View file

@ -11,7 +11,7 @@ import (
"github.com/gabehf/koito/internal/repository"
)
func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Album], error) {
func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Album]], error) {
l := logger.FromContext(ctx)
offset := (opts.Page - 1) * opts.Limit
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
@ -19,7 +19,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
opts.Limit = DefaultItemsPerPage
}
var rgs []*models.Album
var rgs []db.RankedItem[*models.Album]
var count int64
if opts.ArtistID != 0 {
@ -36,7 +36,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesFromArtist: %w", err)
}
rgs = make([]*models.Album, len(rows))
rgs = make([]db.RankedItem[*models.Album], len(rows))
l.Debug().Msgf("Database responded with %d items", len(rows))
for i, v := range rows {
artists := make([]models.SimpleArtist, 0)
@ -45,7 +45,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", v.ID)
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
}
rgs[i] = &models.Album{
rgs[i].Item = &models.Album{
ID: v.ID,
MbzID: v.MusicBrainzID,
Title: v.Title,
@ -54,6 +54,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
VariousArtists: v.VariousArtists,
ListenCount: v.ListenCount,
}
rgs[i].Rank = v.Rank
}
count, err = d.q.CountReleasesFromArtist(ctx, int32(opts.ArtistID))
if err != nil {
@ -71,7 +72,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopAlbumsPaginated: GetTopReleasesPaginated: %w", err)
}
rgs = make([]*models.Album, len(rows))
rgs = make([]db.RankedItem[*models.Album], len(rows))
l.Debug().Msgf("Database responded with %d items", len(rows))
for i, row := range rows {
artists := make([]models.SimpleArtist, 0)
@ -80,16 +81,16 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
l.Err(err).Msgf("Error unmarshalling artists for release group with id %d", row.ID)
return nil, fmt.Errorf("GetTopAlbumsPaginated: Unmarshal: %w", err)
}
t := &models.Album{
Title: row.Title,
MbzID: row.MusicBrainzID,
rgs[i].Item = &models.Album{
ID: row.ID,
MbzID: row.MusicBrainzID,
Title: row.Title,
Image: row.Image,
Artists: artists,
VariousArtists: row.VariousArtists,
ListenCount: row.ListenCount,
}
rgs[i] = t
rgs[i].Rank = row.Rank
}
count, err = d.q.CountTopReleases(ctx, repository.CountTopReleasesParams{
ListenedAt: t1,
@ -100,7 +101,7 @@ func (d *Psql) GetTopAlbumsPaginated(ctx context.Context, opts db.GetItemsOpts)
}
l.Debug().Msgf("Database responded with %d albums out of a total %d", len(rows), count)
}
return &db.PaginatedResponse[*models.Album]{
return &db.PaginatedResponse[db.RankedItem[*models.Album]]{
Items: rgs,
TotalCount: count,
ItemsPerPage: int32(opts.Limit),

View file

@ -18,16 +18,16 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 4)
assert.Equal(t, int64(4), resp.TotalCount)
assert.Equal(t, "Release One", resp.Items[0].Title)
assert.Equal(t, "Release Two", resp.Items[1].Title)
assert.Equal(t, "Release Three", resp.Items[2].Title)
assert.Equal(t, "Release Four", resp.Items[3].Title)
assert.Equal(t, "Release One", resp.Items[0].Item.Title)
assert.Equal(t, "Release Two", resp.Items[1].Item.Title)
assert.Equal(t, "Release Three", resp.Items[2].Item.Title)
assert.Equal(t, "Release Four", resp.Items[3].Item.Title)
// Test pagination
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, "Release Two", resp.Items[0].Title)
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
// Test page out of range
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
@ -57,29 +57,29 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Release Four", resp.Items[0].Title)
assert.Equal(t, "Release Four", resp.Items[0].Item.Title)
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
require.NoError(t, err)
require.Len(t, resp.Items, 2)
assert.Equal(t, int64(2), resp.TotalCount)
assert.Equal(t, "Release Three", resp.Items[0].Title)
assert.Equal(t, "Release Four", resp.Items[1].Title)
assert.Equal(t, "Release Three", resp.Items[0].Item.Title)
assert.Equal(t, "Release Four", resp.Items[1].Item.Title)
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
require.NoError(t, err)
require.Len(t, resp.Items, 3)
assert.Equal(t, int64(3), resp.TotalCount)
assert.Equal(t, "Release Two", resp.Items[0].Title)
assert.Equal(t, "Release Three", resp.Items[1].Title)
assert.Equal(t, "Release Four", resp.Items[2].Title)
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
assert.Equal(t, "Release Three", resp.Items[1].Item.Title)
assert.Equal(t, "Release Four", resp.Items[2].Item.Title)
// test specific artist
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}, ArtistID: 2})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Release Two", resp.Items[0].Title)
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
// Test specify dates
@ -89,11 +89,11 @@ func TestGetTopAlbumsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Release One", resp.Items[0].Title)
assert.Equal(t, "Release One", resp.Items[0].Item.Title)
resp, err = store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Release Two", resp.Items[0].Title)
assert.Equal(t, "Release Two", resp.Items[0].Item.Title)
}

View file

@ -10,7 +10,7 @@ import (
"github.com/gabehf/koito/internal/repository"
)
func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Artist], error) {
func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Artist]], error) {
l := logger.FromContext(ctx)
offset := (opts.Page - 1) * opts.Limit
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
@ -28,7 +28,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopArtistsPaginated: GetTopArtistsPaginated: %w", err)
}
rgs := make([]*models.Artist, len(rows))
rgs := make([]db.RankedItem[*models.Artist], len(rows))
for i, row := range rows {
t := &models.Artist{
Name: row.Name,
@ -37,7 +37,8 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
Image: row.Image,
ListenCount: row.ListenCount,
}
rgs[i] = t
rgs[i].Item = t
rgs[i].Rank = row.Rank
}
count, err := d.q.CountTopArtists(ctx, repository.CountTopArtistsParams{
ListenedAt: t1,
@ -48,7 +49,7 @@ func (d *Psql) GetTopArtistsPaginated(ctx context.Context, opts db.GetItemsOpts)
}
l.Debug().Msgf("Database responded with %d artists out of a total %d", len(rows), count)
return &db.PaginatedResponse[*models.Artist]{
return &db.PaginatedResponse[db.RankedItem[*models.Artist]]{
Items: rgs,
TotalCount: count,
ItemsPerPage: int32(opts.Limit),

View file

@ -18,16 +18,16 @@ func TestGetTopArtistsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 4)
assert.Equal(t, int64(4), resp.TotalCount)
assert.Equal(t, "Artist One", resp.Items[0].Name)
assert.Equal(t, "Artist Two", resp.Items[1].Name)
assert.Equal(t, "Artist Three", resp.Items[2].Name)
assert.Equal(t, "Artist Four", resp.Items[3].Name)
assert.Equal(t, "Artist One", resp.Items[0].Item.Name)
assert.Equal(t, "Artist Two", resp.Items[1].Item.Name)
assert.Equal(t, "Artist Three", resp.Items[2].Item.Name)
assert.Equal(t, "Artist Four", resp.Items[3].Item.Name)
// Test pagination
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, "Artist Two", resp.Items[0].Name)
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
// Test page out of range
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
@ -57,22 +57,22 @@ func TestGetTopArtistsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Artist Four", resp.Items[0].Name)
assert.Equal(t, "Artist Four", resp.Items[0].Item.Name)
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
require.NoError(t, err)
require.Len(t, resp.Items, 2)
assert.Equal(t, int64(2), resp.TotalCount)
assert.Equal(t, "Artist Three", resp.Items[0].Name)
assert.Equal(t, "Artist Four", resp.Items[1].Name)
assert.Equal(t, "Artist Three", resp.Items[0].Item.Name)
assert.Equal(t, "Artist Four", resp.Items[1].Item.Name)
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
require.NoError(t, err)
require.Len(t, resp.Items, 3)
assert.Equal(t, int64(3), resp.TotalCount)
assert.Equal(t, "Artist Two", resp.Items[0].Name)
assert.Equal(t, "Artist Three", resp.Items[1].Name)
assert.Equal(t, "Artist Four", resp.Items[2].Name)
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
assert.Equal(t, "Artist Three", resp.Items[1].Item.Name)
assert.Equal(t, "Artist Four", resp.Items[2].Item.Name)
// Test specify dates
@ -82,11 +82,11 @@ func TestGetTopArtistsPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Artist One", resp.Items[0].Name)
assert.Equal(t, "Artist One", resp.Items[0].Item.Name)
resp, err = store.GetTopArtistsPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Artist Two", resp.Items[0].Name)
assert.Equal(t, "Artist Two", resp.Items[0].Item.Name)
}

View file

@ -11,14 +11,14 @@ import (
"github.com/gabehf/koito/internal/repository"
)
func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[*models.Track], error) {
func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts) (*db.PaginatedResponse[db.RankedItem[*models.Track]], error) {
l := logger.FromContext(ctx)
offset := (opts.Page - 1) * opts.Limit
t1, t2 := db.TimeframeToTimeRange(opts.Timeframe)
if opts.Limit == 0 {
opts.Limit = DefaultItemsPerPage
}
var tracks []*models.Track
var tracks []db.RankedItem[*models.Track]
var count int64
if opts.AlbumID > 0 {
l.Debug().Msgf("Fetching top %d tracks on page %d from range %v to %v",
@ -33,7 +33,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksInReleasePaginated: %w", err)
}
tracks = make([]*models.Track, len(rows))
tracks = make([]db.RankedItem[*models.Track], len(rows))
for i, row := range rows {
artists := make([]models.SimpleArtist, 0)
err = json.Unmarshal(row.Artists, &artists)
@ -50,7 +50,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
AlbumID: row.ReleaseID,
Artists: artists,
}
tracks[i] = t
tracks[i].Item = t
tracks[i].Rank = row.Rank
}
count, err = d.q.CountTopTracksByRelease(ctx, repository.CountTopTracksByReleaseParams{
ListenedAt: t1,
@ -73,7 +74,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksByArtistPaginated: %w", err)
}
tracks = make([]*models.Track, len(rows))
tracks = make([]db.RankedItem[*models.Track], len(rows))
for i, row := range rows {
artists := make([]models.SimpleArtist, 0)
err = json.Unmarshal(row.Artists, &artists)
@ -90,7 +91,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
AlbumID: row.ReleaseID,
Artists: artists,
}
tracks[i] = t
tracks[i].Item = t
tracks[i].Rank = row.Rank
}
count, err = d.q.CountTopTracksByArtist(ctx, repository.CountTopTracksByArtistParams{
ListenedAt: t1,
@ -112,7 +114,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
if err != nil {
return nil, fmt.Errorf("GetTopTracksPaginated: GetTopTracksPaginated: %w", err)
}
tracks = make([]*models.Track, len(rows))
tracks = make([]db.RankedItem[*models.Track], len(rows))
for i, row := range rows {
artists := make([]models.SimpleArtist, 0)
err = json.Unmarshal(row.Artists, &artists)
@ -129,7 +131,8 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
AlbumID: row.ReleaseID,
Artists: artists,
}
tracks[i] = t
tracks[i].Item = t
tracks[i].Rank = row.Rank
}
count, err = d.q.CountTopTracks(ctx, repository.CountTopTracksParams{
ListenedAt: t1,
@ -141,7 +144,7 @@ func (d *Psql) GetTopTracksPaginated(ctx context.Context, opts db.GetItemsOpts)
l.Debug().Msgf("Database responded with %d tracks out of a total %d", len(rows), count)
}
return &db.PaginatedResponse[*models.Track]{
return &db.PaginatedResponse[db.RankedItem[*models.Track]]{
Items: tracks,
TotalCount: count,
ItemsPerPage: int32(opts.Limit),

View file

@ -18,19 +18,19 @@ func TestGetTopTracksPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 4)
assert.Equal(t, int64(4), resp.TotalCount)
assert.Equal(t, "Track One", resp.Items[0].Title)
assert.Equal(t, "Track Two", resp.Items[1].Title)
assert.Equal(t, "Track Three", resp.Items[2].Title)
assert.Equal(t, "Track Four", resp.Items[3].Title)
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
assert.Equal(t, "Track Two", resp.Items[1].Item.Title)
assert.Equal(t, "Track Three", resp.Items[2].Item.Title)
assert.Equal(t, "Track Four", resp.Items[3].Item.Title)
// ensure artists are included
require.Len(t, resp.Items[0].Artists, 1)
assert.Equal(t, "Artist One", resp.Items[0].Artists[0].Name)
require.Len(t, resp.Items[0].Item.Artists, 1)
assert.Equal(t, "Artist One", resp.Items[0].Item.Artists[0].Name)
// Test pagination
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 2, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, "Track Two", resp.Items[0].Title)
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
// Test page out of range
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Limit: 1, Page: 10, Timeframe: db.Timeframe{Period: db.PeriodAllTime}})
@ -60,41 +60,41 @@ func TestGetTopTracksPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track Four", resp.Items[0].Title)
assert.Equal(t, "Track Four", resp.Items[0].Item.Title)
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodMonth}})
require.NoError(t, err)
require.Len(t, resp.Items, 2)
assert.Equal(t, int64(2), resp.TotalCount)
assert.Equal(t, "Track Three", resp.Items[0].Title)
assert.Equal(t, "Track Four", resp.Items[1].Title)
assert.Equal(t, "Track Three", resp.Items[0].Item.Title)
assert.Equal(t, "Track Four", resp.Items[1].Item.Title)
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodYear}})
require.NoError(t, err)
require.Len(t, resp.Items, 3)
assert.Equal(t, int64(3), resp.TotalCount)
assert.Equal(t, "Track Two", resp.Items[0].Title)
assert.Equal(t, "Track Three", resp.Items[1].Title)
assert.Equal(t, "Track Four", resp.Items[2].Title)
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
assert.Equal(t, "Track Three", resp.Items[1].Item.Title)
assert.Equal(t, "Track Four", resp.Items[2].Item.Title)
// Test filter by artists and releases
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, ArtistID: 1})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track One", resp.Items[0].Title)
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track Two", resp.Items[0].Title)
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
// when both artistID and albumID are specified, artist id is ignored
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Period: db.PeriodAllTime}, AlbumID: 2, ArtistID: 1})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track Two", resp.Items[0].Title)
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
// Test specify dates
@ -104,11 +104,11 @@ func TestGetTopTracksPaginated(t *testing.T) {
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track One", resp.Items[0].Title)
assert.Equal(t, "Track One", resp.Items[0].Item.Title)
resp, err = store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Timeframe: db.Timeframe{Month: 6, Year: 2024}})
require.NoError(t, err)
require.Len(t, resp.Items, 1)
assert.Equal(t, int64(1), resp.TotalCount)
assert.Equal(t, "Track Two", resp.Items[0].Title)
assert.Equal(t, "Track Two", resp.Items[0].Item.Title)
}