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,144 @@
|
||||
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<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 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<SpotifyAlbum>(
|
||||
"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<TrackPage>(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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user