119 lines
6.3 KiB
Markdown
119 lines
6.3 KiB
Markdown
# 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
|
|
|
|
- [x] Proto: `Track.provider_item_id` (7), `Track.is_captured` (8),
|
|
`LibraryNode.is_captured` (10), `LibraryNodeChild.is_captured` (8). Regenerate.
|
|
- [x] 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.)_
|
|
- [x] 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 `w` → `CaptureNode{ 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`.
|