diff --git a/AGENTS.md b/AGENTS.md index 5d42913..420772c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,8 @@ Three crates with a strict ownership split — **core owns truth; CLI and server Capture flow: locator → `determine_source()` (`crates/archivr-core/src/capture.rs`) routes by platform/shorthand (`yt:`, `x:`, `tweet:` …) → platform downloader (`downloader/ytdlp.rs`, `tweets.rs`, `singlefile.rs`, `http.rs`, `local.rs`) stages into `temp/` → SHA3-256 dedup (`hash.rs`, `downloader/store.rs`) moves blobs to `raw/A/B/HASH.EXT` → rows written to `archivr.sqlite` (runs, entries, artifacts, blobs) → served via `/api/archives/:id/...`. `CaptureConfig` carries per-request toggles (uBlock, reader mode, Freedium mirror, etc.); when `via_freedium` is set, the fetch URL is rewritten through `freedium-mirror.cfd` while the canonical DB URL stays the original locator. +YouTube playlists and channels produce a **parent container entry** with each video captured as a child entry. `downloader/ytdlp.rs` handles the playlist probe (fetching per-video quality metadata before archiving), the multi-video download loop, and sync mode (skipping already-archived videos when re-archiving a playlist or channel). + Per-archive layout (created by `archivr init`): `.archivr/` (name, store_path, `archivr.sqlite`) + sibling `store/` (`raw/`, `raw_tweets/`, `structured/`, `temp/`). Server-level auth lives in a **separate** `archivr-auth.sqlite` (users, sessions, API tokens, role bits GUEST=1/USER=2/ADMIN=4/OWNER=8). The server mounts multiple archives from a TOML registry (`crates/archivr-server/src/registry.rs`); routes are parameterized by `:archive_id`. @@ -78,6 +80,7 @@ No CI is configured; no rustfmt.toml/clippy.toml — default `cargo fmt`/`clippy - `crates/archivr-server/src/main.rs` — server bootstrap: config load, archive mounting, auth DB init, stalled-job recovery (running → failed on startup). - `crates/archivr-server/src/routes.rs` — all HTTP handlers and the router; grep here first for API work. - `crates/archivr-core/src/capture.rs` — `perform_capture()`, `Source` enum, shorthand parsing. +- `crates/archivr-core/src/downloader/ytdlp.rs` — yt-dlp integration; YouTube playlist/channel probe and download, sync mode logic. - `crates/archivr-core/src/database.rs` — single source of truth for all SQLite schema and queries (both archive and auth DBs). - `frontend/src/App.jsx` / `frontend/src/api.js` — frontend root state and API surface. - `docker/config.example.toml` — server config schema: `bind`, `auth_db_path`, repeated `[[archives]]` (`id`, `label`, `archive_path`). diff --git a/ARCHIVR-MENTAL-MODEL.md b/ARCHIVR-MENTAL-MODEL.md index ec90e05..eae3d4c 100644 --- a/ARCHIVR-MENTAL-MODEL.md +++ b/ARCHIVR-MENTAL-MODEL.md @@ -64,6 +64,17 @@ label = "Personal" archive_path = "/path/to/archive/.archivr" ``` +## Entry Nesting + +Entries support a two-level parent/child hierarchy. A **container entry** (playlist or channel) holds zero or more child entries (individual videos). Container entries have no primary media artifact of their own; their `total_artifact_bytes` is the sum of their children's bytes. + +Rules: +- Maximum nesting depth is 2 (root → child). Children cannot have children. +- The UI shows child entries collapsed under their parent, expandable with a chevron. +- Container entries are created by playlist/channel captures. Single-video and all other source types produce a standalone root entry with no children. + +If a feature touches how entries are parented or how the UI groups them, start in `archivr-core` (`database.rs` for schema, `archive.rs` for listing, `capture.rs` for creation). + ## How To Run It There are two user-facing binaries: @@ -155,6 +166,7 @@ sequenceDiagram | Capture orchestration, `Source` routing, `CaptureConfig` | `crates/archivr-core/src/capture.rs` | | Archive opening, listing entries, entry detail, runs | `crates/archivr-core/src/archive.rs` | | Download/save behavior | `crates/archivr-core/src/downloader/` | +| YouTube playlist/channel download, playlist probe, sync mode | `crates/archivr-core/src/downloader/ytdlp.rs` and `capture.rs` | | CLI commands, argument parsing, terminal output | `crates/archivr-cli/src/main.rs` | | Server API routes | `crates/archivr-server/src/routes.rs` | | Auth model (users, sessions, tokens, roles) | `crates/archivr-server/src/auth.rs` | diff --git a/crates/archivr-core/src/database.rs b/crates/archivr-core/src/database.rs index e158734..71b82e8 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -1638,12 +1638,15 @@ pub fn delete_entry(conn: &Connection, entry_uid: &str) -> Result { // shared blobs with any subtree member, excluding every subtree ID simultaneously. cascade_cached_bytes_after_subtree_delete(conn, &subtree_ids)?; - // Null the FK that has no ON DELETE action (covers root and all descendants). + // Null the FK that has no ON DELETE action. Must cover: + // - The entry itself (child entry: root_entry_id = playlist root, not self) + // - All descendants (root entry: children have root_entry_id = entry_id) conn.execute( "UPDATE archive_run_items SET produced_entry_id = NULL - WHERE produced_entry_id IN ( - SELECT id FROM archived_entries WHERE root_entry_id = ?1 - )", + WHERE produced_entry_id = ?1 + OR produced_entry_id IN ( + SELECT id FROM archived_entries WHERE root_entry_id = ?1 + )", [entry_id], )?;