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 }]> = [ ["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 = { ".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((resolve, reject) => { const server = createServer(getRequestListener(app.fetch)); server.once("error", reject); server.listen(port, "127.0.0.1", () => resolve()); }); }