Files
Aurora-SpotiRip/endpoints/spotify-artist-albums_GET.ts
R3ap3r 3178b8f4f7 Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback
- Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks
- Electron desktop app (castLabs Widevine) with a permanent sidebar layout
- Linux and Windows packaging scripts
2026-09-25 23:20:57 -05:00

135 lines
3.6 KiB
TypeScript

import superjson from "superjson";
import {
appendSetCookies,
ensureSpotifyAccessToken,
} from "../helpers/spotifyServer";
import type {
OutputType,
SpotifyArtistAlbum,
} from "./spotify-artist-albums_GET.schema";
type ImageRef = { url?: string };
type SpotifyArtist = {
id?: string;
name?: string;
images?: ImageRef[];
external_urls?: { spotify?: string };
};
type SpotifyAlbum = {
id?: string;
name?: string;
album_type?: string;
release_date?: string;
total_tracks?: number;
images?: ImageRef[];
external_urls?: { spotify?: string };
};
type AlbumPage = {
items?: SpotifyAlbum[];
total?: number;
offset?: number;
limit?: number;
next?: string | null;
};
async function spotifyGet<T>(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 firstImage(images?: ImageRef[]) {
return images?.find((entry) => entry.url)?.url;
}
export async function handle(request: Request) {
try {
const url = new URL(request.url);
const artistId = url.searchParams.get("artistId")?.trim();
const offset = Math.max(0, Number(url.searchParams.get("offset") || 0) || 0);
if (!artistId) {
return new Response(
superjson.stringify({ error: "Artist 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 [artist, page] = await Promise.all([
spotifyGet<SpotifyArtist>(
"https://api.spotify.com/v1/artists/" + encodeURIComponent(artistId),
token,
),
spotifyGet<AlbumPage>(
"https://api.spotify.com/v1/artists/" +
encodeURIComponent(artistId) +
"/albums?include_groups=album,single,compilation&limit=50&offset=" +
offset,
token,
),
]);
if (!artist.id) {
throw new Error("Spotify did not return the requested artist.");
}
const seen = new Set<string>();
const albums: SpotifyArtistAlbum[] = [];
for (const album of page.items || []) {
if (!album.id || seen.has(album.id)) continue;
seen.add(album.id);
albums.push({
id: album.id,
name: album.name || "Untitled release",
albumType: album.album_type,
releaseDate: album.release_date,
totalTracks: album.total_tracks,
imageUrl: firstImage(album.images),
externalUrl: album.external_urls?.spotify,
});
}
const output: OutputType = {
artist: {
id: artist.id,
name: artist.name || "Unknown artist",
imageUrl: firstImage(artist.images),
externalUrl: artist.external_urls?.spotify,
},
albums,
total: page.total,
nextOffset: page.next
? offset + (page.limit || 50)
: undefined,
};
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 artist discography.";
return new Response(superjson.stringify({ error: message }), { status: 500 });
}
}