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
+26
View File
@@ -231,6 +231,32 @@ WHERE NOT EXISTS (
ORDER BY p.cnt DESC, t.id
LIMIT $2;
-- name: ListMostPlayedTracksForArtist :many
-- 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).
WITH plays AS (
SELECT track_id, count(*) AS cnt
FROM play_events
WHERE user_id = sqlc.arg(user_id) AND was_skipped = false
GROUP BY track_id
)
SELECT sqlc.embed(t),
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 = sqlc.arg(artist_id)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id
)
ORDER BY p.cnt DESC, t.id
LIMIT sqlc.arg(result_limit);
-- name: ListLastPlayedArtistsForUser :many
-- M6a: artists ranked by max(play_events.started_at) for the user, with
-- a derived cover_album_id via a representative-album lateral join (most
+26
View File
@@ -60,3 +60,29 @@ ON CONFLICT (seed_artist_id, candidate_mbid, source) DO UPDATE SET
candidate_name = EXCLUDED.candidate_name,
score = EXCLUDED.score,
fetched_at = now();
-- name: ListSimilarArtistsForArtist :many
-- In-library artists similar to a seed artist, ranked by best similarity
-- score across sources (deduped per candidate). cover_album_id + album_count
-- mirror ListArtistsAlphaWithCovers so the strip renders identical cards.
SELECT sqlc.embed(artists),
cov.id AS cover_album_id,
cnt.album_count::bigint AS album_count
FROM (
SELECT artist_b_id, max(score) AS sim_score
FROM artist_similarity
WHERE artist_a_id = sqlc.arg(seed_artist_id)
GROUP BY artist_b_id
) s
JOIN artists ON artists.id = s.artist_b_id
LEFT JOIN LATERAL (
SELECT id FROM albums
WHERE artist_id = artists.id AND cover_art_path IS NOT NULL
ORDER BY created_at DESC LIMIT 1
) cov ON true
LEFT JOIN LATERAL (
SELECT count(*) AS album_count
FROM albums WHERE artist_id = artists.id
) cnt ON true
ORDER BY s.sim_score DESC, artists.sort_name
LIMIT sqlc.arg(result_limit);