add video backend server, fix styling and more #54
@@ -55,17 +55,21 @@ async function fetchPlaybackProgress(movieId, token) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePlaybackProgress(movieId, token, { time, duration, keepalive } = {}) {
|
async function savePlaybackProgress(movieId, token, { time, duration, imdbId, keepalive } = {}) {
|
||||||
try {
|
try {
|
||||||
// Retry once on rare upsert races (409).
|
// Retry once on rare upsert races (409).
|
||||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
|
const payload = { time, duration };
|
||||||
|
const safeImdb = typeof imdbId === 'string' ? imdbId.trim() : '';
|
||||||
|
if (safeImdb) payload.imdb_id = safeImdb;
|
||||||
|
|
||||||
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, {
|
const res = await fetch(`/api/movies/${encodeURIComponent(movieId)}/progress`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ time, duration }),
|
body: JSON.stringify(payload),
|
||||||
keepalive: !!keepalive,
|
keepalive: !!keepalive,
|
||||||
});
|
});
|
||||||
if (res.ok) return true;
|
if (res.ok) return true;
|
||||||
@@ -80,7 +84,7 @@ async function savePlaybackProgress(movieId, token, { time, duration, keepalive
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||||
if (!videoEl || !movieId || !token || !userId) return () => {};
|
if (!videoEl || !movieId || !token || !userId) return () => {};
|
||||||
|
|
||||||
const key = progressStorageKey(userId, movieId);
|
const key = progressStorageKey(userId, movieId);
|
||||||
@@ -162,7 +166,7 @@ function setupVideoProgress({ videoEl, movieId, token, userId }) {
|
|||||||
|
|
||||||
// Icon-only saving indicator (avoid noisy text updates).
|
// Icon-only saving indicator (avoid noisy text updates).
|
||||||
setProgressUi({ state: 'saving', text: '', showText: false, ariaText: 'Sauvegarde…' });
|
setProgressUi({ state: 'saving', text: '', showText: false, ariaText: 'Sauvegarde…' });
|
||||||
const ok = await savePlaybackProgress(movieId, token, { ...snap, keepalive: !!keepalive });
|
const ok = await savePlaybackProgress(movieId, token, { ...snap, imdbId, keepalive: !!keepalive });
|
||||||
if (ok === false) {
|
if (ok === false) {
|
||||||
// Token expired mid-playback; stop spamming requests and prompt user.
|
// Token expired mid-playback; stop spamming requests and prompt user.
|
||||||
stopInterval();
|
stopInterval();
|
||||||
@@ -803,7 +807,7 @@ window.onload = async function () {
|
|||||||
setProgressUi({ state: 'preparing', text: 'Reprise', showText: true, ariaText: 'Prépare la reprise…' });
|
setProgressUi({ state: 'preparing', text: 'Reprise', showText: true, ariaText: 'Prépare la reprise…' });
|
||||||
const userId = normalizeUserId(decoded);
|
const userId = normalizeUserId(decoded);
|
||||||
if (userId) {
|
if (userId) {
|
||||||
setupVideoProgress({ videoEl, movieId: id, token, userId });
|
setupVideoProgress({ videoEl, movieId: id, token, userId, imdbId: movie?.imdb_id });
|
||||||
} else {
|
} else {
|
||||||
// Avoid a "stuck" progress state if the token payload is unexpected.
|
// Avoid a "stuck" progress state if the token payload is unexpected.
|
||||||
setProgressUi({ state: 'warning', text: '—', showText: true, ariaText: "Impossible d'identifier l'utilisateur pour sauvegarder la progression." });
|
setProgressUi({ state: 'warning', text: '—', showText: true, ariaText: "Impossible d'identifier l'utilisateur pour sauvegarder la progression." });
|
||||||
|
|||||||
@@ -645,6 +645,7 @@ router.put("/:id/progress", authenticate, async (req, res) => {
|
|||||||
|
|
||||||
const rawTime = req.body?.time;
|
const rawTime = req.body?.time;
|
||||||
const rawDuration = req.body?.duration;
|
const rawDuration = req.body?.duration;
|
||||||
|
const rawImdbId = req.body?.imdb_id;
|
||||||
|
|
||||||
let time = Number(rawTime);
|
let time = Number(rawTime);
|
||||||
if (!Number.isFinite(time) || time < 0) time = 0;
|
if (!Number.isFinite(time) || time < 0) time = 0;
|
||||||
@@ -660,6 +661,10 @@ router.put("/:id/progress", authenticate, async (req, res) => {
|
|||||||
time = Math.min(Math.max(time, 0), duration);
|
time = Math.min(Math.max(time, 0), duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-check the movie in the checklist when the user reaches the "credit roll".
|
||||||
|
// Keep it simple: use the timestamp ratio (>= 95%).
|
||||||
|
const reachedCreditRoll = duration !== null && duration > 0 && time / duration >= 0.95;
|
||||||
|
|
||||||
// Best UX: if the user is basically at the end, reset to 0 so replay starts from the beginning.
|
// Best UX: if the user is basically at the end, reset to 0 so replay starts from the beginning.
|
||||||
if (duration !== null && duration - time <= 3) {
|
if (duration !== null && duration - time <= 3) {
|
||||||
time = 0;
|
time = 0;
|
||||||
@@ -671,6 +676,27 @@ router.put("/:id/progress", authenticate, async (req, res) => {
|
|||||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||||
).select("time duration updatedAt");
|
).select("time duration updatedAt");
|
||||||
|
|
||||||
|
// If they hit the 95% threshold, mark as watched for checklist.
|
||||||
|
// Prefer imdb_id provided by the client; fall back to DB lookup if absent/invalid.
|
||||||
|
if (reachedCreditRoll) {
|
||||||
|
let imdbId = typeof rawImdbId === "string" ? rawImdbId.trim() : "";
|
||||||
|
if (!isValidImdbId(imdbId)) imdbId = "";
|
||||||
|
|
||||||
|
if (!imdbId) {
|
||||||
|
const movieDoc = await Movie.findById(id).select("imdb_id").lean();
|
||||||
|
const mImdb = typeof movieDoc?.imdb_id === "string" ? movieDoc.imdb_id.trim() : "";
|
||||||
|
if (isValidImdbId(mImdb)) imdbId = mImdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imdbId) {
|
||||||
|
// Only add if not already present; if user manually unchecked, it can be re-added on next 95% hit.
|
||||||
|
await User.updateOne(
|
||||||
|
{ _id: userId, "watchedMovies.imdb_id": { $ne: imdbId } },
|
||||||
|
{ $push: { watchedMovies: { imdb_id: imdbId, watchedDate: new Date() } } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
time: updated?.time ?? 0,
|
time: updated?.time ?? 0,
|
||||||
duration: typeof updated?.duration === "number" ? updated.duration : null,
|
duration: typeof updated?.duration === "number" ? updated.duration : null,
|
||||||
|
|||||||
Reference in New Issue
Block a user