From 44623098aee159d637f9c5e2ce3668da19ef46d3 Mon Sep 17 00:00:00 2001 From: rays Date: Thu, 10 Sep 2026 01:55:46 +0000 Subject: [PATCH] Mark episodes read when finished, not when started play() set read=1 the instant playback began. The default view is the Unread tab, so pressing play removed the episode from the list being looked at, which reads as the episode going missing. All four affected rows had position=0: started, never listened to. Read is now set on 'ended' or past 90% of the duration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe --- PROGRESS.md | 29 +++++++++++++++++++++++++++++ web/index.html | 23 +++++++++++++++++------ 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index 6fc97bf..c5b5e8c 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -56,6 +56,35 @@ and until now nothing set them. --- +## 2026-09-10 — Fixed: pressing play made an episode vanish from the list + +Reported as "where did Session Zero go, and why do parts share a number?". + +Two separate things, one of them a real bug. + +**The bug.** `play()` marked an episode read the moment playback started. The default view is the +Unread tab, so pressing play removed the episode from the list you were looking at — indistinguishable +from it going missing. Confirmed from the data: all four read entries had `position = 0`, meaning +playback never passed the 10-second save threshold. They had been started, never listened to. + +Marking read now happens when an episode is actually consumed — on `ended`, or once playback passes +90% — never on play. Two earlier PROGRESS entries described mark-on-play as a deliberate feature; +it was a design error, and this supersedes them. + +Also restored the four affected rows to unread, since their read flag was purely an artifact of the +bug (`UPDATE entries SET read=0 WHERE read=1 AND position=0` — no genuinely-played episode could +match, since playing one writes a position). + +**Not a bug: repeated episode numbers.** Each story part ships as two items — the main episode and a +shorter `Junk in the Trunk` companion — with distinct GUIDs, durations and files, and the publisher +gives both the same `itunes:episode`. Zero duplicated titles across all 131 entries. `Session Zero` +additionally carries a season with no episode number and drops the "Part N" naming, so it does not +match the pattern its siblings follow. + +Verified: the arc shows 11/11 items under the default Unread view again. + +--- + ## 2026-09-10 — Titles: RSS `` is the only source Confirmed against the live feed rather than assumed: 131 feed items, 131 stored, **0 mismatches** — diff --git a/web/index.html b/web/index.html index 49f1168..5c1d875 100644 --- a/web/index.html +++ b/web/index.html @@ -495,7 +495,20 @@ async function epAction(a,e,el){ /* ---------------- player ---------------- */ const audio=$('#audio'); -const player={guid:null,feed:null,entry:null,saveAt:0}; +const player={guid:null,feed:null,entry:null,saveAt:0,marked:false}; + +// Marked read when an episode has actually been listened to -- at the end, or past 90%. +// NOT on play: doing that made the episode vanish from the default Unread list the instant +// you pressed play, which looks exactly like it went missing. +function markPlayed(){ + if(!player.guid||player.marked) return; + player.marked=true; + const e=player.entry; + if(!e||e.read) return; + e.read=true; + api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`, + {method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)).catch(()=>{}); +} function play(e){ const enc=e.enclosures.find(x=>x.path); @@ -512,10 +525,7 @@ function play(e){ $('#pfeed').textContent=f?(f.title||f.id):''; $('#player').classList.add('on'); mediaSession(e,f); - // Playing it is the clearest signal it has been listened to; retention deletes - // read episodes before unread ones. - if(!e.read){ e.read=true; api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`, - {method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)); } + player.marked=false; } audio.play().catch(err=>toast('Playback failed: '+err.message,true)); renderEntries(); @@ -537,6 +547,7 @@ audio.addEventListener('timeupdate',()=>{ 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(); } + if(d && audio.currentTime/d >= 0.9) markPlayed(); }); function savePos(){ if(!player.guid) return; @@ -547,7 +558,7 @@ function savePos(){ new Blob([JSON.stringify({secs:Math.floor(audio.currentTime)})],{type:'application/json'})); } audio.addEventListener('pause',savePos); -audio.addEventListener('ended',()=>{savePos();$('#pplay').textContent='▶'}); +audio.addEventListener('ended',()=>{savePos();markPlayed();$('#pplay').textContent='▶'}); audio.addEventListener('play',()=>$('#pplay').textContent='❚❚'); audio.addEventListener('pause',()=>$('#pplay').textContent='▶'); window.addEventListener('beforeunload',savePos);