From e6537884a31b8211e79f52a17ace2bde450d2114 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 05:25:19 +0000 Subject: [PATCH] feat: Add Python video server and improve playback Co-authored-by: mathieumoran1 --- public/js/app_player.js | 40 +++- python_video_server/README.md | 29 +++ .../__pycache__/server.cpython-312.pyc | Bin 0 -> 10541 bytes python_video_server/requirements.txt | 4 + python_video_server/server.py | 224 ++++++++++++++++++ routes/movies.js | 3 + routes/users.js | 5 +- routes/video.js | 5 +- 8 files changed, 296 insertions(+), 14 deletions(-) create mode 100644 python_video_server/README.md create mode 100644 python_video_server/__pycache__/server.cpython-312.pyc create mode 100644 python_video_server/requirements.txt create mode 100644 python_video_server/server.py diff --git a/public/js/app_player.js b/public/js/app_player.js index d89d1dc..b20f86e 100644 --- a/public/js/app_player.js +++ b/public/js/app_player.js @@ -32,17 +32,26 @@ async function fetchPlaybackProgress(movieId, token) { async function savePlaybackProgress(movieId, token, { time, duration, keepalive } = {}) { try { - await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...(token ? { 'Authorization': `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ time, duration }), - keepalive: !!keepalive, - }); + // Retry once on rare upsert races (409). + for (let attempt = 0; attempt < 2; attempt += 1) { + const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ time, duration }), + keepalive: !!keepalive, + }); + if (res.ok) return true; + if (res.status === 409) continue; + if (res.status === 401) return false; + return true; // best-effort; don't break playback on other errors + } + return true; } catch (_) { // best-effort + return true; } } @@ -59,8 +68,9 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) { const t = Number(videoEl.currentTime); const d = Number(videoEl.duration); return { - time: Number.isFinite(t) && t >= 0 ? t : 0, - duration: Number.isFinite(d) && d > 0 ? d : null, + // Use whole seconds to keep saved values stable. + time: Number.isFinite(t) && t >= 0 ? Math.floor(t) : 0, + duration: Number.isFinite(d) && d > 0 ? Math.floor(d) : null, }; } @@ -107,7 +117,13 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) { localStorage.setItem(key, JSON.stringify({ time: snap.time, duration: snap.duration, at: now })); } catch (_) {} - await savePlaybackProgress(movieId, token, { ...snap, keepalive: !!keepalive }); + const ok = await savePlaybackProgress(movieId, token, { ...snap, keepalive: !!keepalive }); + if (ok === false) { + // Token expired mid-playback; stop spamming requests and prompt user. + stopInterval(); + try { localStorage.removeItem('auth_token'); } catch (_) {} + showAlertVariant('Session expirée. Reconnecte-toi pour continuer à sauvegarder la progression.', 'warning'); + } } function startInterval() { diff --git a/python_video_server/README.md b/python_video_server/README.md new file mode 100644 index 0000000..20990cf --- /dev/null +++ b/python_video_server/README.md @@ -0,0 +1,29 @@ +# Python video server (Range streaming + JWT + Mongo) + +This is a small standalone server that mirrors the app’s `/api/video/:id` behavior: + +- Looks up `movies._id == :id` in MongoDB and reads the `video_file` field +- Streams the file from `VIDEO_FILES_DIR` with **HTTP Range** support (seeking works) +- Auth via the same JWT (`Authorization: Bearer ...`), plus a `video_auth` cookie for `