Skip to content

fix: only trash files PodNotes downloaded - #374

Merged
chhoumann merged 14 commits into
masterfrom
fix/never-trash-user-files
Oct 11, 2026
Merged

chhoumann merged 14 commits into
masterfrom
fix/never-trash-user-files

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

PodNotes now trashes a file only when it knows it downloaded that file itself.

  • A download PodNotes finishes records downloadedByPodNotes: true on its entry. "Remove file" trashes it as before.
  • A feed entry without that record asks first. This covers every download made before this change, including a user's own file that 2.17.1 to 2.19.2 registered as a download when it already sat at the download path. The prompt names the path and says PodNotes cannot confirm it downloaded the file. "Keep file" is the default and removes only the entry. "Delete file" deletes it the way Obsidian's "Deleted files" setting says, which is the trash by default. If Obsidian no longer indexes the file, PodNotes removes the entry and touches nothing on disk.
  • A file added with "Play with PodNotes" is never trashed. Its menu item reads "Remove from Local Files", and removing it keeps the file in the vault.

PodNotes never infers ownership from a feed name, a path, or an enclosure URL. Re-registering an existing download keeps whatever ownership it already had, so an old entry never becomes "owned".

Fixes #354

Scope

  • Covers files added with "Play with PodNotes" and downloads, both new and from older releases.
  • Leaves out the "Download" item that a local file shows after it leaves the playlist. It fails with a network error and never touches the file.
  • docs/docs/local_files.md gains two sentences about the prompt.

Verification

  • Real Obsidian 1.13.7 on headless Linux, isolated vault. On master, "Remove file" on a "Play with PodNotes" file moved it to .trash/. On this branch:
    • A local file, new or stored by a 2022 release, leaves the playlist and stays in the vault.
    • A seeded 2.17-style entry pointing at Recordings/Interview.wav asks first. Enter (Keep file) leaves the file. "Delete file" on another entry trashes it.
    • An entry whose file Obsidian does not index is removed with no prompt and the file stays. On the previous build this case deleted the file permanently.
    • A fresh download records downloadedByPodNotes: true, keeps it when downloaded again, and "Remove file" trashes it with no prompt.
  • npm run test:e2e passes 41 of 41. The new tests fail on the old code: the local-file test because the file lands in .trash, and the historical-entry test because no prompt appears. Unit mutations fail the suite each: protecting only local entries with a media type, dropping the ownership check in either direction, not recording ownership, upgrading or downgrading ownership on reuse, and trashing through the permanent-delete fallback.
  • npm run lint, format:check, typecheck, build, test, and docs:build pass.
Menu or prompt After
master, local file Menu offers Remove file File gone from Fixtures
local file Menu offers Remove from Local Files File still in Fixtures
older download, Keep file Prompt with Keep file and Delete file Interview still in Recordings
older download, Delete file Prompt with Keep file and Delete file Fixture Episode 3 gone from Recordings
fresh download Menu offers Remove file Removed with no prompt

Release impact

data.json download entries gain an optional downloadedByPodNotes: true. There is no migration, and an entry without the field stays valid. After upgrading, removing a download made by an earlier version asks before trashing it.

Note

Fix removal to only trash files PodNotes downloaded

  • Downloads now record a PodNotes ownership marker, persisted in stored entries and kept when an existing registration is reused (src/store/downloads.ts, src/types/DownloadedEpisode.ts).
  • removeDownloadedEpisode in src/downloadEpisode.ts leaves local-file entries untouched, trashes confirmed PodNotes downloads without prompting, and asks a keep/delete confirmation modal (src/ui/ConfirmTrashUnownedFileModal.ts) for unconfirmed indexed feed files. Missing or unindexed entries are only removed from the store.
  • The context menu now labels local files "Remove from Local Files" versus "Remove file" for feed downloads; both use the same removal handler.
  • The persistence decoder only keeps an exact true ownership marker and warns on other values (src/persistence/collectionCodecs.ts).
  • Behavioral Change: downloadedEpisodes.removeEpisode now returns the full stored entry instead of just a path, and foreign files at a resolved download destination now fail with an error naming the conflicting path.

Macroscope summarized eaf9ba8.

