128 lines
3.6 KiB
TypeScript
128 lines
3.6 KiB
TypeScript
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<T>(method: string, params: Record<string, string>): Promise<T> {
|
|
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<UserInfo> {
|
|
const { user } = await callLastfm<RawUserInfo>('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<RecentTracksPage> {
|
|
const params: Record<string, string> = { user: userName, page: `${page}`, limit: `${pageSize}`, to: `${range.to}` };
|
|
if (range.from) params.from = `${range.from}`;
|
|
|
|
const { recenttracks } = await callLastfm<RawRecentTracks>('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<string | null> {
|
|
const { track: info } = await callLastfm<RawTrackInfo>('track.getinfo', { artist, track, autocorrect: '1' });
|
|
return largestImage(info?.album?.image);
|
|
}
|