Autosave playback reset issue #52
+33
-1
@@ -93,6 +93,8 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||
|
||||
const key = progressStorageKey(userId, movieId);
|
||||
let restored = false;
|
||||
let restoreInProgress = false;
|
||||
let pendingRestoreTime = null; // number | null
|
||||
let restoreServerPromise = null;
|
||||
let intervalId = null;
|
||||
let lastSavedAt = 0;
|
||||
@@ -135,10 +137,16 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||
restored = true;
|
||||
|
||||
if (clamped > 1) {
|
||||
// Block any progress saves until the seek completes; otherwise we can overwrite
|
||||
// server progress with an early "0s" save during reload/startup.
|
||||
pendingRestoreTime = clamped;
|
||||
restoreInProgress = true;
|
||||
try { videoEl.currentTime = clamped; } catch (_) {}
|
||||
lastSaveStatus = `Reprise · ${formatClock(clamped)}`;
|
||||
setProgressUi({ state: 'info', text: lastSaveStatus, showText: true, ariaText: `Reprise à ${formatClock(clamped)}` });
|
||||
} else {
|
||||
pendingRestoreTime = null;
|
||||
restoreInProgress = false;
|
||||
lastSaveStatus = 'Début';
|
||||
setProgressUi({ state: 'info', text: 'Début', showText: true, ariaText: 'Lecture depuis le début' });
|
||||
}
|
||||
@@ -151,6 +159,14 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||
}
|
||||
|
||||
async function persist({ keepalive, force } = {}) {
|
||||
// Never persist until the resume logic finished; otherwise the first "0s" timeupdate
|
||||
// on reload can overwrite the user's saved progress on the server.
|
||||
if (!restored) {
|
||||
void tryRestoreProgress();
|
||||
return;
|
||||
}
|
||||
if (restoreInProgress) return;
|
||||
|
||||
// Throttle to avoid spamming the API
|
||||
const now = Date.now();
|
||||
if (!force && !keepalive && now - lastSavedAt < 1500) return;
|
||||
@@ -158,6 +174,16 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||
const snap = getSnapshot();
|
||||
if (!Number.isFinite(snap.time)) return;
|
||||
|
||||
if (pendingRestoreTime !== null) {
|
||||
// Some browsers may not fire seeked reliably. Only start saving once we
|
||||
// observe that currentTime actually reached the restored position.
|
||||
if (snap.time >= Math.max(0, Math.floor(pendingRestoreTime) - 1)) {
|
||||
pendingRestoreTime = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only save if it meaningfully changed (>= 1s)
|
||||
if (!force && lastSavedTime >= 0 && Math.abs(snap.time - lastSavedTime) < 1) return;
|
||||
|
||||
@@ -211,7 +237,13 @@ function setupVideoProgress({ videoEl, movieId, token, userId, imdbId }) {
|
||||
if (seekSaveTimer) window.clearTimeout(seekSaveTimer);
|
||||
seekSaveTimer = window.setTimeout(() => { void persist({ force: true }); }, 400);
|
||||
};
|
||||
const onSeeked = () => { void persist({ force: true }); };
|
||||
const onSeeked = () => {
|
||||
if (restoreInProgress) {
|
||||
restoreInProgress = false;
|
||||
pendingRestoreTime = null;
|
||||
}
|
||||
void persist({ force: true });
|
||||
};
|
||||
const onEnded = () => { stopInterval(); void persist(); };
|
||||
const onError = () => { stopInterval(); };
|
||||
const onTimeUpdate = () => {
|
||||
|
||||
@@ -667,6 +667,23 @@ router.put("/:id/progress", authenticate, async (req, res) => {
|
||||
time = Math.min(Math.max(time, 0), durationCandidate);
|
||||
}
|
||||
|
||||
// Defensive guard:
|
||||
// On some reload/startup sequences (especially on hosted environments),
|
||||
// the client can emit an early "time=0" save *before* it has restored the
|
||||
// user's previous progress (and before it knows duration).
|
||||
// If we already have a meaningful progress saved, never let a "0 with unknown duration"
|
||||
// overwrite it.
|
||||
if (time === 0 && durationCandidate === null) {
|
||||
const existing = await PlaybackProgress.findOne({ userId, movieId: id }).select("time duration updatedAt");
|
||||
if (existing && typeof existing.time === "number" && existing.time > 1) {
|
||||
return res.status(200).json({
|
||||
time: existing.time ?? 0,
|
||||
duration: typeof existing.duration === "number" ? existing.duration : null,
|
||||
updatedAt: existing.updatedAt ? new Date(existing.updatedAt).toISOString() : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-check the movie in the checklist when the user reaches the "credit roll".
|
||||
// Keep it simple: use the timestamp ratio (>= 95%).
|
||||
const reachedCreditRoll = durationCandidate !== null && durationCandidate > 0 && time / durationCandidate >= 0.95;
|
||||
|
||||
Reference in New Issue
Block a user