Removing a file the user played with "Play with PodNotes" from the Local Files playlist must keep that file in the vault. Both tests fail on master: the removal trashes the user's own file.
"Remove file" on a Local Files entry added by "Play with PodNotes" moved the user's own recording to the trash, because removal treated every offline entry as a PodNotes download. Removal now deletes the file only for PodNotes downloads. An entry in the "local file" bucket is dropped from Local Files and its file stays in the vault, which also covers local files already stored in existing data.json files. The menu item for those entries reads "Remove from Local Files".

Fixes #354
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: eaf9ba8
Status: ✅  Deploy successful!
Preview URL: https://51671071.podnotes.pages.dev
Branch Preview URL: https://fix-never-trash-user-files.podnotes.pages.dev

View logs

await obsidian.waitFor(
() =>
obsidian.dev.evalJson<boolean>(
`(() => { const target = ${findTarget}; return target instanceof HTMLElement && target.getBoundingClientRect().width > 0; })()`,
),
{ ...WAIT_OPTS, message: `${selector} is not visible in the PodNotes view.` },
);
await evalJsonAsync<boolean>(obsidian, `(() => { ${findTarget}.click(); return true; })()`);
Releases 2.17.1 to 2.19.2 registered any file already at a download path as
a feed download. Removing such an entry must ask before trashing the file,
a fresh download must record that PodNotes made it, and reusing an older
registration must not claim it. Local Files entries, including legacy ones
without a media type, are never trashed.
A finished download now records downloadedByPodNotes on its entry. Removing
an entry with that flag trashes the file as before. Removing a feed entry
without it, such as a file an older release registered because it already
sat at the download path, opens a prompt that names the path. "Keep file" is
the default and removes only the entry; "Move to trash" trashes the file.

The decision reads the removed stored entry, not the menu's episode object,
so old snapshots without a path or media type cannot change it. Reusing an
earlier registration keeps its flag as it was, and the codec drops any
stored value other than true. Local Files entries are never trashed.
Obsidian 1.13 renders a modal's close control as .modal-header-button, so
the Modal mock and the unit test use that class. The e2e test now closes
its own prompt when it fails partway, so a later run cannot read it.
The Setting row drew a divider and an empty label column above the two buttons. A modal-button-container matches Obsidian's own confirm dialogs.
An unconfirmed entry whose file the vault index does not resolve must be
dropped without a prompt and without touching the disk, and a confirmed
removal must trash the indexed file, never unlink a path. Also cover the
owned direction of download reuse and name the two owned-removal tests
by how their entry came to be.
Removing a download PodNotes cannot confirm it made now resolves the file
through the vault index first. A missing or unindexed file drops the entry
without a prompt, and a confirmed removal trashes the resolved file through
FileManager instead of falling back to a permanent adapter delete.

A download blocked by an existing file now says what was checked. The
registered owner lookup returns the entry it found, and addEpisode takes
download ownership as an option.
deleteFileWrittenByPodNotes falls back to a permanent adapter delete for paths the vault index misses, so only partial downloads and owned downloads go through it. Drop comments that restated names or explained that choice.
Both reuse sites now go through reuseOwnRegistration, which re-registers
this episode's own entry with its recorded ownership and reports whether
the path was free, reused, or held by another file. The fast path ignores
a foreign occupant and the resolved destination treats it as a collision,
as before. registeredOwnerOfPath becomes ownRegistrationAt.

The unindexed-file removal tests now use a dot-folder, which Obsidian
does not index, and the e2e removal test covers that case through the
real Local Files menu.
() =>
evalJsonAsync<boolean>(
obsidian,
`(async () => Boolean(document.querySelector(".modal-container")) || !(await ${original}))()`,
}

async function clickModalButton(obsidian: ObsidianClient, label: string): Promise<void> {
const button = `Array.from(document.querySelectorAll(".modal-container button")).find((button) => button.textContent?.trim() === ${JSON.stringify(label)})`;
...WAIT_OPTS,
message: `Modal button "${label}" did not appear.`,
});
await evalJsonAsync<boolean>(obsidian, `(() => { ${button}.click(); return true; })()`);
@chhoumann chhoumann changed the title fix: never trash a local file from Local Files fix: only trash files PodNotes downloaded Oct 11, 2026
Obsidian uses Delete for FileManager.trashFile, which follows the user's Deleted files setting, so the label stays true when that setting deletes permanently.
@chhoumann
chhoumann merged commit 7476423 into master Oct 11, 2026
12 checks passed
@chhoumann
chhoumann deleted the fix/never-trash-user-files branch October 11, 2026 04:42
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.

"Remove file" on a local file sends the user's original audio to the trash

2 participants