import express from "express"; import expressWs from "express-ws"; import { MediaPlayer } from "./MediaPlayer"; import { searchInvidious, fetchThumbnail } from "./InvidiousAPI"; import { PlaylistItem } from './types'; const app = express(); app.use(express.json()); expressWs(app); const apiRouter = express.Router(); const mediaPlayer = new MediaPlayer(); const withErrorHandling = (func: (req: any, res: any) => Promise) => { return async (req: any, res: any) => { try { await func(req, res); } catch (error: any) { res.status(500).send(JSON.stringify({ success: false, error: error.message })); } }; }; apiRouter.get("/playlist", withErrorHandling(async (req, res) => { const playlist = await mediaPlayer.getPlaylist(); res.send(playlist); })); apiRouter.post("/playlist", withErrorHandling(async (req, res) => { const { url } = req.body as { url: string }; await mediaPlayer.append(url); res.send(JSON.stringify({ success: true })); })); apiRouter.delete("/playlist/:index", withErrorHandling(async (req, res) => { const { index } = req.params as { index: string }; await mediaPlayer.deletePlaylistItem(parseInt(index)); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/playlist/replace", withErrorHandling(async (req, res) => { const { url } = req.body as { url: string }; await mediaPlayer.replace(url); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/play", withErrorHandling(async (req, res) => { await mediaPlayer.play(); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/pause", withErrorHandling(async (req, res) => { await mediaPlayer.pause(); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/skip", withErrorHandling(async (req, res) => { await mediaPlayer.skip(); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/skip/:index", withErrorHandling(async (req, res) => { const { index } = req.params as { index: string }; await mediaPlayer.skipTo(parseInt(index)); res.send(JSON.stringify({ success: true })); })); apiRouter.post("/previous", withErrorHandling(async (req, res) => { await mediaPlayer.previous(); res.send(JSON.stringify({ success: true })); })); apiRouter.get("/nowplaying", withErrorHandling(async (req, res) => { const playingItem = await mediaPlayer.getNowPlaying(); const currentFile = await mediaPlayer.getCurrentFile(); const pauseState = await mediaPlayer.getPauseState(); const volume = await mediaPlayer.getVolume(); const idle = await mediaPlayer.getIdle(); res.send(JSON.stringify({ success: true, playingItem: playingItem, isPaused: pauseState, volume: volume, isIdle: idle, currentFile: currentFile })); })); apiRouter.post("/volume", withErrorHandling(async (req, res) => { const { volume } = req.body as { volume: number }; await mediaPlayer.setVolume(volume); res.send(JSON.stringify({ success: true })); })); apiRouter.ws("/events", (ws, req) => { console.log("Events client connected"); mediaPlayer.subscribe(ws); ws.on("close", () => { console.log("Events client disconnected"); mediaPlayer.unsubscribe(ws); }); }); apiRouter.get("/search", withErrorHandling(async (req, res) => { const query = req.query.q as string; if (!query) { res.status(400) .send(JSON.stringify({ success: false, error: "Query parameter 'q' is required" })); return; } const results = await searchInvidious(query); res.send(JSON.stringify({ success: true, results })); })); apiRouter.get("/thumbnail", withErrorHandling(async (req, res) => { const thumbnailUrl = req.query.url as string; if (!thumbnailUrl) { res.status(400) .send(JSON.stringify({ success: false, error: "URL parameter is required" })); return; } try { const { data, contentType } = await fetchThumbnail(thumbnailUrl); res.set('Content-Type', contentType); data.pipe(res); } catch (error) { console.error('Failed to proxy thumbnail:', error); res.status(500) .send(JSON.stringify({ success: false, error: 'Failed to fetch thumbnail' })); } })); apiRouter.get("/favorites", withErrorHandling(async (req, res) => { const favorites = await mediaPlayer.getFavorites(); res.send(JSON.stringify(favorites)); })); apiRouter.post("/favorites", withErrorHandling(async (req, res) => { const item = req.body as PlaylistItem; await mediaPlayer.addFavorite(item); res.send(JSON.stringify({ success: true })); })); apiRouter.delete("/favorites/:id", withErrorHandling(async (req, res) => { const { id } = req.params; await mediaPlayer.removeFavorite(parseInt(id)); res.send(JSON.stringify({ success: true })); })); apiRouter.delete("/favorites", withErrorHandling(async (req, res) => { await mediaPlayer.clearFavorites(); res.send(JSON.stringify({ success: true })); })); // Serve static files for React app (after building) app.use(express.static("dist/frontend")); // Mount API routes under /api app.use("/api", apiRouter); // Serve React app for all other routes (client-side routing) app.get("*", (req, res) => { res.sendFile("dist/frontend/index.html", { root: "." }); }); const port = process.env.PORT || 3000; const server = app.listen(port, () => { console.log(`Server is running on port ${port}`); }); // Add graceful shutdown handling const shutdown = async () => { console.log('Received shutdown signal. Closing server...'); server.close(() => { console.log('Server closed'); process.exit(0); }); // Force termination after some timeout (10sec) setTimeout(() => { console.log('Forcing server shutdown'); process.exit(1); }, 10000); }; // Handle various shutdown signals process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);