import { env } from '../env'; import { HttpError, fetchJson } from '../lib/http'; import { ProviderAuthError, type PollingProvider } from './types'; interface LastfmTrack { name: string; artist: { '#text': string }; album: { '#text': string }; image: { size: string; '#text': string }[]; '@attr'?: { nowplaying?: string }; } const api = (params: Record) => `https://ws.audioscrobbler.com/2.0/?${new URLSearchParams({ ...params, api_key: env.LASTFM_API_KEY!, format: 'json' })}`; export async function lastfmUserExists(username: string): Promise<{ name: string } | null> { try { const res = await fetchJson<{ user: { name: string } }>(api({ method: 'user.getinfo', user: username })); return res ? { name: res.user.name } : null; } catch (e) { if (e instanceof HttpError && e.status === 404) return null; throw e; } } export const lastfmProvider: PollingProvider = { id: 'lastfm', async poll(conn) { let res; try { res = await fetchJson<{ recenttracks: { track: LastfmTrack[] | LastfmTrack } }>( api({ method: 'user.getrecenttracks', user: conn.externalUserId, limit: '1' }), ); } catch (e) { if (e instanceof HttpError && e.status === 404) throw new ProviderAuthError('last.fm user not found'); throw e; } const raw = res?.recenttracks.track; const first = Array.isArray(raw) ? raw[0] : raw; if (!first || first['@attr']?.nowplaying !== 'true') return null; const art = first.image.find((i) => i.size === 'extralarge')?.['#text'] || null; return { track: { title: first.name, artist: first.artist['#text'], album: first.album['#text'] || null, // Last.fm serves a grey star placeholder when it has no art artworkUrl: art && !art.includes('2a96cbd8b46e442fc41c2b86b821562f') ? art : null, }, isPlaying: true, progressMs: null, }; }, };