feat(server): similar-artists + per-user artist top-tracks endpoints

GET /api/artists/{id}/similar — in-library artists ranked by similarity
score (deduped across sources), ArtistRef list with cover + album count.
GET /api/artists/{id}/top-tracks — current user's most-played tracks for
the artist (skips excluded, quarantine filtered).
This commit is contained in:
2026-06-06 22:30:41 -04:00
parent a766b7193f
commit 63b25e65ad
6 changed files with 281 additions and 0 deletions
+77
View File
@@ -97,6 +97,83 @@ func (q *Queries) ListLastPlayedArtistsForUser(ctx context.Context, arg ListLast
return items, nil
}
const listMostPlayedTracksForArtist = `-- name: ListMostPlayedTracksForArtist :many
WITH plays AS (
SELECT track_id, count(*) AS cnt
FROM play_events
WHERE user_id = $2 AND was_skipped = false
GROUP BY track_id
)
SELECT t.id, t.title, t.album_id, t.artist_id, t.track_number, t.disc_number, t.duration_ms, t.file_path, t.file_size, t.file_format, t.bitrate, t.mbid, t.genre, t.added_at, t.updated_at,
albums.title AS album_title,
artists.name AS artist_name
FROM plays p
JOIN tracks t ON t.id = p.track_id
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = $1
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = $2 AND q.track_id = t.id
)
ORDER BY p.cnt DESC, t.id
LIMIT $3
`
type ListMostPlayedTracksForArtistParams struct {
ArtistID pgtype.UUID
UserID pgtype.UUID
ResultLimit int32
}
type ListMostPlayedTracksForArtistRow struct {
Track Track
AlbumTitle string
ArtistName string
}
// Top tracks for one artist by this user's completed-play count (skips
// excluded, quarantine filtered). Same projection as
// ListMostPlayedTracksForUser plus an artist_id filter, so the handler
// reuses trackRefFrom(row.Track, row.AlbumTitle, row.ArtistName).
func (q *Queries) ListMostPlayedTracksForArtist(ctx context.Context, arg ListMostPlayedTracksForArtistParams) ([]ListMostPlayedTracksForArtistRow, error) {
rows, err := q.db.Query(ctx, listMostPlayedTracksForArtist, arg.ArtistID, arg.UserID, arg.ResultLimit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListMostPlayedTracksForArtistRow
for rows.Next() {
var i ListMostPlayedTracksForArtistRow
if err := rows.Scan(
&i.Track.ID,
&i.Track.Title,
&i.Track.AlbumID,
&i.Track.ArtistID,
&i.Track.TrackNumber,
&i.Track.DiscNumber,
&i.Track.DurationMs,
&i.Track.FilePath,
&i.Track.FileSize,
&i.Track.FileFormat,
&i.Track.Bitrate,
&i.Track.Mbid,
&i.Track.Genre,
&i.Track.AddedAt,
&i.Track.UpdatedAt,
&i.AlbumTitle,
&i.ArtistName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listMostPlayedTracksForUser = `-- name: ListMostPlayedTracksForUser :many
WITH plays AS (
SELECT track_id, count(*) AS cnt