5.8 KiB
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 underdirs::state_dir()/crabidy. Neither usesdirs::config_dir(). - Every stored audio file has a paired
<name>.cbd-store.tomlsidecar with ahashand at least one[[provider]]entry; the sidecars are the only persisted index (no separate index file). StoreIndexis 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'saliases(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
/fstrack 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 acrossfile/url/link/store/skipped=true; astorevalue with a path separator or empty isStoreName, not accepted.- A
storeplayable resolves only against the instance'sstore_root; an instance without a store root (any non-/crabidymount) treats it as a malformed reference, never a path escape. from_track_storeround-trips: the written toml re-reads asPlayable::Store(name)andto_tracksetsis_captured = truefor it.- Deleting a
/crabidytrack 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_idis 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 andto_trackhas no disk context to canonicalize a relativefileplayable); "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_idis never logged as a secret and carries no token; it is an opaque provider id only.
Save, conflict, current (D5, D6)
wwrites link tomls (no store, no audio);Wwrites 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).
currentis reserved: the playback loop overwrites it on every queue change; a user save namedcurrentis rejected. Saved queues are flat.- Queue
wlink-saves the live queue into/crabidy/<name>and queueWcaptures it viaCaptureLibraryNodeon/crabidy/current; both refuse an existing name. (TheSaveQueueRPC was kept as the queue-wentry point and reimplemented server-side as a link save — seeplan/summary.mddeviations — rather than removed as D6 proposed.) Capture progress streams viaCaptureProgress(unchanged shape).
UI (D8)
- The root library shows a single
crabidychild;queues/bookmarks/capturesno longer appear. - Captured rows are prefixed with
|as the first character of the row (before selection padding), driven byis_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
/crabidyhas no confirmation dialog and no disk-reclamation path (thecapture-deletion.mdconfirm 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.