import express from "express"; import http from "node:http"; import path from "node:path"; import cookieParser from "cookie-parser"; import cors from "cors"; import { config, isDev } from "./config" import { ensureDataPaths } from "./storage/paths" import { initAuth } from "./auth/auth.service" import authRoutes from "./auth/auth.routes" import { requireAuth } from "./auth/auth.middleware" import qbitRoutes from "./qbit/qbit.routes" import torrentRoutes from "./torrent/torrent.routes" import loopRoutes from "./loop/loop.routes" import profilesRoutes from "./loop/profiles.routes" import statusRoutes from "./status/status.routes" import timerRoutes from "./timer/timer.routes" import { QbitClient } from "./qbit/qbit.client" import { detectCapabilities } from "./qbit/qbit.capabilities" import { setQbitClient, setQbitCapabilities } from "./qbit/qbit.context" import { setQbitStatus } from "./status/status.service" import { createSocketServer } from "./realtime/socket" import { initEmitter, emitQbitHealth } from "./realtime/emitter" import { startLoopScheduler } from "./loop/loop.scheduler" import { startEnforcementWorker } from "./enforcement/enforcement.worker" import { startTimerWorker } from "./timer/timer.worker" import { logger } from "./utils/logger" process.on("unhandledRejection", (reason) => { logger.error({ reason }, "Unhandled promise rejection"); }); process.on("uncaughtException", (error) => { logger.error({ error }, "Uncaught exception"); }); let serverStarted = false; const bootstrap = async () => { await ensureDataPaths(); await initAuth(); const app = express(); app.use(cookieParser()); app.use(express.json()); if (isDev) { const fallbackOrigin = `http://localhost:${config.webPort}`; const origins = [config.webOrigin || fallbackOrigin, fallbackOrigin]; app.use( cors({ origin: origins, credentials: true, }) ); } app.use("/api/auth", authRoutes); app.use("/api/qbit", requireAuth, qbitRoutes); app.use("/api/torrent", requireAuth, torrentRoutes); app.use("/api/loop", requireAuth, loopRoutes); app.use("/api/profiles", requireAuth, profilesRoutes); app.use("/api/status", requireAuth, statusRoutes); app.use("/api/timer", requireAuth, timerRoutes); if (!isDev) { app.use(express.static(config.webPublicDir)); app.get("*", (req, res, next) => { if (req.path.startsWith("/api")) { return next(); } return res.sendFile(path.join(config.webPublicDir, "index.html")); }); } const server = http.createServer(app); const io = createSocketServer(server); initEmitter(io); const qbit = new QbitClient(); setQbitClient(qbit); try { const caps = await detectCapabilities(qbit); setQbitCapabilities(caps); setQbitStatus({ ok: true, version: caps.version, capabilities: caps }); emitQbitHealth({ ok: true, version: caps.version, capabilities: caps }); } catch (error) { logger.error({ error }, "Failed to connect to qBittorrent"); setQbitStatus({ ok: false, lastError: (error as Error).message }); emitQbitHealth({ ok: false, lastError: (error as Error).message }); } startLoopScheduler(qbit, config.pollIntervalMs); startEnforcementWorker(config.enforceIntervalMs); startTimerWorker(qbit, config.timerPollMs); server.listen(config.port, () => { serverStarted = true; logger.info(`q-buffer server listening on ${config.port}`); }); }; const startWithRetry = () => { bootstrap().catch((error) => { logger.error({ error }, "Failed to start server"); if (!serverStarted) { setTimeout(startWithRetry, 5000); } }); }; startWithRetry();