fix(web): artistPlays via user.getTop{Tracks,Albums} (getArtistTracks deprecated)

Last.fm returns error 27 "Deprecated" for user.getArtistTracks, so the per-artist
plays modal was 502ing for every artist. Pivot to filtering the user's supported
all-time top tracks + top albums (which carry real per-item play counts) to the
artist — exact personal counts, cheaper, and the clicked artists are the user's
top artists so their items cluster near the front. Adds topTracks(); route + modal
return shape unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-13 21:38:22 +02:00
parent 2291308b90
commit 3ec1547e10
3 changed files with 127 additions and 88 deletions
@@ -18,15 +18,13 @@ async function seedConfig() {
await prisma.config.create({ data: { key: "lastfm.api_key", value: encryptSecret("real-key"), secret: true } });
}
function mockLfmArtistTracks(tracks: unknown[], totalPages = 1) {
vi.spyOn(global, "fetch").mockResolvedValue(
new Response(
JSON.stringify({
artisttracks: { track: tracks, "@attr": { totalPages: String(totalPages), page: "1" } },
}),
{ status: 200 },
),
);
function mockTopEndpoints(tracks: unknown[], albums: unknown[]) {
vi.spyOn(global, "fetch").mockImplementation((url: any) => {
const body = String(url).includes("gettoptracks")
? { toptracks: { track: tracks, "@attr": { totalPages: "1", page: "1" } } }
: { topalbums: { album: albums, "@attr": { totalPages: "1", page: "1" } } };
return Promise.resolve(new Response(JSON.stringify(body), { status: 200 }));
});
}
describe("GET /api/lastfm/artist-plays", () => {
@@ -43,22 +41,28 @@ describe("GET /api/lastfm/artist-plays", () => {
expect(blank.status).toBe(400);
});
it("returns the tally on the happy path", async () => {
it("returns the artist's top tracks + albums (filtered to the artist, real counts)", async () => {
await seedConfig();
mockLfmArtistTracks([
{ name: "Billie Jean", album: { "#text": "Thriller" } },
{ name: "Billie Jean", album: { "#text": "Thriller" } },
{ name: "Beat It", album: { "#text": "Thriller" } },
]);
mockTopEndpoints(
[
{ name: "Billie Jean", playcount: "50", artist: { name: "Michael Jackson" } },
{ name: "Wonderwall", playcount: "99", artist: { name: "Oasis" } }, // different artist — excluded
{ name: "Beat It", playcount: "30", artist: { name: "Michael Jackson" } },
],
[
{ name: "Thriller", playcount: "120", artist: { name: "Michael Jackson" } },
{ name: "Definitely Maybe", playcount: "80", artist: { name: "Oasis" } }, // excluded
],
);
const res = await GET(req("?artist=Michael+Jackson"));
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({
topTracks: [
{ name: "Billie Jean", plays: 2 },
{ name: "Beat It", plays: 1 },
{ name: "Billie Jean", plays: 50 },
{ name: "Beat It", plays: 30 },
],
topAlbums: [{ name: "Thriller", plays: 3 }],
topAlbums: [{ name: "Thriller", plays: 120 }],
sampled: false,
});
});