Repository navigation
fix(playback): persist listened time during playback (#33) - #190
Merged
Merged
Conversation
Playback position was persisted only on teardown — the player's onDestroy, an episode switch, or the episode finishing. On mobile the OS kills the backgrounded process without firing onDestroy, so all progress since the last teardown was lost and the episode restarted from 0 on reopen (also reported after closing Obsidian on macOS). Persist the position periodically while audio plays (5s leading-edge throttle), immediately on pause, and immediately on visibilitychange→hidden (app backgrounded / window hidden) — keeping the existing teardown save — so an abrupt process kill loses at most ~5s. A guard skips writes before the saved position has been restored (which would clobber it with a pre-restore 0); the guard and throttle are re-armed on every in-player episode switch so a switch cannot clobber the next episode's saved resume position. Verified end-to-end in real Obsidian (isolated e2e vault): position now reaches data.json during playback, on pause, and on background — with no teardown — and restores on reload.
Deploying podnotes with
|
| Latest commit: |
044f2a3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c72de8f6.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-33-keep-listened-t.podnotes.pages.dev |
DEFAULT_PODNOTES_DATA in the e2e provisioning script is a hand-maintained mirror of DEFAULT_SETTINGS. Recently-merged PRs changed the defaults without updating the mirror, so the "seeds a data.json that mirrors the real DEFAULT_SETTINGS" test fails on master — turning the Test CI check red for every open PR. Re-sync the three drifted fields: - add autoQueue: true (#108) - add the feedNote default (#163) - set download.path to the per-episode default (#183) Closes #191.
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))
Contributor
|
🎉 This PR is included in version 2.17.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #33 — podcast playback no longer restarts from the beginning after you leave and reopen the app.
The listened position was persisted only on teardown events — the player's
onDestroy, an episode switch, or the episode finishing. On mobile the OS kills the backgrounded process without firingonDestroy, so every second listened since the last teardown was lost and the episode resumed from0. (Also reported "after closing Obsidian" on macOS, where a hard quit can beat the async settings write.)This PR persists the position during playback, so progress is durable no matter how the app terminates.
Root cause (verified on current
master)EpisodePlayer.svelteonly wrote the position viaonDestroy/currentEpisode.set/markAsPlayed. There was notimeupdate/interval save during playback. Reproduced in a real isolated Obsidian vault:137s, waited 3s while "playing" →settings.playedEpisodesstayed{}.data.jsonhad nothing; a simulated cold restart restored0→ 137s lost.The fix (
src/ui/PodcastView/EpisodePlayer.svelte)on:timeupdate(SAVE_POSITION_THROTTLE_MS = 5000, leading-edge). An abrupt kill loses at most ~5s.on:pause(the "stop, then leave" path) writes the exact position, bypassing the throttle.visibilitychange → hiddenlistener captures the exact moment the app is backgrounded / window hidden, before the OS can suspend or kill the process. This is the headline mobile scenario.onDestroystill saves, now via the sharedpersistPlaybackPosition()helper.persistPlaybackPosition()skips writes whileisLoadingis true, so it never overwrites the stored position with the pre-restore0. (This also repairs a latent bug whereonDestroybefore metadata load wrotefinished: true.)isLoading/ throttle are reset in thecurrentEpisodesubscription, so an in-player switch (queue click, auto-advance) cannot clobber the next episode's saved resume position.<EpisodePlayer/>has no{#key}, so the instance is reused across switches and the guard would otherwise stay inert.Writes flow through the existing
setEpisodeTime → EpisodeStatusController → saveSettingschain, which already coalesces, so the cadence stays cheap.Tests (
EpisodePlayer.test.ts)7 tests, mounting the real component and firing real DOM events:
visibilitychange → hiddenpersists immediately.Runtime verification (real Obsidian, isolated e2e vault)
With a real local audio file driven through the player (no teardown):
data.jsonduring playback (e.g.time: 27.16after ~6s) — previously stayed{};on:pausewrote the exact position (423);visibilitychange → hiddenwrote the exact position (480) to disk.Gates
npm run lint,format:check,typecheck,check:a11y,build✅ — fullnpm run testpasses for this change.Release / migration impact
None. No settings shape, storage, API, or URI changes — only more frequent writes to the existing
playedEpisodesmap.CI fix (separate commit)
CI's
Testcheck was red due to a pre-existing failure onmaster, not this change:scripts/provision-obsidian-e2e-vault.test.tsasserts the e2e seedDEFAULT_PODNOTES_DATAmirrorsDEFAULT_SETTINGS, and recent merges (#108autoQueue, #163feedNote, #183download.path) drifted the seed. Filed as #191 and fixed here in a dedicatedfix(devx)commit (Closes #191) so this PR's CI can go green — easily separable if you'd prefer it on its own.