feat(web): artistPlays lib + /api/lastfm/artist-plays (personal per-artist tally)
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect, vi, afterEach, beforeAll } from "vitest";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { encryptSecret } from "@/lib/crypto";
|
||||
import { GET } from "./route";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.LYRA_SECRET_KEY = "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=";
|
||||
});
|
||||
|
||||
function req(qs = "") {
|
||||
return new Request(`http://localhost/api/lastfm/artist-plays${qs}`);
|
||||
}
|
||||
|
||||
async function seedConfig() {
|
||||
await prisma.config.create({ data: { key: "lastfm.username", value: "jonathan", secret: false } });
|
||||
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 },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe("GET /api/lastfm/artist-plays", () => {
|
||||
it("400s when Last.fm creds are unset", async () => {
|
||||
const res = await GET(req("?artist=Michael+Jackson"));
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it("400s when artist param is missing or blank", async () => {
|
||||
await seedConfig();
|
||||
const missing = await GET(req());
|
||||
expect(missing.status).toBe(400);
|
||||
const blank = await GET(req("?artist=%20"));
|
||||
expect(blank.status).toBe(400);
|
||||
});
|
||||
|
||||
it("returns the tally on the happy path", async () => {
|
||||
await seedConfig();
|
||||
mockLfmArtistTracks([
|
||||
{ name: "Billie Jean", album: { "#text": "Thriller" } },
|
||||
{ name: "Billie Jean", album: { "#text": "Thriller" } },
|
||||
{ name: "Beat It", album: { "#text": "Thriller" } },
|
||||
]);
|
||||
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 },
|
||||
],
|
||||
topAlbums: [{ name: "Thriller", plays: 3 }],
|
||||
sampled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("502s when Last.fm throws", async () => {
|
||||
await seedConfig();
|
||||
vi.spyOn(global, "fetch").mockResolvedValue(
|
||||
new Response(JSON.stringify({ error: 10, message: "Invalid API key" }), { status: 200 }),
|
||||
);
|
||||
const res = await GET(req("?artist=Michael+Jackson"));
|
||||
expect(res.status).toBe(502);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { decryptSecret } from "@/lib/crypto";
|
||||
import { artistPlays } from "@/lib/lastfm";
|
||||
|
||||
async function getCreds(): Promise<{ username: string; apiKey: string } | null> {
|
||||
const rows = await prisma.config.findMany({ where: { key: { in: ["lastfm.username", "lastfm.api_key"] } } });
|
||||
const byKey = new Map(rows.map((r) => [r.key, r]));
|
||||
const username = byKey.get("lastfm.username")?.value ?? "";
|
||||
const keyRow = byKey.get("lastfm.api_key");
|
||||
const apiKey = keyRow ? (keyRow.secret ? decryptSecret(keyRow.value) : keyRow.value) : "";
|
||||
if (!username || !apiKey) return null;
|
||||
return { username, apiKey };
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const creds = await getCreds();
|
||||
if (!creds) return Response.json({ error: "Last.fm not configured" }, { status: 400 });
|
||||
|
||||
const artist = new URL(request.url).searchParams.get("artist")?.trim();
|
||||
if (!artist) return Response.json({ error: "artist is required" }, { status: 400 });
|
||||
|
||||
let plays;
|
||||
try {
|
||||
plays = await artistPlays(creds.username, artist, creds.apiKey);
|
||||
} catch (e) {
|
||||
return Response.json({ error: e instanceof Error ? e.message : "Last.fm request failed" }, { status: 502 });
|
||||
}
|
||||
|
||||
return Response.json(plays);
|
||||
}
|
||||
Reference in New Issue
Block a user