Repository navigation
fix(player): clear progress on episode switch to stop end-of-playback glitch (#94) - #194
Conversation
… glitch (#94) When an episode finishes and the queue auto-advances, currentEpisode.set swaps the player to the next episode and its title/artwork update immediately, but the $currentTime/$duration stores keep the finished episode's end values until the new audio fires loadedmetadata. The progress bar therefore stays pinned at 100% and the timestamps show the previous episode's end against the new episode for the whole (network-bound) metadata-fetch window — the visual glitch in #94. Reset $currentTime and $duration to 0 on a genuine in-player episode switch (in the currentEpisode subscription), guarded by a first-fire flag so the synchronous initial subscription does not wipe a restored resume position or flash 0 on mount/remount. onMetadataLoaded -> restorePlaybackTime sets the real position once the next episode loads, and the existing isLoading guard keeps this reset from being persisted. Also clamp non-finite and negative inputs to 0 in formatSeconds so the time display can never render "NaN:NaN:NaN" or negative components during the brief window where duration is unknown or currentTime exceeds a shorter next episode. Verified in real Obsidian: at the auto-advance transition the bar now shows 0% and 00:00:00 instead of the stale 100% / previous-episode timestamps.
Deploying podnotes with
|
| Latest commit: |
ba926bb
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4e457129.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-94-finish-playing.podnotes.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf37e7f002
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
#94) Addresses Codex review on PR #194. The #94 progress reset sets currentTime and duration to 0 the instant the episode changes. If the user switches away again before the next episode's metadata loads (e.g. click B then C from the queue while B is still loading), currentEpisode.set reads ct === dur === 0 and its isFinished = (ct === dur) check persisted the skipped episode as finished at 0:00 — surfacing a never-played episode as played. Guard the finished computation against a zero/unknown duration in both persist paths: currentEpisode.set (store) and persistPlaybackPosition (player). A duration of 0 (or NaN) is never "finished". Genuine finishes (duration > 0 and currentTime === duration) are unaffected. Reproduced and verified fixed in real Obsidian: rapid A->B->C switch now leaves B as finished=false, while an episode played to its real end stays finished=true.
|
@codex review |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
# [2.17.0](2.16.0...2.17.0) (2026-06-22) ### Bug Fixes * behavioral-audit logic and robustness fixes (back-end, 1/2) ([#213](#213)) ([4e2845d](4e2845d)) * **download:** default per-episode download path and migrate empty default ([#183](#183)) ([#186](#186)) ([46a6486](46a6486)) * **download:** prevent Android crash and create missing folders on download ([#178](#178)) ([ecd09d5](ecd09d5)), closes [#113](#113) [#86](#86) [#113](#113) [#86](#86) * **lifecycle:** defer mobile podcast view startup ([#208](#208)) ([8bf9ce4](8bf9ce4)) * **notes:** cap note path length and harden folder creation ([#22](#22), [#87](#87)) ([#192](#192)) ([858e280](858e280)), closes [#87-class](#87) * **playback:** persist listened time during playback ([#33](#33)) ([#190](#190)) ([e3433c2](e3433c2)), closes [#191](#191) [#108](#108) [#163](#163) [#183](#183) * **playback:** play local files and downloads on iOS via resource path ([#100](#100)) ([#184](#184)) ([12c503a](12c503a)) * **player:** clear progress on episode switch to stop end-of-playback glitch ([#94](#94)) ([#194](#194)) ([59dccb3](59dccb3)) * **player:** reveal PodNotes view on Play with PodNotes so local files play ([#84](#84)) ([#198](#198)) ([5953625](5953625)) * **settings:** show labelled Add/Remove buttons in podcast search ([#109](#109)) ([#195](#195)) ([33399f5](33399f5)) * show downloaded episodes in the Local Files playlist ([#176](#176)) ([#177](#177)) ([184188c](184188c)) * **timestamps:** capture into the cursor's table cell without breaking the row ([#165](#165)) ([#203](#203)) ([964e342](964e342)) * **transcription:** always transcribe the currently playing episode ([#182](#182)) ([62b488a](62b488a)), closes [#107](#107) * **uri:** preserve '+' in episode titles and paths for timestamp links ([#181](#181)) ([8ad7aa5](8ad7aa5)), closes [#164](#164) * **view:** reliably reveal PodNotes view via command + ribbon icon ([#55](#55)) ([#199](#199)) ([fa1d708](fa1d708)) ### Features * add podcast segment links ([#205](#205)) ([d97e59e](d97e59e)) * **api:** expose generated episode transcripts ([e264465](e264465)), closes [#105](#105) * behavioral-audit UI and interaction fixes (front-end, 2/2) ([#215](#215)) ([894d93d](894d93d)) * **commands:** add playback rate and media timestamp controls ([#206](#206)) ([a8bb44a](a8bb44a)) * **devx:** isolated per-worktree Obsidian E2E vault wrapper ([#188](#188)) ([a8b7a4a](a8b7a4a)) * **episodes:** add a setting to control the Latest Episodes list length ([#114](#114)) ([#200](#200)) ([7b3e3c6](7b3e3c6)) * **notes:** add {{episodelink}} template tag to resume an episode from its note ([#35](#35)) ([#193](#193)) ([8c1ddd6](8c1ddd6)) * **notes:** add podcast feed-level notes ([#163](#163)) ([#187](#187)) ([db0de47](db0de47)), closes [#161](#161) [#160](#160) * **notes:** ship a Bases-friendly default episode note template ([#160](#160)) ([#201](#201)) ([209431d](209431d)), closes [#163](#163) [#183](#183) * **player:** scale episode title font size to its length ([#81](#81)) ([#202](#202)) ([659b6b8](659b6b8)) * **player:** support video episode playback ([#209](#209)) ([f92a91f](f92a91f)) * **queue:** add setting to disable queue auto-population and auto-advance ([#108](#108)) ([#185](#185)) ([cf3d73c](cf3d73c)) * **queue:** allow reordering the playback queue ([#80](#80)) ([#179](#179)) ([d994d61](d994d61)), closes [#173](#173) * **settings:** import/export settings & templates ([#180](#180)) ([a27d23d](a27d23d)), closes [#162](#162) [#162](#162) * **templates:** add {{currentDate}}, {{episodeNumber}}, {{duration}} template variables ([#189](#189)) ([ec573eb](ec573eb)), closes [#75](#75) [#34](#34) [#88](#88) [163/#186](#186) * **templates:** add episode chapters tag ([#207](#207)) ([9c98863](9c98863)) * **transcripts:** opt-in speaker diarization for transcripts ([#168](#168)) ([#204](#204)) ([a96e12f](a96e12f))
|
🎉 This PR is included in version 2.17.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Fixes the visual glitch reported in #94 when a podcast finishes playing.
When an episode finishes and the queue auto-advances (queue automation is on by default, #108),
queue.playNext()callscurrentEpisode.set(next). The player's title and artwork switch to the next episode immediately, but the$currentTime/$durationSvelte stores keep the finished episode's end values until the new<audio>element firesloadedmetadata. For the whole (network-bound) metadata-fetch window the player therefore renders:00:00:00)…against the new episode — the glitch. A single-episode finish with no auto-advance was already clean.
Root cause
$currentTime/$durationare only refreshed by the next episode'sloadedmetadata; nothing clears the outgoing episode's progress whencurrentEpisodechanges. Additionally, during the<audio>swap the fresh element reportsduration = NaN, so$duration - $currentTimecan momentarily beNaN, whichformatSecondsrendered asNaN:NaN:NaN.Fix
EpisodePlayer.svelte— in thecurrentEpisodesubscription, reset$currentTime/$durationto0on a genuine in-player switch, guarded by a first-fire flag so the synchronous initial subscription does not wipe a restored resume position or flash0on mount/remount.onMetadataLoaded → restorePlaybackTimesets the real position once the next episode loads; the existingisLoadingguard keeps this transient reset out of persisted state (Podcast episode not keeping the listened time #33).formatSeconds.ts— clamp non-finite (NaN/Infinity) and negative inputs to0, so the time display can never renderNaN:NaN:NaNor negative components.Verification
Real Obsidian (isolated worktree e2e vault, auto-advance A→B at the transition frame):
ended(B now current)Tests — added
EpisodePlayerregression test (progress zeroes on switch, no stale full bar /NaNtext) andformatSecondsclamping tests.Gates (Node 22):
lint,format:check,typecheck,build,check:a11y(svelte-check, 0 warnings),test(376 passing).Review — ultracode review + 2 adversarial reviewers: all approved, no blockers. They independently confirmed the persist-before-reset ordering captures the finished episode's position before the reset (no clobber of saved progress), the first-fire guard holds across remount and deep-link (
requestedPlaybackTime) flows,duration.set(0)is safe (bind:durationis read-only and self-corrects ondurationchange), and theNaNclamp is load-bearing during the swap window.Closes #94
🤖 Please review before merging — do not auto-merge.