Skip to content

Architecture

Twenty-five crates, layered so the core depends on no source and the terminal front end never reaches the engine.

anistream is twenty-five crates. A title moves through them from stable to volatile and back: metadata and the ID mapping are stable, the sources that find a stream are the volatile part, and the player, the history and the trackers are stable again.

How a title becomes an episode playing, and how it reaches your trackersMetadata comes from AniList, then Tenrai, then the cache (anistream-meta). The ID mapping gives the title an id for each source and each tracker (anistream-meta). The sources find a stream through the torrent transport, a self-hosted API or a plugin, in the order you set (anistream-providers); they are the one volatile layer. mpv plays it, over JSON IPC (anistream-player). Progress goes into the history, a SQLite database on your disk and the source of truth (anistream-store). Changes to push wait in the outbox, a queue per tracker that is kept while you are offline (anistream-store), and go to AniList, MyAnimeList, Kitsu, Simkl and Trakt (anistream-track), under the ids the ID mapping gives each tracker.STABLEMetadataAniList, then Tenrai,then the cacheanistream-metaSTABLEID mappingthe title’s id forsources and trackersanistream-metaVOLATILESourcestorrent, remote andplugins, in orderanistream-providersSTABLEPlayermpv, over JSON IPCanistream-playerOPT-INTrackersAniList, MAL, Kitsu,Simkl and Traktanistream-trackDURABLEOutboxa queue per tracker,kept while offlineanistream-storeSOURCE OF TRUTHHistorySQLite on your disk,no account neededanistream-storeprogresschanges to pusheach tracker’s idHow a title becomes an episode playing, and how it reaches your trackersMetadata comes from AniList, then Tenrai, then the cache (anistream-meta). The ID mapping gives the title an id for each source and each tracker (anistream-meta). The sources find a stream through the torrent transport, a self-hosted API or a plugin, in the order you set (anistream-providers); they are the one volatile layer. mpv plays it, over JSON IPC (anistream-player). Progress goes into the history, a SQLite database on your disk and the source of truth (anistream-store). Changes to push wait in the outbox, a queue per tracker that is kept while you are offline (anistream-store), and go to AniList, MyAnimeList, Kitsu, Simkl and Trakt (anistream-track), under the ids the ID mapping gives each tracker.STABLEanistream-metaMetadataAniList, then Tenrai, then the cacheSTABLEanistream-metaID mappingthe title’s id for sources and trackersVOLATILEanistream-providersSourcestorrent, remote and plugins, in orderSTABLEanistream-playerPlayermpv, over JSON IPCSOURCE OF TRUTHanistream-storeHistorySQLite on your disk, no account neededDURABLEanistream-storeOutboxa queue per tracker, kept while offlineOPT-INanistream-trackTrackersAniList, MAL, Kitsu, Simkl and Traktprogresschanges to pusheach tracker’s id
From a title to an episode playing, and from your history to your trackers. Each box names the crate that does the work; amber marks the one volatile layer.

anistream-core (types and traits) · -net (HTTP, with a client that matches a browser’s TLS handshake for hosts that answer differently by client, and rate limiting) · -meta (AniList, the metadata failover, ID mapping, filler) · -store (SQLite: the history and the sync outbox) · -providers (torrent, remote, mock) · -player (mpv IPC, the companion-script protocol, the bundled player files, track selection, aniskip) · -discord (Discord rich presence over its local socket) · -playback (the pure half of playback: what to record, skip and pick for tracks) · -mpv-host (libmpv behind the engine’s hosted player protocol, for the desktop apps) · -party (watch parties, a Syncplay protocol client) · -remote (a phone finds an anistream TV on the local network, pairs with it and drives it) · -cast (sending an episode to a TV with Chromecast built in, which the engine has and no app offers yet) · -i18n (the strings the terminal and Linux apps show, from the shared catalogue) · -track (AniList, MAL, Kitsu, Simkl, Trakt, sync) · -plugin (WASM host).

Sources change and disappear, so every volatile piece sits behind a trait and the core depends on none of them.

