A desktop app for browsing and watching anime.
ani-gui is a Rust + SvelteKit desktop application for browsing and watching anime — discovery, search, an embedded player, downloads, persistent watch history, Picture-in-Picture, and OP/ED skip. It began as a graphical front end over pystardust/ani-cli and resolves streams itself now, in Rust.
See docs/architecture.md for the full picture.
| Discovery | Trending and Top Rated rails, a rotating hero — AniList + Kitsu. |
| Search | Full-text against Kitsu, instant as you type. |
| Detail page | Synopsis, episodes with thumbnails, similar-titles strip. |
| Embedded player | HLS / MP4, quality switch, native or custom controls — no mpv window. |
| Subtitles | Upstream .vtt via <track kind="subtitles">. |
| OP / ED skip | aniskip intervals — one-click or fully automatic. |
| Picture-in-Picture | Persists across navigation. |
| Background prefetch | Adjacent episodes warm in advance. |
| Downloads | Per-episode or ranged, progress dock. yt-dlp bundled; ffmpeg sourced per platform (apt Recommends: on .deb, installer-time fetch on Windows, system PATH on AppImage). |
| Watch history | Continue Watching picks up where you left off. Remove a single card or clear the lot from the rail. |
| External player | One click to mpv / VLC / IINA / custom. |
| Watch together | Hand the current stream to Syncplay for a watch party. |
| Trackers | Connect AniList or MyAnimeList — a Watch Later rail on the home page, and your progress synced back automatically as you watch. |
| Localised | English, Brazilian Portuguese, Latin American Spanish, Russian. |
| No telemetry | No analytics or tracking. Outbound traffic is metadata, the stream you picked, a launch-time check of the app's own GitHub releases, and — with an account connected — your tracker's list + progress sync. Localhost-only listener on a kernel-assigned port. See the privacy policy. |
ani-gui is distributed as a desktop bundle. The transport it uses to reach the provider ships inside it, so browsing and playback need nothing else. Downloads additionally want ffmpeg, which each platform sources differently — see the tier notes below.
Platform support tiers:
| Tier | Platform | Status |
|---|---|---|
| 1 | Linux | Actively tested on Ubuntu. Other distros work via AppImage. |
| 2 | Windows | Most features verified end-to-end. Edge cases may surface. |
| — | macOS | Not packaged. No installer is built or shipped; the dev loop runs from source. |
Linux — tier 1 (tested on Ubuntu)
- AppImage — download from the releases page,
chmod +x, double-click. Installffmpegfrom your distro if you want downloads: the AppImage carries yt-dlp but not ffmpeg, and yt-dlp hands off to it whenever a stream arrives as MPEG-TS and has to be repackaged as MP4. Playback itself needs neither. The bundle launches with Chromium's setuid sandbox disabled (AppImage's read-only FUSE mount can't carry the SUID bitchrome-sandboxrequires); the localhost-only architecture means the sandbox isn't load-bearing for the threat model. If you'd rather keep the sandbox, install the.debinstead. - Debian / Ubuntu (
.deb) —sudo apt install ./ani-gui_<version>_amd64.deb. apt pulls in the recommendedffmpegpackage (needed for the download feature) along the way; the post-install script sets thechrome-sandboxSUID bit Electron needs, so the sandbox stays on.sudo dpkg -i …still works but won't auto-install ffmpeg — drop intoapt --fix-broken installor runsudo apt install ffmpegseparately if you used dpkg directly.
Windows — tier 2 (most functions tested)
NSIS installer (.exe). Run it; it installs per-user by default and creates Start menu and desktop shortcuts.
The installer will fetch ffmpeg automatically the first time it runs (~80 MB) so the download feature works out of the box; the impersonating transport and yt-dlp are bundled directly. The ffmpeg fetch runs even when you already have ffmpeg installed via a per-user package manager (scoop, winget user-scope) — the installer's elevated context doesn't see per-user PATH entries, and the bundled copy is what the app uses at runtime in either case.
Tested on Linux. On macOS the dev loop (steps 5–6) launches and browses metadata, but playback needs an impersonating transport and no fetcher stages one there. It does not currently run on Windows: pnpm executes package scripts through cmd.exe there, and the Electron dev script sets environment variables with a POSIX prefix — docs/deferred-work.md tracks making it shell-independent. The packaging scripts (step 7) build per-host artifacts and are the verified Windows flow — run on x86_64 Linux for .AppImage / .deb, on x64 Windows for the NSIS installer. There is no macOS packaging target yet.
- Install Rust (toolchain pinned by
rust-toolchain.toml):curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh . "$HOME/.cargo/env" # (or re-open the shell) so `cargo` is on PATH
- Install Node 20+ and pnpm (via nvm — skip the curl step if you already have nvm or installed Node another way):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash # re-open the shell (or `source ~/.bashrc`) so nvm is on PATH nvm install 20 && nvm use 20 corepack enable && corepack prepare pnpm@latest --activate
- System build deps (Linux):
sudo apt install -y build-essential libssl-dev pkg-config
- Clone and install JS deps:
git clone https://github.com/JoaoPucci/ani-gui.git && cd ani-gui (cd frontend && pnpm install) (cd electron && pnpm install)
- Build the backend binary (required before the first run, and after every Rust change). On x86_64 Linux, also stage the bundled tools next to it once per checkout — playback needs the impersonating transport. The fetcher downloads x86_64 Linux builds (the architecture every package ships for), so skip that step on any other host — the staged directory outranks PATH, and incompatible binaries staged there would shadow any transport you do have:
cd backend && cargo build --bin ani-gui-backend (cd ../electron && pnpm run fetch:linux-deps) # x86_64 Linux only
- Run the dev app — two terminals, started in this order:
# Terminal A — Vite dev server, HMR on :5173 cd frontend && pnpm dev # Terminal B — Electron shell, spawns the backend binary from step 5. # On Windows this also stages the bundled tools (impersonating # transport, yt-dlp) next to the backend binary, so playback works. cd electron && pnpm dev
- Build a distributable bundle:
cd electron # Linux host pnpm package # .AppImage — fast iteration pnpm package:release # .AppImage + .deb # Windows host (any shell — PowerShell, cmd, Git Bash; Rust + Node + pnpm # installed natively. `fetch:win-deps` needs `bsdtar`, which Windows 10+ # ships as `tar.exe`) pnpm package:win # NSIS installer
For lints, git hooks, and the bash test toolchain see docs/development.md.
On first launch the app:
- Spawns the Rust sidecar on a kernel-assigned localhost port (no fixed port, no internet-reachable service).
- Loads the discovery surface.
After that, click anything that looks clickable. The app routes the click through Kitsu / AniList for metadata, its own resolver for the stream, and the embedded player for playback.
Upgrading from a version before 0.12 adds one step: those releases kept their own copy of the ani-cli script under your cache directory and refreshed it at every launch. Nothing reads it now, so the first launch after upgrading deletes it and says so on the /diagnostics page.
Connecting a list provider is optional — the app works fully without an account. From the Account page you can connect AniList or MyAnimeList (OAuth in your browser; the token is stored with your OS keychain via Electron's safeStorage, never in plaintext).
Once connected:
- Watch Later rail — your Plan-to-Watch list surfaces as a rail on the home page, bridged to local cards you can play in one click.
- Automatic progress sync — as you watch, the episode is pushed back to the tracker. The sync only ever moves progress forward (replaying or stepping back never lowers your count), promotes a Plan-to-Watch title to Watching on first play, preserves a Rewatching row, and marks a series Completed when you start the last episode of a finished show.
Everything stays on your machine: your OAuth token is encrypted through your OS keychain and written to the app's user-data directory (the Rust backend never persists it — each request carries its own bearer), and your tracker list is cached in a local SQLite database to render the Watch Later rail. ani-gui runs no server of its own. See the privacy policy for exactly what's sent where.
User settings live in $XDG_CONFIG_HOME/ani-gui/config.toml. The Settings page exposes everything you'd normally edit:
- audio mode (
sub/dub) and quality (best,1080,720,480,worst) - UI locale
- external-player kind, command, and custom arguments
- Syncplay binary path
- image-cache size cap
- auto-play next episode
- auto-skip OP / ED
- custom-vs-native player controls
- whether to enter PiP automatically when you navigate away from a playing video
- the download progress bar
- whether the update check includes pre-releases
- remembering resolved streams for quicker replays (off by default)
Full table with defaults and effects is in docs/architecture.md.
A two-line summary: a Rust sidecar embedded inside an Electron shell speaks to Kitsu / AniList / aniskip and resolves streams from the provider itself. A streaming proxy in the sidecar adds the right Referer: headers and rewrites HLS playlists so the embedded <video> element can play upstream content without CORS or referer issues. SQLite caches metadata; the filesystem caches images.
For the long version — diagrams, cache TTLs, the title-resolution bridge, the PiP architecture — see docs/architecture.md, docs/title-resolution.md, and the rest of docs/.
See docs/development.md.
ani-gui only exists because of the projects it builds on:
- pystardust/ani-cli — the Bash scraper this project grew out of, and the source of the provider pipeline the Rust resolver reimplements.
- Kitsu and AniList for the metadata, posters, and trending data behind the discovery surface.
- aniskip for the community-submitted OP/ED intervals.
- hls.js for the HLS playback inside the embedded player.
ani-gui is a tool. Like any tool, the responsibility for how it's used lies with the user. The app makes no claim on the content it surfaces — it hosts nothing, talks to the same providers you'd reach in a browser, and routes their output through your machine.

