crabidy/quality/crabidy-store.md

5.1 KiB

Quality gates: the crabidy provider and content store

Criteria the implementation must satisfy beyond the automatic tests (fsdy/src/lib.rs, crabidy-server/src/crabidy_store.rs, and the TUI tests). Each gate is pass/fail by reading the code. See architecture/crabidy-store.md. Boxes are unchecked until the implement stage verifies them.

Store layout and de-duplication (D2, D4)

  • The content store is a flat directory under dirs::data_dir()/crabidy; the toml tree is under dirs::state_dir()/crabidy. Neither uses dirs::config_dir().
  • Every stored audio file has a paired <name>.cbd-store.toml sidecar with a hash and at least one [[provider]] entry; the sidecars are the only persisted index (no separate index file).
  • StoreIndex is built by scanning the sidecars at open and updated on every write; lookups go through it (no per-capture directory rescans, no shelling out to grep/rg).
  • Capturing a track already present by (provider, provider_item_id) does no download and reuses the existing store entry; a differing title is appended to that entry's aliases (not duplicated).
  • Capturing content already present by hash (provider id missed) discards the freshly fetched bytes, adds a new [[provider]] entry to the existing sidecar, and points the toml at the existing store name — no duplicate audio.
  • A genuinely new track creates one audio file + one sidecar; on a natural-name collision with different content the name gets a numeral suffix (never overwrites unrelated audio).
  • The download byte budget counts only bytes fetched this run; provider-id and hash hits cost zero budget.
  • An /fs track whose file is already under the store root captures as a no-op reuse; one pointing at a normal file is copied into the store and the original file is left in place.

Playable::Store and fsdy (D2, D7)

  • [playable] validation is exactly-one across file/url/link/store/ skipped=true; a store value with a path separator or empty is StoreName, not accepted.
  • A store playable resolves only against the instance's store_root; an instance without a store root (any non-/crabidy mount) treats it as a malformed reference, never a path escape.
  • from_track_store round-trips: the written toml re-reads as Playable::Store(name) and to_track sets is_captured = true for it.
  • Deleting a /crabidy track removes only its toml; deleting a folder removes only the toml folder. Store audio (outside the toml root) is never removed — verified via the existing "audio must be under the instance root" guard.

Provider identity (D3)

  • Track.provider_item_id is set by tidal (track id), youtube (video id), and fs (canonical source path); it is stable across the paths an item is reached by (search vs playlist vs album).
  • The store keys on (provider, id) where provider is the source track's path root — two different provider ids never collide across providers.
  • provider_item_id is never logged as a secret and carries no token; it is an opaque provider id only.

Save, conflict, current (D5, D6)

  • w writes link tomls (no store, no audio); W writes store-backed tomls and runs the de-dup capture. Both work on a library node and on the queue.
  • A save to an existing /crabidy/<name> is refused with a clear warning and changes nothing on disk; the user must delete and re-save.
  • Saves are atomic: built in a hidden temp sibling and swapped into place on success; a failed/crashed save leaves no partial top-level folder, and any audio already committed to the store persists (making retry cheap).
  • current is reserved: the playback loop overwrites it on every queue change; a user save named current is rejected. Saved queues are flat.
  • SaveQueue is gone; queue w/W go through CaptureLibraryNode on /crabidy/current. Validation errors return synchronously; progress streams via CaptureProgress (unchanged shape).

UI (D8)

  • The root library shows a single crabidy child; queues/bookmarks/ captures no longer appear.
  • Captured rows are prefixed with | as the first character of the row (before selection padding), driven by is_captured; captured tracks are marked even while browsing another provider (tidal/youtube).
  • Deletion on /crabidy has no confirmation dialog and no disk-reclamation path (the capture-deletion.md confirm flow is removed).

Errors and safety (always-on rules)

  • No panics on malformed sidecars, missing store files, partial downloads, or unreadable sources — every defect is a typed error and a bad sidecar is skipped with a warning, never poisoning the index.
  • External calls (downloads) keep their timeouts; the capture runs on a spawned task so the orchestrator keeps serving commands.
  • Store mutation is serialized (the index mutex) so concurrent captures cannot corrupt a sidecar or race the numeral-suffix naming.
  • Stream URLs stay redacted (scheme/host only) in any new log lines; store names and titles are fine to log, tokens are not.