From 891eb67e2d2b96cf1e27eabc45685f8989e769d4 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Sun, 25 Jan 2026 01:00:48 -0500 Subject: [PATCH] Add subtitle UI badge and VTT positioning Documents the disclaimer, shows subtitles in admin movie badges, and offsets VTT subtitles from the bottom. --- README.md | 2 ++ public/js/app_control.js | 2 ++ python_video_server/README.md | 3 +++ routes/movies.js | 10 +++++++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 194cc52..8fd3a71 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Little nodejs + mongodb app that I hack together with ChatGPT and Cursor to keep **Note:** The application interface is in French. +**Disclaimer:** This was a fun, experimental project built with “AI slop” (Cursor), so it may have security issues. For example, `python_video_server` uses the default token‑in‑URL approach to serve video files, which can leave user tokens in logs or browser history. There are ways to improve this, but at this point I’m blinder than Ray Charles. + ![Oscars Pool 2026](https://raw.githubusercontent.com/quoije/oscars-pool/refs/heads/prod/img/preview.png) ## Features diff --git a/public/js/app_control.js b/public/js/app_control.js index 0ba40d1..c846b30 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -2442,12 +2442,14 @@ window.onload = async function () { const hasLowFile = !!(m && m.video_file_low); const hasEmbed = !!(m && m.embed_src); const hasLegacy = !!(m && m.vod_link); + const hasSubtitle = !!(m && m.subtitle_file); if (hasVideo) badges.push({ text: 'Video', cls: 'bg-primary' }); if (hasFile) badges.push({ text: 'Server file', cls: 'bg-dark' }); if (hasLowFile) badges.push({ text: 'Low file', cls: 'bg-secondary' }); if (hasEmbed) badges.push({ text: 'Embed', cls: 'bg-info text-dark' }); if (hasLegacy) badges.push({ text: 'Legacy', cls: 'bg-secondary' }); + if (hasSubtitle) badges.push({ text: 'Subtitles', cls: 'bg-warning text-dark' }); const modeRaw = String(m?.player_mode || 'auto').toLowerCase(); const mode = (modeRaw === 'video' || modeRaw === 'embed' || modeRaw === 'auto') ? modeRaw : 'auto'; diff --git a/python_video_server/README.md b/python_video_server/README.md index 8091ee3..e0ba9a7 100644 --- a/python_video_server/README.md +++ b/python_video_server/README.md @@ -15,7 +15,10 @@ export MONGO_DB_NAME="..." # optional; required if MO export JWT_SECRET="..." export VIDEO_FILES_DIR="/absolute/path/to/public/video" # optional (default: ./public/video) export VIDEO_SESSION_MAX_AGE_SECONDS="28800" # optional (default: 8h) + uvicorn python_video_server.server:app --host 0.0.0.0 --port 8000 +or +python3 -m uvicorn python_video_server.server:app --host 0.0.0.0 --port 8000 --ssl-certfile python_video_server/certs/config/live/[site]/fullchain.pem --ssl-keyfile python_video_server/certs/config/live/[site]/privkey.pem ``` ## Extra environment variables (optional) diff --git a/routes/movies.js b/routes/movies.js index f40a982..a0113bb 100644 --- a/routes/movies.js +++ b/routes/movies.js @@ -122,7 +122,15 @@ async function convertSrtToVtt(filePath) { const content = await fs.promises.readFile(filePath, "utf8"); const normalized = content.replace(/\r\n/g, "\n"); const body = normalized.replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, "$1.$2"); - const vtt = `WEBVTT\n\n${body.trim()}\n`; + const positioned = body.replace( + /^(\d{2}:\d{2}:\d{2}\.\d{3}\s+-->\s+\d{2}:\d{2}:\d{2}\.\d{3})(.*)$/gm, + (_match, timing, settings) => { + const tail = String(settings || ""); + if (/\bline:/i.test(tail)) return `${timing}${tail}`; + return `${timing}${tail} line:90%`; + }, + ); + const vtt = `WEBVTT\n\n${positioned.trim()}\n`; const vttPath = filePath.replace(/\.srt$/i, ".vtt"); await fs.promises.writeFile(vttPath, vtt, "utf8"); try { await fs.promises.unlink(filePath); } catch (_) {}