Skip to content

fix(progress): keep played progress apart for same-titled episodes of one podcast - #504

Merged
chhoumann merged 8 commits into
masterfrom
fix/episode-progress-key
Oct 15, 2026
Merged

chhoumann merged 8 commits into
masterfrom
fix/episode-progress-key

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #448

Two episodes of one podcast with the same title (two "Trailer" items, a rerun) shared one played record, so one resumed at the other's position and finishing one marked both played. There were two causes. Progress was keyed by podcast name plus title. The player also decided "same episode" by that key, so clicking the second episode kept the first one's live position.

  • Progress is keyed by the feed's guid, scoped to the podcast, when the episode has one, and by the title key as before when it has none. The new key starts with two NULs and cannot equal any title key.
  • A record from 2.21.1 (title key) still resolves for its episode until the episode saves its own record. An episode whose title another item of its feed also has (sharedTitle, set when the feed is parsed) never borrows a title record, so one old record never goes to two episodes.
  • The player, the row's "current" check and the Played list compare guids. Marking one episode unplayed no longer resets the other.

There is no schema change, no migration step at load, and no write at load.

Before: both rows played, and the 60 s episode shows 40 s
After: only the 40 s episode is played; the 60 s one shows 30 s left
After a reload: the same

Left out, as follow-ups:

  • Downloads: the second same-titled episode still cannot download, because the file path comes from {{podcast}}/{{title}}. It still fails safely. This needs its own naming design.
  • Queue and playlists still identify episodes by podcast and title. Both twins cannot be queued at once, and finishing one removes the other from the queue and playlists. The transcript cache and media session use the same key.
  • Favorites and playlist entries saved before this version have no guid. They keep the title-keyed progress, so once the episode plays from its feed, the entry and the feed row can show different positions until the entry is added again.

Verification

