feat(web): cache Last.fm browse data + Refresh button (rides ApiCache)
/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>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user