# Architecture For contributors. The repo's `CLAUDE.md` is the terse version of this; read both. ## Two front-ends, one engine `main.rs` dispatches: `--web` → `web::run()` (axum, blocks forever); otherwise `app::App` (eframe/egui desktop GUI). **Both share `downloader::Downloader`**, the single source of truth for the yt-dlp job lifecycle. `Downloader` is **not** async — it spawns an OS thread per yt-dlp process, streams stdout/stderr back over an `mpsc` channel into each `Job`'s log buffer, and the caller pumps `Downloader::poll()` regularly (the egui frame loop and a web background task both do). Anything you add to `Downloader` is automatically available to both UIs. `poll()` also drives the cross-cutting job machinery: auto-retry of transient failures (cooldown + adaptive throttle), the hang watchdog, and the post-download ffmpeg transcode pass. These work by capturing specs onto the `Job` at `start()` time (`RetrySpec`, `ConvertSpec`) and acting on them when the job changes state. ## The settings flow (the easy thing to get wrong) Almost every configurable feature has the same five-touchpoint shape — miss one and it silently half-works: 1. `config.rs` — a field/section + its `Default` + the `default_with_dir` constructor. 2. `download_options.rs` — an `Option<…>` per-channel override (None = defer to global). 3. `downloader.rs` — a resolver merging global config + per-channel override into yt-dlp/ffmpeg args, plus a `pub` field on `Downloader` holding the global default. 4. **Both** UIs render the global setting *and* the per-channel override: desktop in `app.rs`, web in `web_ui/index.html` **and** `web.rs`'s `SettingsPayload` (GET reads config, POST writes config + pushes onto the live `Downloader`). 5. Seed the `Downloader` field at construction **and** on settings-save, in **both** `app.rs` and `web.rs`. `subtitle_defaults`, `youtube_player_clients`, and `convert_defaults` are complete worked examples — grep one end-to-end before adding a setting. ## Filesystem layout `platform::platform_root(channels_root, platform)` = `channels_root.join(dir_name)`. **All** platforms (including YouTube, whose `dir_name` is `channels`) nest under the one configured `backup.directory`. `.source-url` sidecars in each creator folder let re-checks recover the exact URL. Library scanning (`library.rs`) is parallel and consults a `(path, mtime)` SQLite cache to skip re-parsing unchanged `info.json` sidecars. ## Persistence `database.rs` wraps an r2d2 SQLite pool. `Database` is cheaply `Clone` (the pool is an `Arc`), so the parallel scanner takes its own handle. Schema lives in `init_schema()`; new columns are added via idempotent `ALTER TABLE … ADD COLUMN` that swallows the duplicate-column error (no migration framework). The web UI keeps library/notes snapshots in memory; mutating endpoints mirror DB writes onto those caches and bump a version counter (the `/api/library` ETag) so reads stay consistent without a rescan. The long-lived `WebState` mutexes are accessed via `util::LockExt::lock_recover()`, which recovers a poisoned lock instead of cascading one handler's panic into a dead server. ## Web UI is one embedded file `web_ui/index.html` is the entire SPA (HTML+CSS+JS), `include_str!`-baked into the binary at compile time — editing it requires a rebuild. Served `Cache-Control: no-store` so binary upgrades don't strand stale tabs. Progress streams over `/ws/progress` (WebSocket) with an HTTP-poll fallback. ## Anti-bot subsystems `ytdlp_bin.rs` manages the optional self-contained venv at `~/.local/share/catacomb/` (nightly `yt-dlp[default]` + `curl_cffi` + bundled `deno`). `pot_provider.rs` runs `bgutil-pot` for Proof-of-Origin tokens — its yt-dlp plugin must come from the same release as the server binary. `error_class.rs` pattern-matches yt-dlp stderr into actionable classes (order matters in `classify()`: the captcha "Video unavailable" wall is RateLimited, not NotFound). ## Tests - Unit tests are inline `#[cfg(test)]` modules (parsers, resolvers, the error classifier, DB merge logic). - `tests/api.rs` spawns the **real** `--web` binary against a scratch dir and drives the HTTP API with curl — genuine end-to-end coverage of the axum + SQLite + config stack. `cargo test --release` runs both. (A `.forgejo/workflows/test.yml` CI definition exists, but Codeberg runs Woodpecker rather than Forgejo Actions, so it doesn't execute there without a self-hosted runner — run the suite locally.) ## Platform support Tray (`ksni`) and file dialogs (`rfd` xdg-portal) are Linux-only / no-GTK by design — that's why packaging avoids a GTK dependency. Those Linux-only pieces are target-gated: Windows builds use native file dialogs and a no-op tray stub, and the repo now ships a cross-compiled Windows zip. macOS has a local osxcross packaging path, but still needs on-hardware validation, codesigning/notarization, and a real tray backend before it is a normal release artifact.