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:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { PageHead } from "../_ui/page-head";
|
||||
import { ArtistModal } from "../_ui/artist-modal";
|
||||
import { AlbumModal } from "../_ui/album-modal";
|
||||
@@ -45,6 +45,8 @@ export function LastfmClient() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<LoadError | null>(null);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const [refreshTick, setRefreshTick] = useState(0);
|
||||
const forceRef = useRef(false); // next load should bypass the server cache
|
||||
|
||||
const [artistModal, setArtistModal] = useState<{ mbid: string; name: string } | null>(null);
|
||||
const [albumTarget, setAlbumTarget] = useState<{ row: AlbumRow; match: ReleaseGroupMatch } | null>(null);
|
||||
@@ -61,13 +63,19 @@ export function LastfmClient() {
|
||||
function retry() {
|
||||
setReloadKey((k) => k + 1);
|
||||
}
|
||||
function refresh() {
|
||||
forceRef.current = true; // force a live re-fetch, bypassing the server cache
|
||||
setRefreshTick((t) => t + 1);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const path = tab === "Artists" ? "/api/lastfm/top-artists" : "/api/lastfm/top-albums";
|
||||
fetch(`${path}?period=${period}&page=${page}`)
|
||||
const force = forceRef.current;
|
||||
forceRef.current = false;
|
||||
fetch(`${path}?period=${period}&page=${page}${force ? "&refresh=1" : ""}`)
|
||||
.then(async (res) => {
|
||||
if (cancelled) return;
|
||||
if (res.status === 400) {
|
||||
@@ -96,7 +104,7 @@ export function LastfmClient() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [tab, period, page, reloadKey]);
|
||||
}, [tab, period, page, reloadKey, refreshTick]);
|
||||
|
||||
const shownArtists = useMemo(() => {
|
||||
const needle = filter.trim().toLowerCase();
|
||||
@@ -210,6 +218,9 @@ export function LastfmClient() {
|
||||
/>
|
||||
Hide items in my library
|
||||
</label>
|
||||
<button type="button" className="btn sm ghost" onClick={refresh} disabled={loading}>
|
||||
{loading ? "Refreshing…" : "Refresh"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error?.kind === "creds" ? (
|
||||
|
||||
Reference in New Issue
Block a user