b-baka 0.1.1

BitTorrent Acquisition & Keyword Aggregator, a terminal torrent search and download tool
# BAKA Roadmap

BitTorrent Acquisition & Keyword Aggregator.

Goal: a single Rust binary that matches or beats [torlink](https://github.com/baairon/torlink)
in functionality, installs in one command on Windows, and stays small enough to read in an
afternoon.

Status: phases 0 to 6 are done. `baka` with no arguments is the whole product: search,
downloads, seeding and settings in one terminal interface, across every source torlink
has, and the same binary runs headless on a box with no terminal at all. Pushing a `v`
tag builds it for both Windows architectures and publishes it. Phase 7, 1.0, is next.

## Decisions already made

| Question | Answer | Why |
| --- | --- | --- |
| Torrent engine | Embed [`librqbit`]https://crates.io/crates/librqbit | Only mature pure-Rust engine with DHT, uTP, resume and seeding. Writing our own delays parity by months. |
| Interface | TUI first, CLI subcommands alongside | Matches torlink's shape and keeps headless and scripted use possible. |
| Settings | One Settings page, one TOML file, no environment variables | Every knob is in one place you can find without reading docs. |
| Sources | Same curated list as torlink | Parity out of the box, no setup for the user. |
| Platforms for 1.0 | Windows via winget, Scoop and Chocolatey, plus `cargo install b-baka` everywhere | Windows is the primary target. `cargo install` covers Linux and macOS for free. |
| Layout | One crate, several modules | Five crates for a tool this size is overhead, not structure. Split only when compile times justify it. |

## Settings model

torlink spreads its behaviour across env vars, one-off keybinds and flags. BAKA does not.

- One file: `config.toml` in the platform config directory.
- One editor: the Settings page inside the TUI, which reads and writes that file.
- Every persistent setting appears on that page. If a knob exists, it is listed there.
- CLI flags exist only for one-shot overrides of a single run. They never write config.
- No environment variables. Not for paths, not for ports, not for anything.
- A missing or partial config file is never an error. Defaults fill the gaps.

The Settings page is not a phase 7 nicety. It shipped with the TUI in phase 3, because a
setting that has no home ends up as an env var, and that is the thing being avoided.

## Parity checklist

Everything torlink does, tracked to the phase that delivers it.

| Feature | Phase |
| --- | --- |
| Keyword search across curated sources | 1 |
| Magnet link input | 2 |
| Bare infohash input | 2 |
| `.torrent` file path input | 2 |
| Empty search browses a curated library | 4 |
| Background downloads while searching continues | 3 |
| Progress, speed and ETA per download | 3 |
| Settings page covering every persistent option | 3 |
| Per-download folder override | 3 |
| Interrupted downloads resume on next start | 2 |
| Auto seed on completion, pausable from a Seeding tab | 3 |
| Keyboard help overlay | 3 |
| `watch <dir>` intake | 5 |
| `serve` HTTP magnet intake | 5 |
| `files` HTTP serving of finished downloads | 5 |
| Background daemon | 5 |
| Attach a TUI to a running daemon | 7 |

Known gap: torlink uses WebTorrent and therefore reaches WebRTC browser peers.
librqbit speaks TCP and uTP only. This affects browser-seeded swarms, not normal ones.
Not planned for 1.0. Revisit only if real swarms turn out to need it.

## Phase 0: foundations (done)

Target was `baka --version`. What shipped:

- Edition 2024, MSRV pinned and checked in CI. It started at 1.85, the edition floor, and
  moved to 1.88 in phase 2 for the reason recorded there.
- Modules stubbed with the rule that governs each one: `search`, `engine`, `tui`, `server`.
- `config.rs` holds the whole settings model: one struct per group, `Default` impls, and
  load and save to TOML under the platform config dir via `directories`.
- `Settings::fields()` returns every setting with its group, label, description and a
  mutable handle to the value. The phase 3 page renders and edits that list, so a field
  missing from it cannot be reached by a user.
- Missing files, partial files and keys from a newer version all load without an error.
- `baka` prints the art from `stuff/` plus version and settings path, and `baka settings`
  lists every setting as text. Phase 3 replaced both with the interface. `baka settings
  --path` still prints the path alone.
- CI on GitHub Actions: `fmt`, `clippy --all-targets -D warnings`, `test` on Windows and
  Linux, an MSRV job, and `scripts/no-em-dashes.sh`.

## Phase 1: search (done)

Target was `baka search "query"`. What shipped:

- The `Indexer` trait is `url()` plus `parse()`, not one async call. Sources describe a
  request and parse a body, and the requests, timeouts, concurrency and ranking live in
  `search.rs` once. That is what makes every scraper test offline.
- `Torrent`: title, size, seeders, leechers, category, source, infohash and magnet.
- YTS and Nyaa, queried concurrently, each under its own timeout. A source that dies is
  reported by name and the rest of the search still returns.
- Ranking: a full title match first, seeders second. Deduplicated by infohash, keeping
  the best seeded copy.
- `baka search "query" --category anime` limits which sources are asked.
- Fixtures are real saved responses in `src/search/fixtures/`. CI never hits the network.

Two things found while building:

- `yts.mx` does not resolve on every network, and the YTS API itself now points callers at
  `movies-api.accel.li`. That is the base URL BAKA uses.
- YTS reports 0 seeds for most of its catalogue, so `min_seeders` defaults to 0. A floor of
  1 silently hides nearly all of one source.

Phase 4 note: a source that needs a second request per result, 1337x being the obvious one,
does not fit `url()` plus `parse()`. Extend the trait when that source lands, not before.
It landed in phase 4, and the trait needed no second call: `parse` says which page a
magnet is on and the search loop fetches it.

## Phase 2: engine (done)

Target was `baka get <magnet|infohash|path>`. What shipped:

- `Engine` wraps a librqbit `Session`. Everything above it speaks `Input`, `Progress` and
  `State`, so no librqbit type appears outside `engine.rs`.
- `Input::parse` takes a magnet link, a 40 character hex infohash, a 32 character base32
  infohash, or the path to a `.torrent` file. A bare infohash becomes a magnet carrying the
  same public trackers a search result gets, so it can find peers without DHT.
- Session state lives in the platform local data directory, not beside `config.toml`.
  It is resume data, and deleting it costs progress but never settings.
- Adding a torrent sets `overwrite`, without which librqbit refuses to touch files it did
  not create, and a resumed or finished torrent can then neither continue nor seed.
- `Progress` is a poll, not a subscription. Nothing above the engine holds a channel open.
- `baka get` prints one status line in place, resumes what the last run left behind, seeds
  when the setting says to, and treats Ctrl+C as stop rather than kill at every stage.
- Verified end to end against a real 755 MiB Debian torrent: download, completion, seeding,
  restart, recheck, resume.

Two things found while building:

- MSRV moved from 1.85 to 1.88. librqbit 9 uses let chains, which stabilised in 1.88, so
  1.85 cannot build the dependency tree at all.
- Adding a magnet blocks until peers hand over the file list, and that step can outlast the
  download itself. It gets its own line rather than an empty prompt.

Left for phase 3: maximum concurrent downloads, maximum concurrent seeds and stop at ratio.
All three need a queue watching every torrent, which is phase 3 work because that is where
the list of torrents becomes something a user sees. That queue landed there, and `baka get`
runs it too.

## Phase 3: TUI and settings (done)

Target was `baka` with no arguments being the whole product. What shipped:

- `ratatui` 0.30 with its crossterm backend. Four tabs: Search, Downloads, Seeding,
  Settings. A key thread and a one second ticker feed one event loop, so a search, a
  magnet waiting on peers and the interface never wait on each other.
- Search results, downloads and seeds are three views of state the loop refreshes. A
  search runs in a spawned task and arrives as an event, so typing never stalls.
- A magnet link, a bare infohash or a file path typed into the search box is downloaded
  rather than searched. That is what pasting one into a search box is asking for.
- Downloads tab: progress bar, size, speed, time left, peers and state. Seeding tab:
  ratio, shared bytes, upload speed, peers and state.
- A queue in the engine enforces maximum concurrent downloads, maximum concurrent seeds
  and stop at ratio. It is a pure function over a list of torrents, so every rule in it
  is tested without a session. `baka get` runs the same queue.
- Settings page over `Settings::fields()`, so a field that is not on the page cannot
  exist. Arrow keys change a value, `Enter` types one, and a value that does not parse
  says why and keeps the editor open. Changes apply live where the session allows it and
  save when the row is left. A setting that cannot apply live says so on its own row.
- The Search group lists one row per source, built from the indexer registry rather than
  written out by hand, which is what phase 4 needs it to do.
- `baka settings` opens that page on its own and starts no session, so it can be used on
  a machine already running BAKA. `baka settings --path` still prints the path alone.
- Every key the README promises: `/`, `Enter`, `Tab`, `j` `k`, `d`, `D`, `p`, `x`, `c`,
  `s`, `Esc`, `?`, `q`. `?` opens the list of them. One `Esc` closes whatever is open,
  and a second one within the double tap window puts the interface back at its start.
- Branding lands on the empty Search tab: the art, the BAKA wordmark, the name expansion
  and an accent colour that follows the setting. Nothing is playful anywhere else.

Four things found while building:

- librqbit treats pausing an already paused torrent as an error, so the queue compares
  against the current state instead of issuing orders and hoping. The same check makes
  `enforce` safe to call every second.
- A queue has to tell a torrent the user paused apart from one the queue paused, or
  quitting with something paused hands it back running on the next start. That is what
  the wish per torrent is, and it is why `queued` is a state a user can see.
- The session reads its default output folder once at startup, so the folder is named on
  every add instead. That is what turns the download folder into a live setting.
- The clipboard needs a dependency. `arboard` without its image feature is the whole of
  it, and OSC 52 was passed over because it fails silently on the consoles that do not
  support it, which looks like a broken key.

## Phase 4: full source list (done)

Target was source parity with torlink. What shipped:

- Eight sources, one file and one fixture each: YTS, The Pirate Bay, 1337x and
  BitTorrented for films, EZTV, The Pirate Bay, 1337x and BitTorrented for shows, Nyaa
  and SubsPlease for anime, FitGirl for games.
- A source is asked an `Ask`: the words typed, or the shelf to list. An empty search
  box is a browse, and a browse is per category, so a source that serves two of them is
  asked for both listings. Nothing else changed shape, so `baka search` with no query
  browses too.
- A browse shows a share of every category in turn rather than sorting the lot by
  seeders, which would hand the whole screen to whichever category has the biggest
  swarms and bury the other three.
- `Indexer` grew from `url()` to `urls()`, which answers with every mirror worth
  trying and with nothing at all when a source has no listing or will not take a query
  that short. That covers 1337x being reachable on one host and blocked on the next,
  and BitTorrented sitting out a browse, without a second mechanism for either.
- The 1337x problem phase 1 predicted turned out to need no trait change. `parse`
  answers `Found::OnPage` for a result whose magnet is on a page of its own, and the
  search loop fetches the best seeded ten of those and reads the first magnet link out
  of each. Reading a magnet out of a page is the same job for every site, so it lives
  in the loop rather than in the source.
- Infohashes are normalised to hex, base32 magnets included, so the same release from
  Nyaa and from SubsPlease collapses into one result instead of two.
- Results carry the shelf they came from, in the TUI and in `baka search`, which is
  what makes a merged browse readable.

Four things found while building:

- 1337x is behind a Cloudflare challenge on `1337x.to` from some networks, including
  the one this was written on, and answers normally on `www.1377x.to`. That is why
  `urls()` returns a list rather than one address. No client without a browser engine
  can pass that challenge, so a host list is the whole of the answer available.
- apibay writes its numbers as strings in a search and as numbers in its top 100 lists.
  Both shapes have to parse or the browse and the search cannot share a scraper.
- 1337x answers a phrase with anything carrying one of its words, so its rows are
  filtered against the query before the second request is made rather than after. The
  request saved is the point: a junk row costs a whole page fetch.
- EZTV has no keyword search at all, only a feed of new releases. Rather than sit out
  every search, it keeps the entries in that feed that match what was typed, which is
  the honest version of what the API can do.

## Phase 5: headless (done)

Target was BAKA being useful on a server with no terminal attached. What shipped:

- `baka watch [dir]` picks up magnet links, bare infohashes and `.torrent` files. A
  handled file is renamed to `.taken`, or to `.failed` when there was nothing in it,
  so the next look leaves it alone and you can see which was which.
- `baka serve`: `POST` a magnet, an infohash or a file path, one per line. `GET` says
  what is running.
- `baka files`: the download folder over HTTP, with directory listings and range
  requests, so a browser or a player can open a finished film in place.
- `--daemon` on any of the three. It starts this same command without the flag,
  detached and writing nowhere. On Windows that is `DETACHED_PROCESS` plus its own
  process group; on Unix it is its own process group. No service, no named pipe.
- All three run the phase 3 queue on a one second tick, so the concurrency limits and
  stop at ratio mean the same thing with nobody watching, and Ctrl+C stops rather than
  kills.
- A Server group on the Settings page: bind address, both ports and the watch folder.
  The bind address defaults to loopback, because the magnet intake downloads whatever
  it is handed and reaching it from the rest of the network should be a decision.
- The HTTP is written here rather than pulled in. It is a request line, the headers
  worth reading, a body, and a response, and it fits in one file with its tests.

Three things found while building:

- Adding a magnet blocks until peers hand over the file list, which phase 2 already
  knew. It matters more here: a watched folder with ten things in it worked through
  them one at a time, and an HTTP client sat on an open request for the whole wait. All
  three modes now hand the add to a task and answer with what they understood.
- A file still being copied into the watched folder is not ready to be read. How long
  ago it was written is the only signal available without watching the filesystem
  itself, so a file has to have been still for one look before it is taken.
- Path safety needs both halves. Refusing `..` in a request stops the obvious climb,
  and comparing the real path both sides resolve to stops a link inside the folder
  pointing out of it.

Left as it is: one BAKA at a time. The interface, `baka get` and the headless modes all
open the same session, which has been true since phase 2. `baka attach` in phase 7 is
what makes a second one useful rather than a conflict.

## Phase 6: packaging (done)

Target was installing in one command. What shipped:

- `.github/workflows/release.yml` on a `v*` tag. It builds `x86_64-pc-windows-msvc` and
  `aarch64-pc-windows-msvc`, zips each with the README and the licence, and attaches both
  archives and `SHA256SUMS.txt` to a GitHub release.
- The tag is compared against the version in `Cargo.toml` before anything is built. A
  mismatch is the one release mistake that cannot be taken back, because crates.io does
  not let a version be republished.
- winget manifests in `packaging/winget/`, submitted to `microsoft/winget-pkgs` by
  `wingetcreate` on the same tag. `InstallerType: zip` with a nested portable, so winget
  puts `baka` on `PATH` itself.
- The Scoop bucket is this repository. The release job writes `bucket/baka.json` on
  `main`, and `scoop bucket add baka https://github.com/4G0NYY/BAKA` is what points Scoop
  at it.
- `cargo publish` on the same tag, which is what `cargo install b-baka` reads.
- `scripts/manifests.sh` fills the version and the two checksums into both manifest sets
  and refuses to write a file with a placeholder left in it. CI renders them on every
  pull request, so a template that no longer matches its script fails then rather than
  during a release.
- Every route installs the executable and nothing else. Unzipping a built archive and
  asking the binary where its settings are answers `%APPDATA%\baka\config.toml`, which
  is not a path any of the four uninstalls can reach.
- `CONTRIBUTING.md` has the checks to run, the three steps before a tag, what each job in
  the release does, and which secret each one needs.

Four things found while building:

- This phase was written as archives plus an installer. A portable executable does not
  need one. winget's zip and nested portable gives `PATH` integration and an uninstall
  that removes what it extracted, and an MSI would have added a toolchain to CI and a
  second uninstall path aimed at the only thing worth protecting.
- A Scoop bucket does not need a repository of its own. Any repository with a `bucket`
  directory is one, so it lives here: one release job, and no second repository to keep
  in step with this one.
- aarch64 compiles the whole tree, `aws-lc-sys` included, and then needs the ARM64 MSVC
  linker to finish. Without it `rustc` falls back to whatever `link.exe` is on `PATH`,
  which on a machine with Git for Windows is coreutils, and the error it prints has
  nothing to do with the code. The runner image carries the ARM64 tools, so that leg is
  exercised in CI rather than locally.
- A missing secret skips its job instead of failing the release. A release that cannot
  reach crates.io should still put archives on GitHub.

Left open, as planned: Chocolatey, which waits for the winget package to be live, and the
first winget submission itself, which a person at Microsoft reviews.

## Phase 7: 1.0

Target: it stays out of the way.

- `baka attach`: connect a TUI to a running daemon and survive an SSH drop.
- Chocolatey package, once the winget one is live.
- Config migration: a file written by an older version loads without losing settings.
- Search result caching so a repeated query is instant.
- Polish pass: startup time, memory under load, terminal resize, narrow terminals.

## Non-goals

- Hosting, indexing or mirroring any content. BAKA queries public sites and speaks
  BitTorrent. Nothing else.
- Settings anywhere except the Settings page and the file behind it. No env vars, no
  hidden keybinds that quietly persist state, no second config format.
- A web UI. librqbit ships one, and if a browser is wanted, rqbit is already the answer.
- A plugin system or a scripting language. Adding an indexer means adding a file.
- Configuration for its own sake. Every option must earn its place on the page.