import superjson from "superjson"; import { appendSetCookies, ensureSpotifyAccessToken, } from "../helpers/spotifyServer"; import type { OutputType, SpotifyLibraryItem, SpotifyLibrarySection, } from "./spotify-library_GET.schema"; type Page = { items?: T[]; next?: string | null; total?: number; offset?: number; limit?: number; }; type ArtistRef = { name?: string; }; type ImageRef = { url?: string; }; type TrackObject = { id?: string; name?: string; uri?: string; duration_ms?: number; artists?: ArtistRef[]; album?: { name?: string; images?: ImageRef[]; }; external_urls?: { spotify?: string }; }; type SavedTrack = { added_at?: string; track?: TrackObject | null; }; type AlbumObject = { id?: string; name?: string; uri?: string; release_date?: string; total_tracks?: number; artists?: ArtistRef[]; images?: ImageRef[]; external_urls?: { spotify?: string }; }; type SavedAlbum = { added_at?: string; album?: AlbumObject | null; }; type ArtistObject = { id?: string; name?: string; uri?: string; genres?: string[]; images?: ImageRef[]; external_urls?: { spotify?: string }; }; type FollowedArtistsResponse = { artists?: { items?: ArtistObject[]; next?: string | null; total?: number; cursors?: { after?: string | null }; }; }; type ShowObject = { id?: string; name?: string; uri?: string; publisher?: string; total_episodes?: number; images?: ImageRef[]; external_urls?: { spotify?: string }; }; type SavedShow = { added_at?: string; show?: ShowObject | null; }; function image(images?: ImageRef[]) { return images?.find((entry) => entry.url)?.url; } function artists(values?: ArtistRef[]) { return (values || []).map((entry) => entry.name).filter(Boolean).join(", "); } async function spotifyGet(url: string, token: string) { const response = await fetch(url, { headers: { Authorization: "Bearer " + token }, }); if (!response.ok) { const message = await response.text(); if (response.status === 403) { throw new Error( "Spotify did not grant access to this library section. Disconnect and reconnect Spotify to approve the new library permissions.", ); } throw new Error( "Spotify returned " + response.status + ": " + message.slice(0, 220), ); } return (await response.json()) as T; } function offsetCursor(page: { offset?: number; limit?: number; next?: string | null }) { if (!page.next) return undefined; return String((page.offset || 0) + (page.limit || 50)); } export async function handle(request: Request) { try { const url = new URL(request.url); const section = (url.searchParams.get("section") || "tracks") as SpotifyLibrarySection; const cursor = url.searchParams.get("cursor") || undefined; if (!["tracks", "albums", "artists", "shows"].includes(section)) { return new Response( superjson.stringify({ error: "Unknown Spotify library section." }), { 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 output: OutputType = { section, items: [] }; const limit = 50; if (section === "tracks") { const offset = Number(cursor || 0) || 0; const page = await spotifyGet>( "https://api.spotify.com/v1/me/tracks?limit=" + limit + "&offset=" + offset, token, ); output.items = (page.items || []) .map((entry): SpotifyLibraryItem | undefined => { const track = entry.track; if (!track?.id) return undefined; return { id: track.id, type: "track", name: track.name || "Untitled track", subtitle: artists(track.artists), detail: track.album?.name, imageUrl: image(track.album?.images), externalUrl: track.external_urls?.spotify, uri: track.uri, durationMs: track.duration_ms, addedAt: entry.added_at, }; }) .filter((entry): entry is SpotifyLibraryItem => Boolean(entry)); output.total = page.total; output.nextCursor = offsetCursor(page); } else if (section === "albums") { const offset = Number(cursor || 0) || 0; const page = await spotifyGet>( "https://api.spotify.com/v1/me/albums?limit=" + limit + "&offset=" + offset, token, ); output.items = (page.items || []) .map((entry): SpotifyLibraryItem | undefined => { const album = entry.album; if (!album?.id) return undefined; return { id: album.id, type: "album", name: album.name || "Untitled album", subtitle: artists(album.artists), detail: album.release_date, imageUrl: image(album.images), externalUrl: album.external_urls?.spotify, uri: album.uri, addedAt: entry.added_at, total: album.total_tracks, }; }) .filter((entry): entry is SpotifyLibraryItem => Boolean(entry)); output.total = page.total; output.nextCursor = offsetCursor(page); } else if (section === "artists") { const after = cursor ? "&after=" + encodeURIComponent(cursor) : ""; const result = await spotifyGet( "https://api.spotify.com/v1/me/following?type=artist&limit=" + limit + after, token, ); const page = result.artists; output.items = (page?.items || []) .map((artist): SpotifyLibraryItem | undefined => { if (!artist.id) return undefined; return { id: artist.id, type: "artist", name: artist.name || "Unknown artist", subtitle: artist.genres?.slice(0, 3).join(" ยท "), imageUrl: image(artist.images), externalUrl: artist.external_urls?.spotify, uri: artist.uri, }; }) .filter((entry): entry is SpotifyLibraryItem => Boolean(entry)); output.total = page?.total; output.nextCursor = page?.next ? page.cursors?.after || undefined : undefined; } else { const offset = Number(cursor || 0) || 0; const page = await spotifyGet>( "https://api.spotify.com/v1/me/shows?limit=" + limit + "&offset=" + offset, token, ); output.items = (page.items || []) .map((entry): SpotifyLibraryItem | undefined => { const show = entry.show; if (!show?.id) return undefined; return { id: show.id, type: "show", name: show.name || "Untitled show", subtitle: show.publisher, imageUrl: image(show.images), externalUrl: show.external_urls?.spotify, uri: show.uri, addedAt: entry.added_at, total: show.total_episodes, }; }) .filter((entry): entry is SpotifyLibraryItem => Boolean(entry)); output.total = page.total; output.nextCursor = offsetCursor(page); } 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 library."; return new Response(superjson.stringify({ error: message }), { status: 500 }); } }