Repository navigation
feat(notes): add {{episodelink}} template tag to resume an episode from its note (#35) - #193
Conversation
…om its note (#35) Add a no-timestamp obsidian://podnotes deep link so an episode note can link back to the player and resume from the last played location (or the start if unplayed). The resume point is resolved at click time, not baked into the note. - New {{episodelink}} note-template tag (src/utility/buildEpisodeResumeLink.ts) - URIHandler: a link without time reopens the episode and defers the seek to the player's existing saved-progress restore; explicit-time links are unchanged - Docs + unit tests for the link builder, URI handler, and template tag
Deploying podnotes with
|
| Latest commit: |
f7cf896
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b3056058.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-35-note-to-episode.podnotes.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 207a069293
ℹ️ 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".
| const target = isLocalFile(episode) | ||
| ? (episode as LocalEpisode).filePath ?? | ||
| downloadedEpisodes.getEpisode(episode)?.filePath | ||
| : episode.feedUrl; |
There was a problem hiding this comment.
Fall back to downloaded paths without feed URLs
For a non-local file episode that has been downloaded but has no feedUrl (the type allows this, and older/imported episode snapshots can lack it), this else arm returns undefined and {{episodelink}} renders as an empty string even though downloadedEpisodes.getEpisode(episode)?.filePath is a valid vault address the URI handler can resolve through the Local Files projection. Check the downloaded file path before giving up on non-local episodes so downloaded/offline-addressable episodes still get a resume link.
Useful? React with 👍 / 👎.
…nd stale seeks (#35) Address adversarial review of #193: - Finished episodes (saved at time===duration) no longer resume at EOF and auto-advance the queue; the no-time path resolves the resume point explicitly and restarts a finished episode from the beginning (resolveResumeTime), both for non-loaded and already-loaded episodes. - The no-time path now arms requestedPlaybackTime explicitly instead of relying on its absence, so a stale pending seek from an earlier link to the same episode can no longer override the resume on metadata load. - buildEpisodeResumeLink falls back to a downloaded copy's path when an episode lacks feedUrl, and drops a redundant cast (isLocalFile already narrows). - Tests for finished resume (non-loaded + loaded), stale-seek override, and the downloaded-without-feedUrl link; docs note finished -> start behavior.
Review round (ultracode + adversarial) — addressed in
|
# [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
Closes #35.
Adds a
{{episodelink}}note-template tag so a podcast note can link straight back to its episode in the PodNotes player and resume from where you left off — or start at the beginning if it has never been played. This is the workflow the issue author asked for: put the tag in the episode-note template once, and every note gets a "back to the episode" link automatically, no manual timestamp-capturing required.How it works
The crux of #35 is that "last played location" is dynamic — it must be resolved when the link is clicked, not baked in when the note is created. So:
{{episodelink}}emits anobsidian://podnotesdeep link that carries notimeparameter — justepisodeName+url(src/utility/buildEpisodeResumeLink.ts).URIHandlernow treats an absent/emptytimeas "resume": it reopens the episode and defers the seek to the player's existing saved-progress restore (EpisodePlayer.svelte→restoreSavedPlaybackTime), which already resumes fromplayedEpisodes[key].timeor0. No new resume logic — it reuses the well-tested path that powers normal grid-click playback.time(existing timestamp links /{{linktime}}) hit the unchanged explicit-time branches and behave byte-for-byte as before.Addressing mirrors
API.getPodcastTimeFormatted: a streamed episode links by its feed URL (the player refetches the feed and matches by title); a local/downloaded episode links by its on-disk path. When neither is known the tag yields an empty string, so a template degrades gracefully instead of emitting a broken link.Why this approach
restoreSavedPlaybackTimemeans "resume where I left off" stays correct even as playback progress changes after the note is written — and there's no duplicated seek logic to drift.TemplateEnginechange (a single new tag) to stay collision-safe with the in-flight [BUG] Handling of max path length when creating notes #22 work.Backward compatibility
No migration. Existing
obsidian://podnoteslinks always includetime(andencodePodnotesURInever emits an emptytime), so they never reach the new no-time path. The two explicit-time branches inURIHandlerare unchanged.Testing
npm run lint,npm run typecheck,npm run build,npm run format:check: clean.npm run test: 382 passed. New unit tests cover the link builder (remote/local/downloaded/missing-URL/+-encoding), theURIHandlerno-time resume across branches (non-loaded, already-loaded, empty-time, invalid-time still rejected, target's saved resume point preserved), and the{{episodelink}}template tag. The deferred seek itself is covered by the existingEpisodePlayer.test.tsrestore tests.currentTimeresumes to the saved position (240s for a partially-played episode; 0s for an unplayed one). I did not commit those two E2E cases because driving live playback exposed a pre-existing concurrent-writeFilerace ondata.jsonbetween the harness's per-test reseed and the plugin'ssaveSettings(it destabilized the shared-instance suite); the behavior they assert is covered deterministically by the unit tests above.Docs
docs/docs/templates.mddocuments{{episodelink}}in the note-template section.Review
Reviewed via one comprehensive pass plus two adversarial reviewers (backward-compat-break and does-resume-actually-work lenses) — all three returned ship. The one nit (a no-time test seeded a saved time without asserting it) was addressed by asserting the target's saved resume point survives
currentEpisode.set's side effects.