Skip to content

fix(player): clear progress on episode switch to stop end-of-playback glitch (#94) - #194

Merged
chhoumann merged 2 commits into
masterfrom
chhoumann/94-finish-playing-glitch
Jun 16, 2026
Merged

chhoumann merged 2 commits into
masterfrom
chhoumann/94-finish-playing-glitch

Conversation

@chhoumann

Copy link
Copy Markdown
Owner

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() calls currentEpisode.set(next). The player's title and artwork switch to the next episode immediately, but the $currentTime / $duration Svelte stores keep the finished episode's end values until the new <audio> element fires loadedmetadata. For the whole (network-bound) metadata-fetch window the player therefore renders:

  • the progress bar pinned at 100% (full), and
  • the timestamps showing the previous episode's end (elapsed = full duration, remaining = 00:00:00)

…against the new episode — the glitch. A single-episode finish with no auto-advance was already clean.

Root cause

$currentTime / $duration are only refreshed by the next episode's loadedmetadata; nothing clears the outgoing episode's progress when currentEpisode changes. Additionally, during the <audio> swap the fresh element reports duration = NaN, so $duration - $currentTime can momentarily be NaN, which formatSeconds rendered as NaN:NaN:NaN.

Fix

  • EpisodePlayer.svelte — in the currentEpisode subscription, reset $currentTime/$duration to 0 on a genuine in-player switch, 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; the existing isLoading guard 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 to 0, so the time display can never render NaN:NaN:NaN or negative components.

Verification

Real Obsidian (isolated worktree e2e vault, auto-advance A→B at the transition frame):

phase before after
episode ended (B now current) bar 100%, elapsed 00:00:01 (stale A) bar 0%, elapsed 00:00:00 ✅
next frame bar 100%, elapsed 00:00:01 (stale A) bar 0%, elapsed 00:00:00 ✅
metadata loaded bar 0%, 00:00:00 bar 0%, 00:00:00

Tests — added EpisodePlayer regression test (progress zeroes on switch, no stale full bar / NaN text) and formatSeconds clamping 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:duration is read-only and self-corrects on durationchange), and the NaN clamp is load-bearing during the swap window.

Closes #94


🤖 Please review before merging — do not auto-merge.

… 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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 16, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: ba926bb
Status: ✅  Deploy successful!
Preview URL: https://4e457129.podnotes.pages.dev
Branch Preview URL: https://chhoumann-94-finish-playing.podnotes.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/ui/PodcastView/EpisodePlayer.svelte
#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.
@chhoumann

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: ba926bb9d3

ℹ️ 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".

@chhoumann
chhoumann merged commit 59dccb3 into master Jun 16, 2026
2 checks passed
@chhoumann
chhoumann deleted the chhoumann/94-finish-playing-glitch branch June 16, 2026 06:17
github-actions Bot pushed a commit that referenced this pull request Jun 22, 2026
# [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))
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.17.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG/FR] A glitch appears when a podcast finishes playing

1 participant