const express = require("express"); const fs = require("fs"); const path = require("path"); const jwt = require("jsonwebtoken"); const mongoose = require("mongoose"); const Movie = require("../models/Movie"); const router = express.Router(); const SUBTITLE_FILES_DIR = path.resolve(process.env.SUBTITLE_FILES_DIR || path.join(process.cwd(), "subtitles")); function parseCookies(header) { const cookies = {}; const raw = String(header || ""); if (!raw) return cookies; raw.split(";").forEach((part) => { const idx = part.indexOf("="); if (idx <= 0) return; const k = part.slice(0, idx).trim(); const v = part.slice(idx + 1).trim(); if (!k) return; cookies[k] = decodeURIComponent(v); }); return cookies; } function getAuthToken(req) { const cookies = parseCookies(req.headers.cookie); const cookieToken = cookies.video_auth ? String(cookies.video_auth) : ""; if (cookieToken) return cookieToken; const authHeader = String(req.headers.authorization || ""); const headerToken = authHeader.toLowerCase().startsWith("bearer ") ? authHeader.slice(7).trim() : ""; if (headerToken) return headerToken; const qs = typeof req.query?.token === "string" ? req.query.token.trim() : ""; return qs || ""; } function requireAuth(req, res) { const token = getAuthToken(req); if (!token) return { ok: false, error: res.status(401).json({ message: "Missing subtitle auth token" }) }; try { const decoded = jwt.verify(token, process.env.JWT_SECRET); return { ok: true, decoded }; } catch (_) { return { ok: false, error: res.status(401).json({ message: "Invalid or expired subtitle auth token" }) }; } } function isSafeRelativeFile(p) { if (typeof p !== "string") return false; const s = p.trim(); if (!s) return false; if (s.includes("\0")) return false; if (path.isAbsolute(s)) return false; const norm = path.posix.normalize(s.replaceAll("\\", "/")); if (norm.startsWith("../") || norm === "..") return false; return true; } async function streamSubtitle(req, res) { const auth = requireAuth(req, res); if (!auth.ok) return; const { id } = req.params; if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ message: "Invalid movie id" }); } const movie = await Movie.findById(id).select("subtitles subtitle_file subtitle_lang subtitle_label subtitle_default").lean(); if (!movie) return res.status(404).json({ message: "Movie not found" }); let subtitleFile = ""; const subtitleId = req.params?.subtitleId; const subs = Array.isArray(movie?.subtitles) ? movie.subtitles : []; if (subtitleId) { if (subtitleId === "legacy") { subtitleFile = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : ""; } else { const match = subs.find((s) => String(s?._id || "") === String(subtitleId)); subtitleFile = typeof match?.file === "string" ? match.file.trim() : ""; } } else if (subs.length > 0) { const preferred = subs.find((s) => s?.default) || subs[0]; subtitleFile = typeof preferred?.file === "string" ? preferred.file.trim() : ""; } else { subtitleFile = typeof movie?.subtitle_file === "string" ? movie.subtitle_file.trim() : ""; } if (!subtitleFile) return res.status(404).json({ message: "No subtitles configured for this movie" }); if (!isSafeRelativeFile(subtitleFile)) return res.status(400).json({ message: "Invalid subtitle_file path" }); const fullPath = path.resolve(SUBTITLE_FILES_DIR, subtitleFile); if (!fullPath.startsWith(SUBTITLE_FILES_DIR + path.sep) && fullPath !== SUBTITLE_FILES_DIR) { return res.status(400).json({ message: "Invalid subtitle_file path" }); } let stat; try { stat = await fs.promises.stat(fullPath); } catch (_) { return res.status(404).json({ message: "Subtitle file not found on server" }); } if (!stat.isFile()) return res.status(404).json({ message: "Subtitle file not found on server" }); res.setHeader("Content-Type", "text/vtt; charset=utf-8"); res.setHeader("Content-Length", String(stat.size)); res.setHeader("Cache-Control", "private, max-age=0, must-revalidate"); if (req.method === "HEAD") { return res.status(200).end(); } return fs.createReadStream(fullPath).pipe(res); } router.get("/:id/:subtitleId", (req, res) => { streamSubtitle(req, res).catch((err) => { return res.status(500).json({ error: err?.message || "Subtitle streaming error" }); }); }); router.head("/:id/:subtitleId", (req, res) => { streamSubtitle(req, res).catch((err) => { return res.status(500).json({ error: err?.message || "Subtitle streaming error" }); }); }); router.get("/:id", (req, res) => { streamSubtitle(req, res).catch((err) => { return res.status(500).json({ error: err?.message || "Subtitle streaming error" }); }); }); router.head("/:id", (req, res) => { streamSubtitle(req, res).catch((err) => { return res.status(500).json({ error: err?.message || "Subtitle streaming error" }); }); }); module.exports = router;