# qui > Modern web interface for qBittorrent This file contains all documentation content in a single document following the llmstxt.org standard. ## Compatibility # qBittorrent Version Compatibility :::note qui officially supports qBittorrent 4.3.9 and newer as the baseline. The features below may require newer builds as noted, and anything older than 4.3.9 might still connect, but functionality is not guaranteed. ::: qui automatically detects the features available on each qBittorrent instance and adjusts the interface accordingly. Certain features require newer qBittorrent versions and will be disabled when connecting to older instances: | Feature | Minimum Version | Notes | | --- | --- | --- | | **Rename Torrent** | 4.1.0+ (Web API 2.0.0+) | Change the display name of torrents | | **Tracker Editing** | 4.1.5+ (Web API 2.2.0+) | Edit, add, and remove tracker URLs | | **File Priority Controls** | 4.1.5+ (Web API 2.2.0+) | Enable/disable files and adjust download priority levels | | **Rename File** | 4.2.1+ (Web API 2.4.0+) | Rename individual files within torrents | | **Rename Folder** | 4.3.3+ (Web API 2.7.0+) | Rename folders within torrents | | **Per-Torrent Temporary Download Path** | 4.4.0+ (Web API 2.8.4+) | A custom temporary download path may be set when adding torrents | | **Torrent Export (.torrent download)** | 4.5.0+ (Web API 2.8.11+) | Download .torrent files via `/api/v2/torrents/export`; first appeared in 4.5.0beta1 | | **Backups (.torrent archive export)** | 4.5.0+ (Web API 2.8.11+) | qui backups rely on `/torrents/export`; the backup UI is hidden when the endpoint is unavailable | | **Subcategories** | 4.6.0+ (Web API 2.9.0+) | Support for nested category structures (e.g., `Movies/Action`) | | **Torrent Creation** | 5.0.0+ (Web API 2.11.2+) | Create new .torrent files via the Web API | | **Path Autocomplete** | 5.0.0+ (Web API 2.11.2+) | Autocomplete suggestions for path inputs when adding torrents or creating .torrent files | | **External IP Reporting (IPv4/IPv6)** | 5.1.0+ (Web API 2.11.3+) | Exposes `last_external_address_v4` / `_v6` fields | | **Tracker Health Status** | 5.1.0+ (Web API 2.11.4+) | Automatically detects unregistered torrents and tracker issues | | **Share limit action** | 5.2.0+ (Web API **2.15.1**+) | Per-torrent behavior when ratio, seeding time, or inactive seeding limits are hit (stop, remove, remove with content, enable super seeding). qui exposes this when the instance reports Web API **2.15.1** or newer. | | **Share limit mode** | unreleased (Web API **2.16.0**+) | Whether that action runs when **any** configured limit is reached or only when **all** are. Shown only when the instance reports Web API **2.16.0** or newer (newer than action-only support). | :::note Hybrid and v2 torrent creation requires a qBittorrent build that links against libtorrent v2. Builds compiled with libtorrent 1.x ignore the `format` parameter. ::: ## Authentication Compatibility ### API key auth with reverse-proxy Basic Auth qBittorrent API key authentication uses the HTTP `Authorization: Bearer ...` header. Reverse-proxy Basic Auth, such as nginx `auth_basic`, also uses the `Authorization` header. Because a request can only carry one normal `Authorization` value, qBittorrent API key authentication cannot be combined with reverse-proxy Basic Auth in the default setup. Use qBittorrent username/password authentication with reverse-proxy Basic Auth, or bypass Basic Auth for qui's requests to qBittorrent. ## Troubleshooting: Missing Features ### Create Torrent button is not visible The **Create Torrent** button in the header bar is only displayed when qui detects that your qBittorrent instance supports the torrent creation API. If you do not see the button, your qBittorrent version is below **5.0.0** (Web API v2.11.2). To resolve this, upgrade qBittorrent to version 5.0.0 or later and refresh the qui web UI. ### Hybrid and v2 torrent formats are unavailable Even with qBittorrent 5.0.0+, the **hybrid** and **v2** torrent format options require qBittorrent to be built against **libtorrent v2.x**. If your build uses libtorrent 1.x, the torrent creation dialog will display an alert indicating that only the **v1** format is available. This is a build-time dependency of qBittorrent itself and cannot be changed through qui. ### "Too many active torrent creation tasks" error There is a limit on the number of concurrent torrent creation tasks. If you see a **409 Conflict** error with this message, wait for your existing creation tasks to finish before starting new ones. You can monitor active tasks in the torrent creation task list. --- ## Metrics # Prometheus Metrics Prometheus metrics can be enabled to monitor your qBittorrent instances. When enabled, metrics are served on a **separate port** (default: 9074) with **no authentication required** for easier monitoring setup. ## Enable Metrics Metrics are **disabled by default**. Enable them via configuration file or environment variable: ### Config File (`config.toml`) ```toml metricsEnabled = true metricsHost = "127.0.0.1" # Bind to localhost only (recommended for security) metricsPort = 9074 # Standard Prometheus port range # metricsBasicAuthUsers = "user:$2y$10$bcrypt_hash_here" # Optional: basic auth ``` ### Environment Variables ```bash QUI__METRICS_ENABLED=true QUI__METRICS_HOST=0.0.0.0 # Optional: bind to all interfaces if needed QUI__METRICS_PORT=9074 # Optional: custom port QUI__METRICS_BASIC_AUTH_USERS="user:$2y$10$hash" # Optional: basic auth ``` ## Available Metrics - **Torrent counts** by status (downloading, seeding, paused, error) - **Transfer speeds** (upload/download bytes per second) - **Instance connection status** ## Prometheus Configuration Configure Prometheus to scrape the dedicated metrics port (no authentication required): ```yaml scrape_configs: - job_name: 'qui' static_configs: - targets: ['localhost:9074'] metrics_path: /metrics scrape_interval: 30s #basic_auth: #username: prometheus #password: yourpassword ``` All metrics are labeled with `instance_id` and `instance_name` for multi-instance monitoring. --- ## SSO Proxies and CORS When qui is behind an SSO proxy (Cloudflare Access, Pangolin, etc.), expired sessions can redirect API `fetch()` calls to the proxy's auth origin. Browsers block cross-origin redirects unless the **proxy** sends CORS headers, so you may see errors like "CORS request did not succeed" or "NetworkError". In normal same-origin setups, qui does not need any CORS configuration and keeps CORS disabled. ## What qui does - Detects likely SSO/CORS failures on `/api/*` requests. - Performs a single top-level navigation so the SSO login can complete. ## What you must configure - Keep the auth flow same-origin if possible. - Configure CORS **on the SSO proxy** (not in qui) for the auth endpoints. - Allow credentials and handle `OPTIONS` preflight when required. ## Optional qui allowlist If another trusted website running in the user's browser must call qui from a different origin on the user's behalf, set an explicit allowlist: ```bash QUI__CORS_ALLOWED_ORIGINS=https://panel.example.com ``` Only explicit origins are accepted (`http(s)://host[:port]`). Wildcards and path/query/fragment values are rejected. If you still hit CORS errors after proxy configuration, capture the browser console error and open an issue. ## Real-time updates and reverse-proxy buffering qui pushes live torrent, stats, and instance-health updates to the UI over a Server-Sent Events (SSE) stream at `GET /api/stream` (the RSS view uses a similar stream). SSE is a long-lived HTTP response that the server flushes incrementally. Most reverse proxies **buffer responses by default**, which holds events back and makes the UI look frozen or stuck on "reconnecting" until the buffer fills. If the dashboard and torrent list do not update in real time behind your proxy, disable response buffering and allow long-lived connections for the stream endpoint: - **nginx** — for the qui location (or specifically `~ ^/api/stream`): ```nginx proxy_buffering off; proxy_cache off; proxy_read_timeout 1h; proxy_set_header Connection ""; # keep the upstream connection open proxy_http_version 1.1; ``` qui already sends `X-Accel-Buffering: no` style flushing, but `proxy_buffering off` is the reliable switch. - **Traefik** — SSE works without buffering by default; just ensure no `buffering` middleware (`maxResponseBodyBytes` / `memResponseBodyBytes`) is applied to the qui router. - **Caddy** — `reverse_proxy` streams responses without buffering by default; no extra configuration is required. Also make sure any idle/read timeout on the proxy is comfortably longer than a few seconds. qui sends a heartbeat every 5s and the client reconnects automatically, but an aggressive proxy timeout will cause unnecessary reconnects. Compression middlewares should not be applied to `text/event-stream` responses. --- ## API # API Overview ## Documentation Interactive API documentation is available at `/api/docs` using Swagger UI. You can explore all endpoints, view request/response schemas, and test API calls directly from your browser. ## API Keys API keys allow programmatic access to qui without using session cookies. Create and manage them in Settings → API Keys. Include your API key in the `X-API-Key` header: ```bash curl -H "X-API-Key: YOUR_API_KEY_HERE" \ http://localhost:7476/api/instances ``` ## Security Notes - API keys are shown only once when created - save them securely - Each key can be individually revoked without affecting others - Keys have the same permissions as the main user account --- ## Base URL # Base URL Configuration If you need to serve qui from a subdirectory (e.g., `https://example.com/qui/`), you can configure the base URL. ## Using Environment Variable ```bash QUI__BASE_URL=/qui/ ./qui ``` ## Using Configuration File Edit your `config.toml`: ```toml baseUrl = "/qui/" ``` ## With Nginx Reverse Proxy ```nginx # Redirect /qui to /qui/ for proper SPA routing location = /qui { return 301 /qui/; } location /qui/ { proxy_pass http://localhost:7476/qui/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ``` --- ## CLI Commands ## Generate Configuration File Create a default configuration file without starting the server: ```bash # Generate config in OS-specific default location ./qui generate-config # Generate config in custom directory ./qui generate-config --config-dir /path/to/config/ # Generate config with custom filename ./qui generate-config --config-dir /path/to/myconfig.toml ``` ## User Management Create and manage user accounts from the command line: ```bash # Create initial user account ./qui create-user --username admin --password mypassword # Create user with prompts (secure password input) ./qui create-user --username admin # Change password for existing user (no old password required) ./qui change-password --username admin --new-password mynewpassword # Change password with secure prompt ./qui change-password --username admin # Pipe passwords for scripting (works with both commands) echo "mypassword" | ./qui create-user --username admin echo "newpassword" | ./qui change-password --username admin printf "password" | ./qui change-password --username admin ./qui change-password --username admin < password.txt # All commands support custom config/data directories ./qui create-user --config-dir /path/to/config/ --username admin ``` ### Notes - Only one user account is allowed in the system - Passwords must be at least 8 characters long - Interactive prompts use secure input (passwords are masked) - Supports piped input for automation and scripting - Commands will create the database if it doesn't exist - No password confirmation required - perfect for automation ### Reset a Forgotten Password {#reset-password} If you've forgotten your password, use the `change-password` command to set a new one. No old password is required. **Linux / macOS:** ```bash ./qui change-password --username admin --new-password mynewpassword ``` **Windows (Command Prompt):** Navigate to the folder containing `qui.exe` and run: ```batch qui.exe change-password --username admin --new-password mynewpassword ``` **Docker:** ```bash docker exec -it qui change-password --username admin --new-password mynewpassword ``` Replace `admin` with your username and `mynewpassword` with your desired password (minimum 8 characters). ## Update Command Keep your qui installation up-to-date: ```bash # Update to the latest version ./qui update ``` ## Command Line Flags ```bash # Specify config directory (config.toml will be created inside) ./qui serve --config-dir /path/to/config/ # Specify data directory for database and other data files ./qui serve --data-dir /path/to/data/ ``` ## Database Migration Offline SQLite to Postgres migration: ```bash # 0) Stop qui first (no writes during migration) # (example) docker compose stop qui # 1) Create the target Postgres database first (required) # (example) createdb -h localhost -p 5432 -U user qui # (or in psql) CREATE DATABASE qui; # 2) Optional: backup the SQLite file cp /path/to/qui.db /path/to/qui.db.bak # 3) Validate source + destination without importing rows ./qui db migrate \ --from-sqlite /path/to/qui.db \ --to-postgres "postgres://user:pass@localhost:5432/qui?sslmode=disable" \ --dry-run # 4) Apply migration (schema bootstrap + table copy + identity reset) ./qui db migrate \ --from-sqlite /path/to/qui.db \ --to-postgres "postgres://user:pass@localhost:5432/qui?sslmode=disable" \ --apply # 5) Point qui at Postgres and start it again # - config.toml: databaseEngine=postgres + databaseDsn=... # - or env: QUI__DATABASE_ENGINE=postgres + QUI__DATABASE_DSN=... ``` Notes: - Run this while qui is stopped. - Create the target Postgres database before running migration. - `--dry-run` and `--apply` are mutually exclusive. - The command copies all runtime tables except migration history. - The migrator bootstraps schema/tables inside the destination DB, but does not create the database itself. - The output includes per-table row counts for SQLite and Postgres. ### FAQ **Q: Why is `cross_seed_feed_items` row count lower in Postgres after migration?** This is expected when the SQLite file contains historical rows whose `indexer_id` no longer exists in `torznab_indexers`. Postgres enforces the foreign key strictly, so migration keeps only rows that still have valid parent records. You can verify this in SQLite: ```sql SELECT COUNT(*) AS orphaned_rows FROM cross_seed_feed_items f LEFT JOIN torznab_indexers i ON i.id = f.indexer_id WHERE i.id IS NULL; ``` If `orphaned_rows` matches the migration delta (`sqlite_count - postgres_count`), migration behavior is working as intended. --- ## Environment Variables Configuration is stored in `config.toml` (created automatically on first run, or manually with `qui generate-config`). You can also use environment variables: For the complete list (including `config.toml` keys, defaults, and notes), see [Configuration Reference](./reference). ## Server ```bash QUI__HOST=0.0.0.0 # Listen address QUI__PORT=7476 # Port number QUI__BASE_URL=/qui/ # Optional: serve from subdirectory ``` ## CORS ```bash QUI__CORS_ALLOWED_ORIGINS=https://sso.example.com,https://panel.example.com # Optional: explicit CORS allowlist (empty disables CORS) ``` `QUI__CORS_ALLOWED_ORIGINS` accepts comma/space-separated origins. Entries must be explicit `http(s)://host[:port]` values, without wildcards, paths, query strings, fragments, or userinfo. ## Security ```bash QUI__SESSION_SECRET_FILE=... # Path to file containing secret. Takes precedence over QUI__SESSION_SECRET QUI__SESSION_SECRET=... # Auto-generated if not set ``` ## Logging ```bash QUI__LOG_LEVEL=INFO # Options: ERROR, DEBUG, INFO, WARN, TRACE QUI__LOG_PATH=... # Optional: log file path QUI__LOG_MAX_SIZE=50 # Optional: rotate when log file exceeds N megabytes (default: 50) QUI__LOG_MAX_BACKUPS=3 # Optional: retain N rotated files (default: 3, 0 keeps all) ``` When `logPath` is set the server writes to disk using size-based rotation. Adjust `logMaxSize` and `logMaxBackups` in `config.toml` or the corresponding environment variables to control the rotation thresholds and retention. ## Storage ```bash QUI__DATA_DIR=... # Optional: custom runtime data directory (default: next to config) ``` `QUI__DATA_DIR` is always used for runtime assets (logs, tracker icon cache, etc.). With `QUI__DATABASE_ENGINE=sqlite`, `qui.db` is also stored there. ## Database ```bash QUI__DATABASE_ENGINE=sqlite # sqlite or postgres (default: sqlite) QUI__DATABASE_DSN=... # Full Postgres DSN (preferred for Postgres) QUI__DATABASE_HOST=localhost # Postgres host when not using DATABASE_DSN QUI__DATABASE_PORT=5432 # Postgres port when not using DATABASE_DSN QUI__DATABASE_USER=... # Postgres user when not using DATABASE_DSN QUI__DATABASE_PASSWORD=... # Postgres password when not using DATABASE_DSN QUI__DATABASE_NAME=qui # Postgres database name when not using DATABASE_DSN QUI__DATABASE_SSL_MODE=disable # disable, require, verify-ca, verify-full QUI__DATABASE_CONNECT_TIMEOUT=10 # Connect timeout in seconds QUI__DATABASE_MAX_OPEN_CONNS=25 # Postgres pool max open connections QUI__DATABASE_MAX_IDLE_CONNS=5 # Postgres pool max idle connections QUI__DATABASE_CONN_MAX_LIFETIME=300 # Max connection lifetime in seconds ``` ## Cross-Seed ```bash QUI__CROSS_SEED_RECOVER_ERRORED_TORRENTS=false # Optional: recover errored/missingFiles torrents; can add ~25+ minutes per torrent (default: false) ``` ## Tracker Icons ```bash QUI__TRACKER_ICONS_FETCH_ENABLED=false # Optional: set to false to disable remote tracker icon fetching (default: true) ``` ## Updates ```bash QUI__CHECK_FOR_UPDATES=false # Optional: disable update checks and UI indicators (default: true) ``` ## Profiling (pprof) ```bash QUI__PPROF_ENABLED=true # Optional: enable pprof server on :6060 (default: false) ``` ## Metrics ```bash QUI__METRICS_ENABLED=true # Optional: enable Prometheus metrics (default: false) QUI__METRICS_HOST=127.0.0.1 # Optional: metrics server bind address (default: 127.0.0.1) QUI__METRICS_PORT=9074 # Optional: metrics server port (default: 9074) QUI__METRICS_BASIC_AUTH_USERS=user:hash # Optional: basic auth for metrics (bcrypt hashed) ``` ## Authentication ```bash QUI__AUTH_DISABLED=true # Optional: disable built-in auth (default: false) QUI__I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA=true # Required confirmation to actually disable auth QUI__AUTH_DISABLED_ALLOWED_CIDRS=127.0.0.1/32,192.168.1.0/24 # Required when auth is disabled (IPs or CIDRs) ``` Built-in authentication is disabled only when: - `QUI__AUTH_DISABLED=true` - `QUI__I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA=true` - `QUI__AUTH_DISABLED_ALLOWED_CIDRS` is set to one or more allowed IPs/CIDR ranges If auth is disabled and `QUI__AUTH_DISABLED_ALLOWED_CIDRS` is missing or invalid, qui refuses to start and rejects invalid live reloads. `QUI__AUTH_DISABLED_ALLOWED_CIDRS` accepts comma-separated entries. Each entry may be a canonical CIDR (`192.168.1.0/24`) or a single IP (`10.0.0.5`, treated as `/32` or `/128`). Non-canonical CIDRs with host bits set (for example `10.0.0.5/8`) are rejected. `QUI__OIDC_ENABLED=true` cannot be combined with auth-disabled mode. Only use this when qui runs behind a reverse proxy that already handles authentication (e.g., Authelia, Authentik, Caddy with forward_auth). See the [Configuration Reference](./reference#authentication) for a full explanation of the risks. Built-in health endpoints (`/health`, `/healthz/readiness`, `/healthz/liveness`) always allow loopback probes, so the official Docker image healthcheck continues to work even if your allowlist only includes the reverse proxy subnet(s). ## External Programs Configure the allow list from `config.toml`; there is no environment override to keep it read-only from the UI. ## Default Locations - **Linux/macOS**: `~/.config/qui/config.toml` - **Windows**: `%APPDATA%\qui\config.toml` --- ## OIDC # OpenID Connect (OIDC) Set `QUI__OIDC_ENABLED=true` to hand authentication off to an external identity provider. The built-in login screen automatically offers a "Sign in with OIDC" button when the backend detects a valid OIDC configuration. If your provider advertises PKCE (`S256`) support, qui uses it automatically for the authorization flow. No extra qui setting is required. To confirm it is active, inspect `/api/auth/oidc/config` and verify `authorizationUrl` includes both `code_challenge=` and `code_challenge_method=S256`. qui does not currently emit a dedicated "PKCE enabled" log line, so the authorize URL is the easiest check. For the full mapping (TOML keys + environment variables + defaults), see [Configuration Reference](./reference). ## Configuration Options | Variable | Description | |----------|-------------| | `QUI__OIDC_ISSUER` | Issuer URL from your IdP (e.g. `https://auth.example.com/realms/main`) | | `QUI__OIDC_CLIENT_ID` | Client ID registered for qui | | `QUI__OIDC_CLIENT_SECRET` | Client secret generated by the provider | | `QUI__OIDC_CLIENT_SECRET_FILE` | Path to file containing client secret. Takes precedence over `QUI__OIDC_CLIENT_SECRET` | | `QUI__OIDC_REDIRECT_URL` | Must match the redirect URI allowed by the provider | | `QUI__OIDC_DISABLE_BUILT_IN_LOGIN` | Set to `true` to hide the local username/password form when OIDC is enabled | ## Redirect URL Format For a default install, use: ``` http://localhost:7476/api/auth/oidc/callback ``` When reverse proxying, include your base URL: ``` https://host/qui/api/auth/oidc/callback ``` If you run OIDC behind an SSO proxy (Cloudflare Access, Pangolin, etc.), review [SSO proxies and CORS](../advanced/sso-proxy-cors) for browser fetch behavior and proxy-side configuration. ## Example Configuration ```bash QUI__OIDC_ENABLED=true \ QUI__OIDC_ISSUER=https://auth.example.com/realms/main \ QUI__OIDC_CLIENT_ID=qui \ QUI__OIDC_CLIENT_SECRET=super-secret-value \ QUI__OIDC_REDIRECT_URL=https://qui.example.com/api/auth/oidc/callback \ QUI__OIDC_DISABLE_BUILT_IN_LOGIN=true ``` You can set the same options in `config.toml` using the `oidc*` keys generated by `qui generate-config`. --- ## Configuration Reference qui supports configuration via: - `config.toml` (auto-created on first run, or manually via `qui generate-config`) - environment variables (`QUI__...`) to override `config.toml` This page documents both in one place. ## Precedence Highest wins: 1. `QUI__*_FILE` (for supported secrets) 2. `QUI__*` environment variables 3. `config.toml` 4. built-in defaults ## Config File Location Default `config.toml` locations: - Linux/macOS: `~/.config/qui/config.toml` - Windows: `%APPDATA%\\qui\\config.toml` Override with `--config-dir`: - directory path: `--config-dir /path/to/config/` (uses `/path/to/config/config.toml`) - file path (back-compat): `--config-dir /path/to/custom.toml` ## Notes On Reloading qui watches `config.toml` for changes. Some settings are applied immediately (for example logging, tracker icon fetching, and auth-disabled settings). For anything else, restart qui after changes to be safe. ## Settings | TOML key | Environment variable | Type | Default | Notes | |---|---|---:|---|---| | `host` | `QUI__HOST` | string | `localhost` (or `0.0.0.0` in containers) | Bind address for the main HTTP server. | | `port` | `QUI__PORT` | int | `7476` | Port for the main HTTP server. | | `baseUrl` | `QUI__BASE_URL` | string | `/` | Serve qui from a subdirectory (example: `/qui/`). | | `corsAllowedOrigins` | `QUI__CORS_ALLOWED_ORIGINS` | string[] | empty list | Explicit CORS allowlist. Empty disables CORS. Origins must be `http(s)://host[:port]`; wildcards are rejected; default ports are normalized. Restart required. | | `sessionSecret` | `QUI__SESSION_SECRET` / `QUI__SESSION_SECRET_FILE` | string | auto-generated | WARNING: changing breaks decryption of stored instance passwords; you must re-enter them in the UI. | | `logLevel` | `QUI__LOG_LEVEL` | string | `INFO` | `ERROR`, `DEBUG`, `INFO`, `WARN`, `TRACE`. Applied immediately. | | `logPath` | `QUI__LOG_PATH` | string | empty | If empty: logs to stdout. Relative paths resolve relative to the config directory. Applied immediately. | | `logMaxSize` | `QUI__LOG_MAX_SIZE` | int | `50` | MiB threshold before rotation. Applied immediately. | | `logMaxBackups` | `QUI__LOG_MAX_BACKUPS` | int | `3` | Rotated files retained. `0` keeps all. Applied immediately. | | `dataDir` | `QUI__DATA_DIR` | string | empty | If empty: uses the directory containing `config.toml`. Always used for non-database assets (logs, tracker icon cache, etc.). When `databaseEngine=sqlite`, `qui.db` also lives here. Restart recommended. | | `databaseEngine` | `QUI__DATABASE_ENGINE` | string | `sqlite` | `sqlite` or `postgres`. Existing installs should keep `sqlite` unless you migrate. Restart required. | | `databaseDsn` | `QUI__DATABASE_DSN` / `QUI__DATABASE_DSN_FILE` | string | empty | Full Postgres DSN. Preferred when `databaseEngine=postgres`. | | `databaseHost` | `QUI__DATABASE_HOST` | string | `localhost` | Postgres host when not using `databaseDsn`. | | `databasePort` | `QUI__DATABASE_PORT` | int | `5432` | Postgres port when not using `databaseDsn`. | | `databaseUser` | `QUI__DATABASE_USER` | string | empty | Postgres user when not using `databaseDsn`. | | `databasePassword` | `QUI__DATABASE_PASSWORD` / `QUI__DATABASE_PASSWORD_FILE` | string | empty | Postgres password when not using `databaseDsn`. | | `databaseName` | `QUI__DATABASE_NAME` | string | `qui` | Postgres database name when not using `databaseDsn`. | | `databaseSSLMode` | `QUI__DATABASE_SSL_MODE` | string | `disable` | Common values: `disable`, `require`, `verify-ca`, `verify-full`. | | `databaseConnectTimeout` | `QUI__DATABASE_CONNECT_TIMEOUT` | int | `10` | Postgres connect timeout in seconds. | | `databaseMaxOpenConns` | `QUI__DATABASE_MAX_OPEN_CONNS` | int | `25` | Postgres pool max open connections. | | `databaseMaxIdleConns` | `QUI__DATABASE_MAX_IDLE_CONNS` | int | `5` | Postgres pool max idle connections. | | `databaseConnMaxLifetime` | `QUI__DATABASE_CONN_MAX_LIFETIME` | int | `300` | Postgres connection max lifetime in seconds. | | `checkForUpdates` | `QUI__CHECK_FOR_UPDATES` | bool | `true` | Controls update checks and UI indicators. Restart recommended. | | `trackerIconsFetchEnabled` | `QUI__TRACKER_ICONS_FETCH_ENABLED` | bool | `true` | Disable to prevent remote tracker favicon fetches. Applied immediately. | | `crossSeedRecoverErroredTorrents` | `QUI__CROSS_SEED_RECOVER_ERRORED_TORRENTS` | bool | `false` | When enabled, cross-seed automation attempts recovery (pause, recheck, resume) for errored/missingFiles torrents. Can add 25+ minutes per torrent. Restart recommended. | | `pprofEnabled` | `QUI__PPROF_ENABLED` | bool | `false` | Enables pprof server on `:6060` (`/debug/pprof/`). Restart required. | | `metricsEnabled` | `QUI__METRICS_ENABLED` | bool | `false` | Enables a Prometheus metrics server (separate port). Restart required. | | `metricsHost` | `QUI__METRICS_HOST` | string | `127.0.0.1` | Metrics server bind address. Restart required. | | `metricsPort` | `QUI__METRICS_PORT` | int | `9074` | Metrics server port. Restart required. | | `metricsBasicAuthUsers` | `QUI__METRICS_BASIC_AUTH_USERS` | string | empty | Optional basic auth: `user:bcrypt_hash` or `user1:hash1,user2:hash2`. Restart required. | | `externalProgramAllowList` | (none) | string[] | empty list | Restricts which executables can be launched from the UI. Only configurable via `config.toml` (no env override). | | `authDisabled` | `QUI__AUTH_DISABLED` | bool | `false` | Disable all built-in authentication. **Both** this and `I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA` must be `true` for auth to be disabled. See [Authentication](#authentication) below. Applied on config reload. | | `I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA` | `QUI__I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA` | bool | `false` | Required confirmation for `authDisabled`. Acknowledges that running without authentication can lead to unauthorized access to your torrent clients and potential bans from private trackers. Applied on config reload. | | `authDisabledAllowedCIDRs` | `QUI__AUTH_DISABLED_ALLOWED_CIDRS` | string[] | empty list | Required when auth is disabled. Restricts access to specific client IPs/CIDRs. Entries may be canonical CIDRs or single IPs. Applied on config reload. | | `oidcEnabled` | `QUI__OIDC_ENABLED` | bool | `false` | Enable OpenID Connect authentication. Restart required. | | `oidcIssuer` | `QUI__OIDC_ISSUER` | string | empty | OIDC issuer URL. Restart required. | | `oidcClientId` | `QUI__OIDC_CLIENT_ID` | string | empty | OIDC client ID. Restart required. | | `oidcClientSecret` | `QUI__OIDC_CLIENT_SECRET` / `QUI__OIDC_CLIENT_SECRET_FILE` | string | empty | OIDC client secret. Restart required. | | `oidcRedirectUrl` | `QUI__OIDC_REDIRECT_URL` | string | empty | Must match the provider redirect URI (include `baseUrl` when reverse proxying). Restart required. | | `oidcDisableBuiltInLogin` | `QUI__OIDC_DISABLE_BUILT_IN_LOGIN` | bool | `false` | Hide local username/password form when OIDC is enabled. Restart required. | ## Authentication To disable qui's built-in authentication, all of the following are required: ```bash QUI__AUTH_DISABLED=true QUI__I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA=true QUI__AUTH_DISABLED_ALLOWED_CIDRS=127.0.0.1/32,192.168.1.0/24 ``` The second variable exists as an explicit acknowledgement of the risks. `QUI__AUTH_DISABLED_ALLOWED_CIDRS` is mandatory and acts as a hard IP allowlist. If auth is disabled and the value is missing/invalid, qui will refuse to start and reject invalid live reloads. Entries can be: - Canonical CIDR ranges (`192.168.1.0/24`) - Single IPs (`10.0.0.5`), automatically treated as `/32` (IPv4) or `/128` (IPv6) Non-canonical CIDRs with host bits set (for example `10.0.0.5/8`) are rejected. `oidcEnabled` and auth-disabled mode cannot be enabled at the same time. When authentication is disabled: - Requests are allowed only if the direct client IP matches `authDisabledAllowedCIDRs`. - Built-in health endpoints (`/health`, `/healthz/readiness`, `/healthz/liveness`) still allow loopback probes so the official Docker image healthcheck works without adding `127.0.0.1/32` or `::1/128` to your reverse proxy allowlist. - `/api/auth/me` returns a synthetic `admin` user so the frontend works without login. - `/api/auth/validate` returns a synthetic `admin` user so callback/session checks work without login. - The setup screen is skipped entirely. **Only use this if qui is behind a reverse proxy that already handles authentication** (e.g., Authelia, Authentik, Caddy with forward_auth). :::danger Private tracker risks If you use private trackers, running qui without authentication is especially dangerous. Anyone with network access can control your torrent clients — adding, removing, or modifying torrents. Actions performed by unauthorized users (hit-and-runs, ratio manipulation, uploading unwanted content) can get your accounts permanently banned from private trackers, with no way to recover. ::: If `QUI__AUTH_DISABLED` is set without `QUI__I_ACKNOWLEDGE_THIS_IS_A_BAD_IDEA`, qui will log a warning and keep authentication enabled. ## CORS By default, qui does not send CORS allow headers. To allow browser requests from another trusted origin, set `corsAllowedOrigins` (or `QUI__CORS_ALLOWED_ORIGINS`) to an explicit allowlist: ```bash QUI__CORS_ALLOWED_ORIGINS=https://sso.example.com,https://panel.example.com ``` Rules: - only explicit origins are allowed (`http://` or `https://` + host + optional non-default port) - wildcards are rejected (`*`, `https://*.example.com`, etc.) - path/query/fragment/userinfo are rejected - invalid values refuse startup; invalid live reloads are rejected and keep the last valid allowlist For SSO proxy setups, prefer configuring CORS on the proxy auth endpoints first. See [SSO Proxies and CORS](../advanced/sso-proxy-cors). ## Example `config.toml` ```toml host = "0.0.0.0" port = 7476 baseUrl = "/qui/" logLevel = "INFO" logPath = "log/qui.log" logMaxSize = 50 logMaxBackups = 3 trackerIconsFetchEnabled = false externalProgramAllowList = [ "/usr/local/bin", "/home/user/bin/my-script", ] ``` --- ## Automations Automations are a rule-based engine that automatically applies actions to torrents based on conditions. Use them to manage speed limits, delete old torrents, organize with tags and categories, and more. ## How Automations Work Automations are evaluated in **sort order** (first match wins for exclusive actions like delete). Each rule can match torrents using a flexible query builder with nested conditions. - **Automatic** - Background service scans torrents every 20 seconds - **Per-Rule Intervals** - Each rule can have its own interval (minimum 60 seconds, default 15 minutes) - **Per-Rule Notifications** - If notification targets are configured, each rule can opt in or out of sending automation notifications - **Manual** - Click "Apply Now" to trigger immediately (bypasses interval checks) - **Manual dry-run** - Run "Dry-run now" from the workflow dialog or "Run dry-run now" from the workflow menu - **Debouncing** - Same torrent won't be re-processed within 2 minutes ## Query Builder The query builder supports complex nested conditions with AND/OR groups. Drag conditions to reorder them. ### Available Condition Fields #### Identity Fields | Field | Description | | -------- | -------------------------------------------------------- | | Name | Torrent display name (supports cross-category operators) | | Hash | Info hash | | Infohash v1 | BitTorrent v1 info hash | | Infohash v2 | BitTorrent v2 info hash | | Magnet URI | Magnet link for the torrent | | Category | qBittorrent category | | Tags | Set-based tag matching | | State | Status filter (see State Values below) | | Created By | Torrent creator metadata | #### Path Fields | Field | Description | | ------------ | -------------------- | | Save Path | Download location | | Content Path | Full path to content | | Download Path | Session download path from qBittorrent | #### Size Fields (bytes) | Field | Description | | ----------- | -------------------------------------------------------------------------------------- | | Size | Selected file size | | Total Size | Total torrent size | | Completed | Completed bytes | | Downloaded | Bytes downloaded | | Downloaded (Session) | Downloaded in current session | | Uploaded | Bytes uploaded | | Uploaded (Session) | Uploaded in current session | | Amount Left | Remaining bytes | | Free Space | Free space on disk (configurable source - see [Free Space Source](#free-space-source)) | #### Duration Fields (seconds) | Field | Description | | ------------------------ | ------------------------------------- | | Added Age | Time since added | | Completion Age | Time since completed | | Inactive Time | Time since last activity | | Seen Complete Age | Time since torrent was last complete | | ETA | Estimated time to completion | | Reannounce In | Seconds until next announce | | Seeding Time | Time spent seeding | | Time Active | Total active time | | Max Seeding Time | Configured max seeding time | | Max Inactive Seeding Time | Configured max inactive seeding time | | Seeding Time Limit | Torrent seeding time limit | | Inactive Seeding Time Limit | Torrent inactive seeding time limit | #### System Time Fields These fields use qui's current system time when the rule is evaluated. They are useful for time-window automations such as "only run at night" or "apply different actions on weekends." | Field | Description | | ------------------ | ----------------------------------------- | | System Hour | Current hour (`0-23`) | | System Minute | Current minute (`0-59`) | | System Day of Week | Current weekday (`0=Sun` to `6=Sat`) | | System Day | Current day of month (`1-31`) | | System Month | Current month (`1-12`) | | System Year | Current year | #### Progress Fields | Field | Description | | ----------- | ---------------------------- | | Ratio | Upload/download ratio | | Ratio Limit | Configured ratio limit | | Max Ratio | qBittorrent max ratio value | | Uploaded / Size | Uploaded bytes divided by total torrent size. Use this instead of Ratio for cross-seeded torrents. | | Progress | Download progress (0-100%) | | Availability | Distributed copies available | | Popularity | Swarm popularity metric | #### Speed Fields (bytes/s) | Field | Description | | -------------- | ------------------------------ | | Download Speed | Current download speed | | Upload Speed | Current upload speed | | Download Limit | Configured download speed limit | | Upload Limit | Configured upload speed limit | #### Peer/Queue Fields | Field | Description | | --------------- | -------------------------------- | | Active Seeders | Currently connected seeders | | Active Leechers | Currently connected leechers | | Total Seeders | Tracker-reported seeders | | Total Leechers | Tracker-reported leechers | | Trackers Count | Number of trackers | | Queue Priority | Torrent queue priority value | #### Tracker/Status Fields | Field | Description | | --------------- | ------------------------------------------------------------- | | Tracker | Primary tracker (URL, domain, or customization display name) | | Trackers (All) | All tracker URLs/domains/display names for this torrent | | Private | Boolean - is private tracker | | Is Unregistered | Boolean - tracker reports unregistered | | Comment | Torrent comment field | Note: if you have **Settings → Tracker Customizations** configured, the **Tracker** condition can match the display name in addition to the raw URL/domain. #### Mode Fields | Field | Description | | ------------------------- | ------------------------------------------------ | | Auto-managed | Managed by automatic torrent management | | First/Last Piece Priority | First and last pieces are prioritized | | Force Start | Ignores queue limits and starts immediately | | Sequential Download | Downloads pieces sequentially | | Super Seeding | Super-seeding mode enabled | #### Release/Grouping Fields | Field | Description | | ------------------ | ------------------------------------------------------------------------------------------- | | Content Type | Derived from release name parsing (useful for grouping; may be empty) | | Effective Name | Normalized title derived from release parsing (useful for grouping; may be empty) | | Release Source | Parsed release specifier (e.g. `WEBDL`, `WEBRIP`, `BLURAY`; may be empty) | | Release Resolution | Parsed release specifier (e.g. `1080p`; may be empty) | | Release Codec | Parsed release specifier (e.g. `HEVC`; may be empty) | | Release HDR | Parsed release specifier (e.g. `DV`, `HDR`; may be empty) | | Release Audio | Parsed release specifier (e.g. `TrueHD`; may be empty) | | Release Channels | Parsed release specifier (e.g. `5.1`; may be empty) | | Release Group | Parsed release specifier (e.g. `NTb`; may be empty) | | Group Size | Size of the selected group for this condition (requires grouping; see [Grouping](#grouping)) | | Is Grouped | Boolean - true when selected group size > 1 (requires grouping; see [Grouping](#grouping)) | #### Cross-Seed Fields | Field | Description | | ---------------------------------- | -------------------------------------------------------------------------------- | | Exists on Other Instance | Boolean - a matching torrent exists on at least one other active instance | | Seeding on Other Instance | Boolean - a matching torrent is actively seeding on at least one other active instance | | Cross-seed Exists on Same Instance | Boolean - another matching torrent exists on this instance | | Cross-seed Seeding on Same Instance | Boolean - another matching torrent is actively seeding on this instance | #### Filesystem Fields | Field | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Hardlink Scope | `none`, `torrents_only`, or `outside_qbittorrent` (requires local filesystem access) | | Hardlink Scope (Cross-Instance) | `none`, `torrents_only`, or `outside_qbittorrent` considering ALL instances (requires local filesystem access) | | Has Missing Files | Boolean - completed torrent has files missing on disk (requires local filesystem access) | ### State Values The State field matches these status buckets: | State | Description | | -------------- | ---------------------------- | | `downloading` | Actively downloading | | `uploading` | Actively uploading | | `completed` | Download finished | | `stopped` | Paused by user | | `active` | Has transfer activity | | `inactive` | No current activity | | `running` | Not paused | | `stalled` | No peers available | | `stalled_uploading` | Stalled while uploading | | `stalled_downloading` | Stalled while downloading | | `errored` | Has errors | | `tracker_down` | Tracker unreachable | | `checking` | Verifying files | | `checkingResumeData` | Checking resume data | | `moving` | Moving files | | `missingFiles` | Files not found | | `unregistered` | Tracker reports unregistered | ### Operators **String:** equals, not equals, contains, not contains, starts with, ends with, matches regex **Numeric:** `=`, `!=`, `>`, `>=`, `<`, `<=`, between **Boolean:** is, is not **State:** is, is not **Cross-Category (Name field only):** - `EXISTS_IN` - Exact name match in target category - `CONTAINS_IN` - Partial/normalized name match in target category ### Regex Support There are two ways to use regex in filter conditions: **The `matches regex` operator** is a dedicated operator where the value is always treated as a regex pattern. The condition is true if the pattern matches anywhere in the field value. **The regex toggle (`.*` button)** appears next to the value input on other string operators such as `equals`, `contains`, `not contains`, `starts with`, and `ends with`. When enabled, the value is treated as a regex pattern. :::warning Regex toggle overrides the selected operator When the regex toggle is enabled, the selected operator's logic (negation, containment, prefix/suffix matching) is **not applied**. The condition becomes a simple regex match, equivalent to `matches regex`, regardless of which operator is selected in the dropdown. This means `not contains` with the regex toggle enabled does **not** negate the match. It behaves the same as `matches regex` -- if the pattern is found, the condition evaluates to true. To negate a regex match, use the **NOT toggle** (the `IF / IF NOT` button at the start of the condition row) together with the `matches regex` operator. ::: Full RE2 (Go regex) syntax is supported. Patterns are case-insensitive by default. Regex can be used either by selecting **matches regex** or by enabling the **Regex** toggle for a condition: - When regex is enabled, the condition checks whether the regex matches the field value. - `not equals` and `not contains` invert the regex result (true only if the regex does **not** match). - Operators like `equals`, `contains`, `starts with`, and `ends with` are treated as regex match when regex is enabled. - Regex is not implicitly anchored: use `^` and `$` if you want an exact/full-string match (example: `^BHD$`). Field notes: - **Tracker**: checked against multiple candidates (raw URL, extracted domain, and optional customization display name). Negative regex passes only if **none** of the candidates match. - **Tags**: without regex, string operators are applied per-tag. With regex enabled, the regex is matched against the full raw tags string. The UI validates patterns and shows helpful error messages for invalid regex. ### Tag conditions Each tag condition checks against a **single value**. The value field does not support comma-separated lists -- if you enter `tag1, tag2, tag3` as the value, it will be treated as one literal string, not three separate tags. Without regex enabled, tag operators (`equals`, `not equals`, `contains`, `not contains`) compare the condition value against each of the torrent's tags individually. - `equals` / `not equals`: exact tag membership (case-insensitive) - `contains` / `not contains`: substring match per tag (case-insensitive) :::warning Tag operators: `contains` is substring matching `Tags contains tag1` will also match torrents tagged `tag10`. For exact tag membership, prefer `equals` / `not equals` with one condition per tag and combine them with an **OR group**. ::: #### Matching any of multiple tags To check whether a torrent has **any one of** several tags, create an **OR group** with one condition per tag (exact match): | # | Field | Operator | Value | | --- | ----- | -------- | ----- | | 1 | Tags | equals | tag1 | | 2 | Tags | equals | tag2 | | 3 | Tags | equals | tag3 | Group these with **OR** logic so the rule matches when at least one tag is present. To **exclude** torrents that have any of several tags, create an **AND group** of `not equals` conditions (exact match): | # | Field | Operator | Value | | --- | ----- | ---------- | ----- | | 1 | Tags | not equals | tag1 | | 2 | Tags | not equals | tag2 | | 3 | Tags | not equals | tag3 | Group these with **AND** logic -- all three must be true, meaning none of those tags are present. #### Using regex for tag matching As an alternative to multiple conditions, you can use regex to match against several tags in a single condition. When regex is enabled, the pattern matches against the **full raw tag string** (e.g. `cross-seed, noHL, racing`) rather than checking each tag individually. For example, to exclude torrents tagged with `tag1` or `tag2`, use a single condition: - Field: `Tags` - Toggle: `IF NOT` (negate the match) - Operator: `matches regex` - Value: `(^|,\s*)(tag1|tag2)(\s*,|$)` This evaluates the regex against the raw tags string. The delimiter-aware pattern ensures `tag1` does not match `tag10`. The `IF NOT` toggle then negates the result, so the condition is true only for torrents that do **not** have either tag. ### Live impact preview and dry-run Workflow editing includes immediate feedback for delete/category workflows: - **Live impact preview** in the workflow dialog updates automatically as conditions/actions change. - Shows current **impacted count** plus a small preview list of matching torrents. - For category rules, the preview summary splits direct matches and cross-seed expansions. You can also run dry-runs immediately without waiting for interval execution: - **Workflow dialog:** `Dry-run now` - **Workflow list menu:** `Run dry-run now` Dry-run executes the current workflow config as a simulation only and writes results to automation activity. No-match behavior: - Manual dry-runs still log a `dry_run_no_match` summary row when nothing matches. - Scheduled dry-run rules do **not** log no-match rows (to avoid event noise). ## Torrent Sorting & Scoring By default, torrents matched by an automation are processed oldest-first. However, you can customize the **Torrent Priority** to control exactly which torrents are processed first. This is useful for actions like **Delete** combined with **Free Space**, where the priority determines which torrents are removed first to free up space. ### Priority Types | Type | Description | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Default**| Standard oldest-first priority. | | **Simple** | Prioritize by a single numeric, duration, or string field (e.g., `Size`, `Added Age`, `Name`) in ascending or descending order. | | **Score** | Advanced rule-based priority. Torrents are scored based on custom rules, and prioritized in ascending or descending order of their total score. | ### Score-Based Priority Score-based priority allows you to rank torrents using multiple combined factors. You define **Score Rules** that evaluate each torrent and contribute to its total score. Available score rule types: - **Field Multiplier**: Extracts a numeric value from the torrent (like `Size` or `Time Active`), multiplies it by a specified multiplier, and adds it to the score. - **Conditional**: Evaluates a standard query condition (see [Query Builder](#query-builder)). If the condition is true, a static value is added to the score. Torrents are then processed by their final computed score. The computed scores are displayed in the **Live impact preview** so you can verify your scoring logic. ## Tracker Matching This is sort of not needed, since you can already scope trackers outside the workflows. But its available either way. | Pattern | Example | Matches | | ------- | --------------------- | --------------------- | | All | `*` | Every tracker | | Exact | `tracker.example.com` | Only that domain | | Glob | `*.example.com` | Subdomains | | Suffix | `.example.com` | Domain and subdomains | Separate multiple patterns with commas, semicolons, or pipes. All matching is case-insensitive. ## Grouping Grouping lets an automation treat "related torrents" as a single unit for: - **Group-aware conditions**: `GROUP_SIZE`, `IS_GROUPED` - **Group expansion**: apply an action to every torrent in the group (instead of only the matched torrent) - **Strict matching**: grouped expansion runs only when all members satisfy the action conditions ### Group-Scoped Condition Fields `GROUP_SIZE` and `IS_GROUPED` can be scoped per condition row: - Set `groupId` on each `GROUP_SIZE` / `IS_GROUPED` condition if you want explicit per-row grouping. - If a grouped condition row has no `groupId`, qui uses `conditions.grouping.defaultGroupId`. - If no default is configured, legacy unscoped grouped conditions fall back to `cross_seed_content_save_path`. This allows multiple grouped conditions in the same workflow, each using different grouping strategies. ### Action Expansion Some actions accept a `groupId`. When set, qui expands the action to all torrents in that group. Group expansion semantics are strict: - Every member in the expanded group must satisfy the action condition checks for that rule. - If any member fails (or the group cannot be resolved), qui skips the entire grouped action. - There is no "trigger-only fallback" when `groupId` is set. Built-in group IDs: - `cross_seed_content_path`: same Content Path (normalized) - `cross_seed_content_save_path`: same Content Path + Save Path (normalized) - `release_item`: same Content Type + Effective Name - `tracker_release_item`: same Tracker + Content Type + Effective Name - `hardlink_signature`: same physical file set signature (requires local filesystem access) ### Custom Groups (Advanced) Rules can define custom groups via `conditions.grouping.groups[]` with: - `id`: string - `keys`: list of key names combined to form the group key Supported keys: - `contentPath`, `savePath`, `effectiveName`, `contentType`, `tracker` - `rlsSource`, `rlsResolution`, `rlsCodec`, `rlsHDR`, `rlsAudio`, `rlsChannels`, `rlsGroup` - `hardlinkSignature` For content-path-based grouping where `Content Path == Save Path` (ambiguous), you can set: - `ambiguousPolicy: "verify_overlap"` (default for cross-seed groups) with `minFileOverlapPercent` (default `90`) - `ambiguousPolicy: "skip"` ## Actions Actions can be combined (except Delete which must be standalone). Each action supports an optional condition override. ### Speed Limits Set upload and/or download limits. Each field supports these modes: | Mode | Value | Description | | --------- | ----- | ------------------------------------------------------ | | No change | - | Don't modify this field | | Unlimited | 0 | Remove speed limit (qBittorrent treats 0 as unlimited) | | Custom | >0 | Specific limit in KiB/s or MiB/s | Applied in batches for efficiency. ### Share Limits Set ratio limit and/or seeding time limit. Each field supports these modes: | Mode | Value | Description | | ---------- | ----- | -------------------------------------------------- | | No change | - | Don't modify this field | | Use global | -2 | Follow qBittorrent's global share settings | | Unlimited | -1 | No limit for this field | | Custom | >=0 | Specific value (ratio as decimal, time in minutes) | Torrents stop seeding when any enabled limit is reached. #### Share limit action (Web API 2.15.1+) On instances whose qBittorrent Web API is **2.15.1** or newer, **When limits are reached** is available in the torrent share limit dialog and in automation workflows. It controls what happens when a torrent hits its configured ratio, seeding time, or inactive seeding limits. Stored and sent as the same **string enum names** qBittorrent expects for `setShareLimits` (Qt meta-object names, not numeric codes): | Option | Value (`shareLimitAction`) | Description | | ----------------------- | -------------------------- | ------------------------------------------- | | Default (use global) | omit or `default` | Follow qBittorrent's global setting | | Stop torrent | `Stop` | Pause the torrent | | Remove torrent | `Remove` | Remove from client, keep files | | Remove with content | `RemoveWithContent` | Remove from client and delete files | | Enable super seeding | `EnableSuperSeeding` | Switch to super seeding mode | #### Share limits matching mode (Web API 2.16.0+) **Limits matching mode** (match **any** limit vs **all** limits) is a separate Web API capability and requires **2.16.0** or newer. On slightly older 5.2 builds that only expose **2.15.1**, qui still shows the action above but hides this control until you upgrade qBittorrent. Values use **string enum names** for `setShareLimits`: | Option | Value (`shareLimitsMode`) | Description | | -------------------- | ------------------------- | ---------------------------------------- | | Default (use global) | omit or `default` | Follow qBittorrent's global setting | | Match any limit | `MatchAny` | Trigger when any single limit is reached | | Match all limits | `MatchAll` | Trigger only when all limits are reached | These options are hidden when the instance does not report the required Web API version (see [qBittorrent Version Compatibility](../advanced/compatibility.md)). Ratio and seeding time limits above still apply on older instances; only the extra controls are gated. These fields appear in both the torrent share limit dialog and the automation workflow editor when the connected qBittorrent instance supports them. On older instances, the fields are hidden and only the classic ratio/seeding time limits are sent. ### Pause Pause matching torrents. Only pauses if not already stopped. If a resume action is also present, last action wins. ### Resume Resume matching torrents. Only resumes if not already running. If a pause action is also present, last action wins. ### Force Recheck Force recheck matching torrents. - Triggers qBittorrent recheck for matched torrents. - Can be combined with other actions. - Supports optional condition override (like other actions). ### Force Reannounce Force reannounce matching torrents. - Triggers immediate tracker reannounce for matched torrents. - Can be combined with other actions. - Supports optional condition override (like other actions). ### Delete Remove torrents from qBittorrent. **Must be standalone** - cannot combine with other actions. | Mode | Description | | ----------------------------------- | ----------------------------------------------------------------------------- | | `delete` | Remove from client, keep files | | `deleteWithFiles` | Remove with files | | `deleteWithFilesPreserveCrossSeeds` | Remove files but preserve if cross-seeds detected | | `deleteWithFilesIncludeCrossSeeds` | Remove files and also delete all cross-seeded torrents sharing the same files | **Optional grouping (advanced):** Delete actions can specify a `groupId` to expand the deletion to all torrents in that group. - For `delete` (keep files): this is useful when you want "remove from client" to be cross-seed aware. - With `groupId`, keep-files delete is strict all-or-none: if any group member does not satisfy rule conditions, nothing in that group is removed. **Include cross-seeds mode behavior:** When a torrent matches the rule, the system finds other torrents that point to the same downloaded files (cross-seeds/duplicates) and deletes them together. This is useful when you want to fully remove content and all its cross-seeded copies at once. - **Safe expansion**: If qui can't safely confirm another torrent uses the same files, it won't be included in the deletion. - **Safety-first**: If verification can't complete for any reason, the entire group is skipped rather than risking broken torrents. - **Preview**: The delete preview shows all torrents that would be deleted, with cross-seeds marked. **Include hardlinked copies:** When "Include hardlinked copies" is enabled (only available with `deleteWithFilesIncludeCrossSeeds` mode), the system also deletes torrents that share the same underlying physical files via hardlinks, even if they have different Content Paths. - **Requires**: Local Filesystem Access must be enabled on the instance. - **Safe scope**: Only includes hardlinks that are fully contained within qBittorrent's torrent set. Never follows hardlinks to files outside qBittorrent (e.g., your media library). - **Preview**: Hardlink-expanded torrents are marked as "Cross-seed (hardlinked)" in the preview. - **Free Space projection**: When combined with Free Space conditions, hardlink groups are correctly deduplicated in the space projection - torrents sharing the same physical files are only counted once. This is useful when you have hardlinked copies of content across different locations in qBittorrent and want to clean up all copies together. ### Tag Manage tags on torrents. You can add multiple Tag actions in one workflow. | Mode | Description | | -------- | ------------------------------------------------------ | | `full` | Add to matches, remove from non-matches (smart toggle) | | `add` | Only add to matches | | `remove` | Only remove from matches | :::note `mode: remove` removes tags from torrents that match the tag action condition. It does not remove from non-matches. ::: `mode: full` is evaluated within the rule's scope for that run (enabled rule, tracker pattern match, and run eligibility). It is not a client-wide sweep by itself. Options: - **Managed / Replace in Client** - `Managed` (default) applies per-torrent add/remove diffs only. `Replace in client` deletes managed tags from qBittorrent first, then reapplies to current matches. - **Use Tracker as Tag** - Derive tag from tracker domain - **Use Display Name** - Use tracker customization display name instead of raw domain Behavior reference: | Configuration | Behavior | | ----------------------------- | -------- | | `mode: full` + `Managed` | Adds/removes tag for torrents this rule evaluates. No client-wide reset. | | `mode: full` + `Replace in client` | Deletes selected tag(s) client-wide first, then re-adds only current matches. | If you see repeated activity like `+tag=696` every run, that usually means **Replace in client** is enabled for that tag action. Quick troubleshooting: 1. Check logs for `automations: deleted managed tags from client before retagging`. 2. In Automations UI, open enabled rules and verify whether "Replace in client" is enabled on any tag action. 3. Confirm the activity entry's rule list matches the rule you expect. ### Category Move torrents to a different category. Options: - **Include Cross-Seeds** - Also move cross-seeds (matching ContentPath AND SavePath) - **Group ID (advanced)** - Expand category changes to all torrents in the specified group (see [Grouping](#grouping)). If set, this takes precedence over "Include Cross-Seeds". - **Strict grouped matching** - With `groupId`, category expansion applies only when all group members satisfy the category rule checks. - **Block If Cross-Seed In Categories** - Prevent move if another cross-seed is in protected categories ### Move Move torrents to a different path on disk. This is needed to move the contents if AutoTMM is not enabled. Options: - **Group ID (advanced)** - Expand moves to all torrents in the specified group (see [Grouping](#grouping)). The move path is resolved for the matched torrent and then applied to the whole group. - **Strict grouped matching** - With `groupId`, move expansion is all-or-none: every member must satisfy the move rule checks. - **Skip if cross-seeds don't match the rule's conditions** - Skip the move if the torrent has cross-seeds that don't match the rule's conditions. This is ignored when **Group ID** is set. #### Move path templates The move path is evaluated as a **Go template** for each torrent. You can use a fixed path (e.g. `/data/archive`) or template actions to build paths from torrent properties. **Available template variables:** | Variable | Description | | ---------------------- | ---------------------------------------------------------------------------------------- | | `.Name` | Torrent display name | | `.Hash` | Info hash | | `.Category` | qBittorrent category | | `.IsolationFolderName` | Filesystem-safe folder name (hash or sanitized name) | | `.Tracker` | Tracker display name (when available from instance config), otherwise the tracker domain | **Template function:** | Function | Description | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `sanitize` | Makes a string safe for use as a path segment (removes invalid characters). Use for user-controlled values like names, e.g. `{{ sanitize .Name }}`. | **Examples:** - Fixed path (no template actions): `/data/archive` - By category: `/data/{{.Category}}` → e.g. `/data/movies` - By name (safe for paths): `/data/{{ sanitize .Name }}` - By isolation folder: `/data/{{.IsolationFolderName}}` - By tracker: `/data/{{.Tracker}}` (when tracker display name is configured) ### Auto Management Enable or disable qBittorrent's Automatic Torrent Management (AutoTMM) on matching torrents. | Mode | Description | | --------- | ------------------------------------------------ | | `enable` | Enable automatic torrent management on matches | | `disable` | Disable automatic torrent management on matches | When AutoTMM is enabled, qBittorrent automatically moves torrents to the save path configured for their category. Disabling it allows manual control of save paths. If multiple rules match the same torrent with Auto Management actions, the **last matching rule** (by sort order) wins. ### External Program Run a pre-configured external program when torrents match the automation rule. Uses the same programs configured in **Settings → External Programs**. | Field | Description | | ---------------------- | ------------------------------------------ | | **Program** | Select from enabled external programs | | **Condition Override** | Optional condition specific to this action | **Behavior:** - Executes asynchronously (fire-and-forget) to avoid blocking automation processing - Can be combined with other actions (speed limits, share limits, pause, tag, category) - Only enabled programs appear in the dropdown - Activity is logged with rule name, torrent details, and success/failure status :::note The program must be enabled in Settings → External Programs to appear in the automation dropdown. ::: :::note When multiple rules match the same torrent with External Program actions enabled, the **last matching rule** (by sort order) determines which program executes for that torrent. Only one program runs per torrent per automation cycle. ::: :::warning The program's executable path must be present in the application's allowlist. Programs that are disabled or have forbidden paths will not run—attempts are rejected and logged in the activity log with the rule name and torrent details. ::: **Use cases:** - Run post-processing scripts when torrents complete - Notify external systems (webhooks, notifications) when conditions are met - Trigger media library scans after category changes - Execute cleanup scripts for old or stalled torrents ### Export to Instance Export a torrent's `.torrent` file from the current instance and add it to a different qBittorrent instance. This is useful for migrating torrents between instances — for example, moving from a seedbox to a local instance for long-term seeding. This action assumes the data already exists on the target (via rclone, Quickdrop for Deluge, etc.) and uses `skip_checking=true` by default. | Field | Description | | ------------------ | ------------------------------------------------------------------------ | | **Target instance** | Destination qBittorrent instance (cannot be the same as source) | | **Save path** | Save path on target instance (Go template supported, see below) | | **Category** | Category to assign on target instance (dropdown from target's categories) | | **Tags** | Tags to apply on target instance | | **Skip checking** | Skip hash check on target (default: enabled) | | **Paused** | Add torrent paused on target | | **Content layout** | `Original`, `Subfolder`, or `NoSubfolder` | | **Condition Override** | Optional condition specific to this action | **Behavior:** - Executes asynchronously to avoid blocking automation processing - **Cannot combine with Delete** — the API rejects rules that have both export and delete enabled - Duplicate detection: before exporting, qui checks if the torrent already exists on the target instance and skips it if found - After adding to the target, qui verifies the torrent appeared and is healthy. If verification fails, the torrent is automatically cleaned up from the target so it can be retried on the next run - Cross-seed group members are **not** automatically exported. To export a group, chain with Category/Tag actions using group expansion - Activity is logged with rule name, torrent details, target instance, and success/failure status - Dry-run is supported — shows what would be exported without actually transferring :::note When multiple rules match the same torrent with Export to Instance actions, the **last matching rule** (by sort order) determines the export configuration for that torrent. Only one export runs per torrent per automation cycle. ::: #### Save path templates The save path field supports Go templates, the same as the [Move action](#move-path-templates). | Variable | Description | | ---------------------- | ---------------------------------------------------------------------------------------- | | `.Name` | Torrent display name | | `.Hash` | Info hash | | `.Category` | qBittorrent category (on source instance) | | `.IsolationFolderName` | Filesystem-safe folder name (hash or sanitized name) | | `.Tracker` | Tracker display name (when available from instance config), otherwise the tracker domain | | Function | Description | | ---------- | --------------------------------------------------------------------------- | | `sanitize` | Makes a string safe for use as a path segment (removes invalid characters). | **Examples:** - Fixed path: `/data/torrents` - By category: `/data/{{.Category}}` - By tracker: `/data/{{.Tracker}}` If no save path is set but a category is configured, qBittorrent's Automatic Torrent Management is enabled so the target uses the category's configured path. ## Cross-Seed Awareness Automations detect cross-seeded torrents (same content/files) and can handle them specially: - **Detection** - Cross-seed condition fields use the same matching logic as **Filter Cross-Seeds**: content path, exact name, and release metadata. Same-instance checks exclude the current torrent itself. - **Delete Rules**: - Use `deleteWithFilesPreserveCrossSeeds` to keep files if cross-seeds exist - Use `deleteWithFilesIncludeCrossSeeds` to delete matching torrents and all their cross-seeds together - **Category Rules** - Enable "Include Cross-Seeds" to move related torrents together - **Blocking** - Prevent category moves if cross-seeds are in protected categories ### Hardlink Detection The `HARDLINK_SCOPE` field lets automations distinguish between torrents whose files are hardlinked into an external library (Sonarr, Radarr, etc.) and torrents that exist only within qBittorrent. This is the foundation for safe "Remove Upgraded Torrents" automations. #### How scope is determined When an automation references `HARDLINK_SCOPE`, qui builds a hardlink index by calling `Lstat()` on every file of every torrent in qBittorrent. For each file it extracts: - The **inode** and **device ID** — uniquely identifying the file on disk. - The **nlink count** — the total number of hardlinks to that inode, as reported by the filesystem. It then counts how many unique file paths across the entire qBittorrent torrent set point to each inode. The scope for each torrent is determined by comparing these two numbers: | Scope | Condition | Meaning | | --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `none` | No file has `nlink > 1` | No hardlinks detected. | | `torrents_only` | At least one file has `nlink > 1`, and no file has `nlink > uniquePathCount` | Hardlinks exist, but only between torrents in qBittorrent. No external library links. | | `outside_qbittorrent` | Any file has `nlink > uniquePathCount` | Something outside qBittorrent has hardlinked the file — typically a Sonarr/Radarr library import. | :::note `HARDLINK_SCOPE` only reflects hardlink metadata. Cross-seeds are detected separately (ContentPath matching), so a torrent can have `HARDLINK_SCOPE = none` and still be cross-seeded. ::: #### Unknown scope and safety behavior If qui cannot `Lstat()` **any** file in a torrent — due to wrong paths, missing permissions, or inaccessible storage — that torrent receives no scope entry. All `HARDLINK_SCOPE` conditions evaluate to `false` for that torrent, regardless of the operator or value. This is a safety measure to prevent unintended deletions of torrents qui cannot fully inspect. To diagnose this, enable debug logging and look for the "hardlink index built" log message, which reports an `inaccessible` count. #### Docker volume requirements For hardlink scope detection to work in Docker: 1. **Paths must match exactly.** qui must be able to read files at the same paths qBittorrent reports. If qBittorrent says a torrent's save path is `/data/torrents/radarr/`, qui must be able to access `/data/torrents/radarr/` inside its container. 2. **Same underlying storage.** Both containers must share the same host mount so that inode numbers are consistent. If qui and qBittorrent access the same files through different host mounts or different bind-mount configurations, inode numbers may not match. 3. **Single mount, not subdivided.** Mount the common parent directory rather than mounting subdirectories separately. For example, if your data lives under `/mnt/media/data` on the host: ```yaml services: qui: volumes: - /home/user/docker/qui:/config - /mnt/media/data:/data # single mount covering both torrents and library ``` Avoid mounting both `/mnt/media/data/torrents:/data/torrents` **and** `/mnt/media/data:/data` — the overlapping mounts can cause inconsistent inode visibility. Use a single mount at the common parent. #### Filesystem limitations Hardlink scope detection depends on the kernel reporting accurate `nlink` values in stat results. Some filesystems do not do this: - **FUSE-based filesystems** (sshfs, mergerfs, rclone mount) may report `nlink = 1` for all files regardless of actual hardlink count. - **Some NAS appliance filesystems** and **overlay filesystems** (overlayfs) may behave similarly. - **Network filesystems** (NFS, CIFS/SMB) generally report accurate nlink values but behavior varies by server implementation. On affected filesystems, every torrent appears to have scope `none` because nlink is always 1. There is no workaround within qui — this is a kernel/filesystem limitation. If you suspect this issue, run `stat` on a file you know is hardlinked and check the "Links" count. Hardlinks also cannot span across different filesystems. If your torrent data and media library are on separate filesystems (or separate Docker volumes backed by different host paths), Sonarr/Radarr will copy instead of hardlink, and scope detection has nothing to detect. #### Example: Remove Upgraded Torrents This automation deletes torrents that have been replaced by an upgrade in Sonarr/Radarr. It targets torrents where the library hardlink no longer exists (the arr removed or re-linked it during upgrade), the torrent has been seeding for at least 7 days, and the category matches your arr categories. :::tip Use `HARDLINK_SCOPE` with `NOT_EQUAL` to `outside_qbittorrent` rather than `EQUAL` to `none`. This way torrents with scope `torrents_only` (cross-seeded but not in a library) are also eligible for cleanup, while any torrent still linked into your media library is protected. ::: ```json { "name": "Remove Upgraded Torrents", "trackerPattern": "*", "trackerDomains": ["*"], "conditions": { "schemaVersion": "1", "delete": { "enabled": true, "mode": "deleteWithFilesPreserveCrossSeeds", "condition": { "operator": "AND", "conditions": [ { "operator": "OR", "conditions": [ { "field": "CATEGORY", "operator": "EQUAL", "value": "radarr" }, { "field": "CATEGORY", "operator": "EQUAL", "value": "radarr.cross" }, { "field": "CATEGORY", "operator": "EQUAL", "value": "tv-sonarr" }, { "field": "CATEGORY", "operator": "EQUAL", "value": "tv-sonarr.cross" } ] }, { "field": "HARDLINK_SCOPE", "operator": "NOT_EQUAL", "value": "outside_qbittorrent" }, { "field": "SEEDING_TIME", "operator": "GREATER_THAN_OR_EQUAL", "value": "604800" } ] } } } } ``` This works because when Sonarr/Radarr upgrades a release, the old library hardlink is removed. The old torrent's files then have `nlink == 1` (scope `none`) or are only linked to other torrents (scope `torrents_only`). Either way, the scope is not `outside_qbittorrent`, so the automation matches and deletes the torrent after the seeding time requirement is met. If the automation is matching torrents you expect to be protected, verify: 1. qui can access all torrent files at the paths qBittorrent reports (check debug logs for inaccessible files). 2. Your filesystem reports accurate nlink values (`stat ` should show Links > 1 for hardlinked files). 3. Your Docker volume mounts do not overlap or subdivide the storage in a way that breaks inode consistency. ### Cross-Instance Hardlink Detection The `HARDLINK_SCOPE_CROSS` field extends hardlink detection across **all** configured qBittorrent instances. While `HARDLINK_SCOPE` only considers torrents within a single instance, `HARDLINK_SCOPE_CROSS` accounts for hardlinks to files managed by any instance with local filesystem access enabled. This is essential for multi-instance setups where cross-seeds are hardlinked across instances. Without cross-instance awareness, those hardlinks appear as `outside_qbittorrent` even though they point to files managed by another qBittorrent instance. #### Scope values | Scope | Meaning | | --------------------- | ------------------------------------------------------------------------ | | `none` | No hardlinks detected. | | `torrents_only` | All hardlinks are accounted for across all qBittorrent instances. | | `outside_qbittorrent` | Hardlinks exist to files outside all qBittorrent instances. | #### Combining with HARDLINK_SCOPE Use both fields together to distinguish cross-instance hardlinks from truly external links: | Combination | Interpretation | | --- | --- | | `HARDLINK_SCOPE = outside_qbittorrent` AND `HARDLINK_SCOPE_CROSS = torrents_only` | Hardlinks point to other qBittorrent instances only (cross-seeds). No media library copy. | | `HARDLINK_SCOPE = outside_qbittorrent` AND `HARDLINK_SCOPE_CROSS = outside_qbittorrent` | Hardlinks point outside all instances — typically a media library import. | | `HARDLINK_SCOPE = torrents_only` | All hardlinks within this instance. `HARDLINK_SCOPE_CROSS` will also be `torrents_only`. | #### Prerequisites `HARDLINK_SCOPE_CROSS` requires: 1. **Local Filesystem Access** enabled on **all** instances whose files you want considered. Instances without it are skipped — their files won't be scanned, and unresolved hardlinks will conservatively show as `outside_qbittorrent`. 2. **Same filesystem** across all instances. Hardlinks cannot cross filesystem boundaries. 3. **Matching paths in Docker** — same volume mount requirements as `HARDLINK_SCOPE`, applied to every instance. #### Performance Cross-instance scanning only runs when: - A rule uses `HARDLINK_SCOPE_CROSS` - The single-instance scan found torrents with unresolved outside links When triggered, it uses cached torrent and file data from other instances (no extra API calls) and only calls `Lstat()` on files that might resolve the unaccounted hardlinks. Scanning stops as soon as all deficits are resolved. A safety budget of 500,000 `Lstat()` calls limits the cross-instance scan. If the budget is exhausted before all deficits are resolved, the remaining torrents conservatively report `outside_qbittorrent`. This prevents excessive filesystem operations in large multi-instance setups. A warning is logged if the budget is reached. #### Example: noHL tagging in multi-instance setups This rule tags torrents with `noHL` when they have no media library hardlinks, even if they have cross-instance hardlinks to other qBittorrent instances: ```json { "name": "Tag noHL (multi-instance)", "trackerPattern": "*", "trackerDomains": ["*"], "conditions": { "schemaVersion": "1", "tags": [ { "enabled": true, "mode": "add", "tags": ["noHL"], "condition": { "operator": "AND", "conditions": [ { "field": "HARDLINK_SCOPE_CROSS", "operator": "NOT_EQUAL", "value": "outside_qbittorrent" }, { "field": "STATE", "operator": "EQUAL", "value": "uploading" } ] } } ] } } ``` This works because `HARDLINK_SCOPE_CROSS != outside_qbittorrent` matches both `none` (no hardlinks) and `torrents_only` (hardlinks only between qBittorrent instances). Torrents with a media library copy (`outside_qbittorrent`) are excluded from the tag. ## Missing Files Detection The `Has Missing Files` field detects whether any files belonging to a completed torrent are missing from disk. - Only checks **completed torrents** - Returns `true` if **any** file is missing from its expected path :::note Requires "Local filesystem access" enabled on the instance. ::: ## Important Behavior ### Settings Only Set Values Automations apply settings but **do not revert** when disabled or deleted. If a rule sets upload limit to 1000 KiB/s, affected torrents keep that limit until manually changed or another rule applies a different value. ### Efficient Updates Only sends API calls when the torrent's current setting differs from the desired value. No-op updates are skipped. ### Processing Order - **First match wins** for delete actions (delete ends torrent processing, no further rules evaluated) - **Last rule wins** for speed limits, share limits, category, external program, and export to instance actions - **Accumulative** for tag actions (tags are combined across matching rules) ### Free Space Condition Behavior When using the **Free Space** condition in delete rules, the system uses intelligent cumulative tracking: 1. **Configurable processing order** - Torrents are processed according to the automation's Torrent Priority (Default, Simple, or Score). This allows you to prioritize cleanups (e.g., largest files first, or lowest score first). 2. **Cumulative space tracking** - As each torrent is marked for deletion, its size is added to the projected free space (only when the delete mode actually frees disk bytes). 3. **Stop when satisfied** - Once `Free Space + Space To Be Cleared` exceeds your threshold, remaining torrents no longer match. 4. **Cross-seed aware** - Cross-seeded torrents sharing the same files are only counted once to avoid overestimating freed space **Preview Views for Free Space Rules** When previewing a delete rule with a Free Space condition, a toggle allows switching between two views: | View | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Needed to reach target** | Shows only the torrents that would be removed right now to reach your free-space target. This is the default view and reflects actual delete behavior. | | **All eligible** | Shows all torrents this rule could remove while free space is low. Useful for understanding the full scope of what the rule could potentially delete (may include cross-seeds that don't directly match filters). | The toggle only appears for delete rules that use the Free Space condition. **Preview features:** - **Path column** - Shows the content path for each torrent with copy-to-clipboard support - **Export CSV** - Download the full preview list (all pages) as a CSV file for external analysis **Cross-seed expansion in previews:** Cross-seeds are only expanded and displayed in the preview when using `Remove with files (include cross-seeds)` mode. In this mode, the preview shows all torrents that would be deleted together, with cross-seeds clearly marked. Other delete modes don't expand cross-seeds in the preview since they either preserve cross-seeds or don't consider them specially. **Delete mode affects space projection:** | Delete Mode | Space Added to Projection | | -------------------------------------- | ------------------------- | | Remove with files | Full torrent size | | Preserve cross-seeds (no cross-seeds) | Full torrent size | | Preserve cross-seeds (has cross-seeds) | 0 (files kept) | **How preserve cross-seeds works:** - Cross-seed detection checks if any other torrent shares the same Content Path at evaluation time (before any removals). - If multiple torrents share the same files, removing them all in one rule run will still keep the files on disk. No disk space is freed from that group because each torrent sees the others as cross-seeds. - Only non-cross-seeded torrents contribute to the free-space projection when using preserve mode. **Example:** With 400GB free and a rule "Delete if Free Space < 500GB" using `Remove with files`, the system deletes oldest torrents until the cumulative freed space reaches 100GB, then stops. A 50GB torrent and its cross-seed (same files) only count as 50GB freed, not 100GB. :::note The UI and API prevent combining `Remove (keep files)` mode with Free Space conditions. Since keep-files doesn't free disk space, such a rule could never satisfy the free space target and would match indefinitely. ::: :::note After removing files, qui waits ~5 minutes before running Free Space deletes again to allow qBittorrent to refresh its disk free space reading. The UI prevents selecting 1 minute intervals for Free Space delete rules. ::: #### Free Space Source By default, Free Space uses qBittorrent's reported free space (based on its default download location). If you have multiple disks or want to manage a specific mount point, select "Path on server" and enter the path to that disk. | Source | Description | | --------------------- | ------------------------------------------------ | | Default (qBittorrent) | Uses qBittorrent's reported free space | | Path on server | Reads free space from a specific filesystem path | :::note Path on server requires "Local Filesystem Access" to be enabled on the instance. ::: If you want to manage multiple disks, create one workflow per disk and set a different Path on server for each workflow. :::note On Windows, Path on server is not supported and Free Space always uses qBittorrent's reported free space. The UI disables the option and switches legacy workflows back to the default when opened. ::: ### Batching Torrents are grouped by action value and sent to qBittorrent in batches of up to 50 hashes per API call. ## Activity Log All automation actions are logged with: - Torrent name and hash - Rule name and action type - Outcome (success/failed) with reasons - Action-specific details Activity is retained for 7 days by default. View the log in the Automations section for each instance. ## Example Rules ### Delete Old Completed Torrents Remove torrents completed over 30 days ago when disk space is low: - Condition: `Completion On Age > 30 days` AND `State is completed` AND `Free Space < 500GB` - Action: Remove with files Deletes matching torrents in the configured priority order (e.g., oldest first), stopping once enough space would be freed to exceed 500GB. ### Speed Limit Private Trackers Limit upload on private trackers: - Tracker: `*` - Condition: `Private is true` - Action: Upload limit 10000 KiB/s ### Tag Stalled Torrents Auto-tag torrents with no activity: - Tracker: `*` - Condition: `Last Activity Age > 7 days` - Action: Tag "stalled" (mode: add) ### Clean Unregistered Torrents Remove torrents the tracker no longer recognizes: - Tracker: `*` - Condition: `Is Unregistered is true` - Action: Delete (keep files) ### Maintain Minimum Free Space Keep at least 200GB free by removing oldest completed torrents: - Tracker: `*` - Condition: `Free Space < 200GB` AND `State is completed` - Action: Remove with files (preserve cross-seeds) Removes torrents from the client in the configured priority order, until enough space is projected to be freed. Cross-seeded torrents keep their files on disk and don't contribute to the projection. If only cross-seeded torrents match, this may remove many torrents without freeing any disk space. ### Clean Up Old Content with Cross-Seeds Remove completed torrents and all their cross-seeded copies when they're old enough: - Tracker: `*` - Condition: `Completion On Age > 30 days` AND `State is completed` - Action: Remove with files (include cross-seeds) When a torrent matches, any other torrents pointing to the same downloaded files are deleted together. Useful for complete cleanup when you no longer need any copy of the content. ### Organize by Tracker Move torrents to tracker-named categories: - Tracker: `tracker.example.com` - Action: Category "example" with "Include Cross-Seeds" enabled ### Post-Processing on Completion Run a script when torrents finish downloading: - Tracker: `*` - Condition: `State is completed` AND `Progress = 100` - Action: External Program "post-process.sh" ### Notify on Stalled Torrents Alert an external monitoring system when torrents stall: - Tracker: `*` - Condition: `State is stalled` AND `Last Activity Age > 24 hours` - Action: External Program "send-alert" + Tag "stalled" (mode: add) --- ## Backups # Backups & Restore qui can take scheduled or ad-hoc snapshots of a qBittorrent instance. Each snapshot includes the torrent archive, tags, categories (with save paths), and cached `.torrent` blobs so that you can recreate the original state later. If you manage multiple instances, the Backups page also includes **Save changes to all instances** so you can copy the current backup schedule/settings to every compatible instance in one step. ## Restore Modes Once backups are enabled for an instance the backlog UI exposes a **Restore** action for each run. Restores support three distinct modes: ### Incremental Safest option. Creates any categories, tags, or torrents that are missing from the live instance but never modifies or removes existing data. Use this when you just want to seed new items into an active qBittorrent without touching what is already there. ### Overwrite Performs the incremental work **and** updates existing resources to match the snapshot (e.g. adjusts category save paths or rewrites per-torrent categories/tags). It still refuses to delete anything. This works well when your live instance has drifted but you do not want to prune it. ### Complete Full reconciliation. Runs the overwrite steps and then deletes categories, tags, and torrents that are not present in the snapshot. This is ideal when you need to roll an instance back to an earlier point in time, but it should only be used when you are certain the snapshot is authoritative. ## Preview Before Restore Every restore begins with a dry-run preview so you can inspect planned changes. Unsupported differences (such as mismatched infohashes or file sizes) are surfaced as warnings; they require manual follow-up regardless of mode. ## Importing Backups Downloaded backups can be imported into any qui instance. Useful for migrating to a new server or recovering after data loss. Click **Import** on the Backups page and select the backup file. All export formats are supported. --- ## autobrr Integration qui integrates with autobrr through webhook endpoints, enabling real-time cross-seed detection when autobrr announces new releases. ## How It Works 1. autobrr sees a new release from a tracker 2. autobrr sends the torrent name and indexer identifier to qui's `/api/cross-seed/webhook/check` endpoint 3. qui searches your qBittorrent instances for matching content 4. qui responds with: - `200 OK` – matching torrent is complete and ready to cross-seed - `202 Accepted` – matching torrent exists but still downloading; retry later - `404 Not Found` – no matching torrent exists 5. On `200 OK`, autobrr sends the torrent file to `/api/cross-seed/apply` ## Setup ### 1. Create an API Key in qui - Go to **Settings → API Keys** - Click **Create API Key** - Name it (e.g., "autobrr webhook") - Copy the generated key ### 2. Configure autobrr External Filter :::important Create a **new autobrr filter dedicated to qui**. ::: :::note The **External** webhook (`/api/cross-seed/webhook/check`) only answers: "is this ready to cross-seed?" It does **not** add a torrent to qBittorrent. You must also set up the **Action** in [Apply Endpoint](#apply-endpoint). ::: :::tip **Docker Compose:** if autobrr and qui are both containers, `localhost` inside autobrr is the autobrr container, not qui. Use your qui container hostname instead (often the Compose service name), for example: `http://qui:7476/api/cross-seed/webhook/check`. ::: In your new autobrr filter, go to **External** tab → **Add new**: | Field | Value | | ------------------------- | ---------------------------------------------------- | | Type | `Webhook` | | Name | `qui` | | On Error | `Reject` | | Endpoint | `http://localhost:7476/api/cross-seed/webhook/check` | | HTTP Method | `POST` | | HTTP Request Headers | `X-API-Key=YOUR_QUI_API_KEY` | | Expected HTTP Status Code | `200` | **Data (JSON):** ```json { "torrentName": {{ toRawJson .TorrentName }}, "instanceIds": [1], "indexer": {{ toRawJson .Indexer }} } ``` To search all instances, omit `instanceIds`: ```json { "torrentName": {{ toRawJson .TorrentName }}, "indexer": {{ toRawJson .Indexer }} } ``` **Field descriptions:** - `torrentName` (required): The release name as announced - `instanceIds` (optional): qBittorrent instance IDs to scan. Omit to search all instances. - `indexer` (optional): autobrr indexer identifier (for example `hdb`). Required for qui's HDBits-specific missing-collection fallback on `/check`. - `findIndividualEpisodes` (optional): Override the global episode matching setting ### 3. Configure Retry Handling Use autobrr's **Retry** block to handle `202 Accepted` responses: - **Retry HTTP status code(s):** `202` - **Maximum retry attempts:** `10` - **Retry delay in seconds:** `4` ## Apply Endpoint When `/check` returns `200 OK`, send the torrent to `/api/cross-seed/apply`: **Action setup in autobrr:** | Field | Value | | ----------- | -------------------------------------------------------------------- | | Action Type | `Webhook` | | Name | `qui cross-seed` | | Endpoint | `http://localhost:7476/api/cross-seed/apply?apikey=YOUR_QUI_API_KEY` | **Payload (JSON):** ```json { "torrentData": "{{ .TorrentDataRawBytes | toString | b64enc }}", "instanceIds": [1], "indexer": {{ toRawJson .Indexer }} } ``` **Field descriptions:** - `torrentData` (required) - Base64-encoded torrent file bytes - `instanceIds` (optional) - Target instances (omit to apply to any matching instance) - `indexer` (optional) - autobrr indexer identifier (for example `hdb`). When "Use indexer name as category" mode is enabled, qui uses this identifier value as the category; ignored otherwise - `tags` (optional) - Override webhook tags from settings - `category` (optional) - Override category. Takes precedence over `indexer` - `startPaused` (optional) - Override whether torrents are added paused - `skipIfExists` (optional) - Skip adding if the torrent already exists - `findIndividualEpisodes` (optional) - Override the global episode matching setting Cross-seeded torrents are added paused with `skip_checking=true`. qui polls the torrent state and auto-resumes if progress meets the size tolerance threshold. If progress is too low, it remains paused for manual review. ### Troubleshooting: autobrr matches, but nothing gets added to qBittorrent Use this when autobrr shows the filter accepted the release (or your Discord notification fires), but you never see a new torrent in qBittorrent. 1. **Confirm you added the `/apply` Action** - The External webhook (`/check`) does not add torrents. - You need an autobrr **Action** (Webhook) that calls `/api/cross-seed/apply` (above). 2. **Fix Docker networking if you're using containers** - `http://localhost:7476/...` only works if autobrr can reach qui on its own `localhost`. - In Docker Compose, use the qui service hostname (example): `http://qui:7476/api/cross-seed/apply?apikey=...`. 3. **Double-check auth** - `/check`: header `X-API-Key=...` - `/apply`: query string `?apikey=...` (as shown in this guide) 4. **Verify qui can talk to qBittorrent** - qui UI: **Settings → Instances → Test Connection** 5. **Check paused torrents** - Cross-seeds are often added **paused**. Look in qBittorrent's paused list (and any cross-seed tag/category you configured). If you still can't see why, jump to [Cross-Seed Troubleshooting](troubleshooting). ## Webhook Source Filters By default, the webhook endpoint scans **all** torrents on your instances when looking for matches. You can configure filters to exclude certain categories or tags from being matched: - **Exclude Categories:** Skip torrents in specific categories (e.g., `cross-seed-link`) - **Exclude Tags:** Skip torrents with specific tags (e.g., `no-cross-seed`) - **Include Categories:** Only match against torrents in these categories (leave empty for all) - **Include Tags:** Only match against torrents with these tags (leave empty for all) This is useful when: - You have a legacy cross-seed category that shouldn't be re-matched - Certain content types should never be considered for cross-seeding - You want to exclude torrents with specific metadata tags :::note Exclude filters take precedence over include filters. Tag matching is case-sensitive. When both category and tag include filters are configured, a torrent must pass both filter checks (matching at least one allowed category AND at least one allowed tag). ::: Configure in qui UI: **Cross-Seed → Auto → Webhook / autobrr** ## Season Pack Webhook qui also supports a dedicated season-pack flow through separate endpoints. When autobrr announces a season pack, qui checks your instances for matching individual episodes, links whatever is already local, and lets qBittorrent fetch the remainder after recheck when coverage is sufficient. This uses different endpoints (`/api/cross-seed/season-pack/check` and `/api/cross-seed/season-pack/apply`) and requires a separate autobrr filter. See [Season Packs](season-packs) for full setup instructions. --- ## Directory Scanner Directory Scanner (Dir Scan) scans local folders to find cross-seed opportunities for content already on disk. Unlike Library Scan (which queries qBittorrent's torrent list), Dir Scan works directly with files on the filesystem. Configure it in **Cross-Seed > Dir Scan**. ## Requirements - At least one qBittorrent instance must have **Local filesystem access** enabled in Instance Settings. - qui must be able to read the files directly (same host or shared mounts as the target qBittorrent instance). - Prowlarr or Jackett must be configured with at least one enabled indexer. - Optional: Sonarr/Radarr configured in **Settings > Integrations** for external ID lookups (IMDb/TMDb/TVDb). ## How to Choose Your Scan Path Dir Scan treats each **immediate child** of your configured path as one "searchee." It does not treat the path itself as a single searchee, and it does not recurse into subfolders to create additional searchees. **Example:** If you configure `/data/media/movies`: ```plaintext /data/media/movies/ ├── Movie.2024.1080p.BluRay/ <- searchee 1 │ ├── movie.mkv │ └── movie.nfo ├── Another.Movie.2023.2160p/ <- searchee 2 │ └── movie.mkv └── standalone.mkv <- searchee 3 ``` Each immediate child (folder or file) becomes one searchee. Files within `Movie.2024.1080p.BluRay/` are grouped together as part of that searchee. ### Correct path choices | Content type | Recommended path | Why | |-------------|------------------|-----| | Movies | `/data/media/movies` | Each movie folder is one searchee | | TV Shows | `/data/media/tv` | Each show folder is one searchee | | Music | `/data/media/music` | Each album folder is one searchee | ### Incorrect path choices | Path | Problem | |------|---------| | `/data/media` containing `movies/` + `tv/` + `music/` | Only 3 searchees total (the category folders themselves) | | `/data/media/movies/Movie.2024.1080p.BluRay` | Only 1 searchee; scans that specific movie only | :::tip Create one Dir Scan entry per category folder. Don't point at a parent folder containing multiple category subfolders. ::: ## Docker and Path Mapping When qui and qBittorrent run in separate containers or see different mount points, you need path mapping. ### "Local filesystem access" explained Enabling **Local filesystem access** on a qBittorrent instance tells qui: 1. qui can read files directly from the filesystem (same paths or mapped paths). 2. qui should use file-based matching (inode checks, size verification) rather than relying solely on qBittorrent's API. This requires qui to have read access to the actual files, either on the same host or via shared network/volume mounts. ### Recommended: Use the same volume paths The simplest setup is to mount volumes at the same path in both containers: ```yaml title="docker-compose.yml" services: qui: volumes: - /mnt/storage:/mnt/storage qbittorrent: volumes: - /mnt/storage:/mnt/storage ``` When both containers see `/data/media/movies`, no path mapping is needed. Leave **qBittorrent Path Prefix** empty. ### Path mapping example (different mount points) Your setup: - qui container mounts: `-v /mnt/storage:/data` - qBittorrent container mounts: `-v /mnt/storage:/downloads` qui sees files at `/data/media/movies/Movie.2024/movie.mkv` qBittorrent sees the same file at `/downloads/media/movies/Movie.2024/movie.mkv` Configure Dir Scan: - **Directory Path**: `/data/media/movies` - **qBittorrent Path Prefix**: `/downloads/media/movies` When qui finds a match, it tells qBittorrent to add the torrent pointing at `/downloads/media/movies/Movie.2024/` instead of `/data/media/movies/Movie.2024/`. ## How It Works For each configured scan directory, qui: 1. Enumerates immediate children of the directory path. 2. For each child (folder or file), recursively collects all files within. 3. Groups files into a "searchee" with parsed release info. 4. Uses configured *arr instances to resolve external IDs when possible. 5. Searches enabled indexers via Torznab. 6. Downloads torrent files and matches their file lists against what's on disk. 7. If a match is found, adds the torrent to the target qBittorrent instance. :::note Categories + AutoTMM Dir Scan adds torrents using an explicit `savepath` to point qBittorrent at the existing files on disk. That forces **AutoTMM off** for Dir Scan injections. Dir Scan categories come only from **Dir Scan → Default Category** and per-directory **Category override**. Cross-Seed → Rules category modes (affix / indexer / custom) do not apply to Dir Scan. If you later enable AutoTMM on an injected torrent, qBittorrent may relocate files based on its default save path + category rules. ::: :::info Torznab searches run through the shared scheduler at background priority, so they queue behind interactive, RSS, and completion cross-seed work. If the global scan concurrency limit is reached, new scans show as `queued` until a scan slot is available. Dir Scan may also pause between downloading candidate torrent files from an indexer. This is intentional and helps avoid hammering Prowlarr/indexers (especially for private trackers), but it can make scans take longer when many candidates need checking. ::: ### Already-seeding detection Dir Scan maintains a FileID index (inode + device on Unix) to track files already present in qBittorrent. It skips: - Files that are already part of a seeding torrent - Torrents whose infohash already exists in qBittorrent This avoids redundant searches and duplicate additions. If a torrent is removed from qBittorrent (for example, by an automation rule that removes torrents with missing files), its files are no longer tracked in the index. The next scan of whichever directory contains those files will treat them as new searchees and search indexers for them again. ### Recheck Behavior - **Full matches**: Torrent is added with "skip hash check" enabled. Seeding starts immediately. - **Partial matches** (when enabled): Torrent is added without skipping hash check. qBittorrent verifies existing data and downloads missing files. ## What Gets Scanned ### Included file types **Video:** `.mkv`, `.mp4`, `.avi`, `.m4v`, `.wmv`, `.mov`, `.ts`, `.m2ts`, `.vob`, `.mpg`, `.mpeg`, `.webm`, `.flv` **Audio:** `.flac`, `.mp3`, `.wav`, `.aac`, `.ogg`, `.m4a`, `.wma`, `.ape`, `.alac`, `.dsd`, `.dsf`, `.dff` **Extras:** `.nfo`, `.sfv`, `.srt`, `.sub`, `.idx`, `.ass`, `.ssa` Extras are included in releases and can affect partial-match behavior (a torrent with an `.nfo` you don't have may trigger a partial match instead of full). ### Disc layouts Folders containing `BDMV/`, `VIDEO_TS/`, or `AUDIO_TS/` structures are treated as disc-based media. All files within these structures are included regardless of extension. ### Skipped items - **Hidden files and folders** (names starting with `.`) - **Symlinks** (explicitly skipped to avoid loops and permission issues) - **Files with permission errors** (scan continues, file is skipped) - **Non-media files** outside disc layouts ## Settings (Global) Open **Dir Scan > Settings**: | Setting | Description | |---------|-------------| | Match Mode | `Strict` matches by filename + exact file size. `Flexible` ignores filenames for primary matching, but matched files must still have the same exact file size. | | Size Tolerance (%) | Allows small differences in total torrent size when filtering candidates before file matching. | | Minimum Piece Ratio (%) | For partial matches, minimum percent of torrent data that must exist on disk. | | Max searchees per run | Limits how many eligible searchees are processed per run. `0` = unlimited. Useful for making progress across restarts. | | Only process items changed within the last (days) | Excludes stale work items before search. Uses video/audio mtimes only for manual/scheduled scans. Webhook-triggered scans ignore this cutoff. `0` = disabled. | | Allow partial matches | Add torrents even if they have extra/missing files compared to disk. | | Download missing files | Downloads files not found on disk for partial matches. Required for season packs and partial releases in hardlink/reflink mode. Enabled by default. | | Skip piece boundary safety check | Allow partial matches where downloading missing files could modify pieces containing existing content. | | Start torrents paused | Add injected torrents in paused state. | | Default Category / Tags | Applied to all injected torrents. Directory-level settings add to these. | In practice: - **Strict** is best when filenames on disk are still close to the release layout. - **Flexible** is best for renamed libraries, but it still requires exact file-size matches for the files it pairs. - **Size Tolerance** only affects which search results are considered based on **total torrent size**. It does **not** allow per-file size mismatches. - Flexible single-file matches may still be rejected when the candidate lacks corroborating title or external ID evidence. This prevents false positives when an indexer falls back from ID-based search to plain title search. ### "Max searchees per run" explained This setting limits how many **top-level folders/files** Dir Scan will process in a single run. - If your directory is a TV root like `/mnt/storage/media/tv`, then each **show folder** is one searchee (for example `Show.Name/`, `Another.Show/`). - If your directory is a movies root like `/mnt/storage/media/movies`, then each **movie folder** is one searchee (for example `Movie.Title (2024)/`, `Another.Movie (2023)/`). So if **Max searchees per run = 5**, Dir Scan will process up to **5 show folders** (TV) or **5 movie folders** (movies) per run, then stop and persist per-file progress for the next run. The next run rechecks the directory, skips already-final files, and retries unfinished work. See [Incremental progress and resets](#incremental-progress-and-resets). This is **not** a cap on the total number of indexer searches. TV folders can trigger multiple searches (season-level + per-episode heuristics), even though they still count as a single top-level searchee. ### "Only process items changed within the last (days)" explained This setting reduces tracker/API load by excluding stale content before search begins. - Movies/music are included only when the item's newest video/audio file is within the cutoff. - TV is evaluated at the season/episode work-item level so one fresh episode does not pull an entire older show back in. - Season-pack searches are kept only when all episode files in that season work item are within the cutoff; otherwise qui falls back to fresh episode-level work only. - Cutoff is computed as `now - N days` (for example, `7` means “older than 7 days”). - The timestamp used is filesystem **modified time (mtime)** from video/audio files only, not subtitles, extras, release date, or qBittorrent add time. - Webhook-triggered scans ignore the cutoff entirely and trust the webhook path as the freshness signal. - `0` disables age filtering. Example with `7` days: - `Movie.2024/` has only an `.srt` updated yesterday while the `.mkv` is old -> skipped. - `Show.Name/Season 01/` has one fresh episode and nine old ones -> only the fresh episode stays in scope. - `Old.Show.S01/` has all episode files older than 7 days -> skipped. ## Directories Each scan directory has its own configuration: | Setting | Description | |---------|-------------| | Directory Path | The path qui scans (immediate children become searchees). | | qBittorrent Path Prefix | Path mapping for container setups. See [Docker and Path Mapping](#docker-and-path-mapping). | | Target qBittorrent Instance | Where matched torrents are added. Must have Local filesystem access enabled. | | Category override | Overrides the global Default Category for this directory. | | Additional tags | Added on top of the global Dir Scan tags. | | Scan Interval (minutes) | How often to rescan (minimum 60 minutes, default 1440 = 24 hours). | | Enabled | Enable/disable without deleting the configuration. | ## Operational Behavior ### Concurrent scans Only one scan runs per directory at a time. If a scheduled scan triggers while another scan is running, it will not start a second run for that directory. ### Incremental progress and resets Dir Scan persists per-file progress and skips unchanged searchees whose files are already in a final state (matched/no match/already seeding/in qBittorrent). This is **not** an exact checkpoint resume. When you start a new run after canceling or restarting qui, Dir Scan: - rechecks the directory from the top - keeps finished files skipped if they are unchanged - retries unfinished or errored files From a user perspective, this behaves like **restart with preserved progress**, not “continue from the exact file where it stopped.” If you want to force a directory to be re-processed from scratch, use **Reset Scan Progress** for that directory in the UI. This clears the tracked file state for that directory. ### Scheduled vs manual scans - **Scheduled scans** run based on the configured interval (minimum 60 minutes). - **Manual scans** can be triggered from the UI at any time via the "Scan Now" button. Both types can be canceled from the UI while running. The UI keeps the **last 10 run entries** per directory. Older run rows are pruned automatically. ### Webhook trigger You can trigger a scan automatically when Sonarr, Radarr, Lidarr, or Readarr imports content. The webhook endpoint natively understands *arr webhook payloads — no custom scripts needed. ```http POST /api/dir-scan/webhook/scan?apikey=YOUR_API_KEY ``` qui extracts the path from the *arr payload (`series.path`, `movie.folderPath`, `artist.path`, or `author.path`), matches it against the Dir Scan **Directory Path** values configured in qui, and uses the provided path itself as the scan root. It does not use qBittorrent path prefixes for this lookup. On success, the response includes `runId`, `directoryId`, `directoryPath`, and `scanRoot`. Each Dir Scan directory can also define **Allowed Download Clients**. When set, native *arr webhook scans only run if the webhook `downloadClient` matches one of those names. Leave the list empty to accept all clients. Matching is case-insensitive and trims surrounding whitespace. Direct simple-mode `{"path": ...}` callers are not filtered by download client. #### Setting up in Sonarr / Radarr 1. Go to **Settings → Connect → Add → Webhook**. 2. Set **Name** to something like `qui Dir Scan`. 3. Under **Notification Triggers**, enable **On File Import**. Optionally enable **On File Upgrade** if you also want scans after upgrades. In Sonarr, **On Import Complete** also works. 4. Set **Webhook URL** to: ```text http://your-qui-host:7476/api/dir-scan/webhook/scan?apikey=YOUR_API_KEY ``` 5. Set **Method** to `POST`. 6. Leave **Username** and **Password** empty (auth is handled by the API key in the URL). 7. Click **Test** or **Save**. The built-in **Test** action is accepted as a no-op health check and does not start a scan. The same steps apply to Lidarr and Readarr. :::tip The webhook uses query-param API key authentication (`?apikey=...`), the same pattern as the cross-seed webhook. You can also use the `X-API-Key` header instead. ::: #### How path matching works qui uses longest-prefix matching against the configured Dir Scan **Directory Path** values to choose which directory settings apply. The actual scan root is the path from the webhook payload. For example, if you have directories configured for `/data/media/movies` and `/data/media/tv`, and Sonarr sends `series.path: "/data/media/tv/Show Name"`, qui matches `/data/media/tv` and scans `/data/media/tv/Show Name`. In split-mount setups, the *arr app must send the same library path that qui sees on disk. If Sonarr/Radarr uses a different mount path than qui, the webhook will not find a matching directory. #### Response codes | Status Code | Meaning | |-------------|---------| | `200` | Webhook accepted but skipped by directory filters. Example: `{"skipped": true, "reason": "download client not allowed"}` | | `202` | Scan accepted. If the directory is idle, qui starts the run immediately. If a webhook scan is already running for that directory, qui keeps one follow-up queued run and merges later webhook paths into it. Example: `{"runId": 42, "directoryId": 3, "directoryPath": "/data/media/tv", "scanRoot": "/data/media/tv/Show Name"}` | | `204` | Test webhook accepted. No scan started | | `400` | Invalid JSON payload, or no supported path field was found in the request body | | `404` | No enabled directory matches the path in the payload | | `409` | Request conflicts with directory state, such as multiple matching directories | | `500` | Internal server error — scan could not be started due to an internal failure | If a second webhook arrives while the same directory is already scanning, qui returns `202` again. It does not reject the request or require client-side retries. Instead, it updates one queued follow-up run for that directory and expands the queued `scanRoot` to the nearest common ancestor when needed. Webhook-triggered scans also ignore the global age cutoff. This avoids false skips when Sonarr/Radarr imports files that preserve old filesystem mtimes. #### Allowed download clients Use **Allowed Download Clients** on a Dir Scan directory when only specific Sonarr/Radarr clients should trigger scans for that path. - Leave it empty to allow all webhook clients. - Add exact client names as shown in Sonarr or Radarr, such as `SABnzbd`, `NZBGet`, or `qBittorrent`. - Matching is case-insensitive and ignores leading/trailing whitespace. - If the webhook is otherwise valid but the client is missing or not allowed, qui returns `200` and skips starting a scan. - Direct simple-mode callers using `{"path": ...}` bypass this filter because they do not provide *arr download client metadata. #### Simple mode You can also call the webhook directly with a plain path (useful for scripts or other tools): ```bash curl -X POST "http://localhost:7476/api/dir-scan/webhook/scan?apikey=YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"path": "/data/media/movies/Movie Name (2024)"}' ``` ### Scan phases Each scan progresses through phases: 1. **Scanning** - Reading directory contents and building searchee list 2. **Searching** - Querying indexers for each searchee 3. **Injecting** - Adding matched torrents to qBittorrent 4. **Final state** - Success, Failed, or Canceled The UI shows current phase and progress during active scans. ## Hardlink/Reflink Modes If the target qBittorrent instance has hardlink or reflink mode enabled, Dir Scan uses the same behavior as other cross-seed methods: - Builds a link tree matching the incoming torrent's layout. - Adds the torrent pointing at that tree (`contentLayout=Original`). Full matches use `skip_checking=true`; partial matches allow qBittorrent to verify existing data and download missing files safely into the link tree. :::note Partial matches in link tree mode (hardlink or reflink) require **Download missing files** to be enabled in Dir Scan settings. Without it, partial link tree injections are rejected. ::: See: - [Hardlink Mode](hardlink-mode) - [Link Directories](link-directories) ### Fallback to regular mode When link-tree creation fails (hardlinking across filesystems, permission issues), Dir Scan falls back to regular add behavior **if** the instance has **Fallback to regular mode** enabled. Otherwise, the candidate fails. Filesystem fallback adds the torrent against the matched source files instead of the link-tree directory, so qui requires a full 100% recheck before auto-resume. If **Skip recheck** is enabled, the fallback candidate is skipped. For partial or otherwise non-perfect fallback matches, qui runs piece-boundary protection before adding the torrent. This fallback check is always enforced, even when **Skip piece boundary safety check** is enabled for regular reuse mode. ## Scanning Your *arr Library Dir Scan can scan Sonarr/Radarr library folders, but be careful with partial matches: :::warning With **Allow partial matches** enabled, qBittorrent may download missing files (extras like `.nfo`, subtitles) directly into your *arr-managed library folder. This can create unexpected files alongside your media. ::: For a "read-only" scan of your library: 1. Disable **Allow partial matches** (full matches only). 2. Disable **Fallback to regular mode** on the target instance so hardlink failures don't add torrents directly against your library path. The safer setup is usually: - Scan your completed downloads/staging folder instead of the final library, and/or - Use hardlink/reflink mode so cross-seeds live under your configured link-tree base directory. ## Troubleshooting ### Recent Scan Runs The **Recent Scan Runs** panel on the Dir Scan page shows: - Added count (successful injections) - Failed count (matches that couldn't be added) - Timestamps and duration Click a run to see details including failure reasons for individual items. ### Common issues **No results found:** - Verify at least one indexer is enabled and not rate-limited. - Check that the scan path contains valid media files. - Ensure the target instance has Local filesystem access enabled. **Permissions errors:** - qui must have read access to the scan path. - Check container volume mounts if running in Docker. **Wrong path mapping:** - Verify qBittorrent Path Prefix matches how qBittorrent sees the same files. - Test by checking a torrent's save path in qBittorrent's UI. **Rate limiting:** - Indexers may throttle requests. Check **Scheduler Activity** on the Indexers page. - Consider reducing scan frequency or limiting to fewer indexers. For cross-seed-wide issues (matching behavior, hardlink failures, recheck problems), see [Troubleshooting](troubleshooting). --- ## OPS/RED (Gazelle) qui can cross-seed between Orpheus (OPS) and Redacted (RED) using the trackers' Gazelle JSON APIs. :::tip TL;DR - Want the best OPS/RED cross-seed coverage: enable Gazelle and set **both** API keys. - If you set **only one** key, Gazelle matching still works, but coverage is **partial**: - OPS-sourced torrents need the **RED** key (because qui queries the opposite site) - RED-sourced torrents need the **OPS** key (because qui queries the opposite site) - "Library Scan" (Seeded Torrent Search) can run in Gazelle-only mode without Torznab. Use it sparingly and prefer an interval of **10+ seconds**. ::: ## What It Does When Gazelle matching is enabled: - OPS/RED source torrents query **only the opposite site** (RED -> OPS, OPS -> RED) - Non-OPS/RED source torrents can still be checked against whichever Gazelle sites you configured - Torznab can run in parallel, but for per-torrent searches (manual/completion/library scan) OPS/RED Torznab indexers are excluded only when **both** Gazelle keys are configured (so partial-key setups keep Torznab as fallback) - If Torznab is unavailable, qui can still return a successful empty result for a torrent that was handled by Gazelle. This includes cases where a local prefilter proves the target tracker content is already present and no remote Gazelle request is needed. Gazelle support exists to better target music specific handling with OPS/RED. qui uses tracker-native APIs that can search by Gazelle release metadata and source-specific infohashes. Direct Gazelle API use gives qui OPS/RED-specific matching and lets Gazelle-only scans run without any Torznab backend, but it also means API pacing and key coverage are handled separately from Torznab indexer rules. ## When It Applies OPS/RED source detection is based on the announce/tracker URL: - RED announce host: `flacsfor.me` - OPS announce host: `home.opsfet.ch` These map to the Gazelle API sites: - RED API host: `redacted.sh` - OPS API host: `orpheus.network` ## Keys And Coverage (ELI5) You can configure one key or both. What qui can query depends on what you seed. - If a torrent is sourced from **OPS**, qui tries to find it on **RED**. That requires a **RED key**. - If a torrent is sourced from **RED**, qui tries to find it on **OPS**. That requires an **OPS key**. If you only set one key, expect this: - Mixed OPS+RED libraries: some torrents will be "no match" simply because qui cannot query the needed opposite site. - Non-OPS/RED torrents: qui will query whichever Gazelle sites you configured (one or both). ## What Happens If Gazelle Isn't Configured If Gazelle is disabled or no API keys are set: - qui falls back to Torznab (Jackett/Prowlarr) where available - Gazelle-only modes (Torznab disabled) cannot run ## How It Matches In order: 1. Infohash match using Gazelle-style `info["source"]` swap logic (see [nemorosa](https://github.com/KyokoMiki/nemorosa)) 2. Filename search + exact total size 3. Filename search + filelist verification (size multiset) If the target tracker is down or errors, the torrent is treated as **no match** and the run continues (best-effort). ## Configuration UI: **Cross-Seed -> Rules -> Gazelle (OPS/RED)** - Enable Gazelle matching - Set one or both API keys - Keys are encrypted at rest and redacted in API/UI responses ## Common Issues ### "torznab disabled but gazelle not configured" You tried to run in Gazelle-only mode (Torznab disabled), but qui has no usable Gazelle client. Fix: - Enable Gazelle - Set at least one API key - If you changed `session_secret`, re-enter the key(s) (old encrypted values cannot be decrypted) - For best OPS/RED coverage, set **both** keys ### Only One Key Set This is supported, but coverage is partial. Example: only RED key is set. - OPS-sourced torrents can be checked against RED - RED-sourced torrents cannot be checked against OPS ## Rate Limiting Requests to OPS/RED are rate-limited and **shared across the whole qui process**, so running multiple qBittorrent instances does not multiply API pressure. Gazelle and Torznab also differ in how time-based search constraints are applied: - With Torznab enabled, Library Scan keeps the conservative per-torrent interval floor used for indexer searches. - With Torznab disabled and Gazelle configured, Library Scan can use a lower interval floor because requests go directly to the tracker APIs instead of through Torznab indexers. - The search cooldown is recorded only after qui actually attempts a remote Gazelle or Torznab request. Local preflight failures, no-backend skips, or local-prefilter skips do not mark the torrent as recently searched. - Duplicate torrent cooldown history is propagated only after the representative torrent has made a cooldown-worthy remote request. ### Library Scan Without Torznab Seeded Torrent Search (Library Scan) can run with **no enabled Torznab indexers** if Gazelle is configured. In that mode: - All source torrents are still processed - Matches come only from configured Gazelle sites (RED/OPS) - You can lower the Library Scan interval below the Torznab floor (minimum 5 seconds), but actual request pacing still respects the shared OPS/RED API rate limits - Recommended: 10+ seconds to reduce API pressure (interval is per-torrent pacing; each torrent can trigger multiple API calls) --- ## Hardlink Mode Hardlink mode is an opt-in cross-seeding strategy that creates a hardlinked copy of the matched files laid out exactly as the incoming torrent expects, then adds the torrent pointing at that hardlink tree. This can make cross-seed alignment simpler and faster, because qBittorrent can start seeding immediately without file rename alignment. ## When to Use - You want cross-seeds to have their own on-disk directory structure (per tracker / per instance / flat), while still sharing data blocks with the original download. - You want to avoid qBittorrent rename-alignment and hash rechecks for layout differences. ## Requirements - Requires **Local filesystem access** on the target qBittorrent instance. - Hardlink base directory must be on the **same filesystem/volume** as the instance's download paths (hardlinks can't cross filesystems). - qui must be able to read the instance's content paths and write to the hardlink base directory. :::tip Multi-filesystem setups If your downloads span multiple filesystems (e.g., `/mnt/disk1`, `/mnt/disk2`), you can specify **multiple base directories** separated by commas. qui will automatically select the first directory that's on the same filesystem as the source files. Example: `/mnt/disk1/cross-seed, /mnt/disk2/cross-seed, /mnt/disk3/cross-seed` ::: :::warning[Docker: Local Filesystem Access] Several qui features require access to the same filesystem paths that qBittorrent uses (orphan scan, hardlinks, reflinks, automations). Mount the same paths qBittorrent uses - paths must match exactly: ```yaml volumes: - /config:/config - /data/torrents:/data/torrents # Must match qBittorrent's path ``` After mounting, enable **Local Filesystem Access** on each instance in qui's Instance Settings. ::: ## Behavior - Hardlink mode is a **per-instance setting** (not per request). Each qBittorrent instance can have its own hardlink configuration. - Torrents added via hardlink/reflink mode always use an explicit `savepath` (the link-tree root), which forces **AutoTMM off**. Enabling AutoTMM after adding can move files out of the link tree. - By default, if a hardlink cannot be created (no local access, filesystem mismatch, invalid base dir, etc.), the cross-seed **fails**. - Enable **"Fallback to regular mode"** to allow failed hardlink operations to use regular cross-seed mode instead of failing. Filesystem fallback uses a full recheck; see [troubleshooting](troubleshooting#when-rechecks-are-required-reuse-mode). - When fallback handles a partial or otherwise non-perfect match, qui runs a piece-boundary safety check before adding the torrent to qBittorrent. This fallback check is always enforced, even if **Skip piece boundary safety check** is enabled for regular reuse mode. - Hardlinked torrents are still categorized using your existing cross-seed category rules (category affix, indexer name, or custom category); the hardlink preset only affects on-disk folder layout. ## Directory Layout Configure in Cross-Seed → Hardlink Mode → (select instance): - **Hardlink base directory**: Path(s) on the qui host where hardlink trees are created. For multi-filesystem setups, specify multiple paths separated by commas (e.g., `/mnt/disk1/cross-seed, /mnt/disk2/cross-seed`). - **Directory preset**: - `flat`: `base/TorrentName--shortHash/...` - `by-tracker`: `base//TorrentName--shortHash/...` - `by-instance`: `base//TorrentName--shortHash/...` ### Isolation Folders For `by-tracker` and `by-instance` presets, qui determines whether an isolation folder is needed based on the torrent's file structure: - **Torrents with a root folder** (e.g., `Movie/video.mkv`, `Movie/subs.srt`) → files already have a common top-level directory, no isolation folder needed - **Rootless torrents** (e.g., `video.mkv`, `subs.srt` at top level) → isolation folder added to prevent file conflicts When an isolation folder is needed, it uses a human-readable format: `` (e.g., `My.Movie.2024.1080p.BluRay--abcdef12`). For the `flat` preset, an isolation folder is always used to keep each torrent's files separated. ## How to Enable 1. Enable "Local filesystem access" on the qBittorrent instance in Instance Settings. 2. In Cross-Seed → Hardlink Mode, expand the instance you want to configure. 3. Enable "Hardlink mode" for that instance. 4. Set "Hardlink base directory": - Single filesystem: `/mnt/data/cross-seed` - Multiple filesystems: `/mnt/disk1/cross-seed, /mnt/disk2/cross-seed, /mnt/disk3/cross-seed` 5. Choose a directory preset (`flat`, `by-tracker`, `by-instance`). 6. Optionally enable "Fallback to regular mode" if you want failed hardlinks to use regular cross-seed mode instead of failing. ## Pause Behavior By default, hardlink-added torrents start seeding immediately (since `skip_checking=true` means they're at 100% instantly). If you want hardlink-added torrents to remain paused, enable the "Skip auto-resume" option for your cross-seed source (Completion, RSS, Webhook, etc.). When hardlink/reflink mode creates a complete link tree with no extra files to download, qui adds the torrent with hash checking skipped and does not trigger an automatic recheck. If qBittorrent instead reports `missing files`, see [Hardlink/reflink cross-seed shows "missing files"](troubleshooting#hardlinkreflink-cross-seed-shows-missing-files). When the incoming torrent has extra files that are not present in the matched torrent, qui adds the torrent paused, triggers a recheck, and resumes it only after qBittorrent reports progress at or above the configured threshold. If hardlink/reflink mode falls back to regular mode for a partial or non-perfect match, the fallback add is stricter: qui first checks piece boundaries, then adds the torrent paused only when the check passes. Safe fallback adds require a full 100% recheck before auto-resume. ## Notes - Hardlinks share disk blocks with the original file but increase the link count. Deleting one link does not necessarily free space until all links are removed. - Windows support: folder names are sanitized to remove characters Windows forbids. Torrent file paths themselves still need to be valid for your qBittorrent setup. - Hardlink mode supports extra files when piece-boundary safe. If the incoming torrent contains extra files not present in the matched torrent (e.g., `.nfo`/`.srt` sidecars), hardlink mode will link the content files and trigger a recheck so qBittorrent downloads the extras. If extras share pieces with content (unsafe), the cross-seed is skipped. - Partial matches (e.g., season packs where only some episodes are on disk) require the **Download missing files** setting to be enabled in [Dir Scan settings](dir-scan#settings-global). Without it, partial link tree injections are rejected. ## Reflink Mode (Alternative) Reflink mode creates copy-on-write clones of the matched files. Unlike hardlinks, reflinks allow qBittorrent to safely modify the cloned files (download missing pieces, repair corrupted data) without affecting the original seeded files. **Key advantage:** Reflink mode **bypasses piece-boundary safety checks**. This means you can cross-seed torrents with extra/missing files even when those files share pieces with existing content—the clones can be safely modified. ### When to Use Reflink Mode - You want to cross-seed torrents that hardlink mode would skip due to "extra files share pieces with content" - Your filesystem supports copy-on-write clones (BTRFS, XFS on Linux; APFS on macOS; ReFS on Windows) - You prefer the safety of copy-on-write over hardlinks ### Reflink Requirements - **Local filesystem access** must be enabled on the target qBittorrent instance. - The base directory must be on the **same filesystem/volume** as the instance's download paths. For multi-filesystem setups, specify multiple paths separated by commas. - The base directory must be a **real filesystem mount**, not a pooled/virtual mount (common examples: `mergerfs`, other FUSE mounts, `overlayfs`). - The filesystem must support reflinks: - **Linux**: BTRFS, XFS (with reflink=1), and similar CoW filesystems - **macOS**: APFS - **Windows**: ReFS on the same volume as the source files and reflink base directory - **FreeBSD**: Not currently supported :::note Windows reflink mode uses ReFS block cloning (requiring a ReFS filesystem). NTFS is not supported. If the matched source path is a symlink, qui resolves it before cloning, and the resolved source plus the reflink base directory still need to be on the same ReFS volume. If reflink creation fails, fallback still depends on the existing "Fallback to regular mode" setting. ::: :::tip On Linux, check the filesystem type with `df -T /path` (you want `xfs`/`btrfs`, not `fuseblk`/`fuse.mergerfs`/`overlayfs`). ::: ### Behavior Differences | Aspect | Hardlink Mode | Reflink Mode | |--------|--------------|--------------| | Piece-boundary check | Skips if unsafe | Never skips (safe to modify clones) | | Recheck | Only when extras or disc layouts require verification | Only when extras or disc layouts require verification | | Disk usage | Zero (shared blocks) | Starts near-zero; grows as modified | ### Disk Usage Implications Reflinks use copy-on-write semantics: - Initially, cloned files share disk blocks with originals (near-zero additional space) - When qBittorrent writes to a clone (downloads extras, repairs pieces), only modified blocks are copied - In worst case (entire file rewritten), disk usage approaches full file size ### How to Enable Reflink Mode 1. Enable "Local filesystem access" on the qBittorrent instance in Instance Settings. 2. In Cross-Seed > Hardlink / Reflink Mode, expand the instance you want to configure. 3. Enable "Reflink mode" for that instance. 4. Set "Base directory": - Single filesystem: `/mnt/data/cross-seed` - Multiple filesystems: `/mnt/disk1/cross-seed, /mnt/disk2/cross-seed` 5. Choose a directory preset (`flat`, `by-tracker`, `by-instance`). 6. Optionally enable "Fallback to regular mode" if you want failed reflinks to use regular cross-seed mode instead of failing. :::note Hardlink and reflink modes are mutually exclusive—only one can be enabled per instance. ::: --- ## Link Directories When **Hardlink mode** or **Reflink mode** is enabled for a qBittorrent instance, qui creates a directory tree that matches the incoming torrent’s expected layout, then adds the torrent pointing at that tree. Because these modes add torrents with an explicit `savepath` (the link-tree root), AutoTMM is always disabled for torrents added via hardlink/reflink mode. This applies to: - Cross-seed searches (RSS, completion, manual, scan) - Directory scan (dirscan) injections ## Settings Configured per qBittorrent instance in **Cross-Seed → Hardlink Mode**: - **Base directory** (`HardlinkBaseDir`): root path where link trees are created. - **Directory preset** (`HardlinkDirPreset`): controls how trees are grouped below the base directory. - **Fallback to regular mode** (`FallbackToRegularMode`): if link-tree creation fails, qui can fall back to “regular mode” instead of skipping/failing. ## Directory Presets qui supports three presets: - `flat`: one folder per torrent under the base directory - Example: `base/Torrent.Name--abcdef12/...` - `by-tracker`: groups by tracker display name, then optional isolation folder - Example: `base/TrackerName/Torrent.Name--abcdef12/...` - `by-instance`: groups by instance name, then optional isolation folder - Example: `base/MyInstance/Torrent.Name--abcdef12/...` ### Tracker Names (by-tracker) For `by-tracker`, qui resolves the folder name using the same fallback chain as cross-seed statistics: 1. **Tracker customization display name** (Settings → Tracker Customizations) 2. Indexer name (from Prowlarr/Jackett) 3. Raw announce domain Folder names are sanitized to be filesystem-safe. ### Isolation Folders For `by-tracker` and `by-instance`, qui adds an isolation folder only when needed: - Torrents with a common root folder don’t need isolation. - “Rootless” torrents (top-level files) use an isolation folder to avoid collisions. For `flat`, an isolation folder is always used. ## Fallback to Regular Mode If **Fallback to regular mode** is enabled, qui will fall back to adding the torrent with a normal `savepath` (pointing at the matched source files) when link-tree creation fails. This is particularly useful when hardlinking can intermittently fail due to filesystem/device boundaries (for example: pooled mounts where two paths look the same but resolve to different underlying devices). Because this fallback uses regular source-file paths instead of the link-tree directory, qui adds the torrent paused, rechecks it, and only auto-resumes after qBittorrent reports 100% complete. If **Skip recheck** is enabled, these fallback candidates are skipped. If fallback is disabled, qui skips/fails the candidate when link-tree creation fails. --- ## Cross-Seed # Cross-Seed Overview qui includes intelligent cross-seeding capabilities that help you automatically find and add matching torrents across different trackers. This allows you to seed the same content on multiple trackers. ## How It Works When you cross-seed a torrent, qui: 1. Finds a matching torrent in your library (same content, different tracker) 2. Adds the new torrent pointing to your existing files 3. Applies the correct category and save path automatically qui supports three modes for handling files: - **Default mode**: Reuses existing files directly. No new files or links are created. May require rename-alignment if the incoming torrent has a different folder/file layout. - **Hardlink mode** (optional): Creates a hardlinked copy of the matched files laid out exactly as the incoming torrent expects, then adds the torrent pointing at that tree. Avoids rename-alignment entirely. - **Reflink mode** (optional): Creates copy-on-write clones (reflinks) of the matched files. Allows safe cross-seeding of torrents with extra/missing files because qBittorrent can write/repair the clones without affecting originals. Disc-based media (Blu-ray/DVD) requires manual verification. See [troubleshooting](troubleshooting#blu-ray-or-dvd-cross-seed-left-paused). ## Prerequisites You need Prowlarr or Jackett to provide Torznab indexer feeds. Add your indexers in **Settings → Indexers** using the "1-click sync" feature to import from Prowlarr/Jackett automatically. Optional: qui can also query OPS/RED directly via the trackers' Gazelle JSON APIs. This complements Torznab, can handle OPS/RED searches even when no Torznab backend is available, and excludes OPS/RED Torznab indexers for per-torrent searches only when **both** Gazelle keys are configured. See [OPS/RED (Gazelle)](gazelle-ops-red). **Optional but recommended:** Configure Sonarr/Radarr instances in **Settings → Integrations** to enable external ID lookups (IMDb, TMDb, TVDb, TVMaze). When configured, qui queries your *arr instances to resolve IDs for cross-seed searches, improving match accuracy on indexers that support ID-based queries. - This is especially helpful for content that is "AKA" type, and can have differing names depending on locale. ## Discovery Methods qui offers several ways to find cross-seed opportunities: ### RSS Automation Scheduled polling of tracker RSS feeds. Configure in the **Auto** tab on the Cross-Seed page. - **Run interval** - How often to poll feeds (minimum 30 minutes) - **Target instances** - Which qBittorrent instances receive cross-seeds - **Target indexers** - Limit to specific indexers or use all enabled ones RSS automation processes the full feed from every enabled indexer on each run, matching against torrents across your target instances. ### Library Scan Deep scan of torrents you already seed to find cross-seed opportunities on other trackers. Configure in the **Scan** tab. - **Source instance** - The qBittorrent instance to scan - **Categories/Tags** - Filter which torrents to include - **Interval** - Delay between processing each torrent (minimum 60 seconds with Torznab enabled; minimum 5 seconds when Torznab is disabled and Gazelle is configured; recommended 10+ seconds for Gazelle-only runs) - **Cooldown** - Skip torrents searched within this window (minimum 12 hours). qui records this only after an actual remote Gazelle or Torznab request, so local preflight failures and local Gazelle skips do not suppress future searches. :::warning Run sparingly. This deep scan touches every matching torrent and queries Torznab and/or Gazelle for each one. Use RSS automation or autobrr for routine coverage; reserve library scan for occasional catch-up passes. ::: ### Auto-Search on Completion Triggers a cross-seed search when torrents finish downloading. Configure in the **Auto** tab under "Auto-search on completion". - **Categories/Tags** - Filter which completed torrents trigger searches - **Target indexers** - Limit completion searches to specific indexers (empty means all enabled) - **Exclude categories/tags** - Skip torrents matching these filters - **Bypass Torznab cache** - When enabled for an instance, completion searches for that instance always perform a fresh Torznab search instead of using cached indexer results. Default: off. Does not affect Gazelle (OPS/RED) searches, which do not use the Torznab cache. - **Search delay** - Wait 0-600 seconds after completion before searching. Default: 0. Use this when post-completion file moves or sister-torrent injection tools need a short head start before qui searches trackers. If a torrent is still **checking** or **moving**, qui waits and runs the completion search afterward instead of searching immediately against an unstable path/state. ### Manual Search Right-click any torrent in the list to access cross-seed actions: - **Search Cross-Seeds** - Query indexers for matching torrents on other trackers - **Filter Cross-Seeds** - Show torrents in your library that share content with the selected torrent (useful for identifying existing cross-seeds) ### Season Pack Assembly Assemble season-pack torrents from individual episodes you already seed. When autobrr announces a season pack, qui checks your qBittorrent instances for matching episodes, links whatever is already local, and lets qBittorrent download the remainder after recheck when coverage passes the configured threshold (default 75%). Sonarr, TVDB, and TVMaze improve the threshold decision when available. Requires local filesystem access and hardlink/reflink mode. See [Season Packs](season-packs) for setup. ## Blocklist Use the per-instance blocklist to prevent specific infohashes from being injected again. - **Manage**: Cross-Seed page → Blocklist tab - **Quick add**: Delete dialog checkbox (only shown for torrents tagged `cross-seed`) --- ## Rules # Cross-Seed Rules Configure matching behavior in the **Rules** tab on the Cross-Seed page. ## Matching - **Find individual episodes** - When enabled, season packs also match individual episodes. When disabled, season packs only match other season packs. Episodes are added with AutoTMM disabled to prevent save path conflicts. - **Size mismatch tolerance** - Maximum size difference percentage (default: 5%). Also determines auto-resume threshold after recheck. - **Skip recheck** - When enabled, skips any cross-seed that would require a recheck (alignment needed, extra files, filesystem fallback, or disc layouts like `BDMV`/`VIDEO_TS`). Applies to all modes including hardlink/reflink. - **Skip piece boundary safety check** - Enabled by default. When enabled, allows cross-seeds even if extra files share torrent pieces with content files. **Warning:** This may corrupt your existing seeded data if content differs. Uncheck this to enable the safety check, or use reflink mode which safely handles these cases. :::note Filesystem fallback and disc layouts (`BDMV`/`VIDEO_TS`) are treated more strictly: they only auto-resume after a full recheck reaches 100%. ::: ## Season Pack Threshold The season-pack webhook uses a separate coverage threshold (default 75%) to decide whether enough local data exists to inject a pack. Season episode totals are sourced from Sonarr first, then TVDB or TVMaze when Sonarr cannot resolve the release. When torrent data is available, qui never uses a total lower than the playable file count in the pack torrent. Incomplete packs are added paused, rechecked, then resumed automatically when qBittorrent reports progress at or above the season-pack threshold. This is configured in **Rules > Season packs**. Instances must have local filesystem access and hardlink or reflink mode enabled to qualify. See [Season Packs](season-packs) for details. Season-pack matching rules live in **Rules > Season packs** and affect only the season-pack webhook flow. ## Categories Choose one of three mutually exclusive category modes: ### Category Affix (default) Adds a configurable affix to the matched torrent's category. Prevents Sonarr/Radarr from importing cross-seeded files as duplicates. In **regular mode** (no hardlink/reflink), AutoTMM is inherited from the matched torrent. **Affix Mode:** - **Suffix** (default): Appends the affix to the category (e.g., `movies` → `movies.cross`) - **Prefix**: Prepends the affix to the category (e.g., `movies` → `cross/movies`) **Affix Value:** The text to add (default: `.cross`). Common examples: - `.cross` using suffix mode → `tv.cross`, `movies.cross` - `cross/` using prefix mode → `cross/tv`, `cross/movies` :::tip Prefix mode with a trailing `/` creates nested categories1 in qBittorrent, making it easy to group all cross-seeds under a parent category. Filtering by `cross` returns all cross-seeds (`cross/movies`, `cross/tv`, etc.). ::: :::warning Avoid using a leading `/` in suffix mode (e.g., `/cross-seed`). This creates the cross-seed as a **child** of the original category1, so setting your category to `movies` in Radarr would also return `movies/cross-seed` torrents, potentially causing conflicts. Use prefix mode instead if you want nested categories. ::: *1 Nested categories require subcategories to be enabled (Instance Preferences → Files → Enable Subcategories).* ### Use indexer name as category Sets category to the indexer name (e.g., `TorrentDB`). AutoTMM is always disabled; uses explicit save paths. ### Custom category Uses a fixed category name for all cross-seeds (e.g., `cross-seed`). AutoTMM is always disabled; uses explicit save paths. ## Source Tagging Configure tags applied to cross-seed torrents based on how they were discovered: | Tag Setting | Description | Default | |-------------|-------------|---------| | RSS Automation Tags | Torrents added via RSS feed polling | `["cross-seed"]` | | Seeded Search Tags | Torrents added via seeded torrent search | `["cross-seed"]` | | Completion Search Tags | Torrents added via completion-triggered search | `["cross-seed"]` | | Webhook Tags | Torrents added via `/apply` webhook | `["cross-seed"]` | | Inherit source torrent tags | Also copy tags from the matched source torrent | - | ## External Program Optionally run an external program after successfully injecting a cross-seed torrent. ## Category Behavior Details ### autoTMM (Auto Torrent Management) autoTMM behavior depends on which category mode is active: | Category Mode | autoTMM Behavior | |---------------|------------------| | **Category Affix** | Inherited from matched torrent (regular mode only; hardlink/reflink disables autoTMM) | | **Indexer name** | Always disabled (explicit save paths) | | **Custom** | Always disabled (explicit save paths) | When autoTMM is inherited (affix mode): - If matched torrent uses autoTMM, cross-seed uses autoTMM - If matched torrent has manual path, cross-seed uses same manual path When autoTMM is disabled (indexer/custom modes), cross-seeds always use explicit save paths derived from the matched torrent's location. :::note Hardlink/reflink mode always adds torrents with an explicit `savepath` pointing at the link tree, which forces autoTMM off. Dir Scan injections are separate from cross-seed rules and also always add with explicit `savepath` (autoTMM off). ::: ### Save Path Determination Priority order: 1. Base category's explicit save path (if configured in qBittorrent) 2. Matched torrent's current save path (fallback) **Examples:** *Suffix mode (default):* - `tv` category has save path `/data/tv` - Cross-seed gets `tv.cross` category with save path `/data/tv` - Files are found because they're in the same location *Prefix mode:* - `movies` category has save path `/data/movies` - Cross-seed gets `cross/movies` category with save path `/data/movies` - Nested `cross/` parent in qBittorrent groups all cross-seeds together ## Best Practices **Do:** - Use autoTMM consistently across your torrents - Let qui create cross-seed categories automatically - Keep category structures simple - Use prefix mode with `/` (e.g., `cross/`) if you want all cross-seeds grouped under one parent category **Don't:** - Manually move torrent files after adding them - Create cross-seed categories manually with different paths - Mix autoTMM and manual paths for the same content type --- ## Season Packs qui can assemble season-pack torrents from individual episodes you already seed. When autobrr announces a season pack, qui checks your qBittorrent instances for completed, release-compatible episodes and, if enough local data is present, builds a linked directory tree, adds the torrent, and lets qBittorrent download anything still missing. ## How It Works 1. autobrr sees a season pack release 2. autobrr sends the torrent name (and optionally the torrent file) to qui's `/api/cross-seed/season-pack/check` endpoint 3. If a torrent file is provided, qui parses its file list to determine playable episode files. If not, qui uses metadata providers for episode counts. 4. qui scans your qBittorrent instances for completed individual episodes that match the season pack's release details 5. qui computes coverage from completed, matching local episodes: - When torrent data is provided, the pack torrent's playable episode files define the expected pack layout - qui asks Sonarr for the season total first, when Sonarr can resolve the show - If Sonarr cannot resolve it, qui falls back to metadata providers: TVDB when configured, then TVMaze - With torrent data, qui never uses a total lower than the playable episode count inside the pack torrent 6. qui responds with: - `200 OK` - coverage meets the threshold, ready to apply - `404 Not Found` - local coverage is too low, the release is not a season pack, or the feature is disabled 7. On `200 OK`, autobrr sends the torrent file to `/api/cross-seed/season-pack/apply` 8. qui links the matched episodes, applies your configured season-pack tags, and adds the season pack torrent 9. If episodes or extras are still missing, qui adds the torrent paused, attempts an automatic recheck, and queues automatic resume. After recheck, qui resumes the torrent when qBittorrent reports progress at or above your configured season-pack coverage threshold. If recheck finishes below that threshold, qui leaves the torrent paused for manual review. Best-effort fallbacks are reported by name, including `automatic recheck failed`, `automatic resume is unavailable`, and `automatic resume queue is full`. ## Coverage Model qui uses a provider-first episode total with the pack torrent as the layout source. For `/check` without torrent data: - qui asks Sonarr for the season episode total first - If Sonarr fails or cannot resolve the show, qui asks TVDB when configured, otherwise TVMaze - If no provider returns a total, qui skips threshold enforcement and only verifies that matching episodes exist For `/check` or `/apply` with torrent data: - The torrent file is the source of truth for the pack layout and playable episode files - qui still asks Sonarr, then TVDB/TVMaze, for a season total - If the provider total is lower than the playable episode count in the torrent, qui uses the playable file count instead The apply endpoint always requires the torrent file and enforces the threshold. When qui falls back to the pack torrent, it: - Counts only playable video files (mkv, mp4, avi, etc.) - Ignores subtitles, NFOs, samples, and other extras - Deduplicates episodes that appear more than once - Rejects packs with zero usable episode files Coverage is then: `matchedLocalEpisodes / coverageTotalEpisodes` For an episode to count toward coverage, it must: - Be fully downloaded (`100%` progress) - Pass the same release-compatibility checks used by normal cross-seeding - Belong to the same episode in the season pack This means mixed variants do **not** count toward coverage. For example, `720p WEB` episodes do not satisfy a `1080p BluRay` season pack. The default threshold is **75%**. Change it in **Cross-Seed > Rules > Season packs** in the qui UI. ## Matching Settings These settings only affect season-pack checks and applies. They do not change normal cross-seed matching in the Rules tab. Defaults are chosen to match common seasonpackarr expectations. | Setting | Default | Effect | Example | | --- | --- | --- | --- | | Ignore REPACK/PROPER differences | On | Treat REPACK and PROPER episodes as compatible with the season pack. | `Show.S01E01.REPACK` matches `Show.S01E01.PROPER` | | Simplify HDR matching | Off | Treat HDR10, HDR10+, and HDR+ as HDR for season-pack matching. | `HDR10+` matches `HDR10` | | Simplify WEB source matching | Off | Treat WEB-DL as WEB for season-pack matching. | `WEB-DL` matches `WEB` | | Ignore year differences | Off | Allow matches when release dates differ or one side omits the year. | `Show.2024.S01E01` matches `Show.2025.S01E01` | ## Apply Model Passing the threshold does **not** require 100% local coverage. When `/apply` runs, qui: - Links every matched episode file it can verify locally - Leaves unmatched episodes and extras for qBittorrent to download - Adds the torrent paused when anything is still missing - Attempts an automatic recheck so qBittorrent can discover the linked bytes - Queues automatic resume after recheck. qui resumes the torrent when qBittorrent reports progress at or above the configured season-pack coverage threshold, so qBittorrent can download the remaining files or pieces. If recheck finishes below that threshold, qui leaves the torrent paused for manual review. If automatic recheck or resume queueing cannot be started, qui reports `automatic recheck failed`, `automatic resume is unavailable`, or `automatic resume queue is full`. If **Skip Recheck** is enabled and the pack is incomplete, qui skips the apply instead of adding a broken torrent. In hardlink mode, incomplete packs are also subject to piece-boundary protection. If pending files share torrent pieces with linked episode files, qui blocks the apply unless **Skip piece boundary safety check** is enabled. Reflink mode avoids that hardlink corruption risk because qBittorrent writes to cloned files instead of the original seeded files. ## Prerequisites - **Local filesystem access** must be enabled on the target instance - **Hardlink or reflink mode** must be enabled on the target instance - season packs always use linked trees - The instance's link-mode base directory must be configured and writable. In the current UI/API this is the same base-directory field used by hardlink/reflink mode. Instances without local filesystem access or a link mode are skipped during eligibility checks. See [Hardlink Mode](hardlink-mode) for setup instructions. ## Setup ### 1. Enable Season Packs in qui - Go to **Cross-Seed > Rules > Season packs** - Enable the feature - Set the coverage threshold (default 75%) - Optionally, add a TVDB API key for improved episode count accuracy. TVMaze is used automatically as a free fallback without any configuration. - Optionally, configure **Category routing** for season pack injects. Add rules that map a resolution (and optionally a source) to a qBittorrent category, then set an **Anything else** fallback category for packs that match no rule. If you run multiple Sonarr instances, point each rule at the category that Sonarr watches on its qBittorrent download client: route `1080p` to `tv-hd` and `2160p` to `tv-uhd`, for example. Sonarr will pick up the assembled pack and hardlink-import its files into your library, so the same on-disk bytes back both the library and every seeded episode. Categories are created on demand when they do not exist yet, and existing categories are used untouched. If no rule matches and no fallback is set, season packs use the global Category Mode configured under **Cross-Seed > Rules > Categories**. #### Category routing Each routing rule matches on a resolution and an optional source: | Field | Values | Effect | | --- | --- | --- | | Resolution | `2160p`, `1080p`, `720p`, `576p`, `480p` | Required. The pack resolution the rule applies to. | | Source | Any, `WEB`, `BluRay`, `Remux`, `HDTV` | Optional. Restricts the rule to a single source, or leave as **Any** to match every source at that resolution. | | Category | A qBittorrent category | Where matching packs are filed. Created on demand if it does not exist. | When more than one rule could match a pack, the most specific rule wins: a rule with an explicit source beats an **Any**-source rule at the same resolution. If no rule matches, qui uses the **Anything else** fallback category. :::tip **Remux** is detected from the release tags, not the source field. A BluRay remux carries the remux tag, so it routes under the **Remux** option rather than the **BluRay** option. Add a separate `Remux` rule when you want remuxes filed away from regular BluRay packs. ::: ### 2. Create an API Key If you don't already have one for autobrr: - Go to **Settings > API Keys** - Click **Create API Key** - Copy the generated key ### 3. Configure autobrr External Filter :::important Create a **separate autobrr filter** for season packs. Do not reuse your existing cross-seed filter - the endpoints and payload are different. ::: :::tip **Docker Compose:** use your qui container hostname instead of `localhost` (often the Compose service name), for example: `http://qui:7476/api/cross-seed/season-pack/check`. ::: In your new autobrr filter, go to **External** tab > **Add new**: | Field | Value | | ------------------------- | --------------------------------------------------------- | | Type | `Webhook` | | Name | `qui season pack` | | On Error | `Reject` | | Endpoint | `http://localhost:7476/api/cross-seed/season-pack/check` | | HTTP Method | `POST` | | HTTP Request Headers | `X-API-Key=YOUR_QUI_API_KEY` | | Expected HTTP Status Code | `200` | **Data (JSON):** ```json { "torrentName": {{ toRawJson .TorrentName }}, "instanceIds": [1], "indexer": {{ toRawJson .Indexer }} } ``` To search all instances, omit `instanceIds`: ```json { "torrentName": {{ toRawJson .TorrentName }}, "indexer": {{ toRawJson .Indexer }} } ``` :::tip The check endpoint does not require the torrent file. Sending only the release name avoids downloading the `.torrent` for every season pack announce. qui uses Sonarr, TVDB, or TVMaze to determine the episode count for threshold enforcement. To include the torrent file in the check request, add `"torrentData": "{{ .TorrentDataRawBytes | toString | b64enc }}"` to the payload. ::: **Field descriptions:** - `torrentName` (required) - The release name as announced - `torrentData` (optional) - Base64-encoded torrent file. When provided, qui parses it to determine playable pack files. When omitted, qui uses metadata providers for episode counts. - `instanceIds` (optional) - qBittorrent instance IDs to scan. Omit to search all eligible instances. - `indexer` (optional) - autobrr indexer identifier. Used when **Use indexer name as category** is enabled. ### 4. Configure the Apply Action When `/check` returns `200 OK`, send the torrent to `/api/cross-seed/season-pack/apply`: **Action setup in autobrr:** | Field | Value | | ----------- | -------------------------------------------------------------------------------- | | Action Type | `Webhook` | | Name | `qui season pack apply` | | Endpoint | `http://localhost:7476/api/cross-seed/season-pack/apply?apikey=YOUR_QUI_API_KEY` | **Payload (JSON):** ```json { "torrentName": {{ toRawJson .TorrentName }}, "torrentData": "{{ .TorrentDataRawBytes | toString | b64enc }}", "instanceIds": [1], "indexer": {{ toRawJson .Indexer }} } ``` **Field descriptions:** - `torrentName` (required) - The release name - `torrentData` (required) - Base64-encoded torrent file - `instanceIds` (optional) - Target instances (omit to apply to any matching instance) - `indexer` (optional) - autobrr indexer identifier. Used when **Use indexer name as category** is enabled. ## API Endpoints | Method | Path | Description | | ------ | ------------------------------------- | -------------------------- | | POST | `/api/cross-seed/season-pack/check` | Check if a pack can be assembled | | POST | `/api/cross-seed/season-pack/apply` | Assemble and add the pack | | GET | `/api/cross-seed/season-pack/runs` | List recent activity | The `/runs` endpoint accepts an optional `limit` query parameter (default 20, max 200). qui keeps the most recent 200 season-pack runs and prunes older rows when new check/apply activity is recorded. `/check` returns `404 Not Found` for expected skips such as below-threshold coverage, disabled season packs, non-season-pack releases, or no eligible instances. `/apply` returns `500 Internal Server Error` when the pack cannot be applied, including skipped recheck-required packs, layout mismatch, add failure, or operational failures while reading qBittorrent state. ## Added Torrent Behavior When qui applies a season pack, it: - Always adds the torrent with an explicit `savepath` pointing at the linked tree - Applies the tags configured in **Cross-Seed > Rules > Season packs** - Adds incomplete packs paused, then best-effort attempts automatic recheck and queues automatic resume. After recheck, qui resumes at or above the configured season-pack coverage threshold; below that threshold, the torrent stays paused for manual review. - Resolves the category in this order: - The category from the matching **Category routing** rule under **Cross-Seed > Rules > Season packs**, choosing the most specific rule when several apply (an explicit-source rule beats an Any-source rule at the same resolution). Recommended for Sonarr integration so the pack lands in Sonarr's download-client category and inherits hardlink-aware imports - Otherwise the **Anything else** fallback category, if set - Otherwise the global cross-seed category rules: custom category if enabled, otherwise category affix mode if enabled, otherwise indexer-name category if enabled, otherwise inheriting the matched episode's category - Creates the resolved category on the target instance if it does not already exist ## Instance Selection When `instanceIds` is omitted or contains multiple instances: 1. qui filters to instances with local filesystem access and hardlink/reflink mode 2. Existing webhook source filters are applied 3. The instance with the highest coverage is selected 4. Ties are broken by highest matched episode count, then lowest instance ID ## Activity Each check and apply request records a season-pack run. qui keeps the most recent 200 runs. Recent runs are shown in **Cross-Seed > Rules > Season packs**. The panel shows the torrent name, phase (`check` or `apply`), status, reason, message, selected instance, matched episodes, total episodes, coverage, link mode, and timestamp. You can also query recent runs directly: ```bash curl -H "X-API-Key: YOUR_QUI_API_KEY" "http://localhost:7476/api/cross-seed/season-pack/runs?limit=20" ``` ## Debugging Start with autobrr: - A rejected check usually appears as `[external webhook status code] not matching: got 404 want: 200` - That means qui answered the season-pack check but did not consider the release ready to apply - Confirm the release used the season-pack filter, not the regular cross-seed filter Then check qui: - Open **Cross-Seed > Rules > Season packs** and find the recent row for the torrent name - Check the phase (`check` or `apply`), status, reason, message, coverage, matched episodes, total episodes, selected instance, and link mode - If the row is missing, autobrr probably did not reach qui or used the wrong endpoint/API key. You can confirm with `/api/cross-seed/season-pack/runs?limit=20`. For deeper logs, set: ```toml loglevel = 'DEBUG' ``` Look for messages containing the torrent name and these clues: - `season pack: failed to resolve Sonarr season total` - Sonarr lookup failed, so qui fell back to metadata providers or skipped threshold enforcement - `season pack: metadata provider lookup failed` - TVDB/TVMaze lookup failed - `load cached torrents for instance` - qBittorrent cache lookup failed, so the check/apply is an operational failure - `unsafe piece boundary with pending files` - hardlink mode blocked an incomplete pack for safety - `torrent added paused; recheck queued` - qui added the pack and queued automatic resume - `Recheck completed below threshold, torrent left paused for manual review` - qBittorrent rechecked below the configured season-pack coverage threshold Use `TRACE` when you need field-level matching details. Then look for `[CROSSSEED-MATCH] Release filtered` entries to see which release field caused an episode to be rejected. --- ## Troubleshooting # Cross-Seed Troubleshooting ## Why didn't my cross-seed get added? ### Rate limiting (HTTP 429) Indexers limit how frequently you can make requests. If you see errors like `"indexer TorrentLeech rate-limited until..."`, qui has recorded the cooldown and will skip that indexer until it's available. Check the **Scheduler Activity** panel on the Indexers page to see which indexers are in cooldown and when they'll be ready. ### Release didn't match qui uses strict matching to ensure cross-seeds have identical files. Both releases must match on: - Title, year, and release group - Resolution (1080p, 2160p) - Source (WEB-DL, BluRay) and collection (AMZN, NF) - Codec (x264, x265) and HDR format - Audio format and channels - Language, edition, cut, and version (v2, v3) - Variants like IMAX, HYBRID, REPACK, PROPER ### Season pack vs episodes By default, season packs only match other season packs. Enable **Find individual episodes** in settings to allow season packs to match individual episode releases. ## Cross-seed search run statuses Library scan and completion search rows use **added**, **skipped**, or **failed** as the top-level outcome. Open the row details to see the per-attempt status and message. | Status or message | Outcome | What it usually means | What to check | | --- | --- | --- | --- | | `exists` | Skipped | The exact torrent infohash is already in the target qBittorrent instance. | This is normally harmless. If you expected a new tracker result, check the source and target indexers in [Cross-Seed Overview](overview#discovery-methods). | | `no_match` | Skipped | qui searched but did not find an existing local torrent with the required files. | Review [release matching](#release-didnt-match), source filters, and the discovery method in [Library Scan](overview#library-scan) or [Auto-Search on Completion](overview#auto-search-on-completion). | | `blocked` | Skipped | The candidate infohash is on the cross-seed blocklist. | Remove it from **Cross-Seed > Blocklist** if you want qui to try it again. See [Blocklist](overview#blocklist). | | `skipped_recheck` | Skipped | The match would require a recheck, but **Skip recheck** is enabled. | See [When Rechecks Are Required](#when-rechecks-are-required-reuse-mode) and [Rules](rules#matching). | | `skipped_unsafe_pieces` | Skipped | The incoming torrent has missing or extra files whose pieces overlap existing content, or a link-mode fallback would leave unsafe unmaterialized pieces. qui skips before adding to avoid modifying existing data. | See [Cross-seed skipped: "extra files share pieces with content"](#cross-seed-skipped-extra-files-share-pieces-with-content) and [Reflink Mode](hardlink-mode#reflink-mode-alternative). | | `below_threshold` | Skipped | The matched files do not meet the configured completion threshold after materialization or recheck. | Check **Size mismatch tolerance** in [Rules](rules#matching), then see [Cross-seed stuck at low percentage after recheck](#cross-seed-stuck-at-low-percentage-after-recheck). | | `requires_hardlink_reflink` | Skipped | The torrent layout would scatter rootless or extra files in regular reuse mode. | Enable [Hardlink Mode](hardlink-mode) or [Reflink Mode](hardlink-mode#reflink-mode-alternative), or download the torrent normally. | | `size_mismatch` | Failed | A search result already exists by infohash, but the earlier content prefilter rejected it because the torrent file list did not match the source sizes. | Compare the torrent files on the trackers. This protects you from treating different content as a valid cross-seed. See [release matching](#release-didnt-match). | | `content_mismatch` | Failed | A search result already exists by infohash, but the earlier content prefilter rejected it for a non-size file-level reason. | Review the row message and enable trace logging if needed. See [How do I see why a release was filtered?](#how-do-i-see-why-a-release-was-filtered). | | `hardlink_error` | Failed | Hardlink mode was enabled but qui could not create or use the hardlink tree. | See [Hardlink mode failed](#hardlink-mode-failed) and [Hardlink Mode requirements](hardlink-mode#requirements). | | `reflink_error` | Failed | Reflink mode was enabled but qui could not create or use the reflink tree. | See [Reflink mode failed](#reflink-mode-failed) and [Reflink Requirements](hardlink-mode#reflink-requirements). | | `no_save_path` | Failed | qui could not find a valid target save path for the cross-seed. The matched torrent has no usable SavePath and the category does not provide an explicit SavePath. | Verify the matched torrent's save path and category save path in qBittorrent, then review [category behavior](rules#category-behavior-details). | | `error`, `alignment_failed`, or `pause_failed` | Failed | qBittorrent rejected the add, a required file or folder rename failed, or qui could not pause a misaligned torrent after an alignment failure. | Check the instance connection, qBittorrent logs, and save path/category behavior in [Rules](rules#category-behavior-details). | Failed search or completion runs can trigger notification events. See [Notifications](../notifications#event-types) for the event keys. :::tip `size_mismatch` failures are generated from the size reported inside of torrent files, not the content on disk. These failures are strong indicators that the cross seeded content has mismatching piece hashes between trackers. One or more trackers had a bad hash copy. The failures are the size mismatches against the selected source torrent used for cross seed searching (typically content in a folder), they are not reports of which trackers actually have bad hashes. If the source torrent is the bad hash, the hash in `debug` logging `[CROSSSEED-ASYNC] Starting async torrent analysis` shows the source hash that was used. ::: :::tip Use [piece boundary protection](rules#matching) to protect content against bad hash torrents. ## Why did my season-pack check return 404? The season-pack check webhook returns `404 Not Found` whenever the pack is not ready to apply. In autobrr this usually appears as `[external webhook status code] not matching: got 404 want: 200`. Common reasons: - **Coverage is below your threshold**: qui did not find enough matching episodes - **Episodes are still downloading**: only fully completed episode torrents count toward coverage - **Release details do not match**: the episodes must match the pack's title, season, and normal release details such as source, resolution, and release group - **No eligible instance was scanned**: the instance needs local filesystem access plus hardlink or reflink mode - **Webhook source filters excluded your episodes**: include/exclude category or tag filters removed them from the scan - **The release is not a season pack** or **season-pack matching is disabled** If the pack should match except for REPACK, HDR, WEB, or year differences, check **Cross-Seed > Rules > Season packs > Matching settings**. Open **Cross-Seed > Rules > Season packs** for recent season-pack activity. It shows the check/apply phase, status, reason, message, coverage, matched episodes, total episodes, selected instance, and link mode. You can also query `/api/cross-seed/season-pack/runs?limit=20` directly. See [Season Packs](season-packs) for the full flow, setup requirements, and season-pack-specific debugging steps. ## How do I see why a release was filtered? Enable trace logging to see detailed rejection reasons: ```toml loglevel = 'TRACE' ``` Look for `[CROSSSEED-MATCH] Release filtered` entries showing exactly which field caused the mismatch (e.g., `group_mismatch`, `resolution_mismatch`, `language_mismatch`). For content-prefilter decisions, `DEBUG` is enough. Look for messages such as: - `crossseed: rejected existing content prefilter candidate after file-level matching` - `[CROSSSEED-SEARCH] Late content filter exclusion` - `[CROSSSEED-APPLY] Failed cached search selection already present after content prefilter rejection` - `[CROSSSEED-SEARCH-AUTO] Existing search result failed due to prior content prefilter rejection` For season-pack checks, `DEBUG` is often enough. Look for the torrent name and messages such as: - `season pack: failed to resolve Sonarr season total` - `season pack: metadata provider lookup failed` - `load cached torrents for instance` - `unsafe piece boundary with pending files` - `torrent added paused; recheck queued` - `Recheck completed below threshold, torrent left paused for manual review` ## When Rechecks Are Required (Reuse Mode) In reuse mode (the default), most cross-seeds are added with hash verification skipped (`skip_checking=true`) and resume immediately. Some scenarios require a recheck: ### 1. Name or folder alignment needed When the cross-seed torrent has a different display name or root folder, qui renames them to match. qBittorrent must recheck to verify files at the new paths. ### 2. Extra files in source torrent When the source torrent contains files not on disk (NFO, SRT, samples not matching allowed extra file patterns), a recheck determines actual progress. ### 3. Hardlink/reflink filesystem fallback When link-tree creation fails because the source files and link-tree base are on different filesystems, or because the filesystem does not support the requested link type, qui can fall back to regular mode if **Fallback to regular mode** is enabled. The torrent is added against the matched source files, not the link-tree directory. These fallback torrents are treated like disc-based content: they are added paused, rechecked, and only auto-resume after qBittorrent reports 100% complete. If **Skip recheck** is enabled, qui skips them instead. With **Skip recheck** enabled, a better workflow would have **Fallback to regular mode** disabled, since all fallbacks require recheck. For partial-in-pack, size-based, renamed, or otherwise non-perfect matches, qui also runs piece-boundary protection before the fallback add. This check is always enforced for link-mode fallback, even when **Skip piece boundary safety check** is enabled for regular reuse mode. If the check fails, qui skips the torrent before adding it to qBittorrent. ### Auto-resume behavior - Default tolerance 5% → auto-resumes at ≥95% completion - Torrents below threshold stay paused for manual investigation - Filesystem fallback and disc-layout torrents require 100% completion before auto-resume - Configure via **Size mismatch tolerance** in Rules ## Hardlink mode failed Common causes: - **Filesystem mismatch**: Hardlink base directory is on a different filesystem/volume than the download paths. Hardlinks cannot cross filesystems. - **Missing local filesystem access**: The target instance doesn't have "Local filesystem access" enabled in Instance Settings. - **Permissions**: qui cannot read the instance's content paths or write to the hardlink base directory. - **Invalid base directory**: The hardlink base directory path doesn't exist and couldn't be created. ## Hardlink/reflink cross-seed shows "missing files" When hardlink or reflink mode creates every file needed by the incoming torrent, qui adds it with hash checking skipped and starts it immediately. No automatic recheck is triggered because there are no missing extras for qBittorrent to discover. If qBittorrent still marks the torrent as `missing files`, the new torrent file most likely does not fully match the existing source/candidate files, even though qui matched them by name and size. Review the matching torrent group on the tracker/s before resuming or rechecking the torrent, as one of the copies has corrupted hash/es. - **Hardlink mode**: Resuming the torrent will overwrite the bad hashes for that torrent, corrupting the existing torrent/s with the other piece hash/es. - **Reflink mode**: Resuming the torrent will leverage copy-on-write to protect the other torrent hash/es. :::tip Torrents with bad hash/es should be reported at their relevant sites. ::: :::warning Ignoring the bad hash/es in hardlink mode and resuming, will cause repeated full torrent rechecks, and downloading bad pieces, on torrents in the matching group, every time a peer requests the mis-matched hash/es and forces qBitTorrent to validate. ::: ## "Files not found" after cross-seed (default mode) This typically occurs in default mode when the save path doesn't match where files actually exist: - Check that the cross-seed's save path matches where files actually exist - Verify the matched torrent's save path in qBittorrent - Ensure the matched torrent has completed downloading (100% progress) ## Reflink mode failed Common causes: - **Filesystem doesn't support reflinks**: The filesystem at the base directory doesn't support copy-on-write clones. On Linux, use BTRFS or XFS (with reflink enabled). On macOS, use APFS. - **Pooled/virtual mount**: The base directory is on a pooled/virtual filesystem (like `mergerfs`, other FUSE mounts, or `overlayfs`) which often does not implement reflink cloning. Use a direct disk mount for both your seeded data and the reflink base directory. - **Filesystem mismatch**: Base directory is on a different filesystem than the download paths. - **Missing local filesystem access**: The target instance doesn't have "Local filesystem access" enabled. - **SkipRecheck enabled**: If reflink mode would require recheck (extra files), it skips the cross-seed. ## Cross-seed skipped: "extra files share pieces with content" In regular reuse mode, this occurs when you have enabled the piece boundary safety check (disabled "Skip piece boundary safety check" in Rules). Link-mode fallback is stricter: for partial or otherwise non-perfect matches, qui always performs the check before adding the torrent to qBittorrent. The incoming torrent has files not present in your matched torrent, and those files share torrent pieces with your existing content. Downloading them could overwrite parts of your existing files. **Solutions:** - **Use reflink mode** (recommended): Enable reflink mode for the instance—it safely clones files so qBittorrent can modify them without affecting originals - **Disable the safety check**: Check "Skip piece boundary safety check" in Rules (the default). The match will proceed but **may corrupt your existing seeded files** if content differs - If reflinks aren't available and you want to avoid any risk, download the torrent fresh ## Cross-seed stuck at low percentage after recheck - Check if the source torrent has extra files (NFO, samples) not present on disk - Verify the "Size mismatch tolerance" setting in Rules - Torrents below the auto-resume threshold stay paused for manual review ## Blu-ray or DVD cross-seed left paused Torrents containing disc-based media (Blu-ray `BDMV` or DVD `VIDEO_TS` folder structures) are always added paused. **Why?** Disc layout torrents are sensitive to file alignment. Even minor path differences can cause qBittorrent to redownload large video segments, potentially corrupting your seeded content. Leaving them paused lets you verify the recheck completed at 100% before resuming. **What to do:** 1. If **Skip recheck** is enabled in Cross-Seed Rules, disc-layout matches will be skipped. 2. Otherwise, qui triggers a recheck automatically and will only auto-resume once the recheck reaches **100%**. 3. If you have auto-resume disabled, resume manually after verifying it reaches 100%. The result message will indicate when this policy applies (example): `"disc layout detected (BDMV), full recheck required"` ## Webhook returns HTTP 400 "invalid character" error This typically means the torrent name contains special characters (like double quotes `"`) that break JSON encoding. The error often looks like: ```json {"level":"error","error":"invalid character 'V' after object key:value pair","time":"...","message":"Failed to decode webhook check request"} ``` **Solution:** In your autobrr webhook configuration, use `toRawJson` instead of quoting the template variable directly: ```json { "torrentName": {{ toRawJson .TorrentName }}, "instanceIds": [1] } ``` **Not:** ```json { "torrentName": "{{ .TorrentName }}", "instanceIds": [1] } ``` The `toRawJson` function (from Sprig) properly escapes special characters and outputs a valid JSON string including the quotes. ## Cross-seed in wrong category - Check your cross-seed settings in qui - Verify the matched torrent has the expected category - For Dir Scan injections, Cross-Seed → Rules category modes do not apply. Dir Scan uses its own Default Category / Category override, and leaving it blank results in no category. ## autoTMM unexpectedly enabled/disabled - In reuse/affix mode (regular mode), autoTMM mirrors the matched torrent's setting (intentional) - In indexer name or custom category mode, autoTMM is always disabled - In hardlink/reflink mode, autoTMM is always disabled (explicit `savepath`) - Dir Scan injections always disable autoTMM (explicit `savepath`) - Check the original torrent's autoTMM status in qBittorrent --- ## External Programs Launch scripts or desktop applications directly from the torrent context menu. Each program definition stores the executable path, optional arguments, and path-mapping rules so qui can pass torrent metadata to your tools. ## Security: Allow List To keep this power feature safe, define an allow list in `config.toml` so only trusted paths can be executed: ```toml externalProgramAllowList = [ "/usr/local/bin/sonarr", "/home/user/bin" # Directories allow any executable inside them ] ``` Leave the list empty to keep the previous behaviour (any path accepted). The allow list lives exclusively in `config.toml`, which the web UI cannot edit, so you retain control over what binaries are exposed. ## Where Programs Run External programs always run on the same machine (or container) that is hosting the qui backend, not on the browser client. Make sure any executable paths, mounts, or environment variables are available to that host process. When you deploy qui inside Docker, the program runs inside the container unless you mount the executable in. ## Creating and Editing a Program 1. Open qui and go to **Settings → External Programs** 2. Click **Create External Program** 3. Fill in the form fields, then press **Create**. Toggle **Enable this program** to make it available in torrent menus 4. Use the edit and delete actions in the list to maintain existing programs ### Field Reference | Field | Description | |-------|-------------| | **Name** | Display label shown in the torrent context menu and settings list. Must be unique. | | **Program Path** | Absolute path to the executable or script. Use the host path seen by the qui backend (e.g. `/usr/local/bin/my-script.sh`, `C:\Scripts\postprocess.bat`, `C:\python312\python.exe`). | | **Arguments Template** | Optional string of command-line arguments. qui substitutes torrent metadata placeholders before spawning the process. | | **Path Mappings** | Optional array of `from → to` prefixes that rewrite remote qBittorrent paths into local mount points. Helpful when qui runs locally but qBittorrent stores data elsewhere. | | **Launch in terminal window** | Opens the program in an interactive terminal window. See [Supported Terminal Emulators](#supported-terminal-emulators) for the list of detected terminals. Disable for GUI apps or background daemons. | | **Enable this program** | Determines whether the program shows up in the torrent context menu. | ## Torrent Placeholders Arguments are parsed with shell-style quoting and each placeholder is replaced with the corresponding torrent value before execution. | Placeholder | Value | |-------------|-------| | `{hash}` | Torrent hash (always lowercase) | | `{name}` | Torrent name | | `{save_path}` | Torrent save path after path mappings are applied | | `{content_path}` | Full content path (file or folder) after path mappings are applied | | `{category}` | Torrent category | | `{tags}` | Comma-separated list of tags | | `{state}` | qBittorrent torrent state string | | `{size}` | Size in bytes | | `{progress}` | Progress value between 0 and 1 rounded to two decimal places | | `{comment}` | Torrent comment | **Example arguments:** ```text "{hash}" "{name}" --save "{save_path}" --category "{category}" --tags "{tags}" ``` ```text D:\Upload Assistant\upload.py {save_path}\{name} ``` qui splits the template into arguments before substitutions are run, so you do not need to wrap values in extra quotes unless the called application expects them. ## Path Mappings Use path mappings when the filesystem paths reported by qBittorrent do not match the paths visible to qui. Each mapping replaces the longest matching prefix. | Remote path (from qBittorrent) | Local path seen by qui | Mapping | |--------------------------------|------------------------|---------| | `/data/torrents` | `/mnt/qbt` | `from=/data/torrents`, `to=/mnt/qbt` | | `Z:\downloads` | `/srv/downloads` | `from=Z:\downloads`, `to=/srv/downloads` | Given the template above, `{save_path}` becomes `/mnt/qbt/Movies` instead of `/data/torrents/Movies`. Be sure to use the same path separator style (`/` vs `\`) as the remote qBittorrent instance. If no mapping matches, the original path is used. ## Launch Modes - **Enable terminal window** for scripts that need interaction or visible output. - **Disable terminal window** for GUI applications or background tasks. Programs run asynchronously - qui does not wait for completion. ### Supported Terminal Emulators When "Launch in terminal window" is enabled, qui automatically detects and uses an available terminal emulator. Detection priority: 1. **TERM_PROGRAM environment variable** - If qui is running inside a terminal, that terminal is preferred 2. **Cross-platform terminals** (checked on all platforms): - WezTerm - Hyper - Kitty - Alacritty 3. **Linux terminals**: - GNOME Terminal - Konsole - Xfce4 Terminal - MATE Terminal - xterm - Terminator 4. **macOS native terminals**: - iTerm2 - Terminal.app 5. **Fallback**: If no terminal is found, the command runs in the background via `sh -c` On Windows, `cmd.exe` is always used. :::tip Terminal windows stay open after the command finishes, allowing you to inspect output. Close the window manually when done. ::: ## Executing Programs 1. Select one or more torrents 2. Right-click to open the context menu 3. Hover **External Programs**, then click the program name 4. qui queues one execution per selected torrent. Results are reported via toast notifications (success, partial success, or failure) Execution requests include the torrents from the currently selected instance only. Disabled programs are hidden from the submenu. Command failures emitted by the host OS are logged at `info`/`debug` level through zerolog; enable debug logging to see the full command line and any non-zero exit codes. ## REST API Automation workflows can manage external programs through the backend API (all endpoints require authentication): | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/external-programs` | List programs | | `POST` | `/api/external-programs` | Create a program | | `PUT` | `/api/external-programs/{id}` | Update a program | | `DELETE` | `/api/external-programs/{id}` | Remove a program | | `POST` | `/api/external-programs/execute` | Execute a program | **Example request:** ```http POST /api/external-programs/execute Content-Type: application/json { "program_id": 2, "instance_id": 1, "hashes": ["c0ffee...", "deadbeef..."] } ``` The response contains a `results` array with per-hash `success` flags and optional error messages. Treat the endpoint as fire-and-forget; it returns once the processes have been spawned. ## Automation Integration External programs can be triggered automatically via automation rules, allowing you to run scripts when torrents match specific conditions. ### Setting Up Automation Triggers 1. Create and enable an external program in **Settings → External Programs** 2. Go to **Automations** and create or edit a rule 3. Add an **External Program** action and select your program 4. Optionally add a condition override specific to this action ### Behavior | Aspect | Description | |--------|-------------| | **Execution** | Programs run asynchronously (fire-and-forget) to avoid blocking automation processing | | **Configuration** | Uses the same program settings (path, arguments, path mappings) as manual execution | | **Availability** | Only enabled programs appear in the automation dropdown | | **Combinable** | Can be combined with other actions (speed limits, share limits, pause, tag, category) | ### Activity Logging Automation-triggered executions are logged in the activity feed with: - Rule name and rule ID that triggered the execution - Torrent name and hash - Success or failure status - Error details if the program failed to start :::note Success is logged after the program actually starts, not when queued. If the program fails to start (e.g., executable not found, permission denied), the error is captured and logged. ::: ### Example Use Cases **Post-processing completed downloads:** - Condition: `State is completed` - Action: External Program that runs a media processing script **Webhook notifications:** - Condition: `Is Unregistered is true` - Action: External Program that sends a notification via curl/webhook **Media library scans:** - Condition: Category changed to "movies" (use category action + external program) - Action: External Program that triggers Plex/Jellyfin scan ## Troubleshooting - **Docker**: The executable must be inside the container or bind-mounted. - **Paths are wrong**: Add or adjust path mappings so `{save_path}` and `{content_path}` resolve to local mount points. - **Multiple torrents**: The program runs once per torrent. Ensure your script handles concurrent executions or uses a locking mechanism. - **Automation not triggering**: Ensure the program is enabled in Settings → External Programs. Disabled programs do not appear in automation dropdowns. --- ## Instance Settings Add and configure qBittorrent instances that qui connects to. Each instance represents a separate qBittorrent WebUI that qui can manage. ## Adding an Instance 1. Open qui and go to **Settings → Instances** 2. Click **Add Instance** 3. Enter connection details and click **Save** ## Instance Configuration On the Dashboard, click the gear icon next to an instance name. In **Settings → Instances**, click the three-dot menu and select **Edit**. ### Connection Settings | Field | Description | |-------|-------------| | **Name** | Display name shown in qui's sidebar and instance selector. | | **Host** | Full URL to qBittorrent WebUI (e.g., `http://localhost:8080`). | | **Skip TLS Verification** | Bypass certificate validation for self-signed certificates. | | **Local Filesystem Access** | Enable for features requiring direct file access. | ### Authentication qui supports multiple authentication methods depending on your setup: | Option | When to Use | |--------|-------------| | **qBittorrent Login** | Enable and enter credentials for standard WebUI authentication. Disable if qBittorrent bypasses auth for localhost or whitelisted IPs. | | **HTTP Basic Auth** | Enable when a reverse proxy adds Basic Authentication in front of qBittorrent. | :::note HTTP Basic Auth is separate from qBittorrent's built-in auth. Enable it when your reverse proxy (nginx, Caddy, etc.) requires credentials before reaching qBittorrent. ::: ## Local Filesystem Access When enabled, qui can access the same filesystem as qBittorrent. This unlocks several features: - **Content File Download** - Download individual files from a torrent's content directly through the browser (right-click a file in the Content tab). - **Hardlink Detection** - Automations can detect whether torrent files have hardlinks to your media library. - **Orphan Scan** - Find files on disk that aren't tracked by any torrent. - **Free Space (Path)** - Automation rules can check free space on specific mount points instead of relying on qBittorrent's reported value. :::warning Only enable this if qui runs on the same machine (or has the same mounts) as qBittorrent. If paths don't match, features will fail silently or produce incorrect results. ::: For Docker deployments, ensure the container has the necessary volume mounts. See [Docker configuration](/docs/getting-started/docker) for details. ## Instance Actions At the bottom of the settings panel: - **Enable / Disable** - Toggle whether qui actively connects to and manages this instance. - **Delete** - Remove the instance from qui. This does not affect qBittorrent itself. ## qBittorrent Preferences The settings dialog includes tabs for configuring qBittorrent's application preferences (speed limits, queue management, connection settings, etc.). These are passed directly to qBittorrent's API and behave identically to the native WebUI settings. --- ## Notifications qui supports both the Notifiarr API and Shoutrrr targets. Configure one or more targets in **Settings → Notifications** and choose which events to send. ## Setup 1. Open **Settings → Notifications**. 2. Add a target name and URL. 3. Pick the events you want. 4. Save and use **Test** to verify delivery. Notes: - Existing targets keep their saved event list when new events are introduced. - Messages may be truncated to keep notifications short and avoid provider limits. - Discord and Notifiarr targets use rich embeds with fields; other services receive plain text. ## Event types | Event key | Description | | --- | --- | | `torrent_added` | A torrent is added (includes tracker, category, tags, and ETA when available). | | `torrent_completed` | A torrent finishes downloading (includes tracker, category, and tags when available). | | `backup_succeeded` | A backup run completes successfully. | | `backup_failed` | A backup run fails. | | `dir_scan_completed` | A directory scan run finishes. | | `dir_scan_failed` | A directory scan run fails. | | `orphan_scan_completed` | An orphan scan run completes (including clean runs). | | `orphan_scan_failed` | An orphan scan run fails. | | `cross_seed_automation_succeeded` | RSS cross-seed automation completes (summary counts and samples). | | `cross_seed_automation_failed` | RSS cross-seed automation fails or completes with errors (summary). | | `cross_seed_search_succeeded` | Seeded search run completes (summary counts and samples). | | `cross_seed_search_failed` | Seeded search run fails or is canceled (summary). | | `cross_seed_completion_succeeded` | Completion search run completes (summary counts and samples). | | `cross_seed_completion_failed` | Completion search run fails. | | `cross_seed_webhook_succeeded` | Webhook check run completes (summary counts and samples). | | `cross_seed_webhook_failed` | Webhook check run fails. | | `automations_actions_applied` | Automation rules applied actions (summary counts and samples; only when actions occur). | | `automations_run_failed` | Automation rules failed to run for an instance (system error). | ## Notifiarr API For prettier output similar to Discord embeds, use the native Notifiarr API scheme: - `notifiarrapi://apikey` - Optional override: `notifiarrapi://apikey?endpoint=https://notifiarr.com/api/v1/notification/qui` ## Shoutrrr URLs Use any Shoutrrr-supported URL scheme. A few examples: - `discord://token@channel` - `notifiarr://apikey` - `slack://token@channel` - `telegram://token@chat-id` - `gotify://host/token` Notifiarr can also include optional parameters such as `channel` or `name`, e.g. `notifiarr://apikey?name=qui&channel=123456789`. See the Shoutrrr documentation for the full list of services and URL formats: https://github.com/nicholas-fedor/shoutrrr --- ## Orphan Scan Finds and removes files in your download directories that aren't associated with any torrent. ## How It Works 1. **Scan roots are determined dynamically** - qui scans all unique `SavePath` directories from your current torrents, not qBittorrent's default download directory 2. Files not referenced by any torrent are flagged as orphans 3. You preview the list before confirming deletion 4. Empty directories are cleaned up after file deletion :::note qui normalizes Unicode paths to canonical NFC form during matching. This avoids false orphans when equivalent composed/decomposed names are reported differently. On normalization-sensitive filesystems, two byte-distinct canonical-equivalent names are treated as one logical path. ::: :::info If you have multiple **active** qBittorrent instances with `Has local filesystem access` enabled, and their torrent `SavePath` directories overlap, qui also protects files referenced by torrents from those other instances (even when scanning a single instance). To do this safely, qui must be able to determine whether scan roots overlap. If any other local-access instance is unreachable/not ready, the scan fails to avoid false positives. ::: :::warning **Disabled instances are not protected.** If you have a disabled instance with local filesystem access that shares save paths with an active instance, its files may be flagged as orphans. Enable the instance or ensure paths don't overlap before scanning. ::: :::warning[Docker: Local Filesystem Access] Several qui features require access to the same filesystem paths that qBittorrent uses (orphan scan, hardlinks, reflinks, automations). Mount the same paths qBittorrent uses - paths must match exactly: ```yaml volumes: - /config:/config - /data/torrents:/data/torrents # Must match qBittorrent's path ``` After mounting, enable **Local Filesystem Access** on each instance in qui's Instance Settings. ::: ## Important: Abandoned Directories Directories are only scanned if at least one torrent points to them. If you delete all torrents from a directory, that directory is no longer a scan root and any leftover files there won't be detected. **Example:** You have torrents in `/downloads/old-stuff/`. You delete all those torrents. Orphan scan no longer knows about `/downloads/old-stuff/` and won't clean it up. ## Settings | Setting | Description | Default | |---------|-------------|---------| | Grace period | Skip files modified within this window | 10 minutes | | Ignore paths | Directories to exclude from scanning | - | | Scan interval | How often scheduled scans run | 24 hours | | Max files per run | Maximum orphan preview entries saved for a run (also caps what can be deleted from that run) | 1,000 | | Auto-cleanup | Automatically delete orphans from scheduled scans | Disabled | | Auto-cleanup max files | Only auto-delete if orphan count is at or below this threshold | 100 |
Orphan Scan skips common OS/NAS metadata, recycle bin, snapshot, and Kubernetes volume-internal entries automatically (case-insensitive). **Ignored files (exact names)** - `.DS_Store` - `.directory` - `desktop.ini` - `Thumbs.db` **Ignored files (name prefixes)** - `.fuse*` (e.g. `.fuse_hidden*`) - `.nfs*` - `._*` - `.goutputstream-*` - `.#*` - `~$*` **Ignored files (name suffixes)** - `*.parts` (qBittorrent partial download files) **Ignored directories (exact names)** - `.AppleDB` - `.AppleDouble` - `.TemporaryItems` - `.Trashes` - `.Recycle.Bin` - `.recycle` - `.snapshot` - `.snapshots` - `.zfs` - `@eaDir` - `$RECYCLE.BIN` - `#recycle` - `lost+found` - `System Volume Information` **Ignored directories (name prefixes)** - `.Trash-*` - `..*` (Kubernetes internals like `..data` and timestamp dirs)
## Max Files Per Run Behavior - Scan scope is still full: qui walks all scan roots each run. - Then it sorts orphan candidates by your selected preview sort. - Then it applies `Max files per run` and marks the run as truncated when more candidates exist. - Deletion only operates on files saved in that run's preview list. **Example:** If 5,000 files are scanned, 2,000 are orphan candidates, and `Max files per run` is 1,000, qui scans all 5,000, saves the top 1,000 candidates for preview/deletion, and marks the run truncated. ### FAQ **Do I need multiple runs to scan everything?** No. Each run scans all roots. Multiple runs are only needed if you want to work through orphan candidates beyond the per-run preview cap. ## Workflow 1. Trigger a scan (manual or scheduled) 2. Review the preview list of orphan files 3. Confirm deletion 4. Files are deleted and empty directories cleaned up ## Preview Features - **Path column** - Shows the full file path with copy-to-clipboard support - **Export CSV** - Download the full preview list (all pages) as a CSV file --- ## Reannounce # Tracker Reannounce qui can automatically fix stalled torrents by reannouncing them to trackers. This helps when a tracker fails to register a new upload immediately, ensuring your torrents start seeding without manual intervention. qBittorrent doesn't retry failed announces quickly. When a tracker is slow to register a new upload or returns an error, you may be stuck waiting for a long time. qui handles this automatically and gracefully. qui never spams trackers. While a tracker is still updating or waiting for a response, qui waits patiently. It only acts once a tracker has responded and there's an actual problem to fix. ## Quick Start 1. Go to **Services** in the main navigation. 2. Select an instance from the dropdown. 3. In the **Tracker Reannounce** section, toggle **Enabled** to turn it on. 4. Click **Save Changes**. That's it! qui will now monitor stalled torrents in the background. ## Configuration ### Timing | Setting | Description | Default | |---------|-------------|---------| | Initial Wait | How long to wait after a torrent is added before checking it | 15s | | Retry Interval | How often to retry within a single reannounce attempt | 7s | | Max Torrent Age | Stop monitoring torrents older than this | 10 mins | | Max Retries | Maximum consecutive retries within a single scan cycle | 50 | Some slow trackers need up to 50 retries at 7s intervals (~6 minutes) to register uploads. ### Monitoring Scope You can choose which torrents to monitor: - **Monitor All Stalled Torrents**: Checks every stalled torrent. - Use **Exclusions** below to ignore specific Categories, Tags, or Trackers (e.g., ignore "public" trackers). - **Custom Filter (Monitor All Disabled)**: - Only checks torrents that match your **Include** rules. - You can still add **Exclusions** to block specific items within those allowed groups. ### Quick Retry By default, qui waits about **2 minutes** between reannounce attempts for the same torrent (a per-torrent cooldown between scans). - **Enable Quick Retry** to use the **Retry Interval** (default 7s) as the cooldown instead. This helps stalled torrents recover faster. - The **Retry Interval** controls both the spacing of retries inside each scan attempt and, with Quick Retry enabled, the cooldown between scans. This is especially useful on trackers that are slow to register new uploads. Some sites take a moment before they recognize a new torrent, which can cause initial stalls—Quick Retry helps work around this automatically. ## Activity Log To see what's happening: 1. Go to **Services** and select your instance. 2. Click the **Activity Log** tab in the Tracker Reannounce section. You will see a real-time feed of every torrent checked, whether the reannounce succeeded, failed, or was skipped (e.g., because the tracker is actually working fine). --- ## Reverse Proxy # Reverse Proxy for External Applications qui includes a built-in reverse proxy that allows external applications like autobrr, Sonarr, Radarr, and other tools to connect to your qBittorrent instances **without needing qBittorrent credentials**. ## How It Works qui maintains a shared session with qBittorrent and proxies requests from your external apps. This eliminates login thrash - automation tools reuse the live session instead of racing to re-authenticate. ## Setup Instructions ### 1. Create a Client Proxy API Key 1. Open qui in your browser 2. Go to **Settings → Client Proxy Keys** 3. Click **"Create Client API Key"** 4. Enter a name for the client (e.g., "Sonarr") 5. Choose the qBittorrent instance you want to proxy 6. Click **"Create Client API Key"** 7. **Copy the generated proxy url immediately** - it's only shown once ### 2. Configure Your External Application Use qui as the qBittorrent host with the special proxy URL format: **Complete URL example:** ``` http://localhost:7476/proxy/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz ``` ## Application-Specific Setup ### Sonarr / Radarr 1. Go to `Settings → Download Clients` 2. Select `Show Advanced` 3. Add a new **qBittorrent** client 4. Set the host and port of qui 5. Add URL Base (`/proxy/...`) - remember to include `/qui/` if you use custom baseurl 6. Click **Test** and then **Save** once the test succeeds ### autobrr 1. Open `Settings → Download Clients` 2. Add **qBittorrent** (or edit an existing one) 3. Enter the full url like: `http://localhost:7476/proxy/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz` 4. Leave username/password blank and press **Test** 5. Leave basic auth blank since qui handles that For cross-seed integration with autobrr, see the [Cross-Seed](/docs/features/cross-seed/autobrr) section. ### cross-seed 1. Open cross-seed config file 2. Add or edit the `torrentClients` section 3. Append the full url following the documentation: ``` torrentClients: ["qbittorrent:http://localhost:7476/proxy/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"], ``` 4. Save the config file and restart cross-seed ### Upload Assistant 1. Open the Upload Assistant config file 2. Add or edit `qui_proxy_url` under the qBitTorrent client settings 3. Append the full url like: `"qui_proxy_url": "http://localhost:7476/proxy/abc123def456ghi789jkl012mno345pqr678stu901vwx234yz",` 4. All other auth type can remain unchanged 5. Save the config file ## Supported Applications This reverse proxy will work with any application that supports qBittorrent's Web API. ## Security Features - **API Key Authentication** - Each client requires a unique key - **Instance Isolation** - Keys are tied to specific qBittorrent instances - **Usage Tracking** - Monitor which clients are accessing your instances - **Revocation** - Disable access instantly by deleting the API key - **No Credential Exposure** - qBittorrent passwords never leave qui ## Intercepted Endpoints The proxy intercepts certain qBittorrent API endpoints to improve performance and enable qui-specific features. Most requests are forwarded transparently to qBittorrent. ### Read Operations (Served from qui) These endpoints are served directly from qui's sync manager for faster response times: | Endpoint | Description | |----------|-------------| | `/api/v2/torrents/info` | Torrent list with standard qBittorrent filtering | | `/api/v2/torrents/search` | Enhanced torrent list with fuzzy search (qui-specific) | | `/api/v2/torrents/categories` | Category list from synchronized data | | `/api/v2/torrents/tags` | Tag list from synchronized data | | `/api/v2/torrents/properties` | Torrent properties | | `/api/v2/torrents/trackers` | Torrent trackers with icon discovery | | `/api/v2/torrents/files` | Torrent file list | These endpoints proxy to qBittorrent and update qui's local state: | Endpoint | Description | |----------|-------------| | `/api/v2/sync/maindata` | Full sync data (updates qui's cache) | | `/api/v2/sync/torrentPeers` | Peer data (updates qui's peer state) | ### Write Operations | Endpoint | Behavior | |----------|----------| | `/api/v2/auth/login` | No-op, returns success if instance is healthy | | `/api/v2/torrents/reannounce` | Delegated to reannounce service when tracker monitoring is enabled | | `/api/v2/torrents/setLocation` | Forwards to qBittorrent, invalidates file cache | | `/api/v2/torrents/renameFile` | Forwards to qBittorrent, invalidates file cache | | `/api/v2/torrents/renameFolder` | Forwards to qBittorrent, invalidates file cache | | `/api/v2/torrents/delete` | Forwards to qBittorrent, invalidates file cache | All other endpoints are forwarded transparently to qBittorrent. --- ## Tracker Icons Cached icons live in your data directory under `tracker-icons/`. With the default SQLite engine this is next to `qui.db`; with Postgres it's still in the same data directory. Icons are stored as 16×16 PNGs; anything larger than 1024×1024 is rejected. qui automatically downloads a favicon the first time it encounters a tracker host and caches it for future sessions. Failed downloads are retried automatically. Set `trackerIconsFetchEnabled = false` in `config.toml` (or `QUI__TRACKER_ICONS_FETCH_ENABLED=false`) to disable these network fetches. ## Add Icons Manually Copy PNGs named after each tracker host (e.g. `tracker.example.com.png`) into the `tracker-icons/` directory. Files are served as-is, so trimming or resizing is up to you, but matching the built-in size (16×16) keeps them crisp and avoids extra scaling. ## Preload a Bundle of Icons If you have a library of icons, preload them via a mapping file: `tracker-icons/preload.json` (also accepts `.js` variants). ### Format The file can be either a plain JSON object or a snippet exported as `const trackerIcons = { ... };`. - Keys must be the real tracker hostnames (e.g. `tracker.example.org`) - If you include a `www.*` host, qui automatically mirrors the icon to the bare hostname when missing - On startup qui decodes each data URL, normalises the image to 16×16, and writes the PNG to `.png` ### JSON Example ```json { "tracker.example.org": "data:image/png;base64,AAA...", "www.tracker.org": "data:image/png;base64,BBB..." } ``` ### JavaScript Example ```js const trackerIcons = { "tracker.example.org": "data:image/png;base64,CCC...", "www.tracker.org": "data:image/png;base64,DDD..." }; ``` ### Community Resources See [Audionut/add-trackers](https://github.com/Audionut/add-trackers/blob/8db05c0e822f9b3afa46ca784644c4e7e400c92b/ptp-add-filter-all-releases-anut.js#L768) for an example icon bundle. --- ## Docker ## Docker Compose {DockerCompose} ```bash docker compose up -d ``` ## Docker Compose (Postgres) {DockerComposePostgres} ```bash docker compose -f docker-compose.postgres.yml up -d ``` ## Standalone ```bash docker run -d \ -p 7476:7476 \ -v $(pwd)/config:/config \ ghcr.io/autobrr/qui:latest ``` ## Local Filesystem Access :::warning[Docker: Local Filesystem Access] Several qui features require access to the same filesystem paths that qBittorrent uses (orphan scan, hardlinks, reflinks, automations). Mount the same paths qBittorrent uses - paths must match exactly: ```yaml volumes: - /config:/config - /data/torrents:/data/torrents # Must match qBittorrent's path ``` After mounting, enable **Local Filesystem Access** on each instance in qui's Instance Settings. ::: ## Unraid Our release workflow builds multi-architecture images (`linux/amd64`, `linux/arm64`, and friends) and publishes them to `ghcr.io/autobrr/qui`, so the container should work on Unraid out of the box. ### Deploy from the Docker tab 1. Open **Docker → Add Container** 2. Set **Name** to `qui` 3. Set **Repository** to `ghcr.io/autobrr/qui:latest` 4. Keep the default **Network Type** (`bridge` works for most setups) 5. Add a port mapping: **Host port** `7476` → **Container port** `7476` 6. Add a path mapping: **Container Path** `/config` → **Host Path** `/mnt/user/appdata/qui` 7. Enable **Advanced View** (top right) 8. Set **Icon URL** to `https://raw.githubusercontent.com/autobrr/qui/main/web/public/icon.png` 9. Set **WebUI** to `http://[IP]:[PORT:7476]` 10. Set **Extra Parameters** to `--user="99:100"` (if you ran qui without this before you will need to change the ownership for the config and hardlink folders to `nobody`) 11. (Optional) add environment variables for advanced settings (e.g., `QUI__BASE_URL`, `QUI__LOG_LEVEL`, `TZ`) 12. Click **Apply** to pull the image and start the container The `/config` mount stores `config.toml`, logs, tracker icon cache, and other runtime assets. If you use the default SQLite engine, `qui.db` is stored there too. Point it at your preferred appdata share so settings persist across upgrades. If the app logs to stdout, check logs via Docker → qui → Logs; if it writes to files, they'll be under `/config`. ### Updating - Use Unraid's **Check for Updates** action to pull a newer `latest` image - If you pinned a specific version tag, edit the repository field to the new tag when you're ready to upgrade - Restart the container if needed after the image update so the new binary is loaded ## Updating ```bash docker compose pull && docker compose up -d ``` --- ## Installation ## Quick Install (Linux x86_64) ```bash # Download and extract the latest release wget $(curl -s https://api.github.com/repos/autobrr/qui/releases/latest | grep browser_download_url | grep linux_x86_64 | cut -d\" -f4) ``` ### Unpack Run with root or sudo. If you do not have root, or are on a shared system, place the binaries somewhere in your home directory like `~/.bin`. ```bash tar -C /usr/local/bin -xzf qui*.tar.gz ``` This will extract qui to `/usr/local/bin`. Note: If the command fails, prefix it with `sudo` and re-run again. ## Manual Download Download the latest release for your platform from the [releases page](https://github.com/autobrr/qui/releases). ## Run ```bash # Make it executable (Linux/macOS) chmod +x qui # Run ./qui serve ``` The web interface will be available at http://localhost:7476 ## Updating qui includes a built-in update command that automatically downloads and installs the latest release: ```bash ./qui update ``` ## First Setup 1. Open your browser to http://localhost:7476 2. Create your account 3. Add your qBittorrent instance(s) 4. Start managing your torrents --- ## Seedbox Installers One-line installers for popular seedbox providers. These scripts automatically configure qui for your specific environment. ```bash wget -O installer.sh https://get.autobrr.com/qui/feral && chmod +x installer.sh && ./installer.sh ``` ```bash wget -O installer.sh https://get.autobrr.com/qui/seedhost && chmod +x installer.sh && ./installer.sh ``` ```bash wget -O installer.sh https://get.autobrr.com/qui/ultra && chmod +x installer.sh && ./installer.sh ``` ```bash wget -O installer.sh https://get.autobrr.com/qui/whatbox && chmod +x installer.sh && ./installer.sh ``` ```bash wget -O installer.sh https://get.autobrr.com/qui/hostingbydesign && chmod +x installer.sh && ./installer.sh ``` ```bash wget -O installer.sh https://get.autobrr.com/qui/bytesized && chmod +x installer.sh && ./installer.sh ``` :::note This installer has not been tested. ::: --- ## Windows # Windows Installation In this guide we will download qui, set it up, and create a Windows Task so it runs in the background without needing a command prompt window open 24/7. ## Download 1. Download the latest Windows release from [GitHub Releases](https://github.com/autobrr/qui/releases/latest). - For most systems, download `qui_x.x.x_windows_amd64.zip`. 2. Extract the archive and place `qui.exe` in a directory, for example `C:\qui`. :::tip Avoid placing qui in `C:\Program Files` — it can cause permission issues with the database and config files. ::: ## Initial Setup 1. Open **Command Prompt** or **PowerShell** and navigate to the directory: ```powershell cd C:\qui ``` 2. Start qui for the first time to generate the default config and create your account: ```powershell .\qui.exe serve ``` 3. Open your browser to [http://localhost:7476](http://localhost:7476) and create your account. 4. Once you've verified it works, stop qui with `Ctrl+C`. We'll set it up as a background task next. ### Configuration qui stores its configuration and runtime data in `%APPDATA%\qui\` by default. With the default SQLite engine, `qui.db` is stored there too. For more details, see the [Configuration](/docs/configuration/environment) section. ## Create a Windows Task To run qui in the background, we'll use **Task Scheduler**. 1. Press the **Windows key** and search for **Task Scheduler**. 2. Click **Create Basic Task** in the right sidebar. 3. **Name:** `qui` — optionally add a description like: *qui torrent management service*. 4. **Trigger:** Select **When the computer starts**. 5. **Action:** Select **Start a Program**. - **Program/script:** Browse to `C:\qui\qui.exe` - **Add arguments:** `serve` - **Start in:** `C:\qui` 6. Check **Open the Properties dialog** before finishing, then click **Finish**. ### Configure the task properties In the Properties dialog: - Under **General**, select **Run whether user is logged on or not**. - Enter your Windows password when prompted. - Optionally check **Run with highest privileges** if you encounter permission issues. Click **OK** to save. ### Start the service Right-click on **qui** in the Task Scheduler list and click **Run**. :::tip To restart the service, click **End** and then **Run** in the right sidebar of Task Scheduler. ::: ## Updating qui has a built-in update command. You must stop the Task Scheduler job first, otherwise Windows will lock the executable and the update will fail. 1. Open **Task Scheduler**, right-click the **qui** task and click **End**. 2. Run the updater: ```powershell .\qui.exe update ``` 3. Right-click the **qui** task again and click **Run** to restart it. ## Reverse Proxy (optional) For remote access, it's recommended to run qui behind a reverse proxy like [Caddy](https://caddyserver.com/) or nginx for TLS and additional security. See the [Base URL](/docs/configuration/base-url) section for reverse proxy configuration examples. ## Finishing Up Once the task is running, qui will be available at [http://localhost:7476](http://localhost:7476). Add your qBittorrent instance(s) and start managing your torrents. --- ## Introduction # qui A web interface for qBittorrent. Manage multiple qBittorrent instances from a single application. ## Features - **Single Binary**: No dependencies, just download and run - **Multi-Instance Support**: Manage all your qBittorrent instances from one place - **Large Collections**: Handles thousands of torrents efficiently - **Themeable**: Multiple color themes available - **Base URL Support**: Serve from a subdirectory (e.g., `/qui/`) for reverse proxy setups - **OIDC Single Sign-On**: Authenticate through your OpenID Connect provider - **External Programs**: Launch custom scripts from the torrent context menu - **Tracker Reannounce**: Automatically fix stalled torrents when qBittorrent doesn't retry fast enough - **Automations**: Rule-based torrent management with conditions, actions (delete, pause, tag, limit speeds), and cross-seed awareness - **Orphan Scan**: Find and remove files not associated with any torrent - **Backups & Restore**: Scheduled snapshots with incremental, overwrite, and complete restore modes - **Cross-Seed**: Automatically find and add matching torrents across trackers with autobrr webhook integration - **Reverse Proxy**: Transparent qBittorrent proxy for external apps like autobrr, Sonarr, and Radarr—no credential sharing needed - **Incognito Mode**: Disguise torrents as Linux ISOs for screen sharing and screenshots ## Browser Extensions Right-click any magnet or torrent link to add it directly to your qBittorrent instances: - [Chrome Extension](https://chromewebstore.google.com/detail/kbjnjgihepmcoilegnghgpmijbecoili) - [Firefox Add-on](https://addons.mozilla.org/en-US/firefox/addon/qui/) ## Quick Start Get started in minutes: 1. [Install qui](/docs/getting-started/installation) 2. Open your browser to http://localhost:7476 3. Create your admin account 4. Add your qBittorrent instance(s) 5. Start managing your torrents ## Community Join our friendly and welcoming community on [Discord](https://discord.autobrr.com/qui)! Connect with fellow autobrr users, get advice, and share your experiences. ## License GPL-2.0-or-later ## Supported Torrent Clients qui currently only supports qBittorrent. It communicates directly with the qBittorrent Web API. Support for other torrent clients such as Deluge, rTorrent, and Transmission is not yet available, but we hope to support them all in the future. For details on which qBittorrent versions are compatible, see the [qBittorrent Version Compatibility](./advanced/compatibility.md) page. --- ## License Management Premium themes require a license key. Each key has a limited number of activation slots — one per server where qui is running. ## Activating a License 1. Open **Settings → Themes** in your qui instance. 2. Click **Add License** and enter your license key. 3. Premium themes are available immediately after activation. ## Moving a License to a New Server If you are replacing a server or reinstalling, you need to free the activation slot used by the old instance before the key will work on the new one. ### If the old server is still accessible 1. Open qui on the **old** server. 2. Go to **Settings → Themes** and click **Remove** next to the license. 3. This deactivates the license on that machine and frees the slot. 4. Activate the same key on the new server. ### If the old server is gone When the old server no longer exists (hardware failure, destroyed VPS, etc.) you cannot remove the license from within qui. Use the license portal instead: 1. Go to [licenses.getqui.com](https://licenses.getqui.com/). 2. Register or log in with **the same email address you used to purchase the license**. 3. Find your license and deactivate the old activation. 4. Activate the key on your new server from **Settings → Themes**. ## Recovering a Lost License Key Log in to [licenses.getqui.com](https://licenses.getqui.com/) with the email address you used at checkout. Your license keys are listed there. ## Troubleshooting ### "License activation limit has been reached" All activation slots for your key are in use. Deactivate an old activation either from the other qui instance (**Settings → Themes → Remove**) or through [licenses.getqui.com](https://licenses.getqui.com/) if that server is no longer available. ### "This license was activated on a different machine" This means the qui database was copied from another server. The stored activation does not match this machine's identity. Remove the license in **Settings → Themes → Remove**, then re-activate it with your key. ### "Unable to reach the license service" The license server is temporarily unreachable. Wait a moment and try again. If the issue persists, check your server's outbound network connectivity. --- ## Support Development qui is developed and maintained by volunteers. Your support helps us continue improving the project. ## Premium Themes Purchase premium themes directly from Settings → Themes in your qui instance. Your license key is delivered instantly after checkout. If you donate with crypto, verify your transaction at [crypto.getqui.com](https://crypto.getqui.com/) to receive a 100% discount code for premium themes. To manage activations or move a license to a new server, see [License Management](./licenses.md). ## Donations If you'd like to support development beyond theme purchases, donations are always appreciated. - **soup** - [Patreon](https://www.patreon.com/c/s0up4200) - [GitHub Sponsors](https://github.com/sponsors/s0up4200) - [Buy Me a Coffee](https://buymeacoffee.com/s0up4200) - [Ko-fi](https://ko-fi.com/s0up4200) - **zze0s** - [GitHub Sponsors](https://github.com/sponsors/zze0s) - [Buy Me a Coffee](https://buymeacoffee.com/ze0s) ### Cryptocurrency Verify your crypto donation at [crypto.getqui.com](https://crypto.getqui.com/) to receive a 100% discount code for premium themes. #### Bitcoin (BTC) #### Ethereum (ETH) #### Litecoin (LTC) #### Monero (XMR) XMR discount codes are handled manually. Reach out on [Discord](https://discord.autobrr.com/qui) or [email s0up4200@pm.me](mailto:s0up4200@pm.me). --- For other currencies or donation methods, [reach out on Discord](https://discord.autobrr.com/qui).