diff --git a/CHANGELOG.md b/CHANGELOG.md index fd58f1c..e60da39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ The long form, with what was wrong before and how it was found, is in ## [Unreleased] +### Fixed + +- A player left open in another tab or on another device no longer saves its older place over + where you have got to since, which could drop an episode out of Currently Listening. + ## [0.6.0] - 2026-09-15 ### Added diff --git a/docs/history.md b/docs/history.md index fffd002..11c6f49 100644 --- a/docs/history.md +++ b/docs/history.md @@ -6,6 +6,28 @@ reasoning lives. New write-ups go at the top. See [README.md](../README.md) for what the thing is. +## 2026-09-15 — A player nobody was listening to, saving over one that was + +After 0.6.0 the Kristen Bell episode dropped out of Currently Listening again, with eleven +minutes left in the player. Its saved position was 41:15 of a file 43:48 long, 94% and finished +by any measure, while the listener was at about 32:48. The first guess was a wrong length from +the feed, and it was wrong: the feed does say 41:23, but no length makes 41:15 unfinished. + +The log had the answer. Saves every ten seconds while it played, then two that were not +playback: one beside the only request for the file since the restart, and the last in the same +millisecond as a page load. A player left paused further on, in another tab or on another +device, saved its own place as its page reloaded. Every save goes through `savePos`, and it +saved whatever the player held whether or not anyone had played it since. + +`savePos` now saves only once the player has played since its last save, so an idle one never +writes, and only playing counts: the seek to where you left off happens paused, and counting it +would save back whatever the list said, however old. A jump back is also saved at once, where +the ten-second check only ever looked forward. The fixture audio does not decode, so the browser +test stands in for a loaded player and counts what `savePos` sends. + +The feed's length, 2 minutes 25 seconds short of the file, still moves this episode's 90% line +earlier than the player's. Not fixed here; the measured length only fills in a missing one. + ## 2026-09-15 — Currently Listening, empty for anyone who opens what they play Issue #14: an episode 32 minutes into 41 was missing from Currently Listening, which said nothing diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 1cc5427..9d1c2ea 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -200,6 +200,26 @@ test('Currently Listening, its own place below Popular, resumes an episode you s await expect(row.locator('.eq')).toBeHidden(); }); +test('a player nobody has played since it last saved does not save again', async ({ page }) => { + // A tab left paused at 41:15 saved that as it reloaded, over the 32:48 another had reached, + // and the episode dropped out of Currently Listening. The fixture audio does not decode, so + // this stands in for a loaded file paused at 2 seconds and counts what savePos sends. + const sent = await page.evaluate(() => { + Object.defineProperty(audio, 'readyState', { get: () => 4 }); + Object.defineProperty(audio, 'currentTime', { get: () => 2, set() {} }); + let n = 0; + navigator.sendBeacon = () => (n++, true); + player.guid = 'ui-2'; player.feed = 'test-show'; player.entry = null; player.moved = false; + savePos(); // what a reload, a pause or the close button calls + const idle = n; + player.moved = true; // what playing sets + savePos(); + savePos(); // and once saved, it is idle again + return [idle, n]; + }); + expect(sent).toEqual([0, 1]); +}); + test('the filter tabs change what is listed', async ({ page }) => { await page.locator('.feed', { hasText: 'Test Show' }).click(); await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 }); diff --git a/web/index.html b/web/index.html index 7946f4a..a52880b 100644 --- a/web/index.html +++ b/web/index.html @@ -1507,7 +1507,7 @@ function play(e,enc=e.enclosures.find(isPlayable)){ // The same file carries on where it was; another of the item's files starts from its top. const resuming = player.guid===e.guid && player.enc===enc.id; if(!resuming){ - player.guid=e.guid; player.feed=e.feed_id; player.entry=e; player.enc=enc.id; + player.guid=e.guid; player.feed=e.feed_id; player.entry=e; player.enc=enc.id; player.moved=false; document.body.classList.toggle('has-video', kindOf(enc)==='video'); audio.src=`/media/${enc.id}`; audio.currentTime=0; @@ -1539,14 +1539,21 @@ audio.addEventListener('timeupdate',()=>{ $('#pcur').textContent=clock(audio.currentTime); $('#pdur').textContent=clock(d); if(d) $('#seek').value=String(Math.round(audio.currentTime/d*1000)); - // Persist roughly every 10s so a reload resumes where you were. - if(player.guid && audio.currentTime-player.saveAt>10){ savePos(); } + // Only playing counts as moving: the seek to where you left off happens paused, and saving + // that would write back whatever the list said, however old. + if(!audio.paused) player.moved=true; + // Persist roughly every 10s so a reload resumes where you were. Either way: a jump back used + // to wait for the next pause to be saved. + if(player.guid && Math.abs(audio.currentTime-player.saveAt)>10){ savePos(); } if(d && audio.currentTime/d >= 0.9) markPlayed(); }); function savePos(){ // Before the file has loaded, currentTime is 0 rather than where you are: saving it then -- // a failed load, or a pause before the seek to where you left off -- wiped the position. - if(!player.guid||!audio.readyState) return; + // Nor from a player nobody has played since it last saved: one left paused in another tab + // saved its older place as that tab reloaded, over where you had got to since. + if(!player.guid||!audio.readyState||!player.moved) return; + player.moved=false; player.saveAt=audio.currentTime; if(player.entry) player.entry.position=Math.floor(audio.currentTime); // The measured length stands in for one the feed left out: without it Currently Listening