diff --git a/client/api/api.ts b/client/api/api.ts index ff69b78..bd2430b 100644 --- a/client/api/api.ts +++ b/client/api/api.ts @@ -367,6 +367,7 @@ type Track = { musicbrainz_id: string; time_listened: number; first_listen: number; + all_time_rank: number; }; type Artist = { id: number; @@ -378,6 +379,7 @@ type Artist = { time_listened: number; first_listen: number; is_primary: boolean; + all_time_rank: number; }; type Album = { id: number; @@ -389,6 +391,7 @@ type Album = { musicbrainz_id: string; time_listened: number; first_listen: number; + all_time_rank: number; }; type Alias = { id: number; @@ -459,9 +462,9 @@ type NowPlaying = { }; type RewindStats = { title: string; - top_artists: Artist[]; - top_albums: Album[]; - top_tracks: Track[]; + top_artists: Ranked[]; + top_albums: Ranked[]; + top_tracks: Ranked[]; minutes_listened: number; avg_minutes_listened_per_day: number; plays: number; diff --git a/client/app/components/ArtistAlbums.tsx b/client/app/components/ArtistAlbums.tsx index 922b5ce..dda7de8 100644 --- a/client/app/components/ArtistAlbums.tsx +++ b/client/app/components/ArtistAlbums.tsx @@ -8,11 +8,11 @@ interface Props { period: string; } -export default function ArtistAlbums({ artistId, name, period }: Props) { +export default function ArtistAlbums({ artistId, name }: Props) { const { isPending, isError, data, error } = useQuery({ queryKey: [ "top-albums", - { limit: 99, period: "all_time", artist_id: artistId, page: 0 }, + { limit: 99, period: "all_time", artist_id: artistId }, ], queryFn: ({ queryKey }) => getTopAlbums(queryKey[1] as getItemsArgs), }); @@ -39,16 +39,20 @@ export default function ArtistAlbums({ artistId, name, period }: Props) {

Albums featuring {name}

{data.items.map((item) => ( - + {item.title}
-

{item.title}

+

{item.item.title}

- {item.listen_count} play{item.listen_count > 1 ? "s" : ""} + {item.item.listen_count} play + {item.item.listen_count > 1 ? "s" : ""}

diff --git a/client/app/components/rewind/Rewind.tsx b/client/app/components/rewind/Rewind.tsx index 2553b35..a22fe15 100644 --- a/client/app/components/rewind/Rewind.tsx +++ b/client/app/components/rewind/Rewind.tsx @@ -8,9 +8,9 @@ interface Props { } export default function Rewind(props: Props) { - const artistimg = props.stats.top_artists[0]?.image; - const albumimg = props.stats.top_albums[0]?.image; - const trackimg = props.stats.top_tracks[0]?.image; + const artistimg = props.stats.top_artists[0]?.item.image; + const albumimg = props.stats.top_albums[0]?.item.image; + const trackimg = props.stats.top_tracks[0]?.item.image; if ( !props.stats.top_artists[0] || !props.stats.top_albums[0] || diff --git a/client/app/components/rewind/RewindTopItem.tsx b/client/app/components/rewind/RewindTopItem.tsx index ffbe488..5093768 100644 --- a/client/app/components/rewind/RewindTopItem.tsx +++ b/client/app/components/rewind/RewindTopItem.tsx @@ -1,7 +1,9 @@ +import type { Ranked } from "api/api"; + type TopItemProps = { title: string; imageSrc: string; - items: T[]; + items: Ranked[]; getLabel: (item: T) => string; includeTime?: boolean; }; @@ -28,23 +30,23 @@ export function RewindTopItem<
-

{getLabel(top)}

+

{getLabel(top.item)}

- {`${top.listen_count} plays`} + {`${top.item.listen_count} plays`} {includeTime - ? ` (${Math.floor(top.time_listened / 60)} minutes)` + ? ` (${Math.floor(top.item.time_listened / 60)} minutes)` : ``}
{rest.map((e) => ( -
- {getLabel(e)} +
+ {getLabel(e.item)} - {` - ${e.listen_count} plays`} + {` - ${e.item.listen_count} plays`} {includeTime - ? ` (${Math.floor(e.time_listened / 60)} minutes)` + ? ` (${Math.floor(e.item.time_listened / 60)} minutes)` : ``}
diff --git a/client/app/routes/MediaItems/Album.tsx b/client/app/routes/MediaItems/Album.tsx index 0de544a..e6f413e 100644 --- a/client/app/routes/MediaItems/Album.tsx +++ b/client/app/routes/MediaItems/Album.tsx @@ -30,6 +30,7 @@ export default function Album() { title={album.title} img={album.image} id={album.id} + rank={album.all_time_rank} musicbrainzId={album.musicbrainz_id} imgItemId={album.id} mergeFunc={mergeAlbums} diff --git a/client/app/routes/MediaItems/Artist.tsx b/client/app/routes/MediaItems/Artist.tsx index f2600be..a23e4cd 100644 --- a/client/app/routes/MediaItems/Artist.tsx +++ b/client/app/routes/MediaItems/Artist.tsx @@ -36,6 +36,7 @@ export default function Artist() { title={artist.name} img={artist.image} id={artist.id} + rank={artist.all_time_rank} musicbrainzId={artist.musicbrainz_id} imgItemId={artist.id} mergeFunc={mergeArtists} diff --git a/client/app/routes/MediaItems/MediaLayout.tsx b/client/app/routes/MediaItems/MediaLayout.tsx index c675fc6..eaf100b 100644 --- a/client/app/routes/MediaItems/MediaLayout.tsx +++ b/client/app/routes/MediaItems/MediaLayout.tsx @@ -28,6 +28,7 @@ interface Props { title: string; img: string; id: number; + rank: number; musicbrainzId: string; imgItemId: number; mergeFunc: MergeFunc; @@ -96,7 +97,15 @@ export default function MediaLayout(props: Props) {

{props.type}

-

{props.title}

+
+

+ {props.title} + + {" "} + #{props.rank} + +

+
{props.subContent}
diff --git a/client/app/routes/MediaItems/Track.tsx b/client/app/routes/MediaItems/Track.tsx index 20258c1..6b6690e 100644 --- a/client/app/routes/MediaItems/Track.tsx +++ b/client/app/routes/MediaItems/Track.tsx @@ -34,6 +34,7 @@ export default function Track() { title={track.title} img={track.image} id={track.id} + rank={track.all_time_rank} musicbrainzId={track.musicbrainz_id} imgItemId={track.album_id} mergeFunc={mergeTracks} diff --git a/db/queries/artist.sql b/db/queries/artist.sql index 863de32..deaad60 100644 --- a/db/queries/artist.sql +++ b/db/queries/artist.sql @@ -81,6 +81,26 @@ FROM ( ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; +-- name: GetArtistAllTimeRank :one +SELECT + artist_id, + rank +FROM ( + SELECT + x.artist_id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + at.artist_id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON t.id = at.track_id + GROUP BY at.artist_id + ) x + ) +WHERE artist_id = $1; + -- name: CountTopArtists :one SELECT COUNT(DISTINCT at.artist_id) AS total_count FROM listens l diff --git a/db/queries/release.sql b/db/queries/release.sql index cb548ed..47aac86 100644 --- a/db/queries/release.sql +++ b/db/queries/release.sql @@ -83,6 +83,25 @@ FROM ( ORDER BY listen_count DESC, x.id LIMIT $3 OFFSET $4; +-- name: GetReleaseAllTimeRank :one +SELECT + release_id, + rank +FROM ( + SELECT + x.release_id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + t.release_id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks t ON l.track_id = t.id + GROUP BY t.release_id + ) x + ) +WHERE release_id = $1; + -- name: CountTopReleases :one SELECT COUNT(DISTINCT r.id) AS total_count FROM listens l diff --git a/db/queries/track.sql b/db/queries/track.sql index 24be467..c69bed5 100644 --- a/db/queries/track.sql +++ b/db/queries/track.sql @@ -124,6 +124,24 @@ FROM ( ORDER BY x.listen_count DESC, x.id LIMIT $3 OFFSET $4; +-- name: GetTrackAllTimeRank :one +SELECT + id, + rank +FROM ( + SELECT + x.id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + t.id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + GROUP BY t.id) x + ) y +WHERE id = $1; + -- name: CountTopTracks :one SELECT COUNT(DISTINCT l.track_id) AS total_count FROM listens l diff --git a/internal/db/psql/album.go b/internal/db/psql/album.go index 630cf1f..f4c614c 100644 --- a/internal/db/psql/album.go +++ b/internal/db/psql/album.go @@ -23,32 +23,13 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu var err error var ret = new(models.Album) - if opts.ID != 0 { - l.Debug().Msgf("Fetching album from DB with id %d", opts.ID) - row, err := d.q.GetRelease(ctx, opts.ID) - if err != nil { - return nil, fmt.Errorf("GetAlbum: %w", err) - } - ret.ID = row.ID - ret.MbzID = row.MusicBrainzID - ret.Title = row.Title - ret.Image = row.Image - ret.VariousArtists = row.VariousArtists - err = json.Unmarshal(row.Artists, &ret.Artists) - if err != nil { - return nil, fmt.Errorf("GetAlbum: json.Unmarshal: %w", err) - } - } else if opts.MusicBrainzID != uuid.Nil { + if opts.MusicBrainzID != uuid.Nil { l.Debug().Msgf("Fetching album from DB with MusicBrainz Release ID %s", opts.MusicBrainzID) row, err := d.q.GetReleaseByMbzID(ctx, &opts.MusicBrainzID) if err != nil { return nil, fmt.Errorf("GetAlbum: %w", err) } - ret.ID = row.ID - ret.MbzID = row.MusicBrainzID - ret.Title = row.Title - ret.Image = row.Image - ret.VariousArtists = row.VariousArtists + opts.ID = row.ID } else if opts.ArtistID != 0 && opts.Title != "" { l.Debug().Msgf("Fetching album from DB with artist_id %d and title %s", opts.ArtistID, opts.Title) row, err := d.q.GetReleaseByArtistAndTitle(ctx, repository.GetReleaseByArtistAndTitleParams{ @@ -58,11 +39,7 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu if err != nil { return nil, fmt.Errorf("GetAlbum: %w", err) } - ret.ID = row.ID - ret.MbzID = row.MusicBrainzID - ret.Title = row.Title - ret.Image = row.Image - ret.VariousArtists = row.VariousArtists + opts.ID = row.ID } else if opts.ArtistID != 0 && len(opts.Titles) > 0 { l.Debug().Msgf("Fetching release group from DB with artist_id %d and titles %v", opts.ArtistID, opts.Titles) row, err := d.q.GetReleaseByArtistAndTitles(ctx, repository.GetReleaseByArtistAndTitlesParams{ @@ -72,19 +49,19 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu if err != nil { return nil, fmt.Errorf("GetAlbum: %w", err) } - ret.ID = row.ID - ret.MbzID = row.MusicBrainzID - ret.Title = row.Title - ret.Image = row.Image - ret.VariousArtists = row.VariousArtists - } else { - return nil, errors.New("GetAlbum: insufficient information to get album") + opts.ID = row.ID + } + + l.Debug().Msgf("Fetching album from DB with id %d", opts.ID) + row, err := d.q.GetRelease(ctx, opts.ID) + if err != nil { + return nil, fmt.Errorf("GetAlbum: %w", err) } count, err := d.q.CountListensFromRelease(ctx, repository.CountListensFromReleaseParams{ ListenedAt: time.Unix(0, 0), ListenedAt_2: time.Now(), - ReleaseID: ret.ID, + ReleaseID: opts.ID, }) if err != nil { return nil, fmt.Errorf("GetAlbum: CountListensFromRelease: %w", err) @@ -92,17 +69,32 @@ func (d *Psql) GetAlbum(ctx context.Context, opts db.GetAlbumOpts) (*models.Albu seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ Timeframe: db.Timeframe{Period: db.PeriodAllTime}, - AlbumID: ret.ID, + AlbumID: opts.ID, }) if err != nil { return nil, fmt.Errorf("GetAlbum: CountTimeListenedToItem: %w", err) } - firstListen, err := d.q.GetFirstListenFromRelease(ctx, ret.ID) + firstListen, err := d.q.GetFirstListenFromRelease(ctx, opts.ID) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err) } + rank, err := d.q.GetReleaseAllTimeRank(ctx, opts.ID) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf("GetAlbum: GetReleaseAllTimeRank: %w", err) + } + + ret.ID = row.ID + ret.MbzID = row.MusicBrainzID + ret.Title = row.Title + ret.Image = row.Image + ret.VariousArtists = row.VariousArtists + err = json.Unmarshal(row.Artists, &ret.Artists) + if err != nil { + return nil, fmt.Errorf("GetAlbum: json.Unmarshal: %w", err) + } + ret.AllTimeRank = rank.Rank ret.ListenCount = count ret.TimeListened = seconds ret.FirstListen = firstListen.ListenedAt.Unix() diff --git a/internal/db/psql/artist.go b/internal/db/psql/artist.go index a67fc4c..7bb50ec 100644 --- a/internal/db/psql/artist.go +++ b/internal/db/psql/artist.go @@ -20,114 +20,60 @@ import ( // this function sucks because sqlc keeps making new types for rows that are the same func (d *Psql) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Artist, error) { l := logger.FromContext(ctx) - if opts.ID != 0 { - l.Debug().Msgf("Fetching artist from DB with id %d", opts.ID) - row, err := d.q.GetArtist(ctx, opts.ID) - if err != nil { - return nil, fmt.Errorf("GetArtist: GetArtist by ID: %w", err) - } - count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{ - ListenedAt: time.Unix(0, 0), - ListenedAt_2: time.Now(), - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err) - } - seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ - Timeframe: db.Timeframe{Period: db.PeriodAllTime}, - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) - } - firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID) - if err != nil && !errors.Is(err, pgx.ErrNoRows) { - return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err) - } - return &models.Artist{ - ID: row.ID, - MbzID: row.MusicBrainzID, - Name: row.Name, - Aliases: row.Aliases, - Image: row.Image, - ListenCount: count, - TimeListened: seconds, - FirstListen: firstListen.ListenedAt.Unix(), - }, nil - } else if opts.MusicBrainzID != uuid.Nil { + if opts.MusicBrainzID != uuid.Nil { l.Debug().Msgf("Fetching artist from DB with MusicBrainz ID %s", opts.MusicBrainzID) row, err := d.q.GetArtistByMbzID(ctx, &opts.MusicBrainzID) if err != nil { return nil, fmt.Errorf("GetArtist: GetArtistByMbzID: %w", err) } - count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{ - ListenedAt: time.Unix(0, 0), - ListenedAt_2: time.Now(), - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err) - } - seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ - Timeframe: db.Timeframe{Period: db.PeriodAllTime}, - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) - } - firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID) - if err != nil && !errors.Is(err, pgx.ErrNoRows) { - return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err) - } - return &models.Artist{ - ID: row.ID, - MbzID: row.MusicBrainzID, - Name: row.Name, - Aliases: row.Aliases, - Image: row.Image, - ListenCount: count, - TimeListened: seconds, - FirstListen: firstListen.ListenedAt.Unix(), - }, nil + opts.ID = row.ID } else if opts.Name != "" { l.Debug().Msgf("Fetching artist from DB with name '%s'", opts.Name) row, err := d.q.GetArtistByName(ctx, opts.Name) if err != nil { return nil, fmt.Errorf("GetArtist: GetArtistByName: %w", err) } - count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{ - ListenedAt: time.Unix(0, 0), - ListenedAt_2: time.Now(), - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err) - } - seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ - Timeframe: db.Timeframe{Period: db.PeriodAllTime}, - ArtistID: row.ID, - }) - if err != nil { - return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) - } - firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID) - if err != nil && !errors.Is(err, pgx.ErrNoRows) { - return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err) - } - return &models.Artist{ - ID: row.ID, - MbzID: row.MusicBrainzID, - Name: row.Name, - Aliases: row.Aliases, - Image: row.Image, - ListenCount: count, - TimeListened: seconds, - FirstListen: firstListen.ListenedAt.Unix(), - }, nil - } else { - return nil, errors.New("insufficient information to get artist") + opts.ID = row.ID } + l.Debug().Msgf("Fetching artist from DB with id %d", opts.ID) + row, err := d.q.GetArtist(ctx, opts.ID) + if err != nil { + return nil, fmt.Errorf("GetArtist: GetArtist by ID: %w", err) + } + count, err := d.q.CountListensFromArtist(ctx, repository.CountListensFromArtistParams{ + ListenedAt: time.Unix(0, 0), + ListenedAt_2: time.Now(), + ArtistID: row.ID, + }) + if err != nil { + return nil, fmt.Errorf("GetArtist: CountListensFromArtist: %w", err) + } + seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ + Timeframe: db.Timeframe{Period: db.PeriodAllTime}, + ArtistID: row.ID, + }) + if err != nil { + return nil, fmt.Errorf("GetArtist: CountTimeListenedToItem: %w", err) + } + firstListen, err := d.q.GetFirstListenFromArtist(ctx, row.ID) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf("GetAlbum: GetFirstListenFromArtist: %w", err) + } + rank, err := d.q.GetArtistAllTimeRank(ctx, opts.ID) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf("GetArtist: GetArtistAllTimeRank: %w", err) + } + return &models.Artist{ + ID: row.ID, + MbzID: row.MusicBrainzID, + Name: row.Name, + Aliases: row.Aliases, + Image: row.Image, + ListenCount: count, + TimeListened: seconds, + AllTimeRank: rank.Rank, + FirstListen: firstListen.ListenedAt.Unix(), + }, nil } // Inserts all unique aliases into the DB with specified source diff --git a/internal/db/psql/track.go b/internal/db/psql/track.go index d511de6..743a20e 100644 --- a/internal/db/psql/track.go +++ b/internal/db/psql/track.go @@ -21,37 +21,13 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac l := logger.FromContext(ctx) var track models.Track - if opts.ID != 0 { - l.Debug().Msgf("Fetching track from DB with id %d", opts.ID) - t, err := d.q.GetTrack(ctx, opts.ID) - if err != nil { - return nil, fmt.Errorf("GetTrack: GetTrack By ID: %w", err) - } - track = models.Track{ - ID: t.ID, - MbzID: t.MusicBrainzID, - Title: t.Title, - AlbumID: t.ReleaseID, - Image: t.Image, - Duration: t.Duration, - } - err = json.Unmarshal(t.Artists, &track.Artists) - if err != nil { - return nil, fmt.Errorf("GetTrack: json.Unmarshal: %w", err) - } - } else if opts.MusicBrainzID != uuid.Nil { + if opts.MusicBrainzID != uuid.Nil { l.Debug().Msgf("Fetching track from DB with MusicBrainz ID %s", opts.MusicBrainzID) t, err := d.q.GetTrackByMbzID(ctx, &opts.MusicBrainzID) if err != nil { return nil, fmt.Errorf("GetTrack: GetTrackByMbzID: %w", err) } - track = models.Track{ - ID: t.ID, - MbzID: t.MusicBrainzID, - Title: t.Title, - AlbumID: t.ReleaseID, - Duration: t.Duration, - } + opts.ID = t.ID } else if len(opts.ArtistIDs) > 0 && opts.ReleaseID != 0 { l.Debug().Msgf("Fetching track from DB from release id %d with title '%s' and artist id(s) '%v'", opts.ReleaseID, opts.Title, opts.ArtistIDs) t, err := d.q.GetTrackByTrackInfo(ctx, repository.GetTrackByTrackInfoParams{ @@ -62,21 +38,19 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac if err != nil { return nil, fmt.Errorf("GetTrack: GetTrackByTrackInfo: %w", err) } - track = models.Track{ - ID: t.ID, - MbzID: t.MusicBrainzID, - Title: t.Title, - AlbumID: t.ReleaseID, - Duration: t.Duration, - } - } else { - return nil, errors.New("GetTrack: insufficient information to get track") + opts.ID = t.ID + } + + l.Debug().Msgf("Fetching track from DB with id %d", opts.ID) + t, err := d.q.GetTrack(ctx, opts.ID) + if err != nil { + return nil, fmt.Errorf("GetTrack: GetTrack By ID: %w", err) } count, err := d.q.CountListensFromTrack(ctx, repository.CountListensFromTrackParams{ ListenedAt: time.Unix(0, 0), ListenedAt_2: time.Now(), - TrackID: track.ID, + TrackID: opts.ID, }) if err != nil { return nil, fmt.Errorf("GetTrack: CountListensFromTrack: %w", err) @@ -84,20 +58,37 @@ func (d *Psql) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Trac seconds, err := d.CountTimeListenedToItem(ctx, db.TimeListenedOpts{ Timeframe: db.Timeframe{Period: db.PeriodAllTime}, - TrackID: track.ID, + TrackID: opts.ID, }) if err != nil { return nil, fmt.Errorf("GetTrack: CountTimeListenedToItem: %w", err) } - firstListen, err := d.q.GetFirstListenFromTrack(ctx, track.ID) + firstListen, err := d.q.GetFirstListenFromTrack(ctx, opts.ID) if err != nil && !errors.Is(err, pgx.ErrNoRows) { return nil, fmt.Errorf("GetAlbum: GetFirstListenFromRelease: %w", err) } + rank, err := d.q.GetTrackAllTimeRank(ctx, opts.ID) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return nil, fmt.Errorf("GetAlbum: GetTrackAllTimeRank: %w", err) + } - track.ListenCount = count - track.TimeListened = seconds - track.FirstListen = firstListen.ListenedAt.Unix() + track = models.Track{ + ID: t.ID, + MbzID: t.MusicBrainzID, + Title: t.Title, + AlbumID: t.ReleaseID, + Image: t.Image, + Duration: t.Duration, + AllTimeRank: rank.Rank, + ListenCount: count, + TimeListened: seconds, + FirstListen: firstListen.ListenedAt.Unix(), + } + err = json.Unmarshal(t.Artists, &track.Artists) + if err != nil { + return nil, fmt.Errorf("GetTrack: json.Unmarshal: %w", err) + } return &track, nil } diff --git a/internal/models/album.go b/internal/models/album.go index 24948f9..a295fe9 100644 --- a/internal/models/album.go +++ b/internal/models/album.go @@ -12,11 +12,5 @@ type Album struct { ListenCount int64 `json:"listen_count"` TimeListened int64 `json:"time_listened"` FirstListen int64 `json:"first_listen"` + AllTimeRank int64 `json:"all_time_rank"` } - -// type SimpleAlbum struct { -// ID int32 `json:"id"` -// Title string `json:"title"` -// VariousArtists bool `json:"is_various_artists"` -// Image uuid.UUID `json:"image"` -// } diff --git a/internal/models/artist.go b/internal/models/artist.go index 7784e51..07f09e6 100644 --- a/internal/models/artist.go +++ b/internal/models/artist.go @@ -12,6 +12,7 @@ type Artist struct { TimeListened int64 `json:"time_listened"` FirstListen int64 `json:"first_listen"` IsPrimary bool `json:"is_primary,omitempty"` + AllTimeRank int64 `json:"all_time_rank"` } type SimpleArtist struct { diff --git a/internal/models/track.go b/internal/models/track.go index 8eb802c..4cb5b04 100644 --- a/internal/models/track.go +++ b/internal/models/track.go @@ -13,4 +13,5 @@ type Track struct { AlbumID int32 `json:"album_id"` TimeListened int64 `json:"time_listened"` FirstListen int64 `json:"first_listen"` + AllTimeRank int64 `json:"all_time_rank"` } diff --git a/internal/repository/artist.sql.go b/internal/repository/artist.sql.go index 3722291..96f00f2 100644 --- a/internal/repository/artist.sql.go +++ b/internal/repository/artist.sql.go @@ -134,6 +134,39 @@ func (q *Queries) GetArtist(ctx context.Context, id int32) (GetArtistRow, error) return i, err } +const getArtistAllTimeRank = `-- name: GetArtistAllTimeRank :one +SELECT + artist_id, + rank +FROM ( + SELECT + x.artist_id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + at.artist_id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks t ON l.track_id = t.id + JOIN artist_tracks at ON t.id = at.track_id + GROUP BY at.artist_id + ) x + ) +WHERE artist_id = $1 +` + +type GetArtistAllTimeRankRow struct { + ArtistID int32 + Rank int64 +} + +func (q *Queries) GetArtistAllTimeRank(ctx context.Context, artistID int32) (GetArtistAllTimeRankRow, error) { + row := q.db.QueryRow(ctx, getArtistAllTimeRank, artistID) + var i GetArtistAllTimeRankRow + err := row.Scan(&i.ArtistID, &i.Rank) + return i, err +} + const getArtistByImage = `-- name: GetArtistByImage :one SELECT id, musicbrainz_id, image, image_source FROM artists WHERE image = $1 LIMIT 1 ` diff --git a/internal/repository/release.sql.go b/internal/repository/release.sql.go index 76789d0..6d12da4 100644 --- a/internal/repository/release.sql.go +++ b/internal/repository/release.sql.go @@ -141,6 +141,38 @@ func (q *Queries) GetRelease(ctx context.Context, id int32) (GetReleaseRow, erro return i, err } +const getReleaseAllTimeRank = `-- name: GetReleaseAllTimeRank :one +SELECT + release_id, + rank +FROM ( + SELECT + x.release_id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + t.release_id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks t ON l.track_id = t.id + GROUP BY t.release_id + ) x + ) +WHERE release_id = $1 +` + +type GetReleaseAllTimeRankRow struct { + ReleaseID int32 + Rank int64 +} + +func (q *Queries) GetReleaseAllTimeRank(ctx context.Context, releaseID int32) (GetReleaseAllTimeRankRow, error) { + row := q.db.QueryRow(ctx, getReleaseAllTimeRank, releaseID) + var i GetReleaseAllTimeRankRow + err := row.Scan(&i.ReleaseID, &i.Rank) + return i, err +} + const getReleaseByArtistAndTitle = `-- name: GetReleaseByArtistAndTitle :one SELECT r.id, r.musicbrainz_id, r.image, r.various_artists, r.image_source, r.title FROM releases_with_title r diff --git a/internal/repository/track.sql.go b/internal/repository/track.sql.go index a18d87a..e2aa084 100644 --- a/internal/repository/track.sql.go +++ b/internal/repository/track.sql.go @@ -438,6 +438,37 @@ func (q *Queries) GetTrack(ctx context.Context, id int32) (GetTrackRow, error) { return i, err } +const getTrackAllTimeRank = `-- name: GetTrackAllTimeRank :one +SELECT + id, + rank +FROM ( + SELECT + x.id, + RANK() OVER (ORDER BY x.listen_count DESC) AS rank + FROM ( + SELECT + t.id, + COUNT(*) AS listen_count + FROM listens l + JOIN tracks_with_title t ON l.track_id = t.id + GROUP BY t.id) x + ) y +WHERE id = $1 +` + +type GetTrackAllTimeRankRow struct { + ID int32 + Rank int64 +} + +func (q *Queries) GetTrackAllTimeRank(ctx context.Context, id int32) (GetTrackAllTimeRankRow, error) { + row := q.db.QueryRow(ctx, getTrackAllTimeRank, id) + var i GetTrackAllTimeRankRow + err := row.Scan(&i.ID, &i.Rank) + return i, err +} + const getTrackByMbzID = `-- name: GetTrackByMbzID :one SELECT id, musicbrainz_id, duration, release_id, title FROM tracks_with_title WHERE musicbrainz_id = $1 LIMIT 1