# unarr > **⚠️ Active development** — unarr is under active development. Features may change, and bugs are expected. [Report issues here](https://github.com/Unarr-app/unarr-cli/issues). [![CI](https://github.com/Unarr-app/unarr-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Unarr-app/unarr-cli/actions/workflows/ci.yml) [![Latest Release](https://img.shields.io/github/v/release/Unarr-app/unarr-cli)](https://github.com/Unarr-app/unarr-cli/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/Unarr-app/unarr-cli)](https://goreportcard.com/report/github.com/Unarr-app/unarr-cli) [![Coverage](https://img.shields.io/codecov/c/github/Unarr-app/unarr-cli)](https://codecov.io/gh/Unarr-app/unarr-cli) [![VirusTotal](https://img.shields.io/badge/VirusTotal-scanned-brightgreen?logo=virustotal)](https://github.com/Unarr-app/unarr-cli/releases) [![Docker Pulls](https://img.shields.io/docker/pulls/unarr/cli)](https://hub.docker.com/r/unarr/cli) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Go Version](https://img.shields.io/github/go-mod/go-version/Unarr-app/unarr-cli)](go.mod) The single-binary terminal client for torrent, debrid, and usenet downloads. **Free and open source.** Built-in torrent engine, debrid (Real-Debrid / AllDebrid), and NZB support. Stream to mpv/vlc, transcode on the fly with hardware acceleration, and manage your library — one binary or a headless daemon with WireGuard split-tunnel and Cloudflare Funnel remote access. ## Installation ### Quick install (Linux/macOS) ```bash curl -fsSL https://unarr.app/install.sh | sh ``` ### PowerShell (Windows) ```powershell irm https://unarr.app/install.ps1 | iex ``` ### Homebrew (macOS/Linux) — coming soon ```bash brew install unarr-app/tap/unarr ``` ### Debian/Ubuntu (.deb) and Fedora/RHEL (.rpm) Published on every release for `amd64` and `arm64`, and covered by the same signed `checksums.txt` as the tarballs: ```bash # Debian/Ubuntu — replace and (amd64 | arm64) curl -fsSLO https://github.com/Unarr-app/unarr-cli/releases/latest/download/unarr__linux_.deb sudo apt install ./unarr__linux_.deb # Fedora/RHEL sudo dnf install https://github.com/Unarr-app/unarr-cli/releases/latest/download/unarr__linux_.rpm ``` The package installs the `unarr` binary only. Run `unarr init` afterwards to link your account, pick a download directory and install the background service — the same step the other install methods end with. `ffmpeg` is a recommended dependency; without it the agent downloads its own static copy. ### Docker ```bash docker run -d --name unarr \ --restart unless-stopped \ --network host \ --read-only --memory 512m \ -v ~/.config/unarr:/config \ -v ~/Media:/downloads \ unarr/cli ``` Run setup first to configure your API key: ```bash docker run -it --rm \ -v ~/.config/unarr:/config \ unarr/cli setup ``` ### Docker Compose ```bash mkdir -p unarr && cd unarr curl -fsSL https://raw.githubusercontent.com/Unarr-app/unarr-cli/main/docker-compose.yml -o docker-compose.yml docker compose up -d ```
docker-compose.yml ```yaml services: unarr: image: unarr/cli:latest container_name: unarr restart: unless-stopped user: "1000:1000" read_only: true tmpfs: - /tmp:size=64m,mode=1777 volumes: - ./config:/config - ~/Media:/downloads - unarr-data:/data environment: - TZ=${TZ:-UTC} # - UNARR_API_KEY=tc_your_key_here deploy: resources: limits: memory: 512M cpus: "2.0" # Host network for full P2P performance network_mode: host # Or use bridge with ports: # ports: # - "6881-6889:6881-6889/tcp" # - "6881-6889:6881-6889/udp" volumes: unarr-data: ```
### Go install ```bash go install github.com/Unarr-app/unarr-cli/cmd/unarr@latest ``` ### GitHub Releases Download prebuilt binaries for Linux, macOS, and Windows from [GitHub Releases](https://github.com/Unarr-app/unarr-cli/releases). ### Build from source ```bash git clone https://github.com/Unarr-app/unarr-cli.git cd unarr-cli make build ``` ## Quick Start ```bash # 1. Run the init wizard (opens browser for API key) unarr init # 2. Search for content unarr search "breaking bad" --type show --quality 1080p # 3. Start the download daemon unarr start ``` ## Commands ### Getting Started | Command | Description | |---------|-------------| | `unarr init` | First-time configuration wizard (API key, download dir, daemon) | | `unarr login` | Authenticate with your account (opens browser) | | `unarr config` | Edit all settings interactively (speed, organization, etc.) | | `unarr config check` | Validate `config.toml` — unknown keys (with suggestions) and out-of-range values; exits non-zero when anything is reported, or with the daemon's own error when the file does not load at all (invalid TOML, a value of the wrong type) | | `unarr config mount` | Configure the optional local mount; provider accounts and credentials are managed on the web | | `unarr migrate` | Import settings and wanted list from Sonarr/Radarr/Prowlarr [pre-beta] | ### Search & Discovery | Command | Description | |---------|-------------| | `unarr search ` | Search for movies and TV shows with advanced filters | | `unarr inspect ` | TrueSpec analysis — quality, codec, seed health | | `unarr popular` | Show popular movies and TV shows | | `unarr recent` | Show recently added content | | `unarr watch ` | Find where to watch — streaming + torrents | ### Downloads & Streaming | Command | Description | |---------|-------------| | `unarr download ` | One-shot download (no daemon needed) | | `unarr downloads` | List the download queue (alias: `unarr dl`) | | `unarr downloads pause\|resume ` | Pause a download / continue it | | `unarr downloads stop ` | Cancel it and stop it coming back after a restart | | `unarr downloads retry ` | Start it over | | `unarr downloads purge` | Forget queued downloads that are not running | | `unarr stream ` | Stream a torrent directly to mpv/vlc/browser | | `unarr mount ` | Configure and activate the persistent read-only remote folder (directory is optional) | | `unarr umount` | Disable and unmount the remote folder (`unarr unmount` is an alias) | | `unarr mount serve` | Serve that remote library over loopback WebDAV without mounting it | See [Controlling downloads](#controlling-downloads) for the flags, the offline recovery path, and how these interact with the website. ### Library | Command | Description | |---------|-------------| | `unarr scan ` | Scan a folder, analyze video files with ffprobe, sync quality data | | `unarr library clean` | Sweep download & library dirs for orphaned files (dry-run by default) | | `unarr library stats` | Report library health, composition & quality — read-only (`--json` for scripts) | | `unarr organize preview` | Show the folders/file names new downloads get with your naming layout (or try `--naming plex`, `--series-format …`) — touches nothing | ### Daemon Management | Command | Description | |---------|-------------| | `unarr start` | Start the download daemon (foreground) | | `unarr stop` | How to stop the running daemon | | `unarr status` | Show daemon status and active downloads (`--sessions` for live sessions, `--json` for raw) | | `unarr daemon install` | Install as system service (systemd/launchd) | | `unarr daemon uninstall` | Remove the system service | | `unarr vpn status` | Show managed-VPN config and live tunnel state | | `unarr vpn enable` | Turn the managed VPN on | | `unarr vpn disable` | Turn the managed VPN off | ### System & Diagnostics | Command | Description | |---------|-------------| | `unarr stats` | Show catalog statistics | | `unarr doctor` | Diagnose configuration and connectivity | | `unarr report` | Prepare and review private technical events; save locally or separately consent to upload (alias: `unarr reports`). See [privacy and delivery availability](REPORTS.md). | | `unarr mirrors` | Manage mirror failover list (list / update / test) | | `unarr logs` | Read the daemon log — `-f`, `--since`, `--level`, `--grep`, `-n` (alias: `unarr daemon logs`) | | `unarr clean` | Remove temporary files, logs, and cached data | | `unarr backup` | Back up / restore config and the download queue (create / list / restore / prune). See [Backup](#backup) | | `unarr upgrade` | Update unarr to the latest version (alias: `unarr self-update`) | | `unarr version` | Show version info | | `unarr completion ` | Generate shell completion scripts | --- ## Search Search the catalog with advanced filters. Results include quality scores, seed health, and metadata from 30+ sources. ```bash unarr search "inception" --sort seeders --min-rating 7 --lang es unarr search "breaking bad" --type show --quality 1080p unarr search "matrix" --json | jq '.results[].title' ``` **Filters:** | Flag | Description | Values | |------|-------------|--------| | `--type` | Content type | `movie`, `show` | | `--quality` | Video quality | `480p`, `720p`, `1080p`, `2160p` | | `--lang` | Audio language (ISO 639) | `es`, `en`, `fr`, `de`, ... | | `--genre` | Genre | `Action`, `Comedy`, `Drama`, `Horror`, ... | | `--year-min` | Minimum release year | `2020` | | `--year-max` | Maximum release year | `2026` | | `--min-rating` | Minimum IMDb/TMDb rating | `0`-`10` | | `--sort` | Sort order | `relevance`, `seeders`, `year`, `rating`, `added` | | `--limit` | Results per page | `1`-`50` | | `--page` | Page number | `1`, `2`, ... | | `--country` | Country for streaming info | `US`, `ES`, `GB`, ... | ## Inspect TrueSpec analysis — parse a torrent and show detailed quality specs. ```bash unarr inspect "Oppenheimer.2023.1080p.BluRay.x265" unarr inspect abc123def456abc123def456abc123def456abc1 unarr inspect "magnet:?xt=urn:btih:ABC123&dn=Movie.2023.1080p" ``` Accepts magnet URIs, 40-character info hashes, or torrent file names. Shows quality, codec, size, seeds, languages, source, quality score, health, and alternatives. ## Watch Find where to watch — streaming services alongside torrent options. ```bash unarr watch "oppenheimer" --country ES unarr watch "breaking bad" --json ``` Shows legal streaming options first (subscription, free, rent, buy), then torrent alternatives. ## Stream Stream a torrent directly to a media player without waiting for the full download. ```bash unarr stream abc123def456abc123def456abc123def456abc1 unarr stream "magnet:?xt=urn:btih:..." --port 8080 unarr stream --player mpv unarr stream --no-open # just print the URL ``` Downloads pieces sequentially and serves the video over a local HTTP server. Auto-detects mpv, vlc, or your default browser. **Subtitles.** When the source file contains embedded text subtitles (SRT, ASS, PGS in an MKV), the daemon extracts them as WebVTT sidecars during the same transcode pass and serves them alongside the HLS stream. The web player lists available subtitle tracks automatically — no separate subtitle download needed. **Seek-anywhere (copy-VOD).** For sources with browser-compatible codecs (H.264 + AAC), the daemon uses a copy pass instead of re-encoding. This enables full random-seek across the entire duration from the first play, at near-zero CPU cost. **Audio tracks.** Multi-audio MKVs expose all tracks (e.g. `fr`, `en`, `es`) in the player's audio menu. Switching tracks starts a new session from the current position. ## Download One-shot download by info hash or magnet link (no daemon required). ```bash unarr download abc123def456abc123def456abc123def456abc1 unarr download "magnet:?xt=urn:btih:..." --method torrent ``` ## Controlling downloads `unarr downloads` lists and controls whatever the daemon on this machine is working on. It talks to the daemon directly over loopback — no website, no account lookup — so it keeps working when the server is unreachable or the download has fallen out of the dashboard. ```bash unarr downloads # what is running right now (alias: unarr dl) unarr downloads pause # menu: tick what to pause unarr downloads stop # menu: tick what to stop unarr downloads pause --all # pause everything unarr downloads stop --all # stop everything (asks to confirm) unarr downloads pause 31ec4169 # or name it directly unarr downloads resume 31ec4169 # continue where it left off unarr downloads stop 31ec4169 --delete # cancel and delete the partial file unarr downloads retry 31ec4169 # start it over unarr downloads purge # forget queued leftovers unarr downloads --json # machine-readable, for scripts ``` **Run an action with no id and you get a menu** of the downloads it can apply to — space to tick, enter to confirm, esc to cancel. Only the relevant ones are listed: `pause` shows what is running, `resume` shows what is not. Ids may be abbreviated to any unique prefix — the 8-character ids printed here and in the logs are enough. An ambiguous prefix is refused rather than guessed. Two actions ask before they run: `--delete` (files leave the disk) and `stop --all` (nothing resumes by itself afterwards). `--yes` skips the question, and in a script or a pipe — where there is no terminal to answer on — the command refuses instead of hanging. `unarr download stop|pause|resume|retry|list|purge` works too, as an alias. **Actions reach the website.** Whatever you do here (or from the desktop tray) is reported to unarr.app, so the dashboard reflects it within a few seconds instead of showing a download that stopped minutes ago. It works the other way round as well: cancelling on the website stops the download on this machine. ### When a download will not stop If a download survives cancelling on the website, its task row is gone — you cancelled it and then removed it from the list — so the server has no way left to reach the agent, while the agent keeps restarting it from its **resume queue** on every launch. The agent now detects this by itself: a task the server no longer knows about is dropped after a few status reports. To end it immediately: ```bash unarr downloads stop # daemon running ``` With the daemon stopped, `--force` edits the on-disk resume queue instead — the file that would otherwise bring the download back: ```bash unarr downloads # lists the queue even with no daemon unarr downloads stop --force unarr downloads purge --force # clear the whole queue ``` The queue lives at `active-tasks.json` in the data dir (`~/.local/share/unarr/` on Linux, `~/Library/Application Support/unarr/` on macOS, `%LOCALAPPDATA%\unarr\` on Windows). Deleting it only forgets pending downloads — media files and config are untouched. ### From the desktop tray unarr-desktop lists the active downloads in its menu with **Pause / Resume / Stop / Stop and delete files** under each one. Same control plane, same reporting back to the website. Beyond five downloads the menu points at `unarr downloads`, which has no limit. ## Daemon The daemon receives download tasks from the web dashboard and executes them automatically. ```bash # Start in foreground (Ctrl+C to stop) unarr start # Or install as a system service (auto-starts on boot) unarr daemon install # Check status unarr status # Uninstall the service unarr daemon uninstall ``` The daemon connects via WebSocket for instant task delivery, with automatic HTTP fallback. It supports torrent, debrid, and usenet downloads concurrently, reports progress to the web dashboard, and handles graceful shutdown. **Service locations:** - Linux: `~/.config/systemd/user/unarr.service` (systemd) - macOS: `~/Library/LaunchAgents/com.torrentclaw.unarr.plist` (launchd) ## Metrics The daemon keeps one view of everything that is moving right now — downloads, in-browser HLS streams and the remote-folder mount — with each session's rate, bytes moved, uptime, health (`ok` / `degraded` / `stalled`) and stall count. ```bash unarr status --sessions # table unarr status --sessions --json # the same snapshot the website reads ``` `--json` only changes `--sessions`: a plain `unarr status --json` still prints the usual text status. A session is **degraded** as soon as it is impaired (a download receiving no data, a transcode below realtime, a mount read that moves no bytes) and **stalled** once that lasts 30 s. Each stall is counted **once**, not once per check, and it only ends after 10 s of healthy flow, so a rate flapping around the threshold is one stall. ### Publishing to the web The daemon sends that same snapshot to your unarr account, so the website's sessions page can show it. It is on by default and does not depend on the Prometheus switch below. ```toml [metrics] publish = false # stop sending the session snapshot to the web ``` - **Cadence:** every 5 s while at least one session exists. When the last one ends, one empty snapshot is sent, then one every 30 s as a heartbeat. Stopping the daemon sends one last empty snapshot (waiting at most 2 s for it), so the website clears this machine's sessions straight away. - **What is sent:** the `unarr status --sessions --json` document (see [`internal/metrics/SCHEMA.md`](internal/metrics/SCHEMA.md)) and the agent id: session kind, state, health, rates, bytes, progress, stall counts, the VPN state and the session titles — the release or file names of what the website already asked this machine to download or play. No file paths, IPs, tokens or URLs: a title that looks like an absolute path is cut to its last element, and one that looks like a URL is sent empty. - **Limits:** at most 50 sessions and 64 KiB per snapshot; anything beyond that is dropped from the end of the list (the summary still counts it). - **Failures never hold up the daemon:** one request at a time on its own goroutine, 5 s timeout, nothing queued — a snapshot that does not arrive is replaced by the next one. A website that does not support it yet (HTTP 404) is asked again every 10 minutes, with one log line. Other errors are logged once per failure streak. - Changing `publish` takes effect after `unarr daemon restart`. ### Prometheus endpoint (optional) Off by default: nothing listens until you opt in. ```toml [metrics] enabled = true # listen = "127.0.0.1:9877" # default: loopback only # token = "change-me" # required if listen is not loopback ``` `GET /metrics` returns the Prometheus text format and always needs a token, sent as `Authorization: Bearer ` (or the `X-Unarr-Control-Token` header). With no `token` set, the control-plane token is used — it is in the daemon state file, but it changes on every restart, so for a Prometheus server set your own `token`. A non-loopback `listen` without a `token` is refused. ```yaml # prometheus.yml scrape_configs: - job_name: unarr authorization: { credentials: change-me } static_configs: [{ targets: ["127.0.0.1:9877"] }] ``` | Metric | Type | Labels | |--------|------|--------| | `unarr_info` | gauge | `version` | | `unarr_uptime_seconds` | gauge | | | `unarr_sessions_active` | gauge | `kind` = `download`, `hls`, `stream`, `mount` | | `unarr_sessions_stalled` | gauge | | | `unarr_session_bytes_total` | counter | `kind` | | `unarr_session_rate_bytes_per_second` | gauge | `kind` | | `unarr_session_stalls_total` | counter | `kind` | | `unarr_vpn_up`, `unarr_vpn_enabled`, `unarr_vpn_required` | gauge | | | `unarr_downloads` | gauge | `state` (`downloading`, `paused`, …) | There are no per-title or per-hash labels, so cardinality stays fixed. The format is written by hand instead of pulling in `client_golang`, which would add several MB and a chain of dependencies to the binary for ten fixed series. ### `GET /sessions` (local control plane) The same snapshot is served to local tools (the website's unified view, the desktop tray) at `GET /sessions` on the loopback control plane, with the `X-Unarr-Control-Token` from the daemon state file. The JSON schema (`schemaVersion` 1) is documented in [`internal/metrics/SCHEMA.md`](internal/metrics/SCHEMA.md). ## VPN unarr can route your **downloads** through a managed WireGuard VPN, so peers and trackers see the VPN server's IP instead of yours. It runs entirely in userspace (wireguard-go + a gVisor netstack) — **no root, no `wg-quick`, no changes to your OS routing table**. ```bash # Turn it on (writes [downloads.vpn] enabled = true to your config) unarr vpn enable # Restart the daemon so it brings the tunnel up at startup unarr daemon restart # or: unarr start (if not installed as a service) # Check it's working — shows the exit server when the tunnel is up unarr vpn status # Verify your account is provisioned (queries the API) unarr vpn status --check # Turn it off again unarr vpn disable ``` **Split-tunnel — read this:** only the torrent client's traffic goes through the VPN. Your browser, `curl`, and every other app keep using your **real IP** — that is by design. To check the VPN is working, look at `unarr vpn status` (or the peer/announce IP), **not** your browser's "what's my IP". To protect your other devices (phone, laptop), use the **OpenVPN credentials** from your profile — those support ~10 concurrent devices and do **not** share the agent's WireGuard slot. **When does it fetch the config?** Once, at daemon startup. There's no periodic refresh — after changing your exit server in the web panel or re-provisioning, restart the daemon to pick it up. If the fetch fails the daemon logs a `[vpn]` line and downloads in the clear (never refuses to run). **Self-hosted / personal VPN:** instead of the managed config, point unarr at a local WireGuard `.conf`: ```toml [downloads.vpn] config_file = "/path/to/wg.conf" # takes precedence over `enabled` ``` ## Diagnostics ```bash # Run all diagnostic checks unarr doctor # Update to the latest version unarr self-update unarr self-update --force # reinstall even if up to date ``` `unarr doctor` checks: config file, API key, server connectivity (with latency), agent registration, download directory, disk space, and version. ### Updating unarr unarr supports three update paths. Pick whichever fits your workflow. **1. Manual self-update (always available).** ```bash unarr self-update # interactive update to latest unarr self-update --force # reinstall same version unarr self-update --allow-unsigned # accept releases without checksum signature ``` The CLI downloads the new release archive over HTTPS (from GitHub Releases at `github.com/Unarr-app/unarr-cli/releases/download/v/`, falling back to the web origin if GitHub is unreachable), verifies SHA-256, swaps the binary in place (`.backup` kept next to it), and restarts the systemd user unit if the daemon is running. **2. Auto-apply on server signal (default, since 0.9.6).** When you press **"Force update now"** on the web (Settings → Agent → Force update), the server sets a flag your daemon polls every sync (~3 s). On the next sync the daemon downloads the new binary, replaces itself, and exits — `systemd Restart=always` respawns on the new version. No SSH, no terminal access required. Works headless on NAS / Docker. The button shows an amber warning if your agent is below 0.9.6 (older daemons see the signal but only log "run unarr update" — the operator must run the command manually that one time). **Opt out of auto-apply.** Some users prefer reviewing CHANGELOG before applying. Disable in `config.toml`: ```toml [daemon] auto_upgrade = false ``` With `auto_upgrade = false`, pressing the web button still flags your agent (so the daemon logs the new version on next sync), but the daemon will not download / replace anything — you run `unarr self-update` when you're ready. **3. Docker auto-restart with a new tag.** ```bash docker pull unarr/cli:latest docker compose up -d ``` Tags published: `latest`, `1.2`, `1.2.2`, ... — pin to a minor (`1.2`) for opt-in patch updates without surprises. ## Logs ```bash unarr logs # last 50 lines unarr logs -f # follow unarr logs -n 200 --level warn # only warnings and errors unarr logs --since 2h --grep 'usenet|nzb' # last 2 hours, matching a regex unarr logs --since "2025-01-20 09:00" --level error unarr logs --boot # startup output — when it won't start unarr logs rotate # trim now, if rotation is on and over budget ``` `unarr daemon logs` is the same command (kept as an alias, `-f` / `-n` included). On Linux, where the systemd unit sends output to the journal, this streams from `journalctl --user -u unarr` instead of a file; everywhere else it reads `unarr.log` from the data directory and transparently continues into the rotated copies when the live file holds fewer lines than you asked for. Severity is inferred from the line itself (the daemon logs free-form text), so `--level` is a reading aid, not a guarantee. `--grep` is a case-insensitive regular expression. `--since` takes an age (`30m`, `2h`, `7d`, `1w`) or a stamp (`2025-01-20`, `2025-01-20 09:00`, `09:00` for today). **Two files.** A daemon started by a service launcher owns `unarr.log` and writes everything it logs there. The launcher itself captures a second, much smaller `unarr.boot.log`: the start banner, a fatal error from a start that never got going, a crash dump — output that never reaches the daemon's own log. `unarr logs --boot` reads it, and that is where to look when the daemon does not come up at all. On a systemd install there is no such file: the startup output is in the journal, so `unarr logs` already shows it and `--boot` says so. **On a systemd install, `log_max_size_mb` and `log_max_files` do nothing at all.** The unit sends the daemon's output to the journal, so there is no `unarr.log` to rotate and retention is journald's business, not unarr's — set it in `journald.conf` (`SystemMaxUse`, `MaxRetentionSec`); the defaults are 10% of the filesystem, capped at 4 GB. Check what it currently costs with `journalctl --user -u unarr --disk-usage`. Setting `log_max_size_mb = 1` there and expecting a bounded disk is the one misreading worth calling out: it bounds nothing. (`unarr logs rotate` is correspondingly a no-op that exits 0 — it will not conjure an empty `unarr.log` just to have something to rotate.) **Rotation is OPT-IN, and off by default** (`log_max_size_mb = 0`). Out of the box unarr never rotates anything: `unarr.log` and `unarr.boot.log` just grow. Bound them with `unarr clean`, or with an external `logrotate` — and if you use one, it must use `copytruncate`, since the daemon holds its descriptor for the whole run and has no reopen signal. Set `log_max_size_mb` to a number of MB to turn rotation on. `unarr.log` is then rotated once it reaches that size, keeping `log_max_files` copies (default 3), and `unarr.boot.log` gets its own fixed 2 MB cap with one rotated copy — independent of the size you set, but switched on by the same key. A running daemon rotates its own log by renaming it aside, so rotation does not wait for a restart; `unarr logs rotate` is for the case where the daemon is stopped, and it says so rather than doing nothing if a running daemon owns the file. **What you accept by turning it on.** Rotation is not a solved problem here, and this is why it does not ship on. Any second process holding the log can block the rename or the truncate — an open `unarr logs -f`, an antivirus scanner, OneDrive or Dropbox syncing the data directory, Windows Search. Windows is the strict case: a holder there can deny write access outright, so a rotation that cannot proceed is reported and the log keeps growing. Residual failure modes that remain even when it does proceed (a reader holding a rotated slot, two rotators racing over one staging file, a live-but-offline daemon losing its ownership claim) are listed under "Deuda abierta" in [`docs/plans/daemon-log-ownership.md`](docs/plans/daemon-log-ownership.md) — read it before enabling this on an install you care about. On Windows only, the boot log's trim is baked into the launcher script at install time, so re-run `unarr daemon install` after enabling rotation for that one to take effect. The global `--log-level` flag overrides `[daemon] log_level` for a single command. ## Clean Remove temporary files, logs, resume data, and other artifacts generated by unarr. Shows what will be removed and asks for confirmation before deleting. ```bash unarr clean # Show files and confirm before removing unarr clean --dry-run # Show what would be removed (no prompt) unarr clean --yes # Skip confirmation unarr clean --all # Also remove the data directory ``` **Cleans:** log files (including the rotated `unarr.log.1`, `.2`, … copies), daemon state, stale usenet resume files (> 7 days), stale replaced-file backups (> 7 days, kept by library upgrades), stream temp data, upgrade temp files, and stale atomic-write temps. Recent resume files are kept to preserve download progress for paused or interrupted downloads. Never removes your config file, downloaded media, or partial torrent/debrid downloads. ## Backup Snapshot your local unarr state into one verified zip, and restore it later or on another machine. ```bash unarr backup create # → /backups/unarr-backup-.zip unarr backup create --out ~/unarr.zip # a file, or a directory unarr backup create --encrypt # passphrase-encrypted, secrets included unarr backup list # tags [encrypted] [scheduled] [pre-restore] unarr backup restore unarr.zip # dry-run: verifies and shows what would change unarr backup restore unarr.zip --apply # writes (daemon must be stopped) unarr backup prune --keep 3 # drop all but the newest 3 manual/scheduled backups unarr backup prune --pre-restore # trim old pre-restore snapshots (newest 3 + last 30 days kept) ``` **What is in it:** `config.toml` and `active-tasks.json` (the download resume queue), plus a `manifest.json` (unarr version, schema, OS/arch, date, SHA-256 and size of every file). An archive holds up to 128 MiB of files (uncompressed, all together — some 150,000 queued tasks), of which `config.toml` may take at most 4 MiB and `manifest.json` 1 MiB (a real config is well under 100 KB). `create` checks those bounds, and that `config.toml` parses and the daemon can load the queue, before writing anything: it refuses an archive that `restore` would refuse, so every backup it writes can be restored. **What is never in it:** anything ephemeral or machine-bound — `daemon.state.json` (PID and the local control-plane token), `unarr.lock`, the agent TLS private key under `certs/`, logs, caches (including the `library.json` scan cache, rebuilt by a rescan), partial-download progress and your media. **Secrets — excluded by default.** A plain archive is a file people copy to a NAS or a cloud folder, so it must be safe to lose. `[auth] api_key`, `[agent] agent_hash` and `[downloads] webdav_username` / `webdav_password` and `[metrics] token` are blanked in the copy of `config.toml` (the file stays valid TOML; your comments and layout are untouched) and resolved download URLs / NZB passwords are stripped from the queue. The result is checked by parsing it: if a credential is written in a form unarr cannot blank (a dotted key, an inline table, a multi-line string), the backup is refused rather than written with the secret inside — use `--encrypt` for such a config. Restoring a secret-free archive keeps the credentials already on the machine (the dry-run names any it cannot carry over; if the archive's `auth.api_url` or `auth.mirrors` would send them to a server this machine does not use, the dry-run and `--apply` warn — a warning, not a refusal, since pointing a machine at another server is legitimate; when this machine's own `config.toml` does not parse, so there is nothing to compare against, they say so and name the archive's `auth.api_url` and `auth.mirrors` the credentials will go to) and the `[agent] id` they belong to: when this machine already has an `api_key` or `agent_hash`, its own id stays instead of the archive's — even an empty one, in which case `[agent] id` is left empty rather than paired with credentials that are not its own — and both the dry-run and `--apply` say so (run `unarr login` if this is a new machine). On a machine with no credentials yet, the archive's id is restored; run `unarr login` afterwards. Debrid downloads in a restored queue have no link of their own and need a fresh one from the server. `--encrypt` is the opt-in for a full copy: the whole archive is sealed with AES-256-GCM using a key derived from your passphrase (PBKDF2-HMAC-SHA256, 600k rounds, random salt and nonce per archive — Go standard library only). The passphrase comes from a prompt, `--passphrase-file`, or `UNARR_BACKUP_PASSPHRASE`; an empty one is refused, and it cannot be recovered. VPN keys live in the `.conf` that `[downloads.vpn] config_file` points to; that file is not backed up. **Restore is safe by default.** It refuses a bad manifest, a newer schema, any checksum mismatch, an archive or a file over its size bound, any archive member that is not one of the two known files (so a crafted `../` path cannot write elsewhere) or not a regular file (a symlink, a directory, a device), and content unarr could not load — a `config.toml` that does not parse the way unarr reads it (checked as it would be written, with this machine's credentials merged in) or a queue the daemon would drop on load (decoded with its own task type: one field of the wrong type, such as a `queuedAt` that is not a time, and it starts with an empty queue). Those checks run before anything is written or snapshotted. A restore is all or nothing: a queue an older unarr backed up that the daemon cannot load blocks the `config.toml` beside it too (there is no partial restore). A secret-free archive is also refused when this machine's own credentials are written in a form unarr cannot load, since they would be merged in: move `config.toml` aside, restore, then `unarr login`. The checksums prove integrity, not authorship: whoever holds an unencrypted archive can edit it and recompute them, so restore only archives you made — or encrypted ones, which the passphrase authenticates. A secret-free archive whose `config.toml` sets a credential anyway was edited after it was made, and is refused. An unencrypted archive with secrets included (what a pre-restore snapshot is, or an edited archive relabelled) is restored, but when its `api_key` / `agent_hash` are not this machine's, or it changes `auth.api_url` without being this machine's own identity, the dry-run and `--apply` warn. Without `--apply` nothing is written. With `--apply` it takes the daemon's instance lock for the whole restore — so it refuses to run while the daemon is up (`unarr stop` first) and a daemon cannot start halfway through — saves your current state as `unarr-prerestore-.zip` in `/backups` (a verbatim copy with your current secrets, 0600; it stays beside `config.toml` even when `[backup] dir` points elsewhere — `--dir` overrides; it is taken even when the current `config.toml` does not parse or is over its 4 MiB cap, so a good backup can replace a broken config, though `restore` will not bring that broken file back — unzip the snapshot for it; only the 128 MiB bound applies, and past it the error names the file to move aside before running the restore again), then replaces each file atomically. If a write fails, the files already replaced are put back. Because every snapshot holds secrets in the clear, each successful `--apply` then trims the older ones: the newest 3 and any younger than 30 days are kept, and never the one just taken. A failed restore removes nothing, and `unarr backup prune --pre-restore` applies the same retention on demand (plain `prune` and the daemon never touch snapshots). A symlinked `config.toml` is updated through the link. An encrypted archive — or a secret-free one restored before `unarr login` — brings the archived agent id with it: restore it onto a replacement machine, not onto a second one while the original still runs, or both daemons share one identity. **Scheduled backups** run inside the daemon (restart it after changing them). They are always secret-free, and a backup missed while the daemon was off is taken at its next start. An invalid table is logged at start-up and not scheduled; a failed backup is retried an hour later: ```toml [backup] enabled = true # default: false frequency = "daily" # "daily" or "weekly" time = "03:00" # local time, 24h # weekday = "sunday" # weekly only retention_count = 3 # scheduled backups to keep # dir = "/srv/unarr-backups" # default: /backups ``` `retention_count` only ever deletes scheduled backups (`unarr-scheduled-.zip`): a manual archive — say the encrypted one holding your secrets — is never pruned behind your back. `unarr config check` validates this table. `dir` must be an absolute path: a relative one would depend on the directory unarr happens to run from, so the daemon does not schedule it and `unarr backup create` / `list` / `prune` refuse it before touching anything (their `--dir` and `--out` flags are ordinary command-line paths, relative to where you run them). The default folder is next to the `config.toml` in use (so `--config` gets its own) rather than in the data dir, because `unarr clean --all` wipes the data dir on Linux and Windows. On macOS the data dir *is* the config dir, so `unarr clean --all` removes the backups too — point `dir` elsewhere there. ## Library clean (hygiene sweep) `unarr library clean` reconciles your **download directory** and **Movies/TV library dirs**, reporting — and with `--apply`, removing — the deterministic junk that accretes there. It **never** removes a valid, playable video (>= the plausibility floor, default 1 MiB), and only removes duplicates after a full byte-for-byte compare confirms they are identical (a cheap fingerprint just finds the candidates). Everything acted upon is confined to the configured download/movies/tv directories. ```bash unarr library clean # report only (DRY-RUN — the default) unarr library clean --apply # actually remove the reported orphans unarr library clean --dry-run # force report-only even with --apply (explicit preview) unarr library clean --dedup-only # only collapse byte-identical duplicate videos ``` **Reports / removes:** - **Download stubs** — video files below the floor (a CDN/expired-link stub, not media). - **Orphaned partials** — `.part`, `.!qB`, `.aria2`, `.tmp`, `.partial` with no active download. - **Duplicate videos** — byte-identical copies of the same file in a directory (keeps one canonical copy, verified by fingerprint = size + first/last 1 MiB). Distinct content (a real 2160p next to a 1080p) is never touched. - **Orphaned subtitles/sidecars** — `.srt`/`.nfo`/`.jpg`/`.par2`/… with no owning video. Per-track sidecars in a `.unarr/` cache dir are checked against the **parent** release dir, so they are only removed when the release itself no longer holds the video. - **Video-less directories** — empty or holding only junk/stubs. - **Media-named directories** — a folder literally named `movie.mkv/` (a mis-created dir). - **Suspect zero-content videos** *(opt-in, `remove_corrupt_videos`)* — a video of the right size (>= floor, so not a stub) whose **first and/or last 1 MiB is all zero bytes**: a corrupt/zero-filled download that is unplayable yet passes the floor and dodges the dedup (its fingerprint differs from the real copy). This is a **strong-but-not-absolute heuristic**, so it defaults **off** and is removed **only** by this manual command with the toggle on — never by the daemon's automatic sweep. `unarr library stats` always counts these (shown as **Suspect (zero-content)**) so you can see them even without enabling removal. ### Automatic cleanup (daemon) The **same sweep runs automatically after every library auto-scan** in the daemon — it is the native, always-on replacement for an external cleanup script. It applies only the safe, deterministic categories, protects any partial modified in the last few minutes (a live download), logs every action, and reports the space freed. Configure it under `[library.cleanup]`: ```toml [library.cleanup] enabled = true # run automatically after each auto-scan (manual command always works) min_video_bytes = "1MiB" # anti-stub floor; a video smaller than this is a stub remove_stubs = true # delete videos below min_video_bytes remove_orphan_partials = true # delete .part/.!qB/.aria2/.tmp/.partial with no active task dedup_exact = true # collapse byte-identical duplicate videos (keeps one canonical copy) remove_orphan_subtitles = true # delete sidecars with no owning video (.unarr → checks parent dir) prune_empty_dirs = true # remove video-less dirs and media-named dirs remove_corrupt_videos = false # OPT-IN: flag/remove right-sized videos whose first/last 1 MiB is all zeros ``` Every category **except `remove_corrupt_videos` defaults on** (each of those is deterministic and non-destructive to valid media). `remove_corrupt_videos` defaults **off** because zero-content detection is a heuristic; enable it deliberately, and even then it is applied **only** by the manual `unarr library clean --apply` — the daemon's automatic sweep never removes a suspect video. Set `enabled = false` to disable the automatic post-scan sweep — the manual `unarr library clean` command still works regardless. ## Library stats (health & composition) `unarr library stats` reports the health, composition and quality of your **Movies/TV library and download dir**. It is a **pure DRY-RUN — it only READS**, never modifies anything on disk. Confined to the configured download/movies/tv directories. ```bash unarr library stats # readable table (dry-run — reads only) unarr library stats --json # emit the full stats struct as JSON (for scripts) unarr library stats --workers 4 # limit concurrent ffprobe workers for the quality pass ``` **Three blocks:** - **Composition** — number of movies, shows, seasons and episodes, plus the **real on-disk space** (allocated blocks, like `du` — not apparent size) per category (Movies vs TV Shows vs Downloads) and the average size per title. - **Health / reclaimable** — the same sweep `unarr library clean` performs, in dry-run: stubs, orphaned partials, duplicates, orphaned sidecars, empty dirs and media-named dirs — grouped by category with the total space reclaimable. Run `unarr library clean --apply` to actually free it. - **Quality** — resolution (2160p/1080p/720p/480p/SD), video codec (h265/h264/av1/other) and HDR breakdown, extracted with ffprobe. A file ffprobe can't read is counted as `unknown` and never aborts the report. Probing a large library end-to-end can take a while (bounded by `--workers`, default 8). `--json` emits the full `LibraryStats` struct (composition, health, quality) for piping into scripts. ## Alias (optional) Create a shell alias for shorter commands: ```bash # Add to ~/.bashrc or ~/.zshrc alias un=unarr # Then use: un search "breaking bad" --type show un popular --limit 5 un start ``` ## Global Flags | Flag | Description | |------|-------------| | `--json` | Output as JSON (for piping to `jq`, scripts) | | `--no-color` | Disable colored output | | `--api-key` | API key (overrides config file and env) | | `--config` | Custom config file path | ## JSON Output All query commands support `--json` for scripting: ```bash # Pipe to jq unarr search "matrix" --json | jq '.results[].title' # Save to file unarr popular --json > popular.json # Use in scripts SEEDS=$(unarr search "inception" --json | jq '.results[0].torrents[0].seeders') ``` ## Configuration ### Config file Location: `~/.config/unarr/config.toml` ```toml [auth] api_key = "tc_your_api_key_here" api_url = "https://unarr.app" [agent] id = "auto-generated-uuid" name = "My PC" [downloads] dir = "~/Media" # Ordered download-method preference. The web honours this list, so anything NOT # listed is disabled — e.g. ["debrid"] means debrid-only and never falls back to # torrent; ["debrid","usenet"] tries debrid then usenet. Omit (or use ["auto"]) # to let the server decide (default: auto — if you have debrid configured, cached # titles use debrid automatically, otherwise torrent). Debrid/usenet must be # configured in your TorrentClaw account — the agent only fetches links the web # resolves. Requires unarr >= 1.1.5-beta. preferred_methods = ["auto"] # e.g. ["debrid"], ["debrid","usenet"], or ["auto"] # preferred_method = "auto" # legacy single value (still supported; superseded by preferred_methods) max_concurrent = 3 max_download_speed = "0" # e.g. "10MB", "500KB", "0" = unlimited max_upload_speed = "0" # Read-only WebDAV export of your organized library (Movies / TV Shows) so # Infuse, Kodi, VLC, etc. can browse and play it without the web player. Opt-in, # off by default. Only GET/HEAD/PROPFIND are served — every write verb is 405'd. # Basic auth: username defaults to "unarr"; password defaults to a stable value # derived from your API key (shown by `unarr status`) unless you set one. The # mount is served on the same port as streaming, at /dav/. Requires unarr >= 1.5.0-beta. webdav_enabled = false # webdav_username = "unarr" # webdav_password = "" # blank = derive from the API key # The mount answers only your local network (LAN, Tailscale, loopback); # everything else gets a 404, so it stays private even when the stream port is # reachable from the internet. Turning this on removes that restriction, and the # Basic auth left protecting it has NO rate limiting — an exposed mount is an # unthrottled password-guessing target. Remote playback does not use WebDAV, so # leaving it off costs you nothing there. # webdav_allow_wan = false # Publish the HTTPS streaming port to your router (UPnP/NAT-PMP) so remote # playback uses your agent's stable direct-TLS address instead of a CloudFlare # quick tunnel, which changes hostname on every restart and gets rate-limited. # On by default: that listener is TLS-only and every request needs a token the # web mints, so nothing unauthenticated is reachable. The lease is renewed # automatically and removed on shutdown. Set to false to never touch your router. # auto_https_upnp = true # Play Usenet (NZB) releases live from the news server instead of downloading the # whole file first. On by default: "Play from Usenet" starts in seconds and the # file is NOT kept in your library. A release that cannot stream (compressed or # password-protected RAR…) falls back to a normal download automatically. Set to # false to always download first. Configs saved by older versions carry an # explicit `false` nobody chose; the first start after upgrading flips it to true # once and records that in the file as `usenet_streaming_default_applied = true` # (keep that line — it is what makes a later `false` stick). Changing it needs a # daemon restart. # usenet_streaming = true [organize] enabled = true movies_dir = "~/Media/Movies" tv_shows_dir = "~/Media/TV Shows" # Folder/file layout for NEW downloads (existing files are never moved): # "default" Movie (2000)/Movie (2000).mkv · Show/Season 01/Show - S01E01.mkv # "plex" adds {imdb-tt…}, the show's year and episode titles — best for # Plex, Infuse and Emby (aliases: "infuse", "emby"), keeps # same-titled shows apart: One Piece (1999) {imdb-tt0388629} # "jellyfin" same, with [imdbid-tt…] naming = "default" # Optional FileBot-style templates overriding the preset. Tokens: {n} title, # {y} year, {imdbid} {tmdbid} {tvdbid}, {collection}, {s00e00} {sxe} {s} {e} # {s00} {e00} {e.pad(4)}, {t} episode title, {vf} 1080p. "/" = folder, # <…> = dropped when a value inside is missing, other braces are literal. # Preview with `unarr organize preview`; restart the daemon after changing. # movie_format = "{n}< ({y})>< {imdb-{imdbid}}>/{n}< ({y})>< {imdb-{imdbid}}>" # series_format = "{n}< ({y})>< {tvdb-{tvdbid}}>/Season {s00}/{n} - {s00e00}< - {t}>" # Library-hygiene sweep — runs automatically after each auto-scan, and manually # via `unarr library clean`. Removes only deterministic junk; a valid video # (>= min_video_bytes) is NEVER removed. See the "Library clean" section above. [library.cleanup] enabled = true # run automatically after each auto-scan min_video_bytes = "1MiB" # anti-stub floor; a video below this is a stub remove_stubs = true # delete videos below min_video_bytes remove_orphan_partials = true # delete .part/.!qB/.aria2/.tmp/.partial with no active task dedup_exact = true # collapse byte-identical duplicate videos (keeps one canonical copy) remove_orphan_subtitles = true # delete sidecars with no owning video (.unarr checks the parent dir) prune_empty_dirs = true # remove video-less dirs and media-named dirs [daemon] auto_upgrade = true # apply server-flagged upgrades in-place (since 0.9.6) status_interval = "3s" # how often a running download reports progress # Log file (unarr.log in the data dir). Rotation is OPT-IN and off by default: # read the limitations in the "Logs" section above before turning it on. With 0 # the log grows until `unarr clean` or an external logrotate trims it. log_max_size_mb = 0 # >0 = rotate unarr.log once it reaches this many MB; 0 = never rotate log_max_files = 3 # rotated copies to keep (unarr.log.1 … .3) log_level = "info" # default minimum severity for `unarr logs`: debug|info|warn|error log_format = "text" # `unarr logs` output: "text" (verbatim) or "json" (json-lines) [notifications] enabled = true # Read by the desktop companion (tray app), not the daemon: which player the # web's Play button opens. Empty = autodetect. See [desktop] below. [desktop] player = "" # player_command = "flatpak run org.videolan.VLC --start-time={start} -- {url}" [general] country = "US" ``` ### Streaming reference The in-browser player on unarr.app streams from the daemon over HLS (HTTP fragments + ffmpeg transcode for codecs the browser can't decode natively). Enabled by default — a fresh install "just works" without editing the TOML. ```toml [downloads.transcode] enabled = true # master switch hw_accel = "auto" # auto | none | nvenc | qsv | vaapi | videotoolbox preset = "veryfast" # libx264 preset video_bitrate = "" # e.g. "5M" caps -b:v; empty = engine fallback (5M) audio_bitrate = "192k" # e.g. "128k", "192k", "256k" max_height = 0 # 0 = no cap; e.g. 720 forces 720p max max_concurrent = 2 # max simultaneous ffmpeg processes ``` #### `[downloads.transcode]` | Key | Type | Default | Notes | |-----|------|---------|-------| | `enabled` | bool | `true` | Real-time HLS transcoding when source codec is browser-incompatible (HEVC, AV1, AC3, DTS). Requires `ffmpeg` + `ffprobe` on PATH. | | `hw_accel` | string | `"auto"` | Hardware accel: `"auto"`, `"none"`, `"nvenc"` (NVIDIA), `"qsv"` (Intel), `"vaapi"` (Linux), `"videotoolbox"` (macOS). | | `preset` | string | `"veryfast"` | libx264 preset. Slower preset = smaller files but higher CPU. Options: `ultrafast`, `superfast`, `veryfast`, `faster`, `fast`, `medium`, `slow`, `slower`, `veryslow`. | | `video_bitrate` | string | `""` | E.g. `"5M"` caps `-b:v`. Empty falls back to the engine default (`5M`). | | `audio_bitrate` | string | `"192k"` | E.g. `"128k"`, `"256k"`. | | `max_height` | int | `0` | `0` = no cap. E.g. `720` forces 720p max — useful on weak GPUs. | | `max_concurrent` | int | `2` | Max simultaneous ffmpeg processes. Increase if hosting multiple users on a beefy box. | If `transcode.enabled = true` but `ffmpeg` / `ffprobe` aren't on PATH, the daemon logs a warning at startup and HLS sessions are rejected at runtime with a clear error — install ffmpeg or set `enabled = false`. #### `[downloads.hls_cache]` — persistent HLS segment cache ```toml [downloads.hls_cache] enabled = true # on by default size_gb = 5 # disk budget; LRU eviction once exceeded dir = "" # custom path; empty = ~/.cache/unarr/hls-cache ``` | Key | Type | Default | Notes | |-----|------|---------|-------| | `enabled` | bool | `true` | Persists finished HLS encodes per `(source, quality, audio_index)`. A second play of the same file at the same quality reuses the segments — no ffmpeg, near-zero CPU, instant playback. Set to `false` to delete segments on session close (original behavior). | | `size_gb` | int | `5` | Cache budget in gigabytes. When exceeded the LRU sweeper evicts the least-recently-used cached encodes hourly. Minimum 1 GB (smaller values are clamped up). | | `dir` | string | `""` | Custom storage path. Empty defaults to `~/.cache/unarr/hls-cache` (Linux/macOS) or the user cache dir (Windows). | **What it does.** First play encodes normally (ffmpeg writes segments). On session close, if every segment is on disk and ffmpeg exited cleanly, the directory is sealed with a `.complete` marker and kept. Next time the same source + quality combo is requested, the daemon serves segments straight from disk — no transcode, no warm-up, no CPU cost. **Why per (source, quality, audio).** Renaming the file or switching quality invalidates the entry: the segments are tied to the exact source bytes and the exact ffmpeg parameters. Re-encoding generates a new key. **Eviction.** A background goroutine wakes every hour. If total cache size exceeds `size_gb`, it deletes the oldest entries (by mtime) until under budget. Active sessions are pinned — they never get evicted mid-play. **Disable.** Either edit the TOML to set `enabled = false`, or remove the cache directory manually (it'll be recreated as needed). Disabling does not delete existing cached segments — drop `dir` (or `~/.cache/unarr/hls-cache`) to reclaim the space. #### `[downloads.vpn]` | Key | Type | Default | Notes | |-----|------|---------|-------| | `enabled` | bool | `false` | Managed VPN: at startup the daemon fetches a WireGuard config from your account and split-tunnels torrent traffic through it. Needs a PRO+ plan with the VPN add-on. Toggle with `unarr vpn enable` / `disable`. | | `config_file` | string | `""` | Self-hosted / personal VPN: path to a local WireGuard `.conf`. **Takes precedence over `enabled`** — when set, the daemon uses this file and never calls the API. | See the [VPN](#vpn) section above for how it works (split-tunnel, no root) and how to protect your other devices. #### `[downloads.funnel]` — public HTTPS hostname for the daemon (CloudFlare Quick Tunnel) ```toml [downloads.funnel] enabled = false # off by default ``` | Key | Type | Default | Notes | |-----|------|---------|-------| | `enabled` | bool | `false` | Spawns `cloudflared tunnel --url http://localhost:` as a child process at daemon startup. Toggle with `unarr funnel on` / `off`. Requires `cloudflared` on PATH. | **What it does.** Without a tunnel, the daemon is reachable on `localhost`, your LAN, and (if installed) Tailscale. That covers the same-machine and Tailscale-connected cases, but the **browser-based player on unarr.app fails on any other network** because HTTPS pages can't fetch HTTP resources ("mixed content"). Enabling the funnel gives the daemon a public `https://.trycloudflare.com` hostname so the web player picks it up and playback works from anywhere — phone on cellular, friend's laptop on a foreign Wi-Fi, anywhere. The Stremio addon already works cross-network (native mpv/VLC players ignore CORS), so this is strictly a web-player fix. **Privacy posture.** Bytes pass through CloudFlare's edge — TorrentClaw never relays content (we don't see your traffic), CloudFlare does. Quick Tunnels are **anonymous** (no CF account required); the registration is unauthenticated and the hostname is a random label, but CF logs request metadata like any CDN would. If you want zero third-party byte access, use Tailscale instead. **Limitations (free Quick Tunnels).** | Aspect | Limit | |--------|-------| | Session lifetime | ~6 hours, then the hostname rotates. cloudflared re-registers automatically; the web picks up the new URL on the next sync. In-flight HLS sessions break across the rotation (browser retries). | | Bandwidth | No documented hard cap, but CF reserves the right to throttle. 1080p HLS (~6 Mbps) is fine; 4K HEVC at 25 Mbps may hit throttling. | | Latency | +20–80 ms vs direct LAN/Tailscale (extra hop browser → CF edge → tunnel). HLS player buffer absorbs it. | | Concurrency | One tunnel serves N viewers. CF rate-limits ~200 req/s, plenty for HLS segments. | | TOS | CloudFlare flags Quick Tunnels as "not for production traffic". They can decommission an abusive tunnel without notice. | For heavy / high-throughput / persistent-URL use cases, switch to a CloudFlare Named Tunnel (free, needs a CF account) or run your own reverse proxy — both out of scope for the bundled command. **Disable.** `unarr funnel off` flips `enabled` to `false` in the TOML and prompts you to restart the daemon. You can also edit `config.toml` directly: ```toml [downloads.funnel] enabled = false ``` **Install cloudflared.** - Linux: `apt install cloudflared` (after adding CF's apt repo) — see . Or pull the static binary from . - macOS: `brew install cloudflared`. - Windows: `winget install --id Cloudflare.cloudflared`. If `cloudflared` is not on PATH the daemon logs a warning at startup and falls back to LAN/Tailscale-only reachability. #### `[desktop]` — which player Play opens Read by the **unarr desktop companion** (the tray app that registers the `unarr://` handler), not by the daemon. Pressing Play on the web sends an `unarr://play` link to it, and this section decides what opens. ```toml [desktop] # Empty = autodetect. Or: "mpv", "vlc", "iina" (macOS), "mpc" (Windows), # "system" (whatever your OS opens video with). player = "" # An explicit command line, for players none of those names reach. # Wins over `player`. See below. # player_command = "flatpak run org.videolan.VLC --start-time={start} -- {url}" ``` | Key | Type | Default | Notes | |-----|------|---------|-------| | `player` | string | `""` | `""` = autodetect. `"mpv"` also resolves **Celluloid** (it embeds mpv and installs no `mpv` binary). `"system"` uses your OS default video app. A player that isn't installed falls back to autodetect **and notifies you** — the setting is never silently ignored. Watching in the browser is not a value here: that's the web's own "Web player" button. | | `player_command` | string | `""` | Explicit command line with placeholders (below). Outranks `player`; while it is set, the tray's Player submenu is shown disabled. | **How the player is chosen**, most specific first: 1. `player_command` — your own command line 2. `player` — a name from the table above 3. autodetect — mpv/Celluloid → VLC → IINA (macOS) / MPC-HC (Windows) 4. your OS default video application 5. the browser, as a last resort, so a click always plays *something* Step 5 is a fallback, not a choice: if you want to watch in the browser, the web has its own "Web player" button, and going through the desktop app to get there would be a second route to the same place. Steps 1 and 4 exist because a Flatpak/Snap/AppImage install exposes no binary on PATH, and mpv.net or SMPlayer are their own spellings. Step 4 asks the system directly (freedesktop MIME association on Linux, LaunchServices on macOS, the registered file association on Windows), so it finds those — but it can only pass a URL, so playback starts from the beginning. Steps 2 and 3 know the player's flags and can pass resume position, title and language preferences. **`player_command` placeholders.** The template is split into arguments here and executed directly — never through a shell — so quoting is the only shell syntax that applies. An argument whose placeholder has no value for a given stream is dropped whole, so `--start={start}` simply disappears when there is nothing to resume. | Placeholder | Value | |---|---| | `{url}` | The stream URL. Appended automatically if the template never mentions it. | | `{web}` | The unarr web player page for this stream (falls back to `{url}`). | | `{start}` | Resume position in seconds. Absent when starting from the beginning. | | `{title}` | Display title for the player window/OSD. | | `{alang}` / `{slang}` | Preferred audio / subtitle languages, comma-separated. | | `{subfile}` | URL of an external subtitle file to side-load. **First one only** — see the note below. | > **`{subfile}` carries a single subtitle.** unarr can side-load several > external subtitle files (AI translations and shared provider subtitles), but a > placeholder always substitutes inside ONE argument, so a template cannot emit > a repeatable flag more than once. `{subfile}` therefore expands to the first > subtitle only, and the argument disappears when there is none. The built-in > players (mpv, Celluloid, VLC) receive **all** of them — use one of those if > you want the full subtitle menu. ```toml # Flatpak VLC (no `vlc` on PATH) player_command = "flatpak run org.videolan.VLC --start-time={start} -- {url}" # mpv.net on Windows player_command = 'C:\Program Files\mpv.net\mpvnet.exe --start={start} --force-media-title={title} -- {url}' # SMPlayer (its own flag spellings) player_command = "smplayer -media-title {title} -start {start} {url}" ``` ### Environment variables Environment variables override config file values: ```bash export UNARR_API_KEY=tc_your_api_key export UNARR_API_URL=https://unarr.app export UNARR_COUNTRY=ES export UNARR_DOWNLOAD_DIR=~/Media # Desktop companion — one-off override without editing the TOML export UNARR_DESKTOP_PLAYER=vlc export UNARR_DESKTOP_PLAYER_COMMAND="flatpak run org.videolan.VLC -- {url}" ``` ### Speed limits Speed limits use human-readable format: ```toml max_download_speed = "10MB" # 10 megabytes/sec max_upload_speed = "1MB" # 1 megabyte/sec max_download_speed = "500KB" # 500 kilobytes/sec max_download_speed = "0" # unlimited (default) ``` ### Telemetry The agent reports a small set of **lifecycle events** to the server — enough to see why an agent that connected never came back: - **Onboarding:** login succeeded, first sync completed, or a start-up failure (missing config, a stream port already in use, a permission error). - **Exit:** whether the daemon was stopped by you, crashed, or exited normally. No file names, library contents, search terms, or download activity are sent — only these lifecycle signals plus the agent's version and OS. It helps us fix the setup problems that make agents fail on the first run. **To turn it off completely:** ```toml [telemetry] enabled = false ``` or with an environment variable (wins over the config file — handy for headless/Docker): ```bash UNARR_TELEMETRY=off unarr start ``` When disabled the agent sends **nothing** — it still registers, syncs, and downloads exactly the same. Telemetry is purely additive. ## Remote library as a local folder (optional) `unarr mount` exposes completed Real-Debrid / AllDebrid / TorBox / Torrin files and compatible NZBs as a local folder, reading media on demand. This is **disabled by default** and independent of the existing WebDAV export of downloaded files. Enable the mount with `unarr mount` and connect providers on the Unarr website. unarr prepares rclone automatically, explains missing filesystem drivers before asking permission to install them, and creates a default mount folder. The normal agent service owns the mount after the command exits and retries it after transient failure or login/reboot. Run `unarr init` first to configure the normal agent. The command confirms saved settings and an activation request; check the folder and agent logs to verify it is mounted. Use `unarr umount` to disable it, `unarr config mount` for advanced local settings, or `unarr mount ` to choose a destination. On the website, **Add to local Usenet folder** sends only the selected NZB to the agent's managed inbox; manual NZB copies remain supported. Persistent `mount`/`umount` use the single default agent configuration; custom `--config` and shell-only overrides are refused before login or service changes. Use `unarr mount serve --config /path/to/config.toml` for a separately managed WebDAV process. Disabling the mount leaves an intentionally stopped agent stopped. See [setup, configuration and limitations](REMOTE_MOUNT.md) and the [performance report](REMOTE_MOUNT_PERFORMANCE.md). ## Shell Completion Generate tab-completion scripts for your shell: ```bash # Bash — add to ~/.bashrc eval "$(unarr completion bash)" # Zsh — add to ~/.zshrc eval "$(unarr completion zsh)" # Fish unarr completion fish > ~/.config/fish/completions/unarr.fish # PowerShell — add to $PROFILE unarr completion powershell >> $PROFILE ``` Completions provide tab-completion for commands, flags, and flag values (e.g. `--type ` shows `movie` and `show`). ## Scan Walk a folder recursively, analyze each video file with ffprobe, and sync quality data to your account. ```bash unarr scan ~/Media # scan default download dir unarr scan /mnt/nas/Movies # scan a specific path unarr scan ~/Media --status # show last scan results without re-scanning unarr scan ~/Media --workers 4 # use 4 parallel ffprobe workers unarr scan ~/Media --no-sync # analyze locally without uploading results ``` The daemon also runs an automatic background scan when it detects new files in the download directory. ## Mirrors Mirrors are alternate base URLs the agent falls back to when the primary domain is unreachable — useful for bypassing DNS blocks, ISP filters, or short-lived outages without restarting the agent. ```bash unarr mirrors list # show currently configured mirrors unarr mirrors update # refresh from the server's canonical list unarr mirrors test # probe every configured mirror for latency and reachability ``` ## Coming Soon These commands are planned for future releases: | Command | Description | |---------|-------------| | `unarr moreseed` | Find same quality with more seeders | | `unarr compare` | Compare two torrents side by side | | `unarr monitor` | Watch for new episodes of a series | | `unarr open` | Open content in the browser | ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, code style, and guidelines. ## License MIT License — see [LICENSE](LICENSE) for details.