diff --git a/client/api/api.ts b/client/api/api.ts index 2b0b665..ff69b78 100644 --- a/client/api/api.ts +++ b/client/api/api.ts @@ -48,32 +48,32 @@ async function getLastListens( async function getTopTracks( args: getItemsArgs -): Promise> { +): Promise>> { let url = `/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`; if (args.artist_id) url += `&artist_id=${args.artist_id}`; else if (args.album_id) url += `&album_id=${args.album_id}`; const r = await fetch(url); - return handleJson>(r); + return handleJson>>(r); } async function getTopAlbums( args: getItemsArgs -): Promise> { +): Promise>> { let url = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`; if (args.artist_id) url += `&artist_id=${args.artist_id}`; const r = await fetch(url); - return handleJson>(r); + return handleJson>>(r); } async function getTopArtists( args: getItemsArgs -): Promise> { +): Promise>> { const url = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`; const r = await fetch(url); - return handleJson>(r); + return handleJson>>(r); } async function getActivity( @@ -407,6 +407,10 @@ type PaginatedResponse = { current_page: number; items_per_page: number; }; +type Ranked = { + item: T; + rank: number; +}; type ListenActivityItem = { start_time: Date; listens: number; @@ -480,6 +484,7 @@ export type { Listen, SearchResponse, PaginatedResponse, + Ranked, ListenActivityItem, InterestBucket, User, diff --git a/client/app/components/TopItemList.tsx b/client/app/components/TopItemList.tsx index adb60ce..4d355b7 100644 --- a/client/app/components/TopItemList.tsx +++ b/client/app/components/TopItemList.tsx @@ -6,11 +6,12 @@ import { type Artist, type Track, type PaginatedResponse, + type Ranked, } from "api/api"; type Item = Album | Track | Artist; -interface Props { +interface Props> { data: PaginatedResponse; separators?: ConstrainBoolean; ranked?: boolean; @@ -18,33 +19,17 @@ interface Props { className?: string; } -export default function TopItemList({ +export default function TopItemList>({ data, separators, type, className, ranked, }: Props) { - const currentParams = new URLSearchParams(location.search); - const page = Math.max(parseInt(currentParams.get("page") || "1"), 1); - - let lastRank = 0; - - const calculateRank = (data: Item[], page: number, index: number): number => { - if ( - index === 0 || - data[index] == undefined || - !(data[index].listen_count === data[index - 1].listen_count) - ) { - lastRank = index + 1 + (page - 1) * 100; - } - return lastRank; - }; - return (
{data.items.map((item, index) => { - const key = `${type}-${item.id}`; + const key = `${type}-${item.item.id}`; return (
({ >
); diff --git a/client/app/routes/Charts/AlbumChart.tsx b/client/app/routes/Charts/AlbumChart.tsx index 96370a9..7bc4eea 100644 --- a/client/app/routes/Charts/AlbumChart.tsx +++ b/client/app/routes/Charts/AlbumChart.tsx @@ -1,7 +1,7 @@ import TopItemList from "~/components/TopItemList"; import ChartLayout from "./ChartLayout"; import { useLoaderData, type LoaderFunctionArgs } from "react-router"; -import { type Album, type PaginatedResponse } from "api/api"; +import { type Album, type PaginatedResponse, type Ranked } from "api/api"; export async function clientLoader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) { export default function AlbumChart() { const { top_albums: initialData } = useLoaderData<{ - top_albums: PaginatedResponse; + top_albums: PaginatedResponse>; }>(); return ( diff --git a/client/app/routes/Charts/ArtistChart.tsx b/client/app/routes/Charts/ArtistChart.tsx index 676700d..f128027 100644 --- a/client/app/routes/Charts/ArtistChart.tsx +++ b/client/app/routes/Charts/ArtistChart.tsx @@ -1,7 +1,7 @@ import TopItemList from "~/components/TopItemList"; import ChartLayout from "./ChartLayout"; import { useLoaderData, type LoaderFunctionArgs } from "react-router"; -import { type Album, type PaginatedResponse } from "api/api"; +import { type Album, type PaginatedResponse, type Ranked } from "api/api"; export async function clientLoader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -21,7 +21,7 @@ export async function clientLoader({ request }: LoaderFunctionArgs) { export default function Artist() { const { top_artists: initialData } = useLoaderData<{ - top_artists: PaginatedResponse; + top_artists: PaginatedResponse>; }>(); return ( diff --git a/client/app/routes/Charts/TrackChart.tsx b/client/app/routes/Charts/TrackChart.tsx index 9e8ee08..023dceb 100644 --- a/client/app/routes/Charts/TrackChart.tsx +++ b/client/app/routes/Charts/TrackChart.tsx @@ -1,7 +1,7 @@ import TopItemList from "~/components/TopItemList"; import ChartLayout from "./ChartLayout"; import { useLoaderData, type LoaderFunctionArgs } from "react-router"; -import { type Album, type PaginatedResponse } from "api/api"; +import { type Track, type PaginatedResponse, type Ranked } from "api/api"; export async function clientLoader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); @@ -15,13 +15,13 @@ export async function clientLoader({ request }: LoaderFunctionArgs) { throw new Response("Failed to load top tracks", { status: 500 }); } - const top_tracks: PaginatedResponse = await res.json(); + const top_tracks: PaginatedResponse = await res.json(); return { top_tracks }; } export default function TrackChart() { const { top_tracks: initialData } = useLoaderData<{ - top_tracks: PaginatedResponse; + top_tracks: PaginatedResponse>; }>(); return ( diff --git a/db/queries/artist.sql b/db/queries/artist.sql index e20326d..863de32 100644 --- a/db/queries/artist.sql +++ b/db/queries/artist.sql @@ -58,18 +58,27 @@ GROUP BY a.id, a.musicbrainz_id, a.image, a.image_source, a.name; -- name: GetTopArtistsPaginated :many SELECT + x.id, + x.name, + x.musicbrainz_id, + x.image, + x.listen_count, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT a.id, a.name, a.musicbrainz_id, a.image, COUNT(*) AS listen_count -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN artist_tracks at ON at.track_id = t.id -JOIN artists_with_name a ON a.id = at.artist_id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY a.id, a.name, a.musicbrainz_id, a.image, a.image_source, a.name -ORDER BY listen_count DESC, a.id + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON at.track_id = t.id + JOIN artists_with_name a ON a.id = at.artist_id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY a.id, a.name, a.musicbrainz_id, a.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: CountTopArtists :one diff --git a/db/queries/release.sql b/db/queries/release.sql index 9f54291..cb548ed 100644 --- a/db/queries/release.sql +++ b/db/queries/release.sql @@ -47,30 +47,40 @@ WHERE r.title = ANY ($1::TEXT[]) -- name: GetTopReleasesFromArtist :many SELECT - r.*, - COUNT(*) AS listen_count, - get_artists_for_release(r.id) AS artists -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN releases_with_title r ON t.release_id = r.id -JOIN artist_releases ar ON r.id = ar.release_id -WHERE ar.artist_id = $5 - AND l.listened_at BETWEEN $1 AND $2 -GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source -ORDER BY listen_count DESC, r.id + x.*, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + r.*, + COUNT(*) AS listen_count, + get_artists_for_release(r.id) AS artists + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN releases_with_title r ON t.release_id = r.id + JOIN artist_releases ar ON r.id = ar.release_id + WHERE ar.artist_id = $5 + AND l.listened_at BETWEEN $1 AND $2 + GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source +) x +ORDER BY listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: GetTopReleasesPaginated :many SELECT - r.*, - COUNT(*) AS listen_count, - get_artists_for_release(r.id) AS artists -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN releases_with_title r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source -ORDER BY listen_count DESC, r.id + x.*, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + r.*, + COUNT(*) AS listen_count, + get_artists_for_release(r.id) AS artists + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN releases_with_title r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source +) x +ORDER BY listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: CountTopReleases :one diff --git a/db/queries/track.sql b/db/queries/track.sql index 933fcc1..24be467 100644 --- a/db/queries/track.sql +++ b/db/queries/track.sql @@ -39,56 +39,89 @@ HAVING COUNT(DISTINCT at.artist_id) = cardinality($3::int[]); -- name: GetTopTracksPaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: GetTopTracksByArtistPaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -JOIN artist_tracks at ON at.track_id = t.id -WHERE l.listened_at BETWEEN $1 AND $2 - AND at.artist_id = $5 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + JOIN artist_tracks at ON at.track_id = t.id + WHERE l.listened_at BETWEEN $1 AND $2 + AND at.artist_id = $5 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: GetTopTracksInReleasePaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 - AND t.release_id = $5 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + AND t.release_id = $5 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; -- name: CountTopTracks :one diff --git a/internal/db/db.go b/internal/db/db.go index 4695967..a0f0f80 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -19,9 +19,9 @@ type DB interface { GetTracksWithNoDurationButHaveMbzID(ctx context.Context, from int32) ([]*models.Track, error) GetArtistsForAlbum(ctx context.Context, id int32) ([]*models.Artist, error) GetArtistsForTrack(ctx context.Context, id int32) ([]*models.Artist, error) - GetTopTracksPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Track], error) - GetTopArtistsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Artist], error) - GetTopAlbumsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Album], error) + GetTopTracksPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Track]], error) + GetTopArtistsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Artist]], error) + GetTopAlbumsPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[RankedItem[*models.Album]], error) GetListensPaginated(ctx context.Context, opts GetItemsOpts) (*PaginatedResponse[*models.Listen], error) GetListenActivity(ctx context.Context, opts ListenActivityOpts) ([]ListenActivityItem, error) GetAllArtistAliases(ctx context.Context, id int32) ([]models.Alias, error) diff --git a/internal/db/psql/top_albums.go b/internal/db/psql/top_albums.go index 8610ce5..652b790 100644 --- a/internal/db/psql/top_albums.go +++ b/internal/db/psql/top_albums.go @@ -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), diff --git a/internal/db/psql/top_albums_test.go b/internal/db/psql/top_albums_test.go index ff0efef..eb4efde 100644 --- a/internal/db/psql/top_albums_test.go +++ b/internal/db/psql/top_albums_test.go @@ -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) } diff --git a/internal/db/psql/top_artists.go b/internal/db/psql/top_artists.go index f66f082..497efbd 100644 --- a/internal/db/psql/top_artists.go +++ b/internal/db/psql/top_artists.go @@ -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), diff --git a/internal/db/psql/top_artists_test.go b/internal/db/psql/top_artists_test.go index 182d96e..7a69ab5 100644 --- a/internal/db/psql/top_artists_test.go +++ b/internal/db/psql/top_artists_test.go @@ -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) } diff --git a/internal/db/psql/top_tracks.go b/internal/db/psql/top_tracks.go index da34efc..89960e8 100644 --- a/internal/db/psql/top_tracks.go +++ b/internal/db/psql/top_tracks.go @@ -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), diff --git a/internal/db/psql/top_tracks_test.go b/internal/db/psql/top_tracks_test.go index 15f898f..934d9b7 100644 --- a/internal/db/psql/top_tracks_test.go +++ b/internal/db/psql/top_tracks_test.go @@ -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) } diff --git a/internal/db/types.go b/internal/db/types.go index 93ff031..46d3c01 100644 --- a/internal/db/types.go +++ b/internal/db/types.go @@ -28,6 +28,11 @@ type PaginatedResponse[T any] struct { CurrentPage int32 `json:"current_page"` } +type RankedItem[T any] struct { + Item T `json:"item"` + Rank int64 `json:"rank"` +} + type ExportItem struct { ListenedAt time.Time UserID int32 diff --git a/internal/repository/artist.sql.go b/internal/repository/artist.sql.go index 3d33446..3722291 100644 --- a/internal/repository/artist.sql.go +++ b/internal/repository/artist.sql.go @@ -269,18 +269,27 @@ func (q *Queries) GetReleaseArtists(ctx context.Context, releaseID int32) ([]Get const getTopArtistsPaginated = `-- name: GetTopArtistsPaginated :many SELECT + x.id, + x.name, + x.musicbrainz_id, + x.image, + x.listen_count, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT a.id, a.name, a.musicbrainz_id, a.image, COUNT(*) AS listen_count -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN artist_tracks at ON at.track_id = t.id -JOIN artists_with_name a ON a.id = at.artist_id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY a.id, a.name, a.musicbrainz_id, a.image, a.image_source, a.name -ORDER BY listen_count DESC, a.id + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON at.track_id = t.id + JOIN artists_with_name a ON a.id = at.artist_id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY a.id, a.name, a.musicbrainz_id, a.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -297,6 +306,7 @@ type GetTopArtistsPaginatedRow struct { MusicBrainzID *uuid.UUID Image *uuid.UUID ListenCount int64 + Rank int64 } func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsPaginatedParams) ([]GetTopArtistsPaginatedRow, error) { @@ -319,6 +329,7 @@ func (q *Queries) GetTopArtistsPaginated(ctx context.Context, arg GetTopArtistsP &i.MusicBrainzID, &i.Image, &i.ListenCount, + &i.Rank, ); err != nil { return nil, err } diff --git a/internal/repository/release.sql.go b/internal/repository/release.sql.go index 3d77eef..76789d0 100644 --- a/internal/repository/release.sql.go +++ b/internal/repository/release.sql.go @@ -321,17 +321,22 @@ func (q *Queries) GetReleasesWithoutImages(ctx context.Context, arg GetReleasesW const getTopReleasesFromArtist = `-- name: GetTopReleasesFromArtist :many SELECT - r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title, - COUNT(*) AS listen_count, - get_artists_for_release(r.id) AS artists -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN releases_with_title r ON t.release_id = r.id -JOIN artist_releases ar ON r.id = ar.release_id -WHERE ar.artist_id = $5 - AND l.listened_at BETWEEN $1 AND $2 -GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source -ORDER BY listen_count DESC, r.id + x.id, x.musicbrainz_id, x.image, x.various_artists, x.image_source, x.title, x.listen_count, x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title, + COUNT(*) AS listen_count, + get_artists_for_release(r.id) AS artists + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN releases_with_title r ON t.release_id = r.id + JOIN artist_releases ar ON r.id = ar.release_id + WHERE ar.artist_id = $5 + AND l.listened_at BETWEEN $1 AND $2 + GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source +) x +ORDER BY listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -352,6 +357,7 @@ type GetTopReleasesFromArtistRow struct { Title string ListenCount int64 Artists []byte + Rank int64 } func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleasesFromArtistParams) ([]GetTopReleasesFromArtistRow, error) { @@ -378,6 +384,7 @@ func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleas &i.Title, &i.ListenCount, &i.Artists, + &i.Rank, ); err != nil { return nil, err } @@ -391,15 +398,20 @@ func (q *Queries) GetTopReleasesFromArtist(ctx context.Context, arg GetTopReleas const getTopReleasesPaginated = `-- name: GetTopReleasesPaginated :many SELECT - r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title, - COUNT(*) AS listen_count, - get_artists_for_release(r.id) AS artists -FROM listens l -JOIN tracks t ON l.track_id = t.id -JOIN releases_with_title r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source -ORDER BY listen_count DESC, r.id + x.id, x.musicbrainz_id, x.image, x.various_artists, x.image_source, x.title, x.listen_count, x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title, + COUNT(*) AS listen_count, + get_artists_for_release(r.id) AS artists + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN releases_with_title r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY r.id, r.title, r.musicbrainz_id, r.various_artists, r.image, r.image_source +) x +ORDER BY listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -419,6 +431,7 @@ type GetTopReleasesPaginatedRow struct { Title string ListenCount int64 Artists []byte + Rank int64 } func (q *Queries) GetTopReleasesPaginated(ctx context.Context, arg GetTopReleasesPaginatedParams) ([]GetTopReleasesPaginatedRow, error) { @@ -444,6 +457,7 @@ func (q *Queries) GetTopReleasesPaginated(ctx context.Context, arg GetTopRelease &i.Title, &i.ListenCount, &i.Artists, + &i.Rank, ); err != nil { return nil, err } diff --git a/internal/repository/track.sql.go b/internal/repository/track.sql.go index 6b11b01..a18d87a 100644 --- a/internal/repository/track.sql.go +++ b/internal/repository/track.sql.go @@ -155,21 +155,32 @@ func (q *Queries) GetAllTracksFromArtist(ctx context.Context, artistID int32) ([ const getTopTracksByArtistPaginated = `-- name: GetTopTracksByArtistPaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -JOIN artist_tracks at ON at.track_id = t.id -WHERE l.listened_at BETWEEN $1 AND $2 - AND at.artist_id = $5 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + JOIN artist_tracks at ON at.track_id = t.id + WHERE l.listened_at BETWEEN $1 AND $2 + AND at.artist_id = $5 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -189,6 +200,7 @@ type GetTopTracksByArtistPaginatedRow struct { Image *uuid.UUID ListenCount int64 Artists []byte + Rank int64 } func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopTracksByArtistPaginatedParams) ([]GetTopTracksByArtistPaginatedRow, error) { @@ -214,6 +226,7 @@ func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopT &i.Image, &i.ListenCount, &i.Artists, + &i.Rank, ); err != nil { return nil, err } @@ -227,20 +240,31 @@ func (q *Queries) GetTopTracksByArtistPaginated(ctx context.Context, arg GetTopT const getTopTracksInReleasePaginated = `-- name: GetTopTracksInReleasePaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 - AND t.release_id = $5 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + AND t.release_id = $5 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -260,6 +284,7 @@ type GetTopTracksInReleasePaginatedRow struct { Image *uuid.UUID ListenCount int64 Artists []byte + Rank int64 } func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTopTracksInReleasePaginatedParams) ([]GetTopTracksInReleasePaginatedRow, error) { @@ -285,6 +310,7 @@ func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTop &i.Image, &i.ListenCount, &i.Artists, + &i.Rank, ); err != nil { return nil, err } @@ -298,19 +324,30 @@ func (q *Queries) GetTopTracksInReleasePaginated(ctx context.Context, arg GetTop const getTopTracksPaginated = `-- name: GetTopTracksPaginated :many SELECT - t.id, - t.title, - t.musicbrainz_id, - t.release_id, - r.image, - COUNT(*) AS listen_count, - get_artists_for_track(t.id) AS artists -FROM listens l -JOIN tracks_with_title t ON l.track_id = t.id -JOIN releases r ON t.release_id = r.id -WHERE l.listened_at BETWEEN $1 AND $2 -GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image -ORDER BY listen_count DESC, t.id + x.id, + x.title, + x.musicbrainz_id, + x.release_id, + x.image, + x.listen_count, + x.artists, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank +FROM ( + SELECT + t.id, + t.title, + t.musicbrainz_id, + t.release_id, + r.image, + COUNT(*) AS listen_count, + get_artists_for_track(t.id) AS artists + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + JOIN releases r ON t.release_id = r.id + WHERE l.listened_at BETWEEN $1 AND $2 + GROUP BY t.id, t.title, t.musicbrainz_id, t.release_id, r.image +) x +ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4 ` @@ -329,6 +366,7 @@ type GetTopTracksPaginatedRow struct { Image *uuid.UUID ListenCount int64 Artists []byte + Rank int64 } func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPaginatedParams) ([]GetTopTracksPaginatedRow, error) { @@ -353,6 +391,7 @@ func (q *Queries) GetTopTracksPaginated(ctx context.Context, arg GetTopTracksPag &i.Image, &i.ListenCount, &i.Artists, + &i.Rank, ); err != nil { return nil, err } diff --git a/internal/summary/summary.go b/internal/summary/summary.go index 518121f..7a2b9d7 100644 --- a/internal/summary/summary.go +++ b/internal/summary/summary.go @@ -9,20 +9,20 @@ import ( ) type Summary struct { - Title string `json:"title,omitempty"` - TopArtists []*models.Artist `json:"top_artists"` // ListenCount and TimeListened are overriden with stats from timeframe - TopAlbums []*models.Album `json:"top_albums"` // ListenCount and TimeListened are overriden with stats from timeframe - TopTracks []*models.Track `json:"top_tracks"` // ListenCount and TimeListened are overriden with stats from timeframe - MinutesListened int `json:"minutes_listened"` - AvgMinutesPerDay int `json:"avg_minutes_listened_per_day"` - Plays int `json:"plays"` - AvgPlaysPerDay float32 `json:"avg_plays_per_day"` - UniqueTracks int `json:"unique_tracks"` - UniqueAlbums int `json:"unique_albums"` - UniqueArtists int `json:"unique_artists"` - NewTracks int `json:"new_tracks"` - NewAlbums int `json:"new_albums"` - NewArtists int `json:"new_artists"` + Title string `json:"title,omitempty"` + TopArtists []db.RankedItem[*models.Artist] `json:"top_artists"` // ListenCount and TimeListened are overriden with stats from timeframe + TopAlbums []db.RankedItem[*models.Album] `json:"top_albums"` // ListenCount and TimeListened are overriden with stats from timeframe + TopTracks []db.RankedItem[*models.Track] `json:"top_tracks"` // ListenCount and TimeListened are overriden with stats from timeframe + MinutesListened int `json:"minutes_listened"` + AvgMinutesPerDay int `json:"avg_minutes_listened_per_day"` + Plays int `json:"plays"` + AvgPlaysPerDay float32 `json:"avg_plays_per_day"` + UniqueTracks int `json:"unique_tracks"` + UniqueAlbums int `json:"unique_albums"` + UniqueArtists int `json:"unique_artists"` + NewTracks int `json:"new_tracks"` + NewAlbums int `json:"new_albums"` + NewArtists int `json:"new_artists"` } func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe db.Timeframe, title string) (summary *Summary, err error) { @@ -37,16 +37,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d summary.TopArtists = topArtists.Items // replace ListenCount and TimeListened with stats from timeframe for i, artist := range summary.TopArtists { - timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe}) + timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ArtistID: artist.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{ArtistID: artist.ID, Timeframe: timeframe}) + listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{ArtistID: artist.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - summary.TopArtists[i].TimeListened = timelistened - summary.TopArtists[i].ListenCount = listens + summary.TopArtists[i].Item.TimeListened = timelistened + summary.TopArtists[i].Item.ListenCount = listens } topAlbums, err := store.GetTopAlbumsPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe}) @@ -56,16 +56,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d summary.TopAlbums = topAlbums.Items // replace ListenCount and TimeListened with stats from timeframe for i, album := range summary.TopAlbums { - timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe}) + timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{AlbumID: album.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{AlbumID: album.ID, Timeframe: timeframe}) + listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{AlbumID: album.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - summary.TopAlbums[i].TimeListened = timelistened - summary.TopAlbums[i].ListenCount = listens + summary.TopAlbums[i].Item.TimeListened = timelistened + summary.TopAlbums[i].Item.ListenCount = listens } topTracks, err := store.GetTopTracksPaginated(ctx, db.GetItemsOpts{Page: 1, Limit: 5, Timeframe: timeframe}) @@ -75,16 +75,16 @@ func GenerateSummary(ctx context.Context, store db.DB, userId int32, timeframe d summary.TopTracks = topTracks.Items // replace ListenCount and TimeListened with stats from timeframe for i, track := range summary.TopTracks { - timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe}) + timelistened, err := store.CountTimeListenedToItem(ctx, db.TimeListenedOpts{TrackID: track.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{TrackID: track.ID, Timeframe: timeframe}) + listens, err := store.CountListensToItem(ctx, db.TimeListenedOpts{TrackID: track.Item.ID, Timeframe: timeframe}) if err != nil { return nil, fmt.Errorf("GenerateSummary: %w", err) } - summary.TopTracks[i].TimeListened = timelistened - summary.TopTracks[i].ListenCount = listens + summary.TopTracks[i].Item.TimeListened = timelistened + summary.TopTracks[i].Item.ListenCount = listens } t1, t2 := db.TimeframeToTimeRange(timeframe)