crabidy/plan/crabidy-store.md

6.3 KiB

Plan: the crabidy provider and content store

Ordered, dependency-first tasks for architecture/crabidy-store.md, each mapped to the test(s) and/or quality/crabidy-store.md gate(s) it satisfies. Green-field: no data migration.

Wire + fsdy foundations

  • Proto: Track.provider_item_id (7), Track.is_captured (8), LibraryNode.is_captured (10), LibraryNodeChild.is_captured (8). Regenerate.
  • fsdy: PlayableSpec.store, Playable::Store, 5-way cardinality + TrackFileError::StoreName, from_track_store, Client.store_root + with_store_root, get_urls_for_track store arm, to_track sets is_captured for store playables. (Gate: Playable::Store & fsdy.)
  • Sweep every Track/LibraryNode/LibraryNodeChild literal for the new fields; workspace cargo check --all-targets green.

Content store core (crabidy_store.rs)

  • Add blake3 dependency (Cargo.toml). (Gate: store layout.)
  • hash_file streams blake3, returns "blake3:<hex>". Unit test on a temp file with a known hash.
  • StoreSidecar/ProviderEntry (de)serialize to the documented TOML shape; round-trip test. (Gate: store layout.)
  • StoreIndex::scan reads all *.cbd-store.toml, skips a malformed one with a warning; insert adds hash + every (provider,id). Test: a store dir with two sidecars + one broken file indexes the two, skips the broken. (Gates: de-dup; errors — bad sidecar never poisons the index.)
  • unique_store_name: returns the natural name, else stem (N)ext on a taken name. Test collisions. (Gate: new track / numeral suffix.)
  • CrabidyStore::open(tree_root, store_root): create both dirs, scan index, build downloader; default_tree_root/default_store_root use state/data dirs. (Gate: store layout — not config_dir.)

Per-track de-dup capture (D4)

  • Capture primitive capture_track(client, track, save_dir, index, budget, progress) -> Outcome, implementing the D4 flow:
    1. already store-backed / fs-under-store → reuse, no fetch;
    2. index.by_provider_id hit → reuse, record alias if title differs;
    3. miss → fetch bytes (download via Downloader, or read the local fs file), hash_file;
    4. index.by_hash hit → add [[provider]], discard bytes, reuse name;
    5. miss → unique_store_name, move/copy into store, write sidecar, index it. Then write the save's track toml with from_track_store (skipped source → skipped toml). (Tests: dedup-by-id, dedup-by-hash, alias, fs-copy-dedup, fs-already-in-store no-op. Gates: de-dup section.)
  • Budget counts only bytes fetched this run; hits cost zero. (Test: two captures of the same track fetch bytes once.)

Save + current (D5, D6)

  • validate: validate_folder_name(name, &["current"]); refuse if /crabidy/<name> exists; Capture requires source is_downloadable. (Test: existing-name refused; Gate: save/conflict.)
  • save: enumerate source via client (reuse capture::enumerate / iterative walk), build into .tmp-<name>, per track link (from_track) or capture_track, then atomic remove-absent + rename swap. Failure removes the temp. (Tests: link save writes links; capture save writes store tomls; failed save leaves no folder but store keeps audio. Gates: save section.)
  • persist_current: write /crabidy/current flat from a queue snapshot (links), atomic replace. (Test: snapshot → flat current with links.)
  • annotate_captured(node): set Track.is_captured per index lookup or store playable; set node/child is_captured when fully captured. (Test: browsing a node with a captured track marks only that track.)

Provider identity (D3)

  • tidal: set Track.provider_item_id to the track id everywhere tidal builds a Track. (Gate: provider identity.)
  • youtube: set it to the video id.
  • fs: set it to the canonical absolute source path (for file playables). (Test: two fs tomls pointing at one file share an id.)

Orchestrator + RPC + playback rewire

  • ProviderOrchestrator: drop queues/bookmarks/captures fields; add one crabidy field. Construct CrabidyStore + an fsdy::Client on its tree_dir() with_editable_top_level(&["current"]).with_downloadable_nodes() .with_deletable_tree().with_store_root(store_dir()). Route crabidy_owns; add the single crabidy root child; call annotate_captured where nodes are returned. Remove the old *_owns for the three. (Gate: UI root; captured marking.)
  • CaptureLibraryNode handler → crabidy_store.validate then spawn crabidy_store.save. Remove the SaveQueue RPC + its command, client method, and playback handler; save_queue path folds into a capture on /crabidy/current. (Gate: save section.)
  • Playback loop persists current through CrabidyStore::persist_current (was QueueStore); saved-queue snapshot path removed. (Test: existing queue persistence tests adapted.)
  • Remove bookmark_store.rs, capture_store.rs, queue_store.rs; move any still-needed helpers into crabidy_store/capture; update lib.rs mods. Keep capture.rs (Downloader, windowed download, Progress, enumerate).

TUI (D8)

  • Path constants → /crabidy, CURRENT_QUEUE_PATH = "/crabidy/current".
  • library.rs::render: prefix | when is_captured; populate UiItem is_captured from Track.is_captured / child is_captured. (TUI test: a captured row renders a leading |.)
  • Queue wCaptureNode{ path:"/crabidy/current", name, download:false }; queue W → same with download:true. Drop QueueSaveAs/SaveQueue UI path.
  • Remove delete_needs_confirmation/ConfirmDelete and the y/N overlay; d on /crabidy deletes directly. (Gate: UI deletion.)
  • Help table (bindings.rs): update w/W/d descriptions for the merged provider. (Existing binding tests updated.)

Docs

  • README: config/dirs table (state vs data), single crabidy provider, | captured marker, removed queues/bookmarks/captures + capture-deletion.
  • architecture/overview.md: provider tree updated.
  • plan/summary.md: what was built + deviations from this plan/architecture.

Verification

  • cargo test --workspace green; cargo clippy --all-targets -- -D warnings and cargo fmt --check clean; markdownlint clean. Run via devenv shell.