Files
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

104 lines
4.3 KiB
TypeScript

import { createServer } from "node:http";
import { existsSync, readFileSync, statSync } from "node:fs";
import { extname, join, normalize, sep } from "node:path";
import { Hono } from "hono";
import { getRequestListener } from "@hono/node-server";
import * as identify from "../endpoints/identify_POST";
import * as lyrics from "../endpoints/lyrics_POST";
import * as metadataEnrich from "../endpoints/metadata-enrich_POST";
import * as spotifyAlbum from "../endpoints/spotify-album_GET";
import * as spotifyArtistAlbums from "../endpoints/spotify-artist-albums_GET";
import * as spotifyAuth from "../endpoints/spotify-auth_GET";
import * as spotifyDownload from "../endpoints/spotify-download_POST";
import * as spotifyDownloadFile from "../endpoints/spotify-download-file_GET";
import * as spotifyDownloadStatus from "../endpoints/spotify-download-status_GET";
import * as spotifyExchange from "../endpoints/spotify-exchange_POST";
import * as spotifyLibrary from "../endpoints/spotify-library_GET";
import * as spotifyLogout from "../endpoints/spotify-logout_POST";
import * as spotifyPlaylist from "../endpoints/spotify-playlist_POST";
import * as spotifyPlaylists from "../endpoints/spotify-playlists_GET";
import * as spotifySession from "../endpoints/spotify-session_GET";
const routes: Array<["get" | "post", string, { handle: (request: Request) => Promise<Response> }]> = [
["post", "identify", identify],
["get", "spotify-auth", spotifyAuth],
["post", "spotify-exchange", spotifyExchange],
["get", "spotify-session", spotifySession],
["post", "spotify-logout", spotifyLogout],
["post", "spotify-playlist", spotifyPlaylist],
["get", "spotify-playlists", spotifyPlaylists],
["post", "metadata-enrich", metadataEnrich],
["post", "lyrics", lyrics],
["get", "spotify-library", spotifyLibrary],
["get", "spotify-artist-albums", spotifyArtistAlbums],
["get", "spotify-album", spotifyAlbum],
["post", "spotify-download", spotifyDownload],
["get", "spotify-download-status", spotifyDownloadStatus],
["get", "spotify-download-file", spotifyDownloadFile],
];
const CONTENT_TYPES: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript",
".css": "text/css",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".woff2": "font/woff2",
".json": "application/json",
".wasm": "application/wasm",
};
function serveFile(path: string) {
return new Response(readFileSync(path), {
headers: {
"Content-Type": CONTENT_TYPES[extname(path)] || "application/octet-stream",
"Cache-Control": extname(path) === ".html" ? "no-cache" : "public, max-age=31536000, immutable",
},
});
}
type ServerHooks = {
onSpotifyLogin?: (setCookies: string[]) => void;
};
export function startApiServer(port: number, distDir: string, hooks: ServerHooks = {}) {
const app = new Hono();
app.use("/_api/*", async (c, next) => {
await next();
console.log(`${new Date().toISOString()} ${c.req.method} ${c.req.path} -> ${c.res.status}`);
});
app.use("/spotify/*", async (c, next) => {
await next();
console.log(`${new Date().toISOString()} ${c.req.method} ${c.req.path}${new URL(c.req.url).search.replace(/code=[^&]+/, "code=…")} -> ${c.res.status}`);
});
for (const [method, name, endpoint] of routes) {
app[method](`/_api/${name}`, async (c) => {
try {
const response = await endpoint.handle(c.req.raw);
if (name === "spotify-exchange" && response.ok) hooks.onSpotifyLogin?.(response.headers.getSetCookie());
return response;
} catch (error) {
console.error(error);
return c.text("Endpoint error: " + (error instanceof Error ? error.message : String(error)), 500);
}
});
}
app.get("*", (c) => {
if (c.req.path.startsWith("/_api/")) return c.notFound();
const candidate = normalize(join(distDir, decodeURIComponent(c.req.path)));
if (candidate.startsWith(distDir + sep) && existsSync(candidate) && statSync(candidate).isFile()) {
return serveFile(candidate);
}
return serveFile(join(distDir, "index.html"));
});
return new Promise<void>((resolve, reject) => {
const server = createServer(getRequestListener(app.fetch));
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
}