# Bookmarks: capturing library subtrees > **Superseded** by `crabidy-store.md`: `/bookmarks` is folded into the single > `/crabidy` provider; a `w`-save writes link tomls into a `/crabidy/` > folder. Kept for the link-vs-store rationale. ## Context and problem statement Queue persistence (architecture/queue-persistence.md) flattens the queue into one folder of link files. The user now wants to **capture whole subtrees**: pressing `w` on a queueable item in the *library* (an artist, an album, a playlist folder) snapshots it into a local tree that **preserves structure** — an artist becomes a folder of album folders, each holding the playable track files. The store is one more local fs provider ("bookmarks"); replaying is plain fs-provider behavior. On top, the created top-level folders must be **renamable and deletable** from the TUI — and the same should hold for saved queues. ## Assumptions (confirmed against the code) - `fsdy::Client` is instance-mountable since queue persistence (`Client::new(provider_root, disk_root)`); a third instance is cheap. - The TUI already routes `e`/`d` through `is_editable`/`is_deletable` flags on `LibraryNodeChild` into the existing `RenameLibraryNode`/`DeleteLibraryNode` rpcs, and the orchestrator already routes those to the `/queues` (and future `/bookmarks`) instances. Making folders modifiable therefore needs **zero TUI changes** — only fsdy must set flags and implement rename/delete. - The capture walk can reuse `get_lib_node` through the `ProviderOrchestrator` (any provider reachable), and `TrackFile::from_track` + `track_file_name` from queue persistence for the leaves. - `w` is unbound in the TUI's `Library` scope; the input overlay handles ask-for-a-name flows and supports prefilling (rename does). ## Decisions ### D1 — Third fsdy instance `/bookmarks`; capture is a server-side walk A `BookmarkStore` (sibling of `QueueStore`) owns `/crabidy/bookmarks/`; the orchestrator mounts a read-only fsdy instance over it at `/bookmarks` and is the only writer. Capture runs on the **orchestrator side** (it must call `get_lib_node` across providers): a new `ProviderCommand::CaptureLibraryNode { path, name }` is handled on a spawned task (like `ResolveTracks` — a big artist walk must not block the loop). The reply arrives when the write finished. Not chosen: capturing client-side in the TUI (would duplicate provider access) or reusing the playback loop (captures are not queue state). ### D2 — Structure fidelity: order-prefixed folders and files The walk mirrors the subtree iteratively (worklist, pre-order): - Each child **node** becomes a folder named `NNNN ` (same zero-padded prefix and sanitizer as queue entries, no suffix) — the case-insensitive listing sort then reproduces the provider's child order, which is meaningful (album track order, discography order). - Each **track** becomes `NNNN <title>.cbd-track.toml` via the existing `TrackFile::from_track` (uniform link playable) — metadata is captured at save time; drift is accepted like everywhere else. - A node carrying both tracks and children (search terms) writes both. - Capturing a *track* selection is allowed: a folder with one link file. - Whole-bookmark writes are tmp-and-swap like queues; an existing bookmark of the same name is overwritten. **Safety caps**: the walk aborts (typed error, temp dir removed) beyond 1 000 directories or 20 000 tracks — a runaway provider tree must not fill the disk. Cycles are impossible under the cap (it bounds total nodes, not depth). Since captured listings rewrite link tracks to their targets, capturing a bookmark re-links to the *original* targets — no link chains ever get written. ### D3 — New rpc `CaptureLibraryNode(path, name)` Additive proto change (the only wire change). Name validation is shared with queue saving (trimmed, no separators/NUL, no leading dot; no reserved names in `/bookmarks`). Mapping: invalid name/source → `invalid_argument`, capture disabled (no config dir) → `failed_precondition`, walk/write failures → `internal`. The response is empty — the TUI stays where it is (unlike `%`-create, capturing is not a navigation; the bookmark appears under `/bookmarks` on the next visit). ### D4 — Mutable top-level folders as an fsdy instance option `fsdy::Client` gains a builder option `with_editable_top_level(reserved_names)`: - The instance-root listing marks child *folders* `is_editable` and `is_deletable`, except reserved names. - `rename_lib_node`: only direct children of the instance root; new title validated like a store name; renaming onto an existing sibling is `InvalidInput` (folders never merge); returns the renamed node (the TUI navigates into it, as with search terms). - `delete_lib_node`: only direct children of the root; `remove_dir_all`; idempotent (already gone → success); returns the refreshed root listing. - Deeper levels stay immutable — the user request covers the *created* folders; restructuring inside a capture is file-manager work. Applied to `/bookmarks` (no reserved names) **and `/queues`** (reserved: `current`, which auto-persist owns — it can be neither renamed nor deleted, and nothing can be renamed onto it). `/fs` keeps the immutable default. Not chosen: implementing rename/delete in the stores — the providers already own path→disk mapping and the rpc routing exists. ### D5 — TUI: `w` in the library scope `Action::LibraryCaptureNode` bound to `w` in `Scope::Library` ("Save selection as bookmark"): opens the input overlay **prefilled with the selected item's title**, gated on the bare selection being queueable (marks are ignored — one capture per invocation). Submit sends `MessageFromUi::CaptureNode { path, name }` → the new rpc. Rename (`e`) and delete (`d`) of bookmark/queue folders ride the existing flows via the D4 flags. ### D6 — Out of scope (explicitly) - Capturing multiple marked items at once; capture progress display. - Rename/delete below the top level; moving bookmarks between folders. - Refreshing a bookmark from its source (re-capture under the same name overwrites — that *is* the refresh). - A creatable `/bookmarks` root (`%`) — bookmarks are created from the source tree. ## Structure ```d2 direction: right server: crabidy-server { orch: ProviderOrchestrator { cap: "capture walk (spawned):\nget_lib_node -> mirror tree" } bstore: BookmarkStore { w: "validate name, caps,\ntmp-and-swap" } } tidal: tidaldy fs: "fsdy /fs" qfs: "fsdy /queues\n(editable top level,\nreserved: current)" bfs: "fsdy /bookmarks\n(editable top level)" disk: "config/crabidy/bookmarks" { shape: cylinder tree: "<name>/NNNN <album>/NNNN <track>.cbd-track.toml" } server.orch.cap -> tidal: "walk source subtree" server.orch.cap -> server.bstore: "write mirrored tree" server.bstore -> disk bfs -> disk: "list + parse (read only)" server.orch -> bfs: "/bookmarks/... (browse, queue, e/d)" server.orch -> qfs: "e/d on saved queues" ``` ## Key flow: capture an artist, rename it, replay an album ```d2 shape: sequence_diagram tui: TUI orch: Orchestrator tidal: tidaldy store: BookmarkStore tui -> orch: "CaptureLibraryNode(/tidal/artists/42, faves)" orch -> tidal: "get_lib_node (artist, albums, ...)" orch -> store: "write faves/0001 Album/0001 Song.cbd-track.toml ..." store -> tui: OK tui -> orch: "RenameLibraryNode(/bookmarks/faves, road faves)" orch -> tui: "renamed node (TUI navigates in)" tui -> orch: "ReplaceQueue([/bookmarks/road%20faves/0001%20Album])" orch -> tui: "resolve walk streams the album's tracks" ``` ## Risks and open questions - **Capture duration**: a large artist means many provider fetches; the TUI's poll loop awaits the rpc like other slow calls (accepted, consistent with search-term creation). The orchestrator loop itself stays free (spawned task). - **Rename/delete racing a re-capture** of the same name: last writer wins on the swap; accepted for a single-user local server. - **Prefix width** (9999 entries per folder) shared with queues; accepted. - Open (future): re-capture/refresh command; capturing marked sets; editable nesting.