crabidy/quality/crabidy-store.md

5.8 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 checked as the implement stage verified them; deviations are noted inline and in plan/summary.md.

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) and youtube (video id), stable across the paths an item is reached by (search vs playlist vs album). fs leaves it empty by design — fs sources de-duplicate by content hash instead (hashing a local file is cheap and to_track has no disk context to canonicalize a relative file playable); "already in the store → no-op" still works via the store-root path check.
  • 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.
  • Queue w link-saves the live queue into /crabidy/<name> and queue W captures it via CaptureLibraryNode on /crabidy/current; both refuse an existing name. (The SaveQueue RPC was kept as the queue-w entry point and reimplemented server-side as a link save — see plan/summary.md deviations — rather than removed as D6 proposed.) Capture progress streams via CaptureProgress (unchanged shape).

UI (D8)

  • The root library shows a single crabidy child; queues/bookmarks/ captures no longer appear.
  • Captured rows carry a trailing at the end of the row (after the action-key brackets, outside them), driven by is_captured; captured tracks are marked even while browsing another provider (tidal/youtube). Node/child marking is shallow: a node is marked captured when all its tracks are captured and it has no child nodes (flat saves); a nested save's top folder is not marked — full-recursion marking is future work.
  • 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.