Save a position only from a player that has played since its last save

A tab left paused further into an episode saved its older place as it
reloaded, over where the listener had got to since, and the episode
dropped out of Currently Listening. A jump back is now saved at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 18:29:10 +00:00
parent 2dee3b722c
commit b9e0d9f3cb
4 changed files with 58 additions and 4 deletions

View File

@@ -10,6 +10,11 @@ The long form, with what was wrong before and how it was found, is in
## [Unreleased] ## [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 ## [0.6.0] - 2026-09-15
### Added ### Added

View File

@@ -6,6 +6,28 @@ reasoning lives. New write-ups go at the top.
See [README.md](../README.md) for what the thing is. 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 ## 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 Issue #14: an episode 32 minutes into 41 was missing from Currently Listening, which said nothing

View File

@@ -200,6 +200,26 @@ test('Currently Listening, its own place below Popular, resumes an episode you s
await expect(row.locator('.eq')).toBeHidden(); 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 }) => { test('the filter tabs change what is listed', async ({ page }) => {
await page.locator('.feed', { hasText: 'Test Show' }).click(); await page.locator('.feed', { hasText: 'Test Show' }).click();
await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 }); await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 });

View File

@@ -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. // 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; const resuming = player.guid===e.guid && player.enc===enc.id;
if(!resuming){ 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'); document.body.classList.toggle('has-video', kindOf(enc)==='video');
audio.src=`/media/${enc.id}`; audio.src=`/media/${enc.id}`;
audio.currentTime=0; audio.currentTime=0;
@@ -1539,14 +1539,21 @@ audio.addEventListener('timeupdate',()=>{
$('#pcur').textContent=clock(audio.currentTime); $('#pcur').textContent=clock(audio.currentTime);
$('#pdur').textContent=clock(d); $('#pdur').textContent=clock(d);
if(d) $('#seek').value=String(Math.round(audio.currentTime/d*1000)); if(d) $('#seek').value=String(Math.round(audio.currentTime/d*1000));
// Persist roughly every 10s so a reload resumes where you were. // Only playing counts as moving: the seek to where you left off happens paused, and saving
if(player.guid && audio.currentTime-player.saveAt>10){ savePos(); } // 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(); if(d && audio.currentTime/d >= 0.9) markPlayed();
}); });
function savePos(){ function savePos(){
// Before the file has loaded, currentTime is 0 rather than where you are: saving it then -- // 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. // 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; player.saveAt=audio.currentTime;
if(player.entry) player.entry.position=Math.floor(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 // The measured length stands in for one the feed left out: without it Currently Listening