Merge pull request #90 from quoije/2026
Fix cookie expiration, ranking by completion time, and checklist confirmation
This commit was merged in pull request #90.
This commit is contained in:
@@ -149,6 +149,25 @@
|
|||||||
<!-- Include confetti-js library -->
|
<!-- Include confetti-js library -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||||
|
|
||||||
|
<!-- Uncheck Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="uncheckConfirmModal" tabindex="-1" aria-labelledby="uncheckConfirmModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="uncheckConfirmModalLabel">Confirmer le retrait</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Es-tu sûr de vouloir retirer <strong id="uncheckMovieTitle"></strong> de ta liste?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmUncheckBtn">Retirer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Reward Modal -->
|
<!-- Reward Modal -->
|
||||||
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
|
<div class="modal fade" id="videoModal" tabindex="-1" aria-labelledby="videoModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
|||||||
+65
-12
@@ -475,18 +475,71 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
const watchedDateCell = document.getElementById(`watched-date-${movie.imdb_id}`);
|
const watchedDateCell = document.getElementById(`watched-date-${movie.imdb_id}`);
|
||||||
const row = event.target.closest('tr'); // Get the parent row
|
const row = event.target.closest('tr'); // Get the parent row
|
||||||
|
|
||||||
if (isChecked) {
|
// If unchecking, show confirmation modal first
|
||||||
watchedDateCell.textContent = new Date().toLocaleString();
|
if (!isChecked) {
|
||||||
if (row) {
|
// Revert the checkbox temporarily until confirmed
|
||||||
row.classList.add('table-success');
|
event.target.checked = true;
|
||||||
row.classList.remove('table-danger');
|
|
||||||
}
|
// Show confirmation modal
|
||||||
} else {
|
const uncheckModal = new bootstrap.Modal(document.getElementById('uncheckConfirmModal'));
|
||||||
watchedDateCell.textContent = '';
|
const movieTitleEl = document.getElementById('uncheckMovieTitle');
|
||||||
if (row) {
|
const confirmBtn = document.getElementById('confirmUncheckBtn');
|
||||||
row.classList.add('table-danger');
|
|
||||||
row.classList.remove('table-success');
|
if (movieTitleEl) movieTitleEl.textContent = movie.title;
|
||||||
}
|
|
||||||
|
// Remove any previous event listener to avoid duplicates
|
||||||
|
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||||
|
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||||
|
|
||||||
|
newConfirmBtn.addEventListener('click', async () => {
|
||||||
|
uncheckModal.hide();
|
||||||
|
|
||||||
|
// Now actually uncheck
|
||||||
|
event.target.checked = false;
|
||||||
|
watchedDateCell.textContent = '';
|
||||||
|
if (row) {
|
||||||
|
row.classList.add('table-danger');
|
||||||
|
row.classList.remove('table-success');
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch('/api/movies/users/updateWatchedMovies', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ imdb_id: movie.imdb_id, isChecked: false })
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedWatchedMoviesRes = await fetch('/api/movies/watchedMovies', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
|
cache: 'no-cache',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updatedWatchedMoviesRes.ok) {
|
||||||
|
const updatedWatchedMovies = await updatedWatchedMoviesRes.json();
|
||||||
|
watchedMoviesInYear = updatedWatchedMovies.filter((wm) => movieImdbIds.has(wm.imdb_id));
|
||||||
|
const updatedWatchedCount = watchedMoviesInYear.length;
|
||||||
|
const updatedPct = totalMoviesCount > 0 ? ((updatedWatchedCount / totalMoviesCount) * 100).toFixed(1) : '0.0';
|
||||||
|
document.getElementById('watched-ratio').innerText = `Vu: ${updatedWatchedCount} / ${totalMoviesCount} (${updatedPct}%)`;
|
||||||
|
updateProgressBar(updatedWatchedCount, totalMoviesCount);
|
||||||
|
|
||||||
|
const moviesLeft = totalMoviesCount - updatedWatchedCount;
|
||||||
|
const moviesPerDay = daysLeft > 0 ? (moviesLeft / daysLeft).toFixed(2) : moviesLeft;
|
||||||
|
document.getElementById('movies-per-day').textContent = `À voir par jour: ${moviesPerDay} films`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
uncheckModal.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking a movie - no confirmation needed
|
||||||
|
watchedDateCell.textContent = new Date().toLocaleString();
|
||||||
|
if (row) {
|
||||||
|
row.classList.add('table-success');
|
||||||
|
row.classList.remove('table-danger');
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch('/api/movies/users/updateWatchedMovies', {
|
await fetch('/api/movies/users/updateWatchedMovies', {
|
||||||
|
|||||||
+55
-3
@@ -352,6 +352,11 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
|||||||
|
|
||||||
const onPlay = () => {
|
const onPlay = () => {
|
||||||
startInterval();
|
startInterval();
|
||||||
|
// Start session keepalive to prevent cookie expiration during long playback
|
||||||
|
const videoSrc = videoEl.currentSrc || videoEl.src;
|
||||||
|
if (videoSrc && token) {
|
||||||
|
startSessionKeepalive(videoSrc, token);
|
||||||
|
}
|
||||||
// Ensure we create/refresh a progress row as soon as playback starts
|
// Ensure we create/refresh a progress row as soon as playback starts
|
||||||
// (so the movies page can show a "resume" state quickly).
|
// (so the movies page can show a "resume" state quickly).
|
||||||
if (!startedOnce) {
|
if (!startedOnce) {
|
||||||
@@ -360,7 +365,7 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
|||||||
startSaveTimer = window.setTimeout(() => { void persist({ force: true }); }, 650);
|
startSaveTimer = window.setTimeout(() => { void persist({ force: true }); }, 650);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onPause = () => { stopInterval(); void persist({ force: true }); };
|
const onPause = () => { stopInterval(); stopSessionKeepalive(); void persist({ force: true }); };
|
||||||
const onSeeking = () => {
|
const onSeeking = () => {
|
||||||
// While scrubbing, browsers fire many events; debounce a forced save so it lands quickly.
|
// While scrubbing, browsers fire many events; debounce a forced save so it lands quickly.
|
||||||
if (seekSaveTimer) window.clearTimeout(seekSaveTimer);
|
if (seekSaveTimer) window.clearTimeout(seekSaveTimer);
|
||||||
@@ -373,8 +378,8 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
|||||||
}
|
}
|
||||||
void persist({ force: true });
|
void persist({ force: true });
|
||||||
};
|
};
|
||||||
const onEnded = () => { stopInterval(); void persist(); };
|
const onEnded = () => { stopInterval(); stopSessionKeepalive(); void persist(); };
|
||||||
const onError = () => { stopInterval(); };
|
const onError = () => { stopInterval(); stopSessionKeepalive(); };
|
||||||
const onTimeUpdate = () => {
|
const onTimeUpdate = () => {
|
||||||
if (videoEl.paused || videoEl.seeking) return;
|
if (videoEl.paused || videoEl.seeking) return;
|
||||||
void persist();
|
void persist();
|
||||||
@@ -405,6 +410,7 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stopInterval();
|
stopInterval();
|
||||||
|
stopSessionKeepalive();
|
||||||
if (seekSaveTimer) window.clearTimeout(seekSaveTimer);
|
if (seekSaveTimer) window.clearTimeout(seekSaveTimer);
|
||||||
if (savedUiTimer) window.clearTimeout(savedUiTimer);
|
if (savedUiTimer) window.clearTimeout(savedUiTimer);
|
||||||
if (startSaveTimer) window.clearTimeout(startSaveTimer);
|
if (startSaveTimer) window.clearTimeout(startSaveTimer);
|
||||||
@@ -805,6 +811,8 @@ function toVideoSessionUrl(videoSrc) {
|
|||||||
// Keyed by sessionUrl + token (token is already in-memory, not persisted here).
|
// Keyed by sessionUrl + token (token is already in-memory, not persisted here).
|
||||||
const _videoSessionCache = new Map(); // key -> expiresAt ms
|
const _videoSessionCache = new Map(); // key -> expiresAt ms
|
||||||
let _currentMovie = null;
|
let _currentMovie = null;
|
||||||
|
let _sessionKeepaliveInterval = null; // interval ID for session keep-alive
|
||||||
|
const SESSION_KEEPALIVE_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
async function ensureVideoSessionForSource(videoSrc, token) {
|
async function ensureVideoSessionForSource(videoSrc, token) {
|
||||||
// Only needed for our protected /api/video/* streams.
|
// Only needed for our protected /api/video/* streams.
|
||||||
@@ -842,6 +850,50 @@ async function ensureVideoSessionForSource(videoSrc, token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force-refresh the video session cookie, bypassing cache.
|
||||||
|
// Used for keep-alive during long playback sessions.
|
||||||
|
async function forceRefreshVideoSession(videoSrc, token) {
|
||||||
|
if (!token) return;
|
||||||
|
if (!isApiVideoUrl(videoSrc)) return;
|
||||||
|
const sessionUrl = toVideoSessionUrl(videoSrc);
|
||||||
|
if (!sessionUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(sessionUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: token ? { 'Authorization': `Bearer ${token}` } : undefined,
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store',
|
||||||
|
});
|
||||||
|
// Update the cache
|
||||||
|
const cacheKey = `${sessionUrl}::${token}`;
|
||||||
|
_videoSessionCache.set(cacheKey, Date.now() + 60_000);
|
||||||
|
} catch (_) {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start periodic session keep-alive while watching a movie.
|
||||||
|
// This prevents the video_auth cookie from expiring during long playback.
|
||||||
|
function startSessionKeepalive(videoSrc, token) {
|
||||||
|
stopSessionKeepalive();
|
||||||
|
if (!token || !isApiVideoUrl(videoSrc)) return;
|
||||||
|
|
||||||
|
_sessionKeepaliveInterval = window.setInterval(() => {
|
||||||
|
void forceRefreshVideoSession(videoSrc, token);
|
||||||
|
}, SESSION_KEEPALIVE_INTERVAL_MS);
|
||||||
|
|
||||||
|
// Also refresh immediately when starting
|
||||||
|
void forceRefreshVideoSession(videoSrc, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSessionKeepalive() {
|
||||||
|
if (_sessionKeepaliveInterval) {
|
||||||
|
window.clearInterval(_sessionKeepaliveInterval);
|
||||||
|
_sessionKeepaliveInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addTokenToApiVideoUrl(rawUrl, token) {
|
function addTokenToApiVideoUrl(rawUrl, token) {
|
||||||
if (!rawUrl || !token) return rawUrl;
|
if (!rawUrl || !token) return rawUrl;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -300,11 +300,21 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
} else {
|
} else {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'd-flex flex-wrap gap-1';
|
wrap.className = 'd-flex flex-wrap gap-1';
|
||||||
completers.forEach((u) => {
|
completers.forEach((u, idx) => {
|
||||||
const name = document.createElement('span');
|
const rank = idx + 1;
|
||||||
name.className = 'badge bg-light text-dark border';
|
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : '';
|
||||||
name.textContent = u?.name || '(sans nom)';
|
const badge = document.createElement('span');
|
||||||
wrap.appendChild(name);
|
badge.className = 'badge bg-light text-dark border';
|
||||||
|
// Show rank, medal (if top 3), and name
|
||||||
|
const rankPrefix = medal ? `${medal} ` : `#${rank} `;
|
||||||
|
// Format completion date if available
|
||||||
|
let dateStr = '';
|
||||||
|
if (u?.completedAt) {
|
||||||
|
const date = new Date(u.completedAt);
|
||||||
|
dateStr = ` (${date.toLocaleDateString('fr-CA')})`;
|
||||||
|
}
|
||||||
|
badge.textContent = `${rankPrefix}${u?.name || '(sans nom)'}${dateStr}`;
|
||||||
|
wrap.appendChild(badge);
|
||||||
});
|
});
|
||||||
tdNames.appendChild(wrap);
|
tdNames.appendChild(wrap);
|
||||||
}
|
}
|
||||||
@@ -663,9 +673,16 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
bonPicksHeader.style.display = showBonPicksColumn ? '' : 'none';
|
bonPicksHeader.style.display = showBonPicksColumn ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by total points (descending)
|
// Sort by total points (descending), then by last watched date (earlier = better rank)
|
||||||
stats.sort((a, b) => {
|
stats.sort((a, b) => {
|
||||||
return (b.totalPoints || 0) - (a.totalPoints || 0);
|
const pointsDiff = (b.totalPoints || 0) - (a.totalPoints || 0);
|
||||||
|
if (pointsDiff !== 0) return pointsDiff;
|
||||||
|
// Tiebreaker: earlier completion wins (user who finished first ranks higher)
|
||||||
|
const aDate = a.lastWatchedAt ? new Date(a.lastWatchedAt).getTime() : Infinity;
|
||||||
|
const bDate = b.lastWatchedAt ? new Date(b.lastWatchedAt).getTime() : Infinity;
|
||||||
|
if (aDate !== bDate) return aDate - bDate;
|
||||||
|
// Final tiebreaker: alphabetical
|
||||||
|
return String(a.name || '').localeCompare(String(b.name || ''), 'fr', { sensitivity: 'base' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear previous rows to prevent duplication
|
// Clear previous rows to prevent duplication
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ def create_video_session(request: Request, response: Response, authorization: Op
|
|||||||
|
|
||||||
secure = request.url.scheme == "https" or (request.headers.get("x-forwarded-proto") == "https")
|
secure = request.url.scheme == "https" or (request.headers.get("x-forwarded-proto") == "https")
|
||||||
encoded = quote(token, safe="")
|
encoded = quote(token, safe="")
|
||||||
cookie_max_age_seconds = int(_env_str("VIDEO_SESSION_MAX_AGE_SECONDS", "28800")) # 8h
|
cookie_max_age_seconds = int(_env_str("VIDEO_SESSION_MAX_AGE_SECONDS", "86400")) # 24h (longer default to avoid expiration during long movie sessions)
|
||||||
cookie_samesite = _env_str("VIDEO_COOKIE_SAMESITE", "Lax") # Lax|Strict|None
|
cookie_samesite = _env_str("VIDEO_COOKIE_SAMESITE", "Lax") # Lax|Strict|None
|
||||||
samesite = (cookie_samesite or "Lax").strip().lower()
|
samesite = (cookie_samesite or "Lax").strip().lower()
|
||||||
if samesite not in ("lax", "strict", "none"):
|
if samesite not in ("lax", "strict", "none"):
|
||||||
|
|||||||
+52
-12
@@ -232,6 +232,17 @@ router.get("/stats", verifyToken, async (req, res) => {
|
|||||||
const watchedCount = watchedMoviesDetails.length;
|
const watchedCount = watchedMoviesDetails.length;
|
||||||
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
|
const watchedRatio = ((watchedCount / totalMoviesCount) * 100).toFixed(1);
|
||||||
|
|
||||||
|
// Calculate the last watched date (used for ranking tiebreakers - earlier completion = better)
|
||||||
|
let lastWatchedAt = null;
|
||||||
|
watchedMoviesDetails.forEach((wm) => {
|
||||||
|
if (wm.watchedDate) {
|
||||||
|
const d = new Date(wm.watchedDate);
|
||||||
|
if (!lastWatchedAt || d > lastWatchedAt) {
|
||||||
|
lastWatchedAt = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Calculate points
|
// Calculate points
|
||||||
const userId = String(user._id);
|
const userId = String(user._id);
|
||||||
const correctPicks = picksByUserId.get(userId) || 0;
|
const correctPicks = picksByUserId.get(userId) || 0;
|
||||||
@@ -251,7 +262,8 @@ router.get("/stats", verifyToken, async (req, res) => {
|
|||||||
moviePoints: moviePoints,
|
moviePoints: moviePoints,
|
||||||
pickPoints: pickPoints,
|
pickPoints: pickPoints,
|
||||||
totalPoints: totalPoints,
|
totalPoints: totalPoints,
|
||||||
pointsConfig: pointsConfig
|
pointsConfig: pointsConfig,
|
||||||
|
lastWatchedAt: lastWatchedAt ? lastWatchedAt.toISOString() : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -300,7 +312,7 @@ router.get("/completions", verifyToken, async (req, res) => {
|
|||||||
totals[String(y)] = set ? set.size : 0;
|
totals[String(y)] = set ? set.size : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await User.find().select("name watchedMovies.imdb_id").lean();
|
const users = await User.find().select("name watchedMovies.imdb_id watchedMovies.watchedDate").lean();
|
||||||
|
|
||||||
const completersByYear = {};
|
const completersByYear = {};
|
||||||
for (const y of years) {
|
for (const y of years) {
|
||||||
@@ -314,27 +326,55 @@ router.get("/completions", verifyToken, async (req, res) => {
|
|||||||
const completers = [];
|
const completers = [];
|
||||||
users.forEach((u) => {
|
users.forEach((u) => {
|
||||||
const watched = Array.isArray(u?.watchedMovies) ? u.watchedMovies : [];
|
const watched = Array.isArray(u?.watchedMovies) ? u.watchedMovies : [];
|
||||||
const watchedSet = new Set(
|
|
||||||
watched
|
|
||||||
.map((wm) => (typeof wm?.imdb_id === "string" ? wm.imdb_id : ""))
|
|
||||||
.filter(Boolean)
|
|
||||||
);
|
|
||||||
|
|
||||||
let count = 0;
|
// Build a map of imdb_id -> watchedDate for this user's movies in this year
|
||||||
for (const imdb of watchedSet) {
|
const watchedInYear = [];
|
||||||
if (yearSet.has(imdb)) count += 1;
|
watched.forEach((wm) => {
|
||||||
}
|
const imdb = typeof wm?.imdb_id === "string" ? wm.imdb_id : "";
|
||||||
|
if (!imdb || !yearSet.has(imdb)) return;
|
||||||
|
watchedInYear.push({
|
||||||
|
imdb_id: imdb,
|
||||||
|
watchedDate: wm?.watchedDate ? new Date(wm.watchedDate) : null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count unique movies watched in this year
|
||||||
|
const uniqueImdbIds = new Set(watchedInYear.map((w) => w.imdb_id));
|
||||||
|
const count = uniqueImdbIds.size;
|
||||||
|
|
||||||
if (count === total) {
|
if (count === total) {
|
||||||
|
// Find the completion date (the latest watchedDate among movies for this year)
|
||||||
|
// This is when the user completed 100% of the checklist
|
||||||
|
let completedAt = null;
|
||||||
|
watchedInYear.forEach((w) => {
|
||||||
|
if (w.watchedDate && (!completedAt || w.watchedDate > completedAt)) {
|
||||||
|
completedAt = w.watchedDate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
completers.push({
|
completers.push({
|
||||||
name: u?.name || "(sans nom)",
|
name: u?.name || "(sans nom)",
|
||||||
watchedCount: count,
|
watchedCount: count,
|
||||||
totalMoviesCount: total,
|
totalMoviesCount: total,
|
||||||
|
completedAt: completedAt ? completedAt.toISOString() : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
completers.sort((a, b) => String(a.name).localeCompare(String(b.name), "fr", { sensitivity: "base" }));
|
// Sort by completion date (earliest first = higher rank), then alphabetically for ties
|
||||||
|
completers.sort((a, b) => {
|
||||||
|
// Users with a completion date come before those without
|
||||||
|
if (a.completedAt && !b.completedAt) return -1;
|
||||||
|
if (!a.completedAt && b.completedAt) return 1;
|
||||||
|
// Both have dates: earliest first
|
||||||
|
if (a.completedAt && b.completedAt) {
|
||||||
|
const dateA = new Date(a.completedAt).getTime();
|
||||||
|
const dateB = new Date(b.completedAt).getTime();
|
||||||
|
if (dateA !== dateB) return dateA - dateB;
|
||||||
|
}
|
||||||
|
// Tie-breaker: alphabetical
|
||||||
|
return String(a.name).localeCompare(String(b.name), "fr", { sensitivity: "base" });
|
||||||
|
});
|
||||||
completersByYear[String(y)] = completers;
|
completersByYear[String(y)] = completers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -92,7 +92,7 @@ router.post("/session", (req, res) => {
|
|||||||
const maxAgeSecondsRaw = typeof process.env.VIDEO_SESSION_MAX_AGE_SECONDS === "string" ? process.env.VIDEO_SESSION_MAX_AGE_SECONDS.trim() : "";
|
const maxAgeSecondsRaw = typeof process.env.VIDEO_SESSION_MAX_AGE_SECONDS === "string" ? process.env.VIDEO_SESSION_MAX_AGE_SECONDS.trim() : "";
|
||||||
const maxAgeSeconds = Number.isFinite(Number(maxAgeSecondsRaw)) && Number(maxAgeSecondsRaw) > 0
|
const maxAgeSeconds = Number.isFinite(Number(maxAgeSecondsRaw)) && Number(maxAgeSecondsRaw) > 0
|
||||||
? Math.floor(Number(maxAgeSecondsRaw))
|
? Math.floor(Number(maxAgeSecondsRaw))
|
||||||
: 60 * 60 * 8; // 8h
|
: 60 * 60 * 24; // 24h (longer default to avoid expiration during long movie sessions)
|
||||||
const cookie = [
|
const cookie = [
|
||||||
`video_auth=${encodeURIComponent(token)}`,
|
`video_auth=${encodeURIComponent(token)}`,
|
||||||
"Path=/api/video",
|
"Path=/api/video",
|
||||||
|
|||||||
Reference in New Issue
Block a user