diff --git a/frontend/app.js b/frontend/app.js index 953d3b2..6111142 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -412,7 +412,10 @@ const Player = { return this.mode === 'dual' ? els.audio : this.master; }, - async loadVideo(videoObj, { preferStream = false } = {}) { + async loadVideo(videoObj, { preferStream = false, resume = true } = {}) { + // When false (auto-advance / prev), skip resuming the saved timestamp and + // start from the beginning (or the A marker, if an A-B loop is set). + this._resumeOnLoad = resume; showSpinner(true); els.placeholder.classList.add('hidden'); // Revoke any previous OPFS blob URL to free memory @@ -464,7 +467,7 @@ const Player = { btn.textContent = '↻ Retry'; btn.addEventListener('click', () => { btn.remove(); - Player.loadVideo(videoObj, { preferStream }); + Player.loadVideo(videoObj, { preferStream, resume }); }); els.playerPane.appendChild(btn); } @@ -503,9 +506,12 @@ const Player = { restoreAbMarkers(); loadRelated(); exitSelectMode(); - // Restore saved playback position + // Restore saved playback position. + // On auto-advance (or prev/next) we do NOT resume the previous timestamp — + // a freshly selected track should start at the beginning, or at the A point + // when an A-B loop is set for it. const id = current.meta.id; - if (data.resumePositions[id] && data.resumePositions[id] > 1) { + if (this._resumeOnLoad && data.resumePositions[id] && data.resumePositions[id] > 1) { const saved = data.resumePositions[id]; const restore = () => { Player.seek(saved); @@ -515,6 +521,15 @@ const Player = { }; Player.master.addEventListener('canplay', restore, { once: true }); Player.master.addEventListener('loadedmetadata', restore, { once: true }); + } else if (abA !== null && abA > 1) { + // Not resuming, but an A marker exists — start the loop from A. + const seekToA = () => { + Player.seek(abA); + Player.master.removeEventListener('canplay', seekToA); + Player.master.removeEventListener('loadedmetadata', seekToA); + }; + Player.master.addEventListener('canplay', seekToA, { once: true }); + Player.master.addEventListener('loadedmetadata', seekToA, { once: true }); } }, @@ -540,7 +555,8 @@ const Player = { const meta = current.meta; current.localUrl = null; toast('Cached copy unavailable — streaming instead…'); - this.loadVideo(meta, { preferStream: true }); + // Preserve the resume intent of the load that just failed. + this.loadVideo(meta, { preferStream: true, resume: this._resumeOnLoad }); return; } // Advance to the next candidate stream; give up with a clear message at the end. @@ -941,7 +957,9 @@ function advanceQueue() { } else { return false; } - Player.loadVideo(queue[queueIndex]); + // Advancing to a new track always starts from the beginning (or its A point), + // never the previous resume timestamp. + Player.loadVideo(queue[queueIndex], { resume: false }); renderUpNext(); return true; } @@ -973,7 +991,7 @@ function playPrev() { if (Player.master.currentTime > 3) { Player.seek(0); return; } if (queueIndex > 0) { queueIndex--; - Player.loadVideo(queue[queueIndex]); + Player.loadVideo(queue[queueIndex], { resume: false }); renderUpNext(); } } @@ -2497,24 +2515,49 @@ function setAbB() { } function clearAb() { abA = null; abB = null; - if (current && current.meta) { delete data.abMarkers[current.meta.id]; persist(); } + if (current && current.meta) { + delete data.abMarkers[current.meta.id]; + const entry = currentPlaylistEntry(); + if (entry) delete entry.ab; + persist(); + } updateAbUI(); toast('A-B loop cleared'); } function saveAbMarkers() { if (!current || !current.meta) return; if (abA !== null || abB !== null) { - data.abMarkers[current.meta.id] = { a: abA, b: abB }; + const marker = { a: abA, b: abB }; + // When playing from a playlist, store the marker on that playlist's own + // copy of the video so each playlist keeps its own A-B loop and it syncs + // to the database alongside the playlist. Otherwise fall back to the + // global per-video map. + const entry = currentPlaylistEntry(); + if (entry) entry.ab = marker; + else data.abMarkers[current.meta.id] = marker; persist(); } } function restoreAbMarkers() { if (!current || !current.meta) { abA = null; abB = null; updateAbUI(); return; } - const saved = data.abMarkers[current.meta.id]; + // Prefer a marker stored on the current playlist's copy of the video; fall + // back to the global per-video marker. + const entry = currentPlaylistEntry(); + const saved = (entry && entry.ab) || data.abMarkers[current.meta.id]; abA = saved ? saved.a : null; abB = saved ? saved.b : null; updateAbUI(); } +// Returns the video entry inside the playlist currently being played, if the +// active playback source is a playlist and the playing video belongs to it. +function currentPlaylistEntry() { + if (!current || !current.meta) return null; + if (!queueSource || !queueSource.startsWith('playlist:')) return null; + const plId = queueSource.slice('playlist:'.length); + const pl = data.playlists.find((p) => p.id === plId); + if (!pl || !Array.isArray(pl.videos)) return null; + return pl.videos.find((v) => v.id === current.meta.id) || null; +} function updateAbUI() { const aSet = abA !== null, bSet = abB !== null; const aBtn = $('abABtn'), bBtn = $('abBBtn'), clrBtn = $('abClearBtn'); diff --git a/frontend/sw.js b/frontend/sw.js index ffdd2be..7a9cda6 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -16,7 +16,7 @@ * 5. SW calls skipWaiting() → takes over → client reloads. * ========================================================================== */ -const VERSION = 'v1.0.2'; // ← bump this on every deploy to bust the cache +const VERSION = 'v1.0.3'; // ← bump this on every deploy to bust the cache const CACHE = 'ytplayer-' + VERSION; // Files that form the installable app shell.