6.3 KiB
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_trackstore arm,to_tracksetsis_capturedfor store playables. (Gate: Playable::Store & fsdy.) - Sweep every
Track/LibraryNode/LibraryNodeChildliteral for the new fields; workspacecargo check --all-targetsgreen.
Content store core (crabidy_store.rs)
- Add
blake3dependency (Cargo.toml). (Gate: store layout.) hash_filestreams 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::scanreads all*.cbd-store.toml, skips a malformed one with a warning;insertadds 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, elsestem (N)exton 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_rootuse 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:- already store-backed / fs-under-store → reuse, no fetch;
index.by_provider_idhit → reuse, record alias if title differs;- miss → fetch bytes (download via
Downloader, or read the local fs file),hash_file; index.by_hashhit → add[[provider]], discard bytes, reuse name;- miss →
unique_store_name, move/copy into store, write sidecar, index it. Then write the save's track toml withfrom_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;Capturerequires sourceis_downloadable. (Test: existing-name refused; Gate: save/conflict.)save: enumerate source viaclient(reusecapture::enumerate/ iterative walk), build into.tmp-<name>, per track link (from_track) orcapture_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/currentflat from a queue snapshot (links), atomic replace. (Test: snapshot → flat current with links.)annotate_captured(node): setTrack.is_capturedper index lookup or store playable; set node/childis_capturedwhen fully captured. (Test: browsing a node with a captured track marks only that track.)
Provider identity (D3)
- tidal: set
Track.provider_item_idto the track id everywhere tidal builds aTrack. (Gate: provider identity.) - youtube: set it to the video id.
- fs: set it to the canonical absolute source path (for
fileplayables). (Test: two fs tomls pointing at one file share an id.)
Orchestrator + RPC + playback rewire
ProviderOrchestrator: dropqueues/bookmarks/capturesfields; add onecrabidyfield. ConstructCrabidyStore+ anfsdy::Clienton itstree_dir()with_editable_top_level(&["current"]).with_downloadable_nodes() .with_deletable_tree().with_store_root(store_dir()). Routecrabidy_owns; add the singlecrabidyroot child; callannotate_capturedwhere nodes are returned. Remove the old*_ownsfor the three. (Gate: UI root; captured marking.)CaptureLibraryNodehandler →crabidy_store.validatethen spawncrabidy_store.save. Remove theSaveQueueRPC + its command, client method, and playback handler;save_queuepath folds into a capture on/crabidy/current. (Gate: save section.)- Playback loop persists
currentthroughCrabidyStore::persist_current(wasQueueStore); 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 intocrabidy_store/capture; updatelib.rsmods. Keepcapture.rs(Downloader, windowed download,Progress,enumerate).
TUI (D8)
- Path constants →
/crabidy,CURRENT_QUEUE_PATH = "/crabidy/current". library.rs::render: prefix|whenis_captured; populateUiItemis_capturedfromTrack.is_captured/ childis_captured. (TUI test: a captured row renders a leading|.)- Queue
w→CaptureNode{ path:"/crabidy/current", name, download:false }; queueW→ same withdownload:true. DropQueueSaveAs/SaveQueueUI path. - Remove
delete_needs_confirmation/ConfirmDeleteand they/Noverlay;don/crabidydeletes directly. (Gate: UI deletion.) - Help table (
bindings.rs): updatew/W/ddescriptions for the merged provider. (Existing binding tests updated.)
Docs
- README: config/dirs table (state vs data), single
crabidyprovider,|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 --workspacegreen;cargo clippy --all-targets -- -D warningsandcargo fmt --checkclean; markdownlint clean. Run viadevenv shell.