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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user