Obsidian 1.14.4 on Linux (Xvfb), isolated worktree vault, Node 22.23.2. Fixture feed "Journey Repeats" with two "Repeat Episode" items (40 s and 60 s WAV, guids repeat-rerun and repeat-original).

  • npm run test:e2e -- -t "#448": red on master 791912f. The 40 s episode started at 30.07 s (the 60 s one's position), data.json held one record, and both rows were played. Green on this branch, including after a reload: the 60 s episode resumes at its own 30 s.
  • npm run test:e2e on a fresh vault, run by an independent verifier at dcb2e1b: 32 files, 241 tests passed. After merging master (build: fail the build on legacy Svelte components and clsx #503, build tooling only; the bundle size is unchanged), -t "#448" passes again.
  • npm run lint, format:check, typecheck, build, test (2788 passed), test:tooling (35 passed), docs:build: all pass. npm run size: 101,597 gzip bytes (master 101,332, +265; budget 103,000).
  • Feed cache (found by V-504): the cache keeps a feed's 75 newest items, so it could drop an older twin, and after a reload with a failed refresh the surviving twin read the 2.21.1 title record. Repeated titles are now marked on the full feed before the cut, as repeated guids already were. The 76-item test fails on 853c0f3 and passes now.
  • Migration: released-2.21.1.json loads through loadSettings with an added same-titled pair that has one legacy record. The unshared episode resumes the legacy record and neither twin borrows it. The first save keeps every legacy key and adds one guid key. A raw 2.21.1 document adopted at runtime through onExternalSettingsChange is not rewritten, and its progress resolves for the guid episode.

Mutations. Each one is a plausible wrong fix, and each fails the named tests:

Mutation Failing test
Guid key on write, title key on read store twins and legacy tests, 2.21.1 migration, player #448, podcasts sharing a guid
Title key on write store twins, 2.21.1 migration, player #448
Legacy fallback that matches both twins store "lends the record saved under the title to neither", 2.21.1 migration
Player sameness by title key player "starts an episode at its own position" (both cases), e2e #448
A guid-less copy matches any twin player "after a copy saved before guids"
Row "current" by title key and media row "is not current for another item of its feed with its title and media"
Played row ignores its own record row "shows a Played row's own record"
Unplay walks every record with the title store "marking one unplayed leaves the other played"
Played list deduped by podcast and title list "lists two played episodes of one feed with one title apart"
sharedTitle never set FeedRepository "marks each item whose title another item of the feed has"
sharedTitle not validated when decoded codec "keeps an episode's shared title mark only when it is true"
Guid key without the podcast store "keeps apart two podcasts' episodes that carry one guid"
Shared-title mark not kept when the cache cuts a twin FeedRepository "a title whose twin the cache cut still lends neither its old record after a failed refresh"
Cached shared-title mark ignored when projecting the same test

Release impact

Storage: new guid-keyed entries in playedEpisodes and a sharedTitle: true mark on saved episodes. Schema version 2 is unchanged and old title keys stay as they are.

With a device still on 2.21.1, both versions read each other's data.json, and nothing is deleted or rewritten back and forth. Progress no longer passes between them for an episode that this version has saved. The older device sees that episode as it was before, and its later progress for it does not reach this version. Its "Mark as unplayed" still resets every record with the title, which includes both twins. Updating every device ends this. A feed that changes an episode's guid starts that episode's progress over, where the title key used to survive.

Docs at release

  • docs/src/content/docs/sync.md, "Use PodNotes on two devices": add "Update PodNotes on every device. A device on 2.21.1 or earlier does not see the progress of an episode you played on 2.22.0 or later, and the newer device does not see that episode's progress from the older one."

Note

Medium Risk
Changes how played progress is keyed, resolved, and synced; cross-device behavior can diverge between 2.21.1 and newer builds until all devices update, but schema version is unchanged and legacy keys are preserved.

Overview
Fixes #448: same-titled episodes in one podcast no longer share playback progress or played state.

Played progress is stored under getProgressKey (podcast-scoped guid when present, otherwise the existing title key). Feeds mark duplicate titles with sharedTitle, and episodes with that flag do not inherit legacy title-key records—so one old row cannot apply to two feed items. Reads still resolve 2.21.1 title-key progress for a unique episode until that episode saves its own guid key; first save adds guid keys without removing legacy keys.

Player and UI treat “the same item” via isSameFeedItem (guid when both have one; otherwise media for shared titles), so switching between twins resumes the correct position. The Played list passes playedKey into rows so guid-keyed records display even when lookup by episode alone would fail.

Persistence validates sharedTitle as strictly true. Queue/playlist identity is unchanged (still title-based)—called out as follow-up in the PR description.

Reviewed by Cursor Bugbot for commit 853c0f3. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Keep separate playback progress for same-titled episodes in one podcast

  • Adds guid-based progress keys: episodes with a guid now store played records under a podcast-and-guid key via getProgressKey in episodeKey.ts, while guid-less episodes keep the old title-based key.
  • Feeds now mark episodes whose title is repeated within the feed with a sharedTitle flag, set in FeedRepository.ts and preserved through cache cuts in keepNewestItems.
  • Marked shared-title episodes no longer borrow the legacy title-keyed progress record; each keeps and resumes its own progress. This is enforced in episodeStatus.ts, episodeListEntry.ts, and the player identity checks in PlayerController.ts.
  • UI episode rows accept an explicit played-record key (playedKey) so rows render the exact guid-keyed record instead of falling back to title lookup.
  • Risk: persisted progress now uses a new NUL-prefixed guid key format alongside legacy title keys. Check episodeStatus.getPlayedEpisodeAliasKeys and getFinishedPlayedEpisodeRecords if consumed out of tree, since legacy title-record aliasing rules changed for guid-bearing episodes.

Macroscope summarized 8174b32.

Reproduces #448 in real Obsidian with a feed that has two "Repeat Episode"
items (40 s and 60 s media, distinct guids). It fails on this commit: the
40 s episode resumes at the 60 s one's position, one record holds both, and
finishing one marks both played.
…es stay apart

Two items of one feed with one title shared one played record, keyed by
podcast name and title, so a rerun resumed where the original stopped and
finishing one marked both played. The player also treated them as one
episode, so playing the second while the first was paused resumed the
first's live position.

Progress is now written under a key made from the podcast name and the
item's guid. Feed projection flags items whose title another item has
(sharedTitle). A record saved under the title alone, as 2.21.1 saved all
progress, still serves an episode whose title is unshared until its first
write, and serves neither of two twins. A copy saved before guids, such as a
2.21.1 favorite, follows the only guid record of its podcast and title.
Unplaying one twin leaves the other alone, and the Played list keeps twins
apart and drops a title record that a guid record replaced. The player
compares guids when both copies carry one.

No load-time migration, no schema change, and no record is deleted.

Related: #448
…n position

Playing the second of two same-titled items while the first was paused took
the same-episode path and resumed the first's live position. This guards the
player's guid comparison in the unit suite, which the e2e test alone did not.

Related: #448
…rogress

A guid is unique only within its feed, so the progress key must stay scoped
by podcast. Related: #448
…copies

A copy saved before guids now matches a same-titled item only by its media,
so it no longer hands its position to the other item. A row compares guids
before media, so a rerun that reuses the original's audio can play. Played
rows show their own record. The guid-less copy no longer looks up guid
records by title: that lookup changed with every play and scanned the whole
history on each row render. A shared-title mark that is not true is dropped
when decoded. Related: #448
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 8174b32
Status: ✅  Deploy successful!
Preview URL: https://780abb40.podnotes.pages.dev
Branch Preview URL: https://fix-episode-progress-key.podnotes.pages.dev

View logs

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 853c0f3. Configure here.

Comment thread src/services/FeedRepository.ts

for (const episode of episodes) {
const keys = [
getProgressKey(episode),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Played list claims a twin finished

Medium Severity

A 2.21.1 title-keyed finished record is supposed to belong to neither same-titled episode, and feed rows honor that via sharedTitle. The Played list still includes that record until a guid key exists, and resolvePlayedEpisode maps it onto the first twin through the shared getEpisodeKey / title lookup. That row shows as played even though getPlayedEpisode will not use the record, so playback starts at zero.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 853c0f3. Configure here.

…ng an old title record

Fails on 853c0f3: after a failed refresh, the surviving twin of a 76-item
feed lost its shared-title mark and read the 2.21.1 title record. Found by
V-504. Related: #448
The cache keeps a feed's 75 newest items. Mark repeated titles on the full
feed before the cut, as repeated guids already are, so the surviving twin
still never borrows an old title record after a reload. Related: #448
@chhoumann
chhoumann merged commit 24a76ea into master Oct 15, 2026
19 checks passed
@chhoumann
chhoumann deleted the fix/episode-progress-key branch October 15, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Same-titled episodes in one podcast share played progress

1 participant