const fastify = require("fastify")({ logger: { level: "error" }, }); const path = require("path"); const jwt = require("jsonwebtoken"); const cron = require("node-cron"); const { initHeadless } = require("./dist/shared/headless"); const { initDatabase } = require("./dist/shared/database"); const { loadExtensions } = require("./dist/shared/extensions"); const {refreshTrendingAnime, refreshTopAiringAnime} = require("./dist/api/anime/anime.service"); const {refreshPopularBooks, refreshTrendingBooks} = require("./dist/api/books/books.service"); const { ensureConfigFile } = require("./dist/shared/config"); const dotenv = require("dotenv"); dotenv.config(); const viewsRoutes = require("./dist/views/views.routes"); const animeRoutes = require("./dist/api/anime/anime.routes"); const booksRoutes = require("./dist/api/books/books.routes"); const proxyRoutes = require("./dist/api/proxy/proxy.routes"); const extensionsRoutes = require("./dist/api/extensions/extensions.routes"); const galleryRoutes = require("./dist/api/gallery/gallery.routes"); const userRoutes = require("./dist/api/user/user.routes"); const listRoutes = require("./dist/api/list/list.routes"); const anilistRoute = require("./dist/api/anilist/anilist"); const localRoutes = require("./dist/api/local/local.routes"); const configRoutes = require("./dist/api/config/config.routes"); const roomRoutes = require("./dist/api/rooms/rooms.routes"); const { setupRoomWebSocket } = require("./dist/api/rooms/rooms.websocket"); const { getConfig } = require('./dist/shared/config'); const { values } = getConfig(); const jwtSecret = values.server?.jwt_secret; fastify.addHook("preHandler", async (request, reply) => { const auth = request.headers.authorization; if (!auth) return; try { const token = auth.replace("Bearer ", ""); request.user = jwt.verify(token, jwtSecret); } catch (e) { return reply.code(401).send({ error: "Invalid token" }); } }); const roomService = require('./dist/api/rooms/rooms.service'); fastify.addHook('onRequest', async (req, reply) => { const isTunnel = !!req.headers['cf-connecting-ip'] || !!req.headers['cf-ray']; if (!isTunnel) return; if (req.url.startsWith('/public/') || req.url.startsWith('/views/') || req.url.startsWith('/src/')) { return; } if (req.url.startsWith('/room')) { const urlParams = new URLSearchParams(req.url.split('?')[1]); const roomId = urlParams.get('id'); if (!roomId) { return reply.code(404).send({ error: 'Room ID required' }); } const room = roomService.getRoom(roomId); if (!room || room.exposed !== true) { return reply.code(404).send({ error: 'Room not found' }); } return; } const wsMatch = req.url.match(/^\/ws\/room\/([a-f0-9]+)/); if (wsMatch) { const roomId = wsMatch[1]; const room = roomService.getRoom(roomId); if (!room || room.exposed !== true) { return reply.code(404).send({ error: 'Room not found' }); } return; } const apiMatch = req.url.match(/^\/api\/rooms\/([a-f0-9]+)/); if (apiMatch) { const roomId = apiMatch[1]; const room = roomService.getRoom(roomId); if (!room || room.exposed !== true) { return reply.code(404).send({ error: 'Room not found' }); } return; } const allowedEndpoints = [ '/api/watch/stream', '/api/proxy', '/api/extensions', '/api/search' ]; for (const endpoint of allowedEndpoints) { if (req.url.startsWith(endpoint)) { console.log('[Tunnel] ✓ Allowing utility endpoint:', endpoint); return; } } return reply.code(404).send({ error: 'Not found' }); }); fastify.register(require("@fastify/static"), { root: path.join(__dirname, "public"), prefix: "/public/", decorateReply: false, }); fastify.register(require("@fastify/static"), { root: path.join(__dirname, "views"), prefix: "/views/", decorateReply: false, }); fastify.register(require("@fastify/static"), { root: path.join(__dirname, "src", "scripts"), prefix: "/src/scripts/", decorateReply: false, }); fastify.register(viewsRoutes); fastify.register(animeRoutes, { prefix: "/api" }); fastify.register(booksRoutes, { prefix: "/api" }); fastify.register(proxyRoutes, { prefix: "/api" }); fastify.register(extensionsRoutes, { prefix: "/api" }); fastify.register(galleryRoutes, { prefix: "/api" }); fastify.register(userRoutes, { prefix: "/api" }); fastify.register(anilistRoute, { prefix: "/api" }); fastify.register(listRoutes, { prefix: "/api" }); fastify.register(localRoutes, { prefix: "/api" }); fastify.register(configRoutes, { prefix: "/api" }); fastify.register(roomRoutes, { prefix: "/api" }); const sleep = ms => new Promise(r => setTimeout(r, ms)); const start = async () => { try { await fastify.register(require('@fastify/websocket')); ensureConfigFile() initDatabase("anilist"); initDatabase("favorites"); initDatabase("cache"); initDatabase("userdata"); initDatabase("local_library"); setupRoomWebSocket(fastify); const refreshAll = async () => { await refreshTrendingAnime(); await sleep(300); await refreshTopAiringAnime(); await sleep(300); await refreshTrendingBooks(); await sleep(300); await refreshPopularBooks(); }; const job = cron.schedule("*/30 * * * *", async () => { try { await refreshAll(); console.log("cache refreshed"); } catch (e) { console.error("refresh failed", e); } }); await loadExtensions(); await initHeadless(); await fastify.listen({ port: 54322, host: "0.0.0.0" }); refreshAll().catch(e => console.error("initial refresh failed", e) ); console.log(`Server running at http://localhost:54322`); const shutdown = async () => { job.stop(); await fastify.close(); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); } catch (err) { fastify.log.error(err); process.exit(1); } }; start();