crabidy/architecture/capture-deletion.md

4.1 KiB

Capture deletion

Deleting under /captures reclaims disk: downloaded audio is the one library content that is expensive to recreate (slow, throttled downloads — architecture/youtube-rustypipe.md), so stale captures must be removable from the TUI, and removal must actually delete the files.

Context

Before this feature, deletion (d) was limited to top-level folders of editable fsdy instances (architecture/bookmarks.md D4): whole captures could be deleted (and were removed from disk via remove_dir_all), but nothing below — no single album, no single track. Deletes were deliberately unconfirmed because every deletable node was cheap to recreate.

Decisions

D1 — deletable tree as an fsdy instance option

fsdy::Client::with_deletable_tree() makes every folder below the instance root deletable (recursively, any depth) and every track file deletable. Only the /captures instance sets it:

  • /queues and /bookmarks keep the top-level-only contract; their nested structure mirrors a snapshot and partial edits are better done by re-saving.
  • Nested folders become deletable but not renamable — renames would break the incremental-capture merge by name (architecture/incremental-captures.md), deletes cannot: a re-capture of the same name simply re-downloads what is missing.
  • The instance root itself and reserved top-level names stay undeletable even on a deletable tree.

D2 — tracks advertise deletability through their node

Wire truth, not client guessing: the TUI must not hardcode which tracks are deletable. But a per-Track flag would touch every Track literal in every provider for a capability only fsdy uses. Instead LibraryNode.tracks_deletable says "this node's listed tracks may be deleted", exactly like the existing is_downloadable inheritance ("tracks inherit their node's blessing", architecture/captures.md D4). Child folders keep using the existing per-child is_deletable.

D3 — a deleted track takes its audio with it, inside the root only

Deleting a track file removes the .cbd-track.toml and the audio its [playable] file points to — that is the point of the feature. Safety boundary: the audio path (relative values resolved against the track file's directory) is canonicalized and must live inside the canonicalized instance root; anything else is kept and logged. So a hand-written track file referencing ~/Music/song.flac from inside the captures folder can never delete foreign data, and ../symlink tricks resolve before the check. A track file that no longer parses is deleted blind (its audio cannot be located; the listing skipped it anyway). Deletes stay idempotent per the proto contract.

D4 — confirmation in the client, scoped to /captures

d on anything under /captures opens a one-line modal prompt (delete <title>? [y/N], red) instead of sending; only y/Y confirms, any other key cancels. Every other deletable (search terms, bookmarks, saved queues) stays a single unconfirmed keypress (architecture/node-editing.md D4) — they are cheap to recreate, and a blanket confirmation would train reflexive y. The scoping is a path check in the TUI (like the /captures never-cache rule in cbd-tui/src/rpc.rs): the server does not know which deletes a client should consider expensive.

Flow

direction: right
tui: cbd-tui {
  d: "d on /captures/…"
  confirm: "delete …? [y/N]"
  d -> confirm
}
server: crabidy-server {
  provider_loop: provider loop
}
fsdy: fsdy /captures instance {
  folder: "folder: remove_dir_all"
  track: "track: toml + contained audio"
}
tui.confirm -> server.provider_loop: y → DeleteLibraryNode
server.provider_loop -> fsdy.folder
server.provider_loop -> fsdy.track
fsdy.folder -> tui: refreshed parent listing

Risks / notes

  • The confirmation prompt occupies the same line as the text-input overlay; both are strictly modal and never open together.
  • Deleting the folder of a running capture is possible; the capture walk recreates directories as it goes and re-downloads on the next run, so the race wastes bandwidth but corrupts nothing.