Local history is the source of truth, and sync is a projection of it. The SQLite history works with no account and no network, and trackers consume it through a durable outbox instead of being consulted for it.

The metadata, mapping, player, tracker and plugin layers use documented public APIs. Only the source layer chooses a content source, it is off by default, and every source is a config edit away from being removed. Two playback-side paths then fetch from whatever host a source named, sidecar subtitles and the HLS mender below, and both are source-agnostic.

How the crates are layered, and the boundaries CI checksanistream-core, the types and traits, depends on no other crate. The lower crates, net, store, meta, providers, track, player and plugin, sit on the core. anistream-model, the commands, events, rows and wording a front end shares with the engine, depends on the core only. anistream-engine depends on the model and the lower crates; its desktop feature holds mpv, watch parties, the browser sign-in and notifications. anistream-tui, the terminal front end, depends on the model and the core and never on the engine. The anistream binary connects the terminal front end to the engine. anistream-ffi carries the engine to the native apps through UniFFI: SwiftUI on Apple, Compose on Android and WinUI 3 on Windows. The Linux app, in Rust, links the engine itself. tools/check-boundaries.sh checks in CI that the engine, the model, anistream-ffi and every crate under them reach no terminal library, that anistream-tui never depends on anistream-engine, and that the engine and anistream-ffi built for iOS or Android without the desktop feature reach no anistream-player, open or keyring.anistreamthe binary: command line,event loop and self-updateanistream-ffithe engine for native frontends, through UniFFINative appsSwiftUI, Composeand WinUI 3anistream-tuithe terminal’s screens,keymap and theme,on ratatuianistream-engineplayback, sources andplugins, sync, downloads,plugin stores, background jobsDESKTOP FEATUREmpv, watch parties, thebrowser sign-in, notificationsneveranistream-modelcommands, events, rowsand wording; no I/OLOWER CRATESanistream-*netstoremetaproviderstrackplayerpluginanistream-coretypes and traits; depends on no other crateChecked in CI by tools/check-boundaries.sh, against the resolved dependency tree:1The engine, the model, anistream-ffi and every crate under them reach no terminal library,not even through a dependency.2anistream-tui depends on the model and the core, never on the engine.3Built for iOS or Android without desktop, the engine and anistream-ffi reach noanistream-player, open or keyring.How the crates are layered, and the boundaries CI checksanistream-core, the types and traits, depends on no other crate. The lower crates, net, store, meta, providers, track, player and plugin, sit on the core. anistream-model, the commands, events, rows and wording a front end shares with the engine, depends on the core only. anistream-engine depends on the model and the lower crates; its desktop feature holds mpv, watch parties, the browser sign-in and notifications. anistream-tui, the terminal front end, depends on the model and the core and never on the engine. The anistream binary connects the terminal front end to the engine. anistream-ffi carries the engine to the native apps through UniFFI: SwiftUI on Apple, Compose on Android and WinUI 3 on Windows. The Linux app, in Rust, links the engine itself. tools/check-boundaries.sh checks in CI that the engine, the model, anistream-ffi and every crate under them reach no terminal library, that anistream-tui never depends on anistream-engine, and that the engine and anistream-ffi built for iOS or Android without the desktop feature reach no anistream-player, open or keyring.Native appsSwiftUI, Compose and WinUI 3anistreamthe binary: thecommand line, eventloop, self-updateanistream-ffithe engine for nativefront ends, throughUniFFIanistream-tuiterminal screens,keymap and theme,on ratatuianistream-engineplayback, sources,sync, downloads,stores, jobsDESKTOPmpv, watch parties,browser sign-in,notificationsneveranistream-modelcommands, events,rows and wording;no I/OLOWER CRATESanistream-*netstoremetaproviderstrackplayerpluginanistream-coretypes and traits; depends onno other crateChecked in CI bytools/check-boundaries.sh, against theresolved dependency tree:1The engine, the model, anistream-ffi andevery crate under them reach no terminallibrary, not even through a dependency.2anistream-tui depends on the model and thecore, never on the engine.3Built for iOS or Android without desktop,the engine and anistream-ffi reach noanistream-player, open or keyring.
Arrows point from a crate to what it depends on. The four plugin store crates and the test kit are left out; they are described below.
  • anistream-model is the protocol between the engine and a front end: the commands a front end sends, the events it gets back, the rows they carry, and the wording they are shown in. It depends only on the core and does no I/O.
  • anistream-engine does the work behind the screens: playback and the player’s setup, sources and plugins, tracker sync, downloads, artwork, plugin stores, HLS mending, choosing the upscaling, and the background jobs, such as the daily update check.
  • anistream-tui is the terminal front end: the screens, the keymap and the theme, on ratatui.
  • anistream is the binary. It holds the command line, self-update, and the terminal’s event loop, which hands the front end’s commands to the engine and its events back.
  • anistream-ffi is the engine for the native apps: SwiftUI on macOS and iOS, Jetpack Compose on Android and Android TV, and WinUI 3 on Windows. It hands them the model’s own commands and events through UniFFI, and takes links, notifications and the OS’s secret store from the app. The Swift package that wraps it is in apple/. The Linux app, written in Rust, links the engine itself.
  • anistream-testkit is used only by tests: a fake AniList, fixture titles and golden files.

The engine and the model use no terminal library, not even through a dependency, so a front end that is not a terminal can run on them. The terminal front end depends on the model and the core, never on the engine. The engine’s desktop feature holds what only a computer can do: mpv, watch parties, the browser sign-in redirect and desktop notifications. A phone build leaves it out. tools/check-boundaries.sh checks all three rules against the resolved dependency tree in CI, the last for iOS and Android.

Four more crates make up plugin stores. anistream-plugin-store is the store client and the reference server. anistream-store-core holds the store’s protocol, rules, storage traits and admin page, and builds natively and to WebAssembly. anistream-store-server is the anistream store … commands and the standalone anistream-store binary. anistream-store-wasm is the store for Cloudflare Workers and Vercel or Netlify functions. Despite its name, anistream-store is the local SQLite history and has nothing to do with them.

The mender sits on the default playback path: HLS goes through a loopback proxy that looks for where the media starts in each segment, since some hosts serve video behind an image header that mpv reads literally and refuses. A healthy segment matches at offset zero and passes through byte-for-byte, and nothing is decrypted. See crates/anistream-engine/src/playback/mend/.

cargo test --workspace # no network
cargo clippy --workspace --all-targets
cargo test -p anistream-tui --test goldens # every screen against its saved golden
cargo run -p anistream-tui --example palette_report # the palette's contrast margins
cargo run -p anistream --example player_preview -- path/to/video.mp4 # the player, no source
cargo run -p anistream-plugin --example plugin_bench --release # what a plugin call costs

There are also live probes. They need real services and are not part of cargo test:

cargo run -p anistream-providers --example stream_probe # torrent path through the VPN
cargo run -p anistream-engine --example playback_probe # torrent → mpv → history
cargo run -p anistream-engine --example sync_probe -- --write # AniList push, then undo
cargo run -p anistream-engine --example mal_probe -- --write # MAL push, then undo
cargo run -p anistream-engine --example simkl_probe -- --write # Simkl push, then undo
cargo run -p anistream-engine --example kitsu_probe -- --write # Kitsu push, then undo
cargo run -p anistream-engine --example mend_probe -- <url> # HLS behind an image header → mpv
cargo run -p anistream-providers --example torrent_probe # an indexer, end to end
cargo run -p anistream-providers --example halt_probe # the guard pausing torrents
cargo run -p anistream-meta --example filler_probe # filler parsing
cargo run -p anistream-meta --example episode_meta_probe # episode titles, stills, numbering
cargo run -p anistream-meta --example anilist_probe # the AniList client
cargo run -p anistream-meta --example dataset_probe # ID-mapping datasets
cargo run -p anistream-engine --example meta_probe # the metadata failover
cargo run -p anistream-engine --example episodes_probe # every stage from title to stream
cargo run -p anistream-engine --example continue_probe # resume vs. sync thresholds