From 00a96913630522239a2313ed055873a1518b0de6 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 30 Jul 2024 23:04:27 -0700 Subject: [PATCH] Add server support for last.fm history fetching This lets us fetch all sorts of data from last.fm for display. We're doing some calculation to determine the last three albums listened to and sending that to the frontend. --- src/lib/types/lastfm.ts | 38 ++++++++ src/routes/api/lastfm/+server.ts | 155 +++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/lib/types/lastfm.ts create mode 100644 src/routes/api/lastfm/+server.ts diff --git a/src/lib/types/lastfm.ts b/src/lib/types/lastfm.ts new file mode 100644 index 0000000..f647f6c --- /dev/null +++ b/src/lib/types/lastfm.ts @@ -0,0 +1,38 @@ +export interface Artist { + name: string + mbid?: string +} + +export interface AlbumImages { + small: string + medium: string + large: string + extralarge: string + mega: string + default: string + itunes?: string +} + +export interface Image { + size: 'small' | 'medium' | 'large' | 'extralarge' | 'mega' | 'itunes' + url: string +} + +export interface Album { + name: string + mbid?: string + artist: Artist + playCount: number + url: string + rank: number + images: AlbumImages +} + +export interface WeeklyAlbumChart { + albums: Album[] + attr: { + user: string + from: string + to: string + } +} diff --git a/src/routes/api/lastfm/+server.ts b/src/routes/api/lastfm/+server.ts new file mode 100644 index 0000000..34c984c --- /dev/null +++ b/src/routes/api/lastfm/+server.ts @@ -0,0 +1,155 @@ +import { LastClient } from '@musicorum/lastfm' +import { + searchItunes, + ItunesSearchOptions, + ItunesMedia, + ItunesEntityMusic +} from 'node-itunes-search' +import type { RequestHandler } from './$types' +import type { Album, AlbumImages } from '$lib/types/lastfm' +import type { LastfmImage } from '@musicorum/lastfm/dist/types/packages/common' + +const LASTFM_API_KEY = process.env.LASTFM_API_KEY +const USERNAME = 'jedmund' +const ALBUM_LIMIT = 3 + +export const GET: RequestHandler = async ({ url }) => { + const client = new LastClient(LASTFM_API_KEY) + + try { + // const albums = await getWeeklyAlbumChart(client, USERNAME) + + const albums = await getRecentAlbums(client, USERNAME, ALBUM_LIMIT) + const enrichedAlbums = await Promise.all( + albums.slice(0, ALBUM_LIMIT).map((album) => enrichAlbumWithInfo(client, album)) + ) + const albumsWithItunesArt = await addItunesArtToAlbums(enrichedAlbums) + + return new Response(JSON.stringify({ albums: albumsWithItunesArt }), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + console.error('Error fetching album data:', error) + return new Response(JSON.stringify({ error: 'Failed to fetch album data' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } +} + +async function getWeeklyAlbumChart(client: LastClient, username: string): Promise { + const chart = await client.user.getWeeklyAlbumChart(username) + return chart.albums.map((album) => ({ + ...album, + images: { small: '', medium: '', large: '', extralarge: '', mega: '', default: '' } + })) +} + +async function getRecentAlbums( + client: LastClient, + username: string, + limit: number +): Promise { + const recentTracks = await client.user.getRecentTracks(username, { limit: 50, extended: true }) + const uniqueAlbums = new Map() + + for (const track of recentTracks.tracks) { + if (uniqueAlbums.size >= limit) break + + const albumKey = `${track.album.mbid || track.album.name}` + if (!uniqueAlbums.has(albumKey)) { + uniqueAlbums.set(albumKey, { + name: track.album.name, + artist: { + name: track.artist.name, + mbid: track.artist.mbid || '' + }, + playCount: 1, // This is a placeholder, as we don't have actual play count for recent albums + images: transformImages(track.images), + mbid: track.album.mbid || '', + url: track.url, + rank: uniqueAlbums.size + 1 + }) + } + } + + return Array.from(uniqueAlbums.values()) +} + +async function enrichAlbumWithInfo(client: LastClient, album: Album): Promise { + const albumInfo = await client.album.getInfo(album.name, album.artist.name) + return { + ...album, + url: albumInfo?.url || '', + images: transformImages(albumInfo?.images || []) + } +} + +async function addItunesArtToAlbums(albums: Album[]): Promise { + return Promise.all(albums.map(searchItunesForAlbum)) +} + +async function searchItunesForAlbum(album: Album): Promise { + const itunesResult = await searchItunesStores(album.name, album.artist.name) + + if (itunesResult && itunesResult.results.length > 0) { + const firstResult = itunesResult.results[0] + console.log(firstResult) + album.images.itunes = firstResult.artworkUrl100.replace('100x100', '600x600') + } + + return album +} + +async function searchItunesStores(albumName: string, artistName: string): Promise { + const stores = ['JP', 'US'] + for (const store of stores) { + const encodedTerm = encodeURIComponent(`${albumName} ${artistName}`) + const result = await searchItunes( + new ItunesSearchOptions({ + term: encodedTerm, + country: store, + media: ItunesMedia.Music, + entity: ItunesEntityMusic.Album, + limit: 1 + }) + ) + + if (result.resultCount > 0) return result + } + + return null +} + +function transformImages(images: LastfmImage[]): AlbumImages { + const transformedImages: AlbumImages = { + small: '', + medium: '', + large: '', + extralarge: '', + mega: '', + default: '' + } + + images.forEach((img) => { + switch (img.size) { + case 'small': + transformedImages.small = img.url + break + case 'medium': + transformedImages.medium = img.url + break + case 'large': + transformedImages.large = img.url + break + case 'extralarge': + transformedImages.extralarge = img.url + break + case 'mega': + transformedImages.mega = img.url + break + } + }) + + return transformedImages +}