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
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user