import superjson from "superjson"; import { appendSetCookies, ensureSpotifyAccessToken, } from "../helpers/spotifyServer"; import type { OutputType, SpotifyAlbumTrack, } from "./spotify-album_GET.schema"; type ArtistRef = { name?: string }; type ImageRef = { url?: string }; type TrackObject = { id?: string; name?: string; uri?: string; duration_ms?: number; track_number?: number; disc_number?: number; artists?: ArtistRef[]; external_urls?: { spotify?: string }; }; type TrackPage = { items?: TrackObject[]; next?: string | null; }; type SpotifyAlbum = { id?: string; name?: string; release_date?: string; total_tracks?: number; artists?: ArtistRef[]; images?: ImageRef[]; external_urls?: { spotify?: string }; tracks?: TrackPage; }; function artistNames(values?: ArtistRef[]) { return (values || []).map((entry) => entry.name).filter(Boolean).join(", "); } function firstImage(images?: ImageRef[]) { return images?.find((entry) => entry.url)?.url; } async function spotifyGet(url: string, token: string) { const response = await fetch(url, { headers: { Authorization: "Bearer " + token }, }); if (!response.ok) { const message = await response.text(); throw new Error( "Spotify returned " + response.status + ": " + message.slice(0, 220), ); } return (await response.json()) as T; } function normalizeTrack(track: TrackObject): SpotifyAlbumTrack | undefined { if (!track.id) return undefined; return { id: track.id, name: track.name || "Untitled track", artist: artistNames(track.artists) || "Unknown artist", uri: track.uri, durationMs: track.duration_ms || 0, trackNumber: track.track_number, discNumber: track.disc_number, externalUrl: track.external_urls?.spotify, }; } export async function handle(request: Request) { try { const url = new URL(request.url); const albumId = url.searchParams.get("albumId")?.trim(); if (!albumId) { return new Response( superjson.stringify({ error: "Album ID is required." }), { status: 400 }, ); } const session = await ensureSpotifyAccessToken(request); if (!session.accessToken) { return new Response( superjson.stringify({ error: "Connect Spotify first." }), { status: 401 }, ); } const token = session.accessToken; const album = await spotifyGet( "https://api.spotify.com/v1/albums/" + encodeURIComponent(albumId), token, ); if (!album.id) { throw new Error("Spotify did not return the requested album."); } const tracks: SpotifyAlbumTrack[] = []; for (const item of album.tracks?.items || []) { const normalized = normalizeTrack(item); if (normalized) tracks.push(normalized); } let next = album.tracks?.next || null; while (next) { const page = await spotifyGet(next, token); for (const item of page.items || []) { const normalized = normalizeTrack(item); if (normalized) tracks.push(normalized); } next = page.next || null; } const output: OutputType = { album: { id: album.id, name: album.name || "Untitled album", artist: artistNames(album.artists) || "Unknown artist", releaseDate: album.release_date, totalTracks: album.total_tracks, imageUrl: firstImage(album.images), externalUrl: album.external_urls?.spotify, }, tracks, }; const headers = new Headers({ "Content-Type": "application/json", "Cache-Control": "no-store", }); appendSetCookies(headers, session.setCookies); return new Response(superjson.stringify(output), { headers }); } catch (error) { const message = error instanceof Error ? error.message : "Unable to load Spotify album."; return new Response(superjson.stringify({ error: message }), { status: 500 }); } }