import { LASTFM_API_KEY } from '$app/env/private'; import type { PlayedTrack } from '#lib/lastfm/types'; const apiUrl = 'https://ws.audioscrobbler.com/2.0/'; const pageSize = 200; const placeholderImageId = '2a96cbd8b46e442fc41c2b86b821562f'; interface RawImage { size: string; '#text': string; } interface RawTrack { name: string; url: string; artist: { '#text': string }; album: { '#text': string }; image?: RawImage[]; date?: { uts: string }; '@attr'?: { nowplaying?: string }; } interface RawRecentTracks { recenttracks: { track: RawTrack[] | RawTrack; '@attr': { totalPages: string }; }; } interface RawUserInfo { user: { name: string; url: string; playcount: string; artist_count: string; album_count: string; track_count: string; registered?: { unixtime: string }; }; } interface RawTrackInfo { track?: { album?: { image?: RawImage[] } }; } export interface UserInfo { name: string; url: string; scrobbles: number; artists: number; albums: number; tracks: number; registeredAt: number | null; } export interface RecentTracksPage { tracks: PlayedTrack[]; nowPlaying: PlayedTrack | null; totalPages: number; } export function hasLastfmKey(): boolean { return Boolean(LASTFM_API_KEY); } async function callLastfm(method: string, params: Record): Promise { const query = new URLSearchParams({ method, api_key: LASTFM_API_KEY ?? '', format: 'json', ...params }); const response = await fetch(`${apiUrl}?${query}`, { headers: { 'User-Agent': 'sirblob-website' } }); if (!response.ok) throw new Error(`Last.fm ${method} returned ${response.status}`); const body = await response.json(); if (body.error) throw new Error(`Last.fm ${method} failed: ${body.message}`); return body as T; } function largestImage(images: RawImage[] | undefined): string | null { const url = images?.at(-1)?.['#text']; return url && !url.includes(placeholderImageId) ? url : null; } function toPlayedTrack(raw: RawTrack): PlayedTrack { return { track: raw.name, artist: raw.artist['#text'], album: raw.album['#text'], url: raw.url, image: largestImage(raw.image), playedAt: raw.date ? Number(raw.date.uts) : null }; } export async function fetchUserInfo(userName: string): Promise { const { user } = await callLastfm('user.getinfo', { user: userName }); return { name: user.name, url: user.url, scrobbles: Number(user.playcount), artists: Number(user.artist_count), albums: Number(user.album_count), tracks: Number(user.track_count), registeredAt: user.registered ? Number(user.registered.unixtime) : null }; } export async function fetchRecentTracksPage( userName: string, page: number, range: { from?: number; to: number } ): Promise { const params: Record = { user: userName, page: `${page}`, limit: `${pageSize}`, to: `${range.to}` }; if (range.from) params.from = `${range.from}`; const { recenttracks } = await callLastfm('user.getrecenttracks', params); const rawTracks = Array.isArray(recenttracks.track) ? recenttracks.track : [recenttracks.track]; const nowPlayingRaw = rawTracks.find((raw) => raw['@attr']?.nowplaying === 'true'); return { tracks: rawTracks.filter((raw) => raw.date).map(toPlayedTrack), nowPlaying: nowPlayingRaw ? toPlayedTrack(nowPlayingRaw) : null, totalPages: Number(recenttracks['@attr'].totalPages) }; } export async function fetchTrackImage(artist: string, track: string): Promise { const { track: info } = await callLastfm('track.getinfo', { artist, track, autocorrect: '1' }); return largestImage(info?.album?.image); }