c63f765c1f
/lastfm hit the Last.fm API live on every load / period switch / page. Cache the
raw Last.fm payload in the shared ApiCache table (12h TTL) while keeping the
own-state annotation (followed/owned/wanted) computed LIVE on top — so a follow
made after the payload was cached shows immediately.
- cached() gains a { force } option to bypass a fresh row (still serves stale if
the forced fetch fails); CACHE_TTL.HOUR added.
- All three routes wrap their Last.fm call: lfm-top-artists / lfm-top-albums
:{user}:{period}:{page}, lfm-artist-plays:{user}:{artist} (keys normalized).
- A "Refresh" button on /lastfm force-bypasses the cache (?refresh=1).
- Factored the copy-pasted getCreds/PERIODS/LIMIT into a route-only _creds.ts
(closes the Last.fm-routes DRY todo #19) — prisma stays out of any client
bundle since lib/lastfm.ts is imported by a client component.
web 150 tests (cache-hit + refresh + own-state-live asserted), tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
33 lines
1.4 KiB
TypeScript
33 lines
1.4 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { decryptSecret } from "@/lib/crypto";
|
|
import type { LfmPeriod } from "@/lib/lastfm";
|
|
|
|
// Shared by the three /api/lastfm routes (was copy-pasted three times). Route-only module
|
|
// (underscore prefix ⇒ not a route), so importing prisma here never reaches a client bundle.
|
|
|
|
export const PERIODS: LfmPeriod[] = ["overall", "7day", "1month", "3month", "6month", "12month"];
|
|
export const LIMIT = 50;
|
|
|
|
export async function getLastfmCreds(): 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 };
|
|
}
|
|
|
|
/** The `period` query param, validated to a supported LfmPeriod (default "overall"). */
|
|
export function parsePeriod(url: URL): LfmPeriod {
|
|
const p = url.searchParams.get("period") ?? "overall";
|
|
return (PERIODS as string[]).includes(p) ? (p as LfmPeriod) : "overall";
|
|
}
|
|
|
|
/** Whether a manual Refresh (cache bypass) was requested. */
|
|
export function wantsRefresh(url: URL): boolean {
|
|
return url.searchParams.get("refresh") === "1";
|
|
}
|