# FAQ
General [#general]
AIOStreams aggregates results from **all** your configured addons before returning them. By default it waits for every addon to respond (or time out) before showing results — if even one addon is slow, the whole response is delayed.
**Fix:** Lower each addon's timeout to the maximum you're willing to wait. 5 000 ms is a sensible default for most setups. You can adjust this per-addon in the Marketplace configuration popup.
AIOStreams also offers two alternative fetching strategies:
* **Dynamic mode** — Returns results as soon as a condition is met (e.g. "5 results received" or "2 seconds elapsed"). This can miss results from slower addons, so use it carefully.
* **Groups** — Splits addons into prioritised groups with independent timeouts. See the [Groups guide](/guides/groups).
If a result appears when an addon is installed directly in Stremio but not inside AIOStreams, it is almost always a **configuration issue**, not a bug.
AIOStreams is a wrapper — it fetches from the addons *you* configure and applies *your* filters. Common causes:
1. **Filters are removing the result** — Enable **Advanced Mode** in the **About** menu, go to **Miscellaneous → Display → Statistic Streams**, enable it with `filter` selected in the **Statistics to Show** dropdown, then repeat your search. Statistic streams will appear showing which filters removed which results.
2. **The addon is not installed or is disabled** — Check the Installed tab in the Addons menu.
3. **Stream type is excluded** — Some public instances (e.g. ElfHosted) forcefully disable P2P, HTTP, and Live stream types.
AIOStreams is a wrapper — it pulls from the same addons you'd otherwise install directly. It won't produce *more* results than those addons would give on their own.
What it *does* do is combine all results into a single, consistently sorted, deduplicated list with your filters and formatting applied.
The exception is AIOStreams' **built-in addons** (Newznab, SeaDex, StremThru Torz, AnimeTosho, TorrentGalaxy, etc.) which are exclusive to AIOStreams and can unlock results you wouldn't otherwise have.
AIOStreams doesn't produce incorrect results — it returns whatever the configured addons send back. The same incorrect results would appear if those addons were installed standalone.
To filter out mismatches, enable the **Matching** filters:
1. Switch to **Advanced Mode** (toggle in the **About** menu).
2. Go to **Filters → Matching**.
3. Enable **Title Matching**, **Season/Episode Matching**, and optionally **Year Matching**.
4. Provide your **TMDB API Key** in **Services** (free — see the [Setup Guide](/configuration/setup)).
There are many options, but you don't have to configure everything yourself. The recommended approach is to import a community template (see the [Setup Guide](/configuration/setup)) which handles most of the configuration for you.
Most users configure AIOStreams once and rarely touch it afterwards. As long as you understand what each option does and don't click things at random, you're unlikely to run into issues.
Resources:
* [Setup Guide](/configuration/setup) — Template-based step-by-step walkthrough
* [Configure Options](/configuration/options) — Full reference for every option
* [Discord](https://discord.viren070.me) — Community help in `#support`
**TorBox Search** is a built-in addon developed specifically for AIOStreams. The official **TorBox** addon is developed by TorBox themselves. Both use the same TorBox Search API, but TorBox Search offers extra capabilities:
* **Multi-debrid support** — Works with all major debrid services simultaneously (still requires a TorBox API key to access the Search API).
* **Pro-only controls** — TorBox Pro users can choose to search Torrents, Usenet, or both; toggle BYOI (Bring Your Own Indexer); and split them into separate addon instances each with independent timeouts.
**Note:** TorBox Search does not search your TorBox *library*. However, items found via the Search API that are already in your library will be marked as *Library* streams.
In most cases, automatically — no action required from you.
* **Server-side changes** apply immediately.
* **Breaking configuration changes** (e.g. an addon migrates to a new API) are handled in an AIOStreams update. Your addon will work again once your instance updates.
* **New options** appear in the marketplace popup after an AIOStreams update — sensible defaults are usually set automatically.
In rare cases (like an addon starting to require a new API key) you'll need to enter the new value manually.
When enabled, AIOStreams splits a multi-service addon into separate, independent addon instances — one per service (e.g. `Addon RD`, `Addon PM`, `Addon TB`).
**Disabled:** One addon handles all services under a single timeout. A slow service can delay or block all results.
**Enabled:** Each instance runs independently with its own timeout. A slow or failing service won't affect the others, so you get faster results from responsive services.
If an addon supports returning raw/P2P results, AIOStreams can **resolve those results against your configured debrid services internally** — this is called Service Wrapping.
Benefits:
* Fewer API calls to your debrid provider.
* You don't need to give your debrid credentials directly to the addon.
* AIOStreams handles file selection and playback.
If an addon only returns debrid-resolved results (e.g. Debridio), enable **Reconfigure Service** instead. Note that this duplicates calls to your debrid provider.
Generally **not recommended**, especially on public instances.
AIOMetadata uses a stable UUID-based manifest URL — meaning AIOStreams will cache its responses against the same URL regardless of config changes, so updates in AIOMetadata may not reflect immediately. It also adds an unnecessary extra hop for every metadata/catalog request.
If you do want it within AIOStreams (e.g. for a local self-hosted setup), add it as a **Custom Addon** and put it at the top of your addon list. After any AIOMetadata config change that alters the manifest, refresh your catalogs and reinstall AIOStreams.
Stable public instances almost never go down, and you can always self-host for full control. You can also install other addons alongside AIOStreams in Stremio — there's no requirement to have only AIOStreams.
Some users keep a backup AIOStreams instance installed (disabled via StremThru Sidekick) and enable it only if the primary goes down, but most find this unnecessary.
By default, AIOStreams restricts which external URLs can be used for Stream Expression sync.
* **On a public instance:** Ask the instance host to whitelist your URL via the `WHITELISTED_SEL_URLS` environment variable.
* **Self-hosting:** Set **SEL sync access** (`SEL_SYNC_ACCESS`) to `all` from the dashboard Settings page (or the env var) to allow all URLs.
By default, AIOStreams restricts arbitrary regex patterns in filters.
* **On a public instance:** Ask the host to add you to `TRUSTED_UUIDS`, or to whitelist the import URL via `WHITELISTED_REGEX_PATTERNS_URLS`.
* **Self-hosting:** Set **Regex filter access** (`REGEX_FILTER_ACCESS`) to `all` from the dashboard Settings page (or the env var).
In **Stremio**, click the **Configure** button next to the AIOStreams addon — this takes you straight to your configuration page.
Alternatively, navigate to `https://your-instance/stremio/configure` and click the login icon (bottom-left corner) to sign in with your UUID and password.
After making changes, go to **Save & Install** and click **Save**.
Most changes apply immediately after saving without a reinstall. **Only reinstall when a pop-up appears telling you to.**
The pop-up typically appears when you:
* Change catalogue names or their order.
* Add your **first** addon that provides a new resource type (e.g. your first subtitle addon).
* Make any change that alters the resources listed in the manifest.
Go to **Save & Install → Backups** and click **Export** to download a JSON backup.
To restore or migrate, use **Import** in the same section. Toggle **Exclude Credentials** before sharing the export with others to strip sensitive API keys from the file.
Self-Hosting [#self-hosting]
Stremio (both desktop and web) **requires addon URLs to be served over HTTPS**. If your self-hosted AIOStreams is accessible only over plain HTTP, Stremio will refuse to connect.
**Fix:** Put a reverse proxy (Traefik, Nginx, Caddy) with a valid TLS certificate in front of AIOStreams. See the [Deployment guide](/getting-started/deployment) for a ready-to-use Traefik example.
You have not mounted a persistent volume for AIOStreams' data directory. Without it, the SQLite database (which stores all user configurations) is lost when the container is recreated.
**Fix:** Add a volume mount in your `compose.yaml` as shown in the [Deployment guide](/getting-started/deployment):
```yaml
volumes:
- ./data:/app/data
```
The TMDB API (`api.themoviedb.org`) is **blocked in India**. This is commonly seen on Oracle VPS instances.
**Fix:** Route TMDB API calls through WARP by adding `api.themoviedb.org:true` to the proxy list in your `.env`. If you're running StremThru alongside AIOStreams, add it to the StremThru tunnel configuration as well.
# Troubleshooting
Results & Filter Issues [#results--filter-issues]
This is almost always a filter configuration issue. To diagnose:
{/* 1. Enable **Advanced Mode** in the **About** menu.
2. Go to **Miscellaneous → Statistics**.
3. Perform a search — the Statistics page will show exactly which filter removed each stream and why. */}
Enable **Advanced Mode** in the **About** menu.
Go to **Miscellaneous → Display → Statistic Streams**.
Make sure it is enabled, and that `filter` is selected in the **Statistics to Show** dropdown.
Repeat a search — There will be a few statistic streams at the top / bottom showing you what results were removed by which filters. This will help you identify any filters that are too restrictive or misconfigured.
Common culprits:
* A **Language** filter set to a language most results don't specify (e.g. English results often don't have a language tag, so setting English as required can exclude them).
* A **Stream Type** filter set to remove certain types (e.g. P2P, HTTP, Live).
If a public instance like ElfHosted is used, P2P, HTTP, and Live stream types are **forcefully disabled** by the host — this is not configurable.
Enable the **Matching** filters to automatically remove mismatched streams:
Enable
**Advanced Mode**
in the
**About**
menu.
Go to **Filters → Matching**.
Enable **Title Matching** and provide your **TMDB API Key** in
**Services** (free — see [Setup Guide](/configuration/setup)).
Enable **Season/Episode Matching** for series content.
Optionally enable
**Year Matching**
to filter out remakes/reboots.
AIOStreams waits for all configured addons to respond before returning results. If one addon is slow, everything is delayed.
**Fixes:**
* Lower each addon's **Timeout** to the maximum you're willing to wait (e.g. 5 000 ms).
* Remove addons you rarely need.
* Consider using **Groups** to prioritise fast addons — see the [Groups guide](/guides/groups).
***
Self-Hosting Issues [#self-hosting-issues]
**Cause:** Stremio requires all addon URLs to be served over **HTTPS**. HTTP is not accepted.
Set up a reverse proxy in front of AIOStreams. Traefik and Caddy can
automatically obtain TLS certificates via Let's Encrypt.
See the [Deployment guide](/getting-started/deployment) for a
ready-to-use Docker Compose + Traefik example.
Verify the certificate is valid by opening{' '}
`https://your-domain/stremio/manifest.json` in a browser. You should see
a JSON response.
Reinstall the addon in Stremio using the HTTPS URL.
If you're testing locally, Stremio Web (app.strem.io) accepts HTTP
`localhost` URLs but the desktop app and mobile apps require HTTPS for
remote URLs.
**Cause:** No persistent volume is mounted for AIOStreams' data directory, so the SQLite database is lost when the container is recreated.
Stop your containers: `docker compose down`
Add a volume mapping to your `compose.yaml` under the AIOStreams service:
```yaml
volumes:
- ./data:/app/data
```
Start containers again: `docker compose up -d`
See the [Deployment guide](/getting-started/deployment) for a complete, production-ready `compose.yaml` example.
**Cause:** `api.themoviedb.org` is blocked in India. This affects Oracle Cloud VPS instances in Indian regions.
**Fix:** Route TMDB calls through WARP (or another proxy):
1. Ensure WARP is running on your VPS (you may already have it for Torrentio).
2. Add `api.themoviedb.org:true` to the proxy list in your AIOStreams `.env`.
3. If you're running StremThru, add the same entry to the StremThru tunnel configuration in its `.env`.
**Cause:** AIOStreams restricts which external URLs can be used for Stream Expression sync by default.
* **On a public instance:** Ask the host to add your URL to `WHITELISTED_SEL_URLS`.
* **Self-hosting:** Add `SEL_SYNC_ACCESS=all` to your `.env` to allow any URL.
**Cause:** AIOStreams restricts arbitrary regex patterns in filters by default.
* **On a public instance:** Ask the host to add your UUID to `TRUSTED_UUIDS`, or to whitelist your import URL via `WHITELISTED_REGEX_PATTERNS_URLS`.
* **Self-hosting:** Add `REGEX_FILTER_ACCESS=all` to your `.env`.
Check the container logs first:
```sh
docker compose logs -f aiostreams
```
Common causes:
* **Port conflict** — Another service is using the same port. Change the host port in `compose.yaml`.
* **Missing environment variable** — Some features require specific env vars (e.g. `PORT`, `BASE_URL`). See [Environment Variables](/configuration/environment-variables).
* **`BASE_URL` mismatch** — `BASE_URL` must match the public URL your browser uses to reach AIOStreams (including the `https://` prefix and no trailing slash).
***
Still stuck? [#still-stuck]
Join the [Discord server](https://discord.viren070.me) and ask in `#support`. Include your container logs and a description of what you've already tried.
# Environment Variables
import { Callout } from 'fumadocs-ui/components/callout';
AIOStreams has two kinds of configuration:
* **Bootstrap variables** — read from the environment **only**, before the
database is available. They configure the things needed to start the process
(server, database, encryption, operator auth, logging). These **must**
be set as environment variables and **cannot** be changed in the dashboard.
* **Runtime settings** — everything else. These are stored in the database and
are **best configured in the dashboard** (Settings page) where they can be
edited live, validated, and changed without redeploying. Every runtime
setting still has a matching environment variable: **if that variable is set,
it becomes a locked override** — the value is forced and the field is shown as
read-only in the UI. Leave it unset to manage the setting from the dashboard.
Recommended approach: set only the **bootstrap** variables in your `.env`,
then configure everything else from the dashboard. The
[`.env.sample`](https://github.com/Viren070/AIOStreams/blob/main/.env.sample)
intentionally only contains bootstrap variables for this reason. Use
environment overrides for runtime settings only when you need to lock a value
***
Bootstrap Variables (environment-only) [#bootstrap-variables-environment-only]
These are validated at startup and cannot be set from the dashboard.
Essential [#essential]
| Variable | Default | Description |
| --------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BASE_URL` | — | **Required.** Public URL of your instance, including protocol (e.g. `https://aiostreams.example.com`). |
| `SECRET_KEY` | — | **Required.** 64-character hex string used to encrypt stored configurations. Generate with `openssl rand -hex 32`. Cannot be changed after first run. The legacy alias `SESSION_SECRET` is still accepted for one minor release. |
| `PORT` | `3000` | Port the server listens on. |
| `DATABASE_URI` | `sqlite://./data/db.sqlite` | Database connection URI. Supports SQLite and PostgreSQL (`postgres://user:pass@host:port/db`). |
| `DISK_CACHE_DIR` | `/cache` | Directory for the disk-backed caches (usenet segments, grabbed NZBs, torrent metadata). Set this to move the caches off the data folder, e.g. onto faster or non-persistent storage. |
| `REDIS_URI` | — | Redis URI for shared caching. Recommended for multi-instance deployments; single-container setups can leave this unset. |
| `REDIS_TIMEOUT` | `5000` | Redis client connect timeout in milliseconds. |
| `SETTINGS_REFRESH_INTERVAL` | `30` | How often (seconds) each instance polls the DB settings version and reloads runtime config if another instance changed a setting via the dashboard. Keeps multi-instance deployments consistent. Set `0` to disable on single-instance setups. |
| `INTERNAL_URL` | `http://localhost:${PORT}` | Internal URL used for communication with built-in addons. The default is fine for almost all setups. |
`SECRET_KEY` is required and must be a 64-character hexadecimal string.
AIOStreams will refuse to start without it, and changing it after first run
makes existing encrypted configurations undecryptable.
Operator Authentication [#operator-authentication]
| Variable | Default | Description |
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AIOSTREAMS_AUTH` | — | Comma-separated `username:password` pairs defining all valid credentials. e.g. `user1:pass1,user2:pass2`. |
| `AIOSTREAMS_AUTH_PERMISSIONS` | — | Per-user permissions as comma-separated `username=perm1\|perm2` entries. Valid permissions: `admin` (dashboard + everything), `createConfig` (creating new configurations), `proxy` (built-in proxy), `service` (built-in usenet engine), `sabnzbd` (SABnzbd-compatible API), or `none` for login-only. Users not listed default to `admin`. An entry written before `createConfig` existed no longer allows creating configurations; add it explicitly. e.g. `alice=admin,bob=proxy\|service\|createConfig`. |
Logging [#logging]
These configure the logger.
| Variable | Default | Description |
| ------------------------ | ---------- | ------------------------------------------------------------------------- |
| `LOG_BUFFER_MAX_BYTES` | `67108864` | Max bytes of recent log lines kept in memory for the dashboard Logs page. |
| `LOG_BUFFER_MAX_ENTRIES` | `200000` | Hard cap on recent log lines kept in memory for the dashboard Logs page. |
Runtime / Build [#runtime--build]
| Variable | Default | Description |
| -------------------------- | ------------ | ----------------------------------------------------------------------- |
| `NODE_ENV` | `production` | `production`, `development`, or `test`. |
| `SYSTEM_LIFECYCLE_ENABLED` | `false` | Allow the dashboard System page to restart/stop the AIOStreams process. |
***
Runtime Settings [#runtime-settings]
Everything below is **stored in the database and editable from the dashboard
Settings page**. The sections, groups, and labels mirror the dashboard exactly.
Set the listed environment variable only if you want to lock that value (it will
appear read-only in the UI). Defaults shown are the built-in defaults used when
neither the environment variable nor a stored value is present.
{/* BEGIN GENERATED ENV REFERENCE */}
{/* This section is auto-generated by scripts/gen-env-docs.ts from the config schemas. Do not edit by hand — run `pnpm run gen:env-docs`. */}
Core [#core]
General [#general]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | ------------------------------- | ---------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AIOSTREAMS_AUTH_REQUIRED` | Auth Required | boolean | `false` | When true, /stremio/configure requires a valid login session (any user in AIOSTREAMS\_AUTH) and the config-write gate (CONFIG\_ACCESS\_KEY) is enforced. When false, the config page is public. |
| `CONFIG_ACCESS_KEY` | Config Access Key | string | *(unset)* | Single key embedded in a config and checked on create/update/serve. If unset while authRequired is true, one is generated and persisted automatically. Rotating it invalidates every existing config until re-saved. *(secret)* |
| `SESSION_TTL_SECONDS` | Session TTL Seconds | duration | `86400` | Lifetime of a login session before the user must log in again. Defaults to 24 hours (1d). |
| `ALIASED_CONFIGURATIONS` | Aliased Configurations | map | *(unset)* | Map of aliases to \{uuid, password} accessible at /stremio/u/\/manifest.json. Env-supplied form: comma-separated `alias:uuid:password` entries. *(secret)* |
| `ENABLE_SEARCH_API` | Enable Search API | boolean | `true` | When true, the /api/v1/search endpoint is mounted and reachable. *(restart required)* |
| `ENABLE_NAB_API` | Enable NAB API | boolean | `true` | When true, the per-user /api/v1/newznab/api and /api/v1/torznab/api endpoints are mounted and reachable. These expose a user’s stream results to newznab/torznab clients (Prowlarr, Sonarr, Radarr) as an indexer, supporting ID and season/episode lookups only. *(restart required)* |
| `PROVIDE_STREAM_DATA` | Provide Stream Data | boolOrList | — | Whether stream metadata is included in Stremio stream responses. `null` (default) auto-detects from User-Agent (AIOStreams/\* always gets it). `true`/`false` overrides for everyone. An IP list enables it only for matching request IPs. |
| `EXPOSE_USER_COUNT` | Expose User Count | boolean | `false` | Include the total user count on the public status endpoint. |
| `STREMIO_ADDONS_CONFIG_ISSUER` | Stremio Addons Config Issuer | string | `https://stremio-addons.net` | Issuer URL declared in the manifest for the Stremio Addons Config integration. |
| `STREMIO_ADDONS_CONFIG_SIGNATURE` | Stremio Addons Config Signature | string | *(unset)* | Signed JWT for the Stremio Addons Config integration. Both issuer and signature must be set for the manifest field to be emitted. *(secret)* |
| `TRUSTED_IPS` | Trusted Ips | list | `["172.17.0.0/16","127.0.0.1/32","::1/128"]` | Comma-separated list of trusted IPs / CIDR ranges. Used when determining the requesting IP. User IP is always trusted via headers regardless of this setting. |
Branding [#branding]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | ---------------- | ------- | ------------------------- | ------------------------------------------------------------------------- |
| `ADDON_NAME` | Addon Name | string | `AIOStreams` | Display name shown in the manifest and UI. |
| `ADDON_ID` | Addon ID | string | `com.aiostreams.viren070` | Reverse-DNS identifier published in the manifest. *(restart required)* |
| `CUSTOM_HTML` | Custom Html | string | — | Optional HTML injected into the configuration page. |
| `ALTERNATE_DESIGN` | Alternate Design | boolean | `false` | Switches the frontend to the alternate design (different logo and theme). |
SSO / OIDC [#sso--oidc]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------- | ----------------------- | ------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AIOSTREAMS_OIDC_ENABLED` | Enabled | boolean | `false` | Allow operators to sign in with an OpenID Connect provider. This governs dashboard and config-page access only; it does not affect Stremio addon URLs. |
| `AIOSTREAMS_OIDC_ISSUER` | Issuer | string | — | OIDC issuer URL. Discovery is performed against \{issuer}/.well-known/openid-configuration. Copy it exactly as your provider states it, including any trailing slash. Register \/api/v1/auth/oidc/callback as an allowed redirect URI with your provider. |
| `AIOSTREAMS_OIDC_CLIENT_ID` | Client ID | string | — | OAuth client ID issued by your provider. |
| `AIOSTREAMS_OIDC_CLIENT_SECRET` | Client Secret | string | *(unset)* | OAuth client secret issued by your provider. *(secret)* |
| `AIOSTREAMS_OIDC_SCOPES` | Scopes | list | `["openid","profile","email"]` | Scopes requested at login. Add the scope that carries group membership (often `groups`) if your provider requires one; Google and Entra ID reject an unknown `groups` scope. |
| `AIOSTREAMS_OIDC_USERNAME_CLAIM` | Username Claim | string | `preferred_username` | Claim used as the AIOStreams username. Use `sub` if your provider lets users rename themselves, since a renamed user otherwise splits their audit history. |
| `AIOSTREAMS_OIDC_LINK_BY_USERNAME` | Link By Username | boolean | `false` | Treat an SSO identity whose username matches an AIOSTREAMS\_AUTH user as that user, rather than refusing the login. Such a session takes its permissions from AIOSTREAMS\_AUTH\_PERMISSIONS and the group mapping is not consulted, so the person is configured once rather than per login method. Only enable this if you control who can authenticate at your provider and who can change their own username there. |
| `AIOSTREAMS_OIDC_USERNAME_PREFIX` | Username Prefix | string | — | Prepended to every SSO username. Set to something like `sso:` to make collisions with local AIOSTREAMS\_AUTH users impossible. |
| `AIOSTREAMS_OIDC_GROUPS_CLAIM` | Groups Claim | string | `groups` | Claim carrying group membership. If absent from the ID token, the userinfo endpoint is consulted automatically. |
| `AIOSTREAMS_OIDC_GROUP_PERMISSIONS` | Group Permissions | map | — | Maps groups to permissions. Comma-separated `group=perm\|perm` entries (valid: admin, proxy, service, sabnzbd, createConfig, none). Group names containing "," or "=" (such as LDAP DNs) must use the JSON object form instead, e.g. \{"cn=admins,ou=groups,dc=example,dc=com":"admin"}. |
| `AIOSTREAMS_OIDC_DEFAULT_PERMISSIONS` | Default Permissions | string | — | Permissions for an SSO user whose groups match nothing above. Empty means such a user is refused, which is the safe default. Set to `admin` only if every user your provider admits should administer this instance. |
| `AIOSTREAMS_OIDC_ALLOW_LOCAL_LOGIN` | Allow Local Login | boolean | `true` | Keep the AIOSTREAMS\_AUTH username/password form available alongside SSO. Turning this off means a provider outage locks everyone out; recover with AIOSTREAMS\_OIDC\_ENABLED=false and a restart. |
| `AIOSTREAMS_OIDC_BUTTON_LABEL` | Button Label | string | `Sign in with SSO` | Text on the SSO button on the login page. |
| `AIOSTREAMS_OIDC_AUTO_REDIRECT` | Auto Redirect | boolean | `false` | Skip the login page and go straight to the provider. Visit `/login?local=1` to reach the password form anyway. |
| `AIOSTREAMS_OIDC_ALLOW_INSECURE_REQUESTS` | Allow Insecure Requests | boolean | `false` | Permit a plain-http issuer URL. This exposes the client secret and tokens on the wire; use only on a trusted network. |
Templates [#templates]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------- | ---------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `FEATURED_TEMPLATE_IDS` | Featured Ids | list | — | Up to 2 template IDs featured on the about page. Defaults to the first 2 available templates when unset. |
| `TEMPLATE_URLS` | Urls | list | — | Remote template URLs to fetch and cache locally. Templates are downloaded once and refreshed on the schedule below. |
| `TEMPLATE_REFRESH_INTERVAL` | Refresh Interval | duration | `86400` | How often remote templates are refreshed (accepts e.g. "12h", "1d"). Set to 0 to disable automatic refresh. |
Metadata [#metadata]
TMDB [#tmdb]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | ------------------- | ------ | --------- | ----------------------------------------------------------------- |
| `TMDB_ACCESS_TOKEN` | TMDB › Access Token | string | *(unset)* | TMDB Read Access Token used for strict title matching. *(secret)* |
| `TMDB_API_KEY` | TMDB › API Key | string | *(unset)* | TMDB API key used for strict title matching. *(secret)* |
TVDB [#tvdb]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | -------------- | ------ | --------- | --------------------------------------------------- |
| `TVDB_API_KEY` | TVDB › API Key | string | *(unset)* | TVDB API key used for fetching metadata. *(secret)* |
Trakt [#trakt]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------- | --------------------- | ------- | ------- | --------------------------------------------------------- |
| `TRAKT_CLIENT_ID` | Trakt › Client ID | string | — | Trakt client ID used for fetching aliases. |
| `FETCH_TRAKT_ALIASES` | Trakt › Fetch Aliases | boolean | `false` | Enable fetching aliases from Trakt. Requires a client ID. |
ID Mappings [#id-mappings]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------------------ | -------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ID_MAPPINGS_ENABLED` | ID Mappings › Enabled | boolean | `true` | Fetch a keyless cross-provider (imdb/tvdb/tmdb) ID mapping dataset used to fill missing ids without extra API calls and to enable the keyless metadata fallback. *(restart required)* |
| `ID_MAPPINGS_TV_URL` | ID Mappings › Tv URL | string | `https://raw.githubusercontent.com/0xConstant1/Wikidata-Fetcher/main/data/tv_mappings.csv` | URL of the TV ID mapping CSV. *(restart required)* |
| `ID_MAPPINGS_MOVIE_URL` | ID Mappings › Movie URL | string | `https://raw.githubusercontent.com/0xConstant1/Wikidata-Fetcher/main/data/movie_mappings.csv` | URL of the movie ID mapping CSV. *(restart required)* |
| `ID_MAPPINGS_REFRESH_INTERVAL` | ID Mappings › Refresh Interval | duration | `86400` | Refresh interval for the ID mapping dataset. *(restart required)* |
Title Conflicts [#title-conflicts]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------- | ------------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TITLE_CONFLICTS_ENABLED` | Title Conflicts › Enabled | boolean | `true` | Detect same-name series (reboots and country variants, e.g. The Office UK vs US) via TMDB/TVDB search so queries and matching filters can disambiguate results. |
Scene Mappings [#scene-mappings]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | -------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `SCENE_MAPPINGS_ENABLED` | Scene Mappings › Enabled | boolean | `true` | Fetch scene title mappings (e.g. "Stephen Colbert" for The Late Show) used for search queries and title matching. *(restart required)* |
| `SCENE_MAPPINGS_URL` | Scene Mappings › URL | string | `https://services.sonarr.tv/v1/scenemapping` | URL of the scene mapping list. *(restart required)* |
| `SCENE_MAPPINGS_REFRESH_INTERVAL` | Scene Mappings › Refresh Interval | duration | `86400` | Refresh interval for the scene mapping list. *(restart required)* |
Anime DB [#anime-db]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------------------- | --------------------------------------------- | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `ANIME_DB_LEVEL_OF_DETAIL` | Anime DB › Level Of Detail | enum | `required` | "none" disables the anime DB; "required" loads only required mappings; "full" loads everything. *(restart required)* |
| `ANIME_DB_FRIBB_MAPPINGS_REFRESH_INTERVAL` | Anime DB › Refresh › Fribb Mappings | duration | `86400` | Refresh interval for the Fribb anime mappings (seconds; accepts e.g. "1d"). *(restart required)* |
| `ANIME_DB_MANAMI_DB_REFRESH_INTERVAL` | Anime DB › Refresh › Manami DB | duration | `604800` | Refresh interval for the Manami anime offline database. *(restart required)* |
| `ANIME_DB_KITSU_IMDB_MAPPING_REFRESH_INTERVAL` | Anime DB › Refresh › Kitsu Imdb Mapping | duration | `86400` | Refresh interval for the Kitsu↔IMDB mapping. *(restart required)* |
| `ANIME_DB_EXTENDED_ANITRAKT_MOVIES_REFRESH_INTERVAL` | Anime DB › Refresh › Extended Anitrakt Movies | duration | `86400` | Refresh interval for the Extended Anitrakt movies dataset. *(restart required)* |
| `ANIME_DB_EXTENDED_ANITRAKT_TV_REFRESH_INTERVAL` | Anime DB › Refresh › Extended Anitrakt Tv | duration | `86400` | Refresh interval for the Extended Anitrakt TV dataset. *(restart required)* |
| `ANIME_DB_ANIME_LIST_REFRESH_INTERVAL` | Anime DB › Refresh › Anime List | duration | `604800` | Refresh interval for the Anime List XML dataset. *(restart required)* |
| `ANIME_DB_ANIMEAPI_REFRESH_INTERVAL` | Anime DB › Refresh › Anime API | duration | `86400` | Refresh interval for the nattadasu/animeApi dataset (anidb/anilist/mal/kitsu/imdb/tmdb/tvdb/trakt mappings). *(restart required)* |
Logging [#logging-1]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | ------------------ | ------- | ------- | ------------------------------------------------------------------------------- |
| `LOG_LEVEL` | Log Level | enum | `info` | How much detail to log. |
| `LOG_FORMAT` | Log Format | enum | `json` | How log lines are printed to the console. `json` is recommended for production. |
| `LOG_SENSITIVE_INFO` | Log Sensitive Info | boolean | `false` | When true, sensitive values may appear in logs. Use only for debugging. |
Network [#network]
HTTP [#http]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------- | ----------------------------- | ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_USER_AGENT` | Default User Agent | string | `AIOStreams/{version}` | Default User-Agent header for outbound HTTP requests. Supports `{version}` and `{random}` placeholders. *(restart required)* |
| `AIOSTREAMS_USER_AGENT` | Aiostreams User Agent | string | `AIOStreams/{version}` | User-Agent identifying AIOStreams to upstream services. Supports `{version}` and `{random}` placeholders. *(restart required)* |
| `REQUEST_HEADER_OVERRIDES` | Hostname User Agent Overrides | map | — | Per-key request header overrides. A key is a hostname (`host`, `*.host`, `*`) or a `[context]` label for a request purpose - `[nzb_grabs]`, `[torrent_grabs]`, `[newznab]`, `[torznab]`. A value is a literal User-Agent (which may use the `{version}` / `{random}` placeholders, like `DEFAULT_USER_AGENT`) or a `{preset}` reference to a built-in header set (`{sabnzbd}`, `{nzbget}`, `{sonarr}`, `{radarr}`, `{prowlarr}`, `{nzbhydra2}`, `{chrome}`). Env shape: `key1:value1,key2:value2,...`. When several keys match a request the most specific one wins - exact host, then wildcard host (`*.host`), then `[context]`, then global `*` - and the chosen value overrides the default user agent. Example: `[nzb_grabs]:{sabnzbd},indexer.com:{prowlarr}` |
| `ADDON_PROXY` | Addon Proxy | list | — | Outbound HTTP proxy URL(s) used when fetching addon endpoints. |
| `ADDON_PROXY_CONFIG` | Addon Proxy Config | map | — | Per-key proxy enablement / index. A key is a hostname (`host`, `*.host`, `*`) or a `[context]` label (`[nzb_grabs]`, `[torrent_grabs]`, `[newznab]`, `[torznab]`). Env shape: `key1:bool\|index,...` - `true`/`false` enable/disable, an index selects an `addonProxy` entry. When several keys match the most specific one wins - exact host, then wildcard host (`*.host`), then `[context]`, then global `*`. Example: `[newznab]:true`. |
| `REQUEST_URL_MAPPINGS` | Request URL Mappings | map | — | Origin-level URL rewrites applied to outbound requests. JSON object of `{origin: replacement}` URLs. |
Proxy [#proxy]
Encryption [#encryption]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------ | ---------------------- | ------- | ------- | ------------------------------------------------- |
| `ENCRYPT_MEDIAFLOW_URLS` | Encryption › Mediaflow | boolean | `true` | Encrypt MediaFlow proxy URLs surfaced to clients. |
| `ENCRYPT_STREMTHRU_URLS` | Encryption › Stremthru | boolean | `true` | Encrypt StremThru proxy URLs surfaced to clients. |
Default [#default]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | -------------------------- | ------ | --------- | ------------------------------------------------------------------------------------------- |
| `DEFAULT_PROXY_ENABLED` | Default › Enabled | json | — | When set, used as the default proxy enabled state for new users. |
| `DEFAULT_PROXY_ID` | Default › ID | string | — | Default proxy service identifier. |
| `DEFAULT_PROXY_URL` | Default › URL | string | — | Default proxy URL. |
| `DEFAULT_PROXY_PUBLIC_URL` | Default › Public URL | string | — | Public-facing default proxy URL surfaced to clients (when different from the internal one). |
| `DEFAULT_PROXY_CREDENTIALS` | Default › Credentials | string | *(unset)* | Credentials for the default proxy. *(secret)* |
| `DEFAULT_PROXY_PUBLIC_IP` | Default › Public IP | string | — | Public IP of the default proxy. |
| `DEFAULT_PROXY_PROXIED_SERVICES` | Default › Proxied Services | list | — | List of serviceIds to proxy by default. JSON array of strings. |
Force [#force]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ------------------------------ | ------- | --------- | ---------------------------------------------------------------------------- |
| `FORCE_PROXY_ENABLED` | Force › Enabled | json | — | Override user choice of whether the proxy is enabled. |
| `FORCE_PROXY_ID` | Force › ID | string | — | Override user choice of proxy service identifier. |
| `FORCE_PROXY_URL` | Force › URL | string | — | Override user choice of proxy URL. |
| `FORCE_PROXY_PUBLIC_URL` | Force › Public URL | string | — | Override user choice of proxy public URL. |
| `FORCE_PROXY_CREDENTIALS` | Force › Credentials | string | *(unset)* | Override user choice of proxy credentials. *(secret)* |
| `FORCE_PROXY_PUBLIC_IP` | Force › Public IP | string | — | Override user choice of proxy public IP. |
| `FORCE_PROXY_DISABLE_PROXIED_ADDONS` | Force › Disable Proxied Addons | boolean | `false` | When forcing a proxy, also disable any addons that already proxy themselves. |
| `FORCE_PROXY_PROXIED_SERVICES` | Force › Proxied Services | list | — | List of serviceIds to force-proxy. JSON array of strings. |
IP [#ip]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | -------------- | ------ | ------- | ------------------------------------------- |
| `PROXY_IP_CACHE_TTL` | IP › Cache TTL | number | `900` | Cache TTL for resolved proxy IPs (seconds). |
Rate Limits [#rate-limits]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------- | ---------- | ------- | -------- | --------------------------------------------------------------- |
| `RATE_LIMIT_STORE` | Store | enum | `memory` | Backend used to track rate-limit counters. *(restart required)* |
| `DISABLE_RATE_LIMITS` | Disabled | boolean | `false` | When true, all rate limiters are disabled. *(restart required)* |
Static [#static]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | --------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
| `STATIC_RATE_LIMIT_WINDOW` | Static › Window | number | `5` | Sliding-window length (seconds) for the static file rate limiter. *(restart required)* |
| `STATIC_RATE_LIMIT_MAX_REQUESTS` | Static › Max Requests | number | `200` | Maximum requests per IP within the window for the static file rate limiter. *(restart required)* |
User API [#user-api]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ----------------------- | ------ | ------- | --------------------------------------------------------------------------------------------- |
| `USER_API_RATE_LIMIT_WINDOW` | User API › Window | number | `5` | Sliding-window length (seconds) for the user API rate limiter. *(restart required)* |
| `USER_API_RATE_LIMIT_MAX_REQUESTS` | User API › Max Requests | number | `5` | Maximum requests per IP within the window for the user API rate limiter. *(restart required)* |
Stream API [#stream-api]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------- |
| `STREAM_API_RATE_LIMIT_WINDOW` | Stream API › Window | number | `10` | Sliding-window length (seconds) for the stream API rate limiter. *(restart required)* |
| `STREAM_API_RATE_LIMIT_MAX_REQUESTS` | Stream API › Max Requests | number | `5` | Maximum requests per IP within the window for the stream API rate limiter. *(restart required)* |
Format API [#format-api]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------- |
| `FORMAT_API_RATE_LIMIT_WINDOW` | Format API › Window | number | `5` | Sliding-window length (seconds) for the format API rate limiter. *(restart required)* |
| `FORMAT_API_RATE_LIMIT_MAX_REQUESTS` | Format API › Max Requests | number | `30` | Maximum requests per IP within the window for the format API rate limiter. *(restart required)* |
Catalog API [#catalog-api]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | -------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
| `CATALOG_API_RATE_LIMIT_WINDOW` | Catalog API › Window | number | `5` | Sliding-window length (seconds) for the catalog API rate limiter. *(restart required)* |
| `CATALOG_API_RATE_LIMIT_MAX_REQUESTS` | Catalog API › Max Requests | number | `5` | Maximum requests per IP within the window for the catalog API rate limiter. *(restart required)* |
Anime API [#anime-api]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ------------------------ | ------ | ------- | ---------------------------------------------------------------------------------------------- |
| `ANIME_API_RATE_LIMIT_WINDOW` | Anime API › Window | number | `60` | Sliding-window length (seconds) for the anime API rate limiter. *(restart required)* |
| `ANIME_API_RATE_LIMIT_MAX_REQUESTS` | Anime API › Max Requests | number | `120` | Maximum requests per IP within the window for the anime API rate limiter. *(restart required)* |
Stremio Stream [#stremio-stream]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------- | ----------------------------- | ------ | ------- | --------------------------------------------------------------------------------------------------- |
| `STREMIO_STREAM_RATE_LIMIT_WINDOW` | Stremio Stream › Window | number | `15` | Sliding-window length (seconds) for the Stremio stream rate limiter. *(restart required)* |
| `STREMIO_STREAM_RATE_LIMIT_MAX_REQUESTS` | Stremio Stream › Max Requests | number | `10` | Maximum requests per IP within the window for the Stremio stream rate limiter. *(restart required)* |
Stremio Catalog [#stremio-catalog]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------- | ------------------------------ | ------ | ------- | ---------------------------------------------------------------------------------------------------- |
| `STREMIO_CATALOG_RATE_LIMIT_WINDOW` | Stremio Catalog › Window | number | `5` | Sliding-window length (seconds) for the Stremio catalog rate limiter. *(restart required)* |
| `STREMIO_CATALOG_RATE_LIMIT_MAX_REQUESTS` | Stremio Catalog › Max Requests | number | `30` | Maximum requests per IP within the window for the Stremio catalog rate limiter. *(restart required)* |
Stremio Manifest [#stremio-manifest]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------------ | ------------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `STREMIO_MANIFEST_RATE_LIMIT_WINDOW` | Stremio Manifest › Window | number | `5` | Sliding-window length (seconds) for the Stremio manifest rate limiter. *(restart required)* |
| `STREMIO_MANIFEST_RATE_LIMIT_MAX_REQUESTS` | Stremio Manifest › Max Requests | number | `5` | Maximum requests per IP within the window for the Stremio manifest rate limiter. *(restart required)* |
Stremio Subtitle [#stremio-subtitle]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------------ | ------------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `STREMIO_SUBTITLE_RATE_LIMIT_WINDOW` | Stremio Subtitle › Window | number | `5` | Sliding-window length (seconds) for the Stremio subtitle rate limiter. *(restart required)* |
| `STREMIO_SUBTITLE_RATE_LIMIT_MAX_REQUESTS` | Stremio Subtitle › Max Requests | number | `10` | Maximum requests per IP within the window for the Stremio subtitle rate limiter. *(restart required)* |
Stremio Meta [#stremio-meta]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------------- | --------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------- |
| `STREMIO_META_RATE_LIMIT_WINDOW` | Stremio Meta › Window | number | `5` | Sliding-window length (seconds) for the Stremio meta rate limiter. *(restart required)* |
| `STREMIO_META_RATE_LIMIT_MAX_REQUESTS` | Stremio Meta › Max Requests | number | `15` | Maximum requests per IP within the window for the Stremio meta rate limiter. *(restart required)* |
Login [#login]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | -------------------- | ------ | ------- | ------------------------------------------------------------------------------------------ |
| `LOGIN_RATE_LIMIT_WINDOW` | Login › Window | number | `300` | Sliding-window length (seconds) for the login rate limiter. *(restart required)* |
| `LOGIN_RATE_LIMIT_MAX_REQUESTS` | Login › Max Requests | number | `5` | Maximum requests per IP within the window for the login rate limiter. *(restart required)* |
OIDC [#oidc]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------- |
| `OIDC_RATE_LIMIT_WINDOW` | OIDC › Window | number | `300` | Sliding-window length (seconds) for the SSO login rate limiter. *(restart required)* |
| `OIDC_RATE_LIMIT_MAX_REQUESTS` | OIDC › Max Requests | number | `20` | Maximum requests per IP within the window for the SSO login rate limiter. *(restart required)* |
Content [#content]
Services [#services]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ------------------- | ---- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_SERVICE_CREDENTIALS` | Default Credentials | map | *(unset)* | Default credentials pre-filled into user configurations when not provided. Format: one `serviceId.credentialId=value` entry per line (use `\n` if your environment cannot store multiline values). Service IDs: `realdebrid`, `alldebrid`, `premiumize`, `debridlink`, `torbox`, `torrin`, `offcloud`, `putio`, `easynews`, `easydebrid`, `debrider`, `pikpak`, `seedr`, `nzbdav`, `altmount`, `stremio_nntp`, `stremthru_newz`, `aiostreams`. Credential IDs vary by service (e.g. `apiKey`, `username`, `password`, `clientId`, `encodedToken`). Example: `realdebrid.apiKey=xxx` / `easynews.username=user` / `easynews.password=pass`. *(secret)* |
| `FORCED_SERVICE_CREDENTIALS` | Forced Credentials | map | *(unset)* | Credentials that override whatever the user has configured and are hidden from the configuration UI. Same `serviceId.credentialId=value` per-line format and service/credential IDs as DEFAULT\*SERVICE\_CREDENTIALS. \*(secret)\_ |
Presets [#presets]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | --------------- | ------ | ------- | ---------------------------------------------------------------------------------------------- |
| `DEFAULT_TIMEOUT` | Default Timeout | number | `7000` | Fallback timeout for preset stream fetching when a preset does not set its own (milliseconds). |
Comet [#comet]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------- | -------------------------- | ------ | ------------------------------- | ------------------------------------------------------------------------------------------------ |
| `COMET_URL` | Comet › URL | list | `["https://comet.feels.legal"]` | Upstream URL(s) for the Comet addon. |
| `DEFAULT_COMET_TIMEOUT` | Comet › Default Timeout | number | — | Default timeout for the Comet addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_COMET_USER_AGENT` | Comet › Default User Agent | string | — | Default User-Agent for the Comet addon. Supports `{version}`/`{random}` placeholders. |
| `COMET_PUBLIC_API_TOKEN` | Comet › Public API Token | list | — | Public API token(s) used by Comet. JSON array or single string. |
Meteor [#meteor]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------- | --------------------------- | ------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `METEOR_URL` | Meteor › URL | list | `["https://meteorfortheweebs.midnightignite.me"]` | Upstream URL(s) for the Meteor addon. |
| `DEFAULT_METEOR_TIMEOUT` | Meteor › Default Timeout | number | — | Default timeout for the Meteor addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_METEOR_USER_AGENT` | Meteor › Default User Agent | string | — | Default User-Agent for the Meteor addon. Supports `{version}`/`{random}` placeholders. |
Mediafusion [#mediafusion]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------------- | --------------------------------------------- | ------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `MEDIAFUSION_URL` | Mediafusion › URL | list | `["https://mediafusion.elfhosted.com"]` | Upstream URL(s) for the MediaFusion addon. |
| `DEFAULT_MEDIAFUSION_TIMEOUT` | Mediafusion › Default Timeout | number | — | Default timeout for the MediaFusion addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_MEDIAFUSION_USER_AGENT` | Mediafusion › Default User Agent | string | — | Default User-Agent for the MediaFusion addon. Supports `{version}`/`{random}` placeholders. |
| `MEDIAFUSION_API_PASSWORD` | Mediafusion › API Password | string | *(unset)* | API password sent to MediaFusion. *(secret)* |
| `MEDIAFUSION_DEFAULT_USE_CACHED_RESULTS_ONLY` | Mediafusion › Default Use Cached Results Only | boolean | `true` | Default value of MediaFusion's "cached results only" toggle. |
| `MEDIAFUSION_FORCED_USE_CACHED_RESULTS_ONLY` | Mediafusion › Forced Use Cached Results Only | json | — | When set, overrides users' "cached results only" toggle. |
Jackettio [#jackettio]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | ------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `JACKETTIO_URL` | Jackettio › URL | list | `["https://jackettio.elfhosted.com"]` | Upstream URL(s) for the Jackettio addon. |
| `DEFAULT_JACKETTIO_TIMEOUT` | Jackettio › Default Timeout | number | — | Default timeout for the Jackettio addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_JACKETTIO_USER_AGENT` | Jackettio › Default User Agent | string | — | Default User-Agent for the Jackettio addon. Supports `{version}`/`{random}` placeholders. |
| `DEFAULT_JACKETTIO_INDEXERS` | Jackettio › Default Indexers | list | `["eztv","thepiratebay","therarbg","yts"]` | Default indexer list applied when creating Jackettio configs. JSON array of strings. |
| `DEFAULT_JACKETTIO_STREMTHRU_URL` | Jackettio › Default Stremthru URL | string | `https://stremthru.13377001.xyz` | Default StremThru URL passed to new Jackettio configs. |
Torrentio [#torrentio]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------------------ | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `TORRENTIO_URL` | Torrentio › URL | list | `["https://torrentio.strem.fun"]` | Upstream URL(s) for the Torrentio addon. |
| `DEFAULT_TORRENTIO_TIMEOUT` | Torrentio › Default Timeout | number | — | Default timeout for the Torrentio addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TORRENTIO_USER_AGENT` | Torrentio › Default User Agent | string | — | Default User-Agent for the Torrentio addon. Supports `{version}`/`{random}` placeholders. |
Orion [#orion]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------- | -------------------------- | ------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `ORION_STREMIO_ADDON_URL` | Orion › URL | list | `["https://5a0d1888fa64-orion.baby-beamup.club"]` | Upstream URL(s) for the Orion addon. |
| `DEFAULT_ORION_TIMEOUT` | Orion › Default Timeout | number | — | Default timeout for the Orion addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_ORION_USER_AGENT` | Orion › Default User Agent | string | — | Default User-Agent for the Orion addon. Supports `{version}`/`{random}` placeholders. |
Peerflix [#peerflix]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------- |
| `PEERFLIX_URL` | Peerflix › URL | list | `["https://addon.peerflix.mov"]` | Upstream URL(s) for the Peerflix addon. |
| `DEFAULT_PEERFLIX_TIMEOUT` | Peerflix › Default Timeout | number | — | Default timeout for the Peerflix addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_PEERFLIX_USER_AGENT` | Peerflix › Default User Agent | string | — | Default User-Agent for the Peerflix addon. Supports `{version}`/`{random}` placeholders. |
Torbox [#torbox]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------- | --------------------------- | ------ | -------------------------------- | ------------------------------------------------------------------------------------------------- |
| `TORBOX_STREMIO_URL` | Torbox › URL | list | `["https://stremio.torbox.app"]` | Upstream URL(s) for the Torbox addon. |
| `DEFAULT_TORBOX_TIMEOUT` | Torbox › Default Timeout | number | — | Default timeout for the Torbox addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TORBOX_USER_AGENT` | Torbox › Default User Agent | string | — | Default User-Agent for the Torbox addon. Supports `{version}`/`{random}` placeholders. |
Easynews [#easynews]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `EASYNEWS_URL` | Easynews › URL | list | `["https://ea627ddf0ee7-easynews.baby-beamup.club"]` | Upstream URL(s) for the Easynews addon. |
| `DEFAULT_EASYNEWS_TIMEOUT` | Easynews › Default Timeout | number | — | Default timeout for the Easynews addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_EASYNEWS_USER_AGENT` | Easynews › Default User Agent | string | — | Default User-Agent for the Easynews addon. Supports `{version}`/`{random}` placeholders. |
Easynews Plus [#easynews-plus]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `EASYNEWS_PLUS_URL` | Easynews Plus › URL | list | `["https://b89262c192b0-stremio-easynews-addon.baby-beamup.club"]` | Upstream URL(s) for the Easynews+ addon. |
| `DEFAULT_EASYNEWS_PLUS_TIMEOUT` | Easynews Plus › Default Timeout | number | — | Default timeout for the Easynews+ addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_EASYNEWS_PLUS_USER_AGENT` | Easynews Plus › Default User Agent | string | — | Default User-Agent for the Easynews+ addon. Supports `{version}`/`{random}` placeholders. |
Easynews Plus Plus [#easynews-plus-plus]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------- | --------------------------------------- | ------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `EASYNEWS_PLUS_PLUS_URL` | Easynews Plus Plus › URL | list | `["https://easynews-cloudflare-worker.jqrw92fchz.workers.dev"]` | Upstream URL(s) for the Easynews++ addon. |
| `DEFAULT_EASYNEWS_PLUS_PLUS_TIMEOUT` | Easynews Plus Plus › Default Timeout | number | — | Default timeout for the Easynews++ addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_EASYNEWS_PLUS_PLUS_USER_AGENT` | Easynews Plus Plus › Default User Agent | string | — | Default User-Agent for the Easynews++ addon. Supports `{version}`/`{random}` placeholders. |
| `EASYNEWS_PLUS_PLUS_PUBLIC_URL` | Easynews Plus Plus › Public URL | string | — | Public-facing URL surfaced to clients (when different from the internal one). |
Debridio [#debridio]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------- |
| `DEBRIDIO_URL` | Debridio › URL | list | `["https://addon.debridio.com"]` | Upstream URL(s) for the Debridio addon. |
| `DEFAULT_DEBRIDIO_TIMEOUT` | Debridio › Default Timeout | number | — | Default timeout for the Debridio addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_USER_AGENT` | Debridio › Default User Agent | string | — | Default User-Agent for the Debridio addon. Supports `{version}`/`{random}` placeholders. |
Debridio TVDB [#debridio-tvdb]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `DEBRIDIO_TVDB_URL` | Debridio TVDB › URL | list | `["https://tvdb-addon.debridio.com"]` | Upstream URL(s) for the Debridio TVDB addon. |
| `DEFAULT_DEBRIDIO_TVDB_TIMEOUT` | Debridio TVDB › Default Timeout | number | — | Default timeout for the Debridio TVDB addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_TVDB_USER_AGENT` | Debridio TVDB › Default User Agent | string | — | Default User-Agent for the Debridio TVDB addon. Supports `{version}`/`{random}` placeholders. |
Debridio TMDB [#debridio-tmdb]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | ------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `DEBRIDIO_TMDB_URL` | Debridio TMDB › URL | list | `["https://tmdb-addon.debridio.com"]` | Upstream URL(s) for the Debridio TMDB addon. |
| `DEFAULT_DEBRIDIO_TMDB_TIMEOUT` | Debridio TMDB › Default Timeout | number | — | Default timeout for the Debridio TMDB addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_TMDB_USER_AGENT` | Debridio TMDB › Default User Agent | string | — | Default User-Agent for the Debridio TMDB addon. Supports `{version}`/`{random}` placeholders. |
Debridio Tv [#debridio-tv]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | -------------------------------- | ------ | -------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `DEBRIDIO_TV_URL` | Debridio Tv › URL | list | `["https://tv.lb.debridio.com"]` | Upstream URL(s) for the Debridio TV addon. |
| `DEFAULT_DEBRIDIO_TV_TIMEOUT` | Debridio Tv › Default Timeout | number | — | Default timeout for the Debridio TV addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_TV_USER_AGENT` | Debridio Tv › Default User Agent | string | — | Default User-Agent for the Debridio TV addon. Supports `{version}`/`{random}` placeholders. |
Debridio Watchtower [#debridio-watchtower]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------- | ---------------------------------------- | ------ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `DEBRIDIO_WATCHTOWER_URL` | Debridio Watchtower › URL | list | `["https://wt-addon.debridio.com"]` | Upstream URL(s) for the Debridio Watchtower addon. |
| `DEFAULT_DEBRIDIO_WATCHTOWER_TIMEOUT` | Debridio Watchtower › Default Timeout | number | — | Default timeout for the Debridio Watchtower addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_WATCHTOWER_USER_AGENT` | Debridio Watchtower › Default User Agent | string | — | Default User-Agent for the Debridio Watchtower addon. Supports `{version}`/`{random}` placeholders. |
Debridio Ic4a [#debridio-ic4a]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `DEBRIDIO_IC4A_URL` | Debridio Ic4a › URL | list | `["https://ic4a.lb.debridio.com"]` | Upstream URL(s) for the Debridio IC4A addon. |
| `DEFAULT_DEBRIDIO_IC4A_TIMEOUT` | Debridio Ic4a › Default Timeout | number | — | Default timeout for the Debridio IC4A addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DEBRIDIO_IC4A_USER_AGENT` | Debridio Ic4a › Default User Agent | string | — | Default User-Agent for the Debridio IC4A addon. Supports `{version}`/`{random}` placeholders. |
Stremthru Store [#stremthru-store]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ------------------------------------ | ------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `STREMTHRU_STORE_URL` | Stremthru Store › URL | list | `["https://stremthru.13377001.xyz/stremio/store"]` | Upstream URL(s) for the StremThru Store addon. |
| `DEFAULT_STREMTHRU_STORE_TIMEOUT` | Stremthru Store › Default Timeout | number | — | Default timeout for the StremThru Store addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STREMTHRU_STORE_USER_AGENT` | Stremthru Store › Default User Agent | string | — | Default User-Agent for the StremThru Store addon. Supports `{version}`/`{random}` placeholders. |
Stremthru Torz [#stremthru-torz]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ----------------------------------- | ------ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `STREMTHRU_TORZ_URL` | Stremthru Torz › URL | list | `["https://stremthru.13377001.xyz/stremio/torz"]` | Upstream URL(s) for the StremThru Torz addon. |
| `DEFAULT_STREMTHRU_TORZ_TIMEOUT` | Stremthru Torz › Default Timeout | number | — | Default timeout for the StremThru Torz addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STREMTHRU_TORZ_USER_AGENT` | Stremthru Torz › Default User Agent | string | — | Default User-Agent for the StremThru Torz addon. Supports `{version}`/`{random}` placeholders. |
Streamfusion [#streamfusion]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `DEFAULT_STREAMFUSION_URL` | Streamfusion › URL | list | `["https://stream-fusion.stremiofr.com"]` | Upstream URL(s) for the StreamFusion addon. |
| `DEFAULT_STREAMFUSION_TIMEOUT` | Streamfusion › Default Timeout | number | — | Default timeout for the StreamFusion addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STREAMFUSION_USER_AGENT` | Streamfusion › Default User Agent | string | — | Default User-Agent for the StreamFusion addon. Supports `{version}`/`{random}` placeholders. |
Sootio [#sootio]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------- | --------------------------- | ------ | ------------------------- | ------------------------------------------------------------------------------------------------- |
| `SOOTIO_URL` | Sootio › URL | list | `["https://sooti.click"]` | Upstream URL(s) for the Sootio addon. |
| `DEFAULT_SOOTIO_TIMEOUT` | Sootio › Default Timeout | number | — | Default timeout for the Sootio addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_SOOTIO_USER_AGENT` | Sootio › Default User Agent | string | — | Default User-Agent for the Sootio addon. Supports `{version}`/`{random}` placeholders. |
Dmm Cast [#dmm-cast]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | ------- | --------------------------------------------------------------------------------------------------- |
| `DEFAULT_DMM_CAST_TIMEOUT` | Dmm Cast › Default Timeout | number | — | Default timeout for the DMM Cast addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DMM_CAST_USER_AGENT` | Dmm Cast › Default User Agent | string | — | Default User-Agent for the DMM Cast addon. Supports `{version}`/`{random}` placeholders. |
Opensubtitles [#opensubtitles]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `OPENSUBTITLES_URL` | Opensubtitles › URL | list | `["https://opensubtitles-v3.strem.io"]` | Upstream URL(s) for the OpenSubtitles addon. |
| `DEFAULT_OPENSUBTITLES_TIMEOUT` | Opensubtitles › Default Timeout | number | — | Default timeout for the OpenSubtitles addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_OPENSUBTITLES_USER_AGENT` | Opensubtitles › Default User Agent | string | — | Default User-Agent for the OpenSubtitles addon. Supports `{version}`/`{random}` placeholders. |
Opensubtitles V3 Plus [#opensubtitles-v3-plus]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------------ | ------------------------------------------ | ------ | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `OPENSUBTITLES_V3_PLUS_URL` | Opensubtitles V3 Plus › URL | list | `["https://opensubtitles.stremio.homes"]` | Upstream URL(s) for the OpenSubtitles V3+ addon. |
| `DEFAULT_OPENSUBTITLES_V3_PLUS_TIMEOUT` | Opensubtitles V3 Plus › Default Timeout | number | — | Default timeout for the OpenSubtitles V3+ addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_OPENSUBTITLES_V3_PLUS_USER_AGENT` | Opensubtitles V3 Plus › Default User Agent | string | — | Default User-Agent for the OpenSubtitles V3+ addon. Supports `{version}`/`{random}` placeholders. |
Marvel Universe [#marvel-universe]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ------------------------------------ | ------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `MARVEL_UNIVERSE_URL` | Marvel Universe › URL | list | `["https://addon-marvel.onrender.com"]` | Upstream URL(s) for the Marvel Universe addon. |
| `DEFAULT_MARVEL_CATALOG_TIMEOUT` | Marvel Universe › Default Timeout | number | — | Default timeout for the Marvel Universe addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_MARVEL_CATALOG_USER_AGENT` | Marvel Universe › Default User Agent | string | — | Default User-Agent for the Marvel Universe addon. Supports `{version}`/`{random}` placeholders. |
Dc Universe [#dc-universe]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | -------------------------------- | ------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `DC_UNIVERSE_URL` | Dc Universe › URL | list | `["https://addon-dc-cq85.onrender.com"]` | Upstream URL(s) for the DC Universe addon. |
| `DEFAULT_DC_UNIVERSE_TIMEOUT` | Dc Universe › Default Timeout | number | — | Default timeout for the DC Universe addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DC_UNIVERSE_USER_AGENT` | Dc Universe › Default User Agent | string | — | Default User-Agent for the DC Universe addon. Supports `{version}`/`{random}` placeholders. |
Star Wars Universe [#star-wars-universe]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------- | --------------------------------------- | ------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_STAR_WARS_UNIVERSE_URL` | Star Wars Universe › URL | list | `["https://addon-star-wars-u9e3.onrender.com"]` | Upstream URL(s) for the Star Wars Universe addon. |
| `DEFAULT_STAR_WARS_UNIVERSE_TIMEOUT` | Star Wars Universe › Default Timeout | number | — | Default timeout for the Star Wars Universe addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STAR_WARS_UNIVERSE_USER_AGENT` | Star Wars Universe › Default User Agent | string | — | Default User-Agent for the Star Wars Universe addon. Supports `{version}`/`{random}` placeholders. |
Anime Kitsu [#anime-kitsu]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | -------------------------------- | ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `ANIME_KITSU_URL` | Anime Kitsu › URL | list | `["https://anime-kitsu.strem.fun"]` | Upstream URL(s) for the Anime Kitsu addon. |
| `DEFAULT_ANIME_KITSU_TIMEOUT` | Anime Kitsu › Default Timeout | number | — | Default timeout for the Anime Kitsu addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_ANIME_KITSU_USER_AGENT` | Anime Kitsu › Default User Agent | string | — | Default User-Agent for the Anime Kitsu addon. Supports `{version}`/`{random}` placeholders. |
Nuvio Streams [#nuvio-streams]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | ---------------------------------- | ------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `NUVIOSTREAMS_URL` | Nuvio Streams › URL | list | `["https://nuviostreams.hayd.uk"]` | Upstream URL(s) for the NuvioStreams addon. |
| `DEFAULT_NUVIOSTREAMS_TIMEOUT` | Nuvio Streams › Default Timeout | number | — | Default timeout for the NuvioStreams addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_NUVIOSTREAMS_USER_AGENT` | Nuvio Streams › Default User Agent | string | — | Default User-Agent for the NuvioStreams addon. Supports `{version}`/`{random}` placeholders. |
Torrent Catalogs [#torrent-catalogs]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | ------------------------------------- | ------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `TORRENT_CATALOGS_URL` | Torrent Catalogs › URL | list | `["https://torrent-catalogs.strem.fun"]` | Upstream URL(s) for the Torrent Catalogs addon. |
| `DEFAULT_TORRENT_CATALOGS_TIMEOUT` | Torrent Catalogs › Default Timeout | number | — | Default timeout for the Torrent Catalogs addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TORRENT_CATALOGS_USER_AGENT` | Torrent Catalogs › Default User Agent | string | — | Default User-Agent for the Torrent Catalogs addon. Supports `{version}`/`{random}` placeholders. |
TMDB Collections [#tmdb-collections]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | ------------------------------------- | ------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `TMDB_COLLECTIONS_URL` | TMDB Collections › URL | list | `["https://61ab9c85a149-tmdb-collections.baby-beamup.club"]` | Upstream URL(s) for the TMDB Collections addon. |
| `DEFAULT_TMDB_COLLECTIONS_TIMEOUT` | TMDB Collections › Default Timeout | number | — | Default timeout for the TMDB Collections addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TMDB_COLLECTIONS_USER_AGENT` | TMDB Collections › Default User Agent | string | — | Default User-Agent for the TMDB Collections addon. Supports `{version}`/`{random}` placeholders. |
RPDB Catalogs [#rpdb-catalogs]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ---------------------------------- | ------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `RPDB_CATALOGS_URL` | RPDB Catalogs › URL | list | `["https://1fe84bc728af-rpdb.baby-beamup.club"]` | Upstream URL(s) for the RPDB Catalogs addon. |
| `DEFAULT_RPDB_CATALOGS_TIMEOUT` | RPDB Catalogs › Default Timeout | number | — | Default timeout for the RPDB Catalogs addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_RPDB_CATALOGS_USER_AGENT` | RPDB Catalogs › Default User Agent | string | — | Default User-Agent for the RPDB Catalogs addon. Supports `{version}`/`{random}` placeholders. |
Streaming Catalogs [#streaming-catalogs]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------- | --------------------------------------- | ------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `STREAMING_CATALOGS_URL` | Streaming Catalogs › URL | list | `["https://7a82163c306e-stremio-netflix-catalog-addon.baby-beamup.club"]` | Upstream URL(s) for the Streaming Catalogs addon. |
| `DEFAULT_STREAMING_CATALOGS_TIMEOUT` | Streaming Catalogs › Default Timeout | number | — | Default timeout for the Streaming Catalogs addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STREAMING_CATALOGS_USER_AGENT` | Streaming Catalogs › Default User Agent | string | — | Default User-Agent for the Streaming Catalogs addon. Supports `{version}`/`{random}` placeholders. |
Anime Catalogs [#anime-catalogs]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ----------------------------------- | ------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `ANIME_CATALOGS_URL` | Anime Catalogs › URL | list | `["https://1fe84bc728af-stremio-anime-catalogs.baby-beamup.club"]` | Upstream URL(s) for the Anime Catalogs addon. |
| `DEFAULT_ANIME_CATALOGS_TIMEOUT` | Anime Catalogs › Default Timeout | number | — | Default timeout for the Anime Catalogs addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_ANIME_CATALOGS_USER_AGENT` | Anime Catalogs › Default User Agent | string | — | Default User-Agent for the Anime Catalogs addon. Supports `{version}`/`{random}` placeholders. |
Doctor Who Universe [#doctor-who-universe]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------- | ---------------------------------------- | ------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `DOCTOR_WHO_UNIVERSE_URL` | Doctor Who Universe › URL | list | `["https://new-who.onrender.com"]` | Upstream URL(s) for the Doctor Who Universe addon. |
| `DEFAULT_DOCTOR_WHO_UNIVERSE_TIMEOUT` | Doctor Who Universe › Default Timeout | number | — | Default timeout for the Doctor Who Universe addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_DOCTOR_WHO_UNIVERSE_USER_AGENT` | Doctor Who Universe › Default User Agent | string | — | Default User-Agent for the Doctor Who Universe addon. Supports `{version}`/`{random}` placeholders. |
Webstreamr [#webstreamr]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ------------------------------- | ------ | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `WEBSTREAMR_URL` | Webstreamr › URL | list | `["https://webstreamr.hayd.uk"]` | Upstream URL(s) for the WebStreamr addon. |
| `DEFAULT_WEBSTREAMR_TIMEOUT` | Webstreamr › Default Timeout | number | — | Default timeout for the WebStreamr addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_WEBSTREAMR_USER_AGENT` | Webstreamr › Default User Agent | string | — | Default User-Agent for the WebStreamr addon. Supports `{version}`/`{random}` placeholders. |
Hdhub [#hdhub]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------- | -------------------------- | ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `HDHUB_URL` | Hdhub › URL | list | `["https://hdhub.thevolecitor.qzz.io"]` | Upstream URL(s) for the HdHub addon. |
| `DEFAULT_HDHUB_TIMEOUT` | Hdhub › Default Timeout | number | — | Default timeout for the HdHub addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_HDHUB_USER_AGENT` | Hdhub › Default User Agent | string | — | Default User-Agent for the HdHub addon. Supports `{version}`/`{random}` placeholders. |
Baguettio [#baguettio]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------------------ | ------ | --------------------------- | ---------------------------------------------------------------------------------------------------- |
| `BAGUETTIO_URL` | Baguettio › URL | list | `["https://baguettio.org"]` | Upstream URL(s) for the Baguettio addon. |
| `DEFAULT_BAGUETTIO_TIMEOUT` | Baguettio › Default Timeout | number | — | Default timeout for the Baguettio addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_BAGUETTIO_USER_AGENT` | Baguettio › Default User Agent | string | — | Default User-Agent for the Baguettio addon. Supports `{version}`/`{random}` placeholders. |
TMDB Addon [#tmdb-addon]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ------------------------------- | ------ | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `TMDB_ADDON_URL` | TMDB Addon › URL | list | `["https://tmdb.elfhosted.com"]` | Upstream URL(s) for the TMDB Addon addon. |
| `DEFAULT_TMDB_ADDON_TIMEOUT` | TMDB Addon › Default Timeout | number | — | Default timeout for the TMDB Addon addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TMDB_ADDON_USER_AGENT` | TMDB Addon › Default User Agent | string | — | Default User-Agent for the TMDB Addon addon. Supports `{version}`/`{random}` placeholders. |
Torrents DB [#torrents-db]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | -------------------------------- | ------ | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| `TORRENTS_DB_URL` | Torrents DB › URL | list | `["https://torrentsdb.com"]` | Upstream URL(s) for the Torrents DB addon. |
| `DEFAULT_TORRENTS_DB_TIMEOUT` | Torrents DB › Default Timeout | number | — | Default timeout for the Torrents DB addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_TORRENTS_DB_USER_AGENT` | Torrents DB › Default User Agent | string | — | Default User-Agent for the Torrents DB addon. Supports `{version}`/`{random}` placeholders. |
Usa Tv [#usa-tv]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------- | --------------------------- | ------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `USA_TV_URL` | Usa Tv › URL | list | `["https://848b3516657c-usatv.baby-beamup.club"]` | Upstream URL(s) for the USA TV addon. |
| `DEFAULT_USA_TV_TIMEOUT` | Usa Tv › Default Timeout | number | — | Default timeout for the USA TV addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_USA_TV_USER_AGENT` | Usa Tv › Default User Agent | string | — | Default User-Agent for the USA TV addon. Supports `{version}`/`{random}` placeholders. |
Argentina Tv [#argentina-tv]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | ------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `ARGENTINA_TV_URL` | Argentina Tv › URL | list | `["https://848b3516657c-argentinatv.baby-beamup.club"]` | Upstream URL(s) for the Argentina TV addon. |
| `DEFAULT_ARGENTINA_TV_TIMEOUT` | Argentina Tv › Default Timeout | number | — | Default timeout for the Argentina TV addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_ARGENTINA_TV_USER_AGENT` | Argentina Tv › Default User Agent | string | — | Default User-Agent for the Argentina TV addon. Supports `{version}`/`{random}` placeholders. |
Brazuca Torrents [#brazuca-torrents]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | ------------------------------------- | ------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `BRAZUCA_TORRENTS_URL` | Brazuca Torrents › URL | list | `["https://94c8cb9f702d-brazuca-torrents.baby-beamup.club"]` | Upstream URL(s) for the Brazuca Torrents addon. |
| `DEFAULT_BRAZUCA_TORRENTS_TIMEOUT` | Brazuca Torrents › Default Timeout | number | — | Default timeout for the Brazuca Torrents addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_BRAZUCA_TORRENTS_USER_AGENT` | Brazuca Torrents › Default User Agent | string | — | Default User-Agent for the Brazuca Torrents addon. Supports `{version}`/`{random}` placeholders. |
Subdl [#subdl]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------- | -------------------------- | ------ | ----------------------------- | ------------------------------------------------------------------------------------------------ |
| `SUBDL_URL` | Subdl › URL | list | `["https://subdl.strem.top"]` | Upstream URL(s) for the SubDL addon. |
| `DEFAULT_SUBDL_TIMEOUT` | Subdl › Default Timeout | number | — | Default timeout for the SubDL addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_SUBDL_USER_AGENT` | Subdl › Default User Agent | string | — | Default User-Agent for the SubDL addon. Supports `{version}`/`{random}` placeholders. |
Subsource [#subsource]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------------------ | ------ | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `SUBSOURCE_URL` | Subsource › URL | list | `["https://subsource.strem.top"]` | Upstream URL(s) for the SubSource addon. |
| `DEFAULT_SUBSOURCE_TIMEOUT` | Subsource › Default Timeout | number | — | Default timeout for the SubSource addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_SUBSOURCE_USER_AGENT` | Subsource › Default User Agent | string | — | Default User-Agent for the SubSource addon. Supports `{version}`/`{random}` placeholders. |
Ai Search [#ai-search]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | ------------------------------ | ------ | ------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `AI_SEARCH_URL` | Ai Search › URL | list | `["https://stremio.itcon.au"]` | Upstream URL(s) for the AI Search addon. |
| `DEFAULT_AI_SEARCH_TIMEOUT` | Ai Search › Default Timeout | number | — | Default timeout for the AI Search addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_AI_SEARCH_USER_AGENT` | Ai Search › Default User Agent | string | — | Default User-Agent for the AI Search addon. Supports `{version}`/`{random}` placeholders. |
Flix Streams [#flix-streams]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `FLIX_STREAMS_URL` | Flix Streams › URL | list | `["https://flixnest.app/flix-streams"]` | Upstream URL(s) for the Flix-Streams addon. |
| `DEFAULT_FLIX_STREAMS_TIMEOUT` | Flix Streams › Default Timeout | number | — | Default timeout for the Flix-Streams addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_FLIX_STREAMS_USER_AGENT` | Flix Streams › Default User Agent | string | — | Default User-Agent for the Flix-Streams addon. Supports `{version}`/`{random}` placeholders. |
Fkstream [#fkstream]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | -------------------------------- | --------------------------------------------------------------------------------------------------- |
| `FKSTREAM_URL` | Fkstream › URL | list | `["https://streamio.fankai.fr"]` | Upstream URL(s) for the FKStream addon. |
| `DEFAULT_FKSTREAM_TIMEOUT` | Fkstream › Default Timeout | number | — | Default timeout for the FKStream addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_FKSTREAM_USER_AGENT` | Fkstream › Default User Agent | string | — | Default User-Agent for the FKStream addon. Supports `{version}`/`{random}` placeholders. |
Aio Subtitle [#aio-subtitle]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | --------------------------------- | ------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `AIOSUBTITLE_URL` | Aio Subtitle › URL | list | `["https://3b4bbf5252c4-aio-streaming.baby-beamup.club"]` | Upstream URL(s) for the AIOSubtitle addon. |
| `DEFAULT_AIOSUBTITLE_TIMEOUT` | Aio Subtitle › Default Timeout | number | — | Default timeout for the AIOSubtitle addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_AIOSUBTITLE_USER_AGENT` | Aio Subtitle › Default User Agent | string | — | Default User-Agent for the AIOSubtitle addon. Supports `{version}`/`{random}` placeholders. |
Subhero [#subhero]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------- | ---------------------------- | ------ | -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `SUBHERO_URL` | Subhero › URL | list | `["https://subhero.chromeknight.dev"]` | Upstream URL(s) for the SubHero addon. |
| `DEFAULT_SUBHERO_TIMEOUT` | Subhero › Default Timeout | number | — | Default timeout for the SubHero addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_SUBHERO_USER_AGENT` | Subhero › Default User Agent | string | — | Default User-Agent for the SubHero addon. Supports `{version}`/`{random}` placeholders. |
Yastream [#yastream]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | ------ | --------------------------------- | --------------------------------------------------------------------------------------------------- |
| `YASTREAM_URL` | Yastream › URL | list | `["https://yastream.tamthai.de"]` | Upstream URL(s) for the yastream addon. |
| `DEFAULT_YASTREAM_TIMEOUT` | Yastream › Default Timeout | number | — | Default timeout for the yastream addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_YASTREAM_USER_AGENT` | Yastream › Default User Agent | string | — | Default User-Agent for the yastream addon. Supports `{version}`/`{random}` placeholders. |
Streamasia [#streamasia]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ------------------------------- | ------ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `STREAMASIA_URL` | Streamasia › URL | list | `["https://stremio-dramacool-addon.xyz"]` | Upstream URL(s) for the StreamAsia addon. |
| `DEFAULT_STREAMASIA_TIMEOUT` | Streamasia › Default Timeout | number | — | Default timeout for the StreamAsia addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_STREAMASIA_USER_AGENT` | Streamasia › Default User Agent | string | — | Default User-Agent for the StreamAsia addon. Supports `{version}`/`{random}` placeholders. |
More Like This [#more-like-this]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ----------------------------------- | ------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `MORE_LIKE_THIS_URL` | More Like This › URL | list | `["https://bbab4a35b833-more-like-this.baby-beamup.club"]` | Upstream URL(s) for the More Like This addon. |
| `DEFAULT_MORE_LIKE_THIS_TIMEOUT` | More Like This › Default Timeout | number | — | Default timeout for the More Like This addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_MORE_LIKE_THIS_USER_AGENT` | More Like This › Default User Agent | string | — | Default User-Agent for the More Like This addon. Supports `{version}`/`{random}` placeholders. |
Content Deep Dive [#content-deep-dive]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------------- | -------------------------------------- | ------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `CONTENT_DEEP_DIVE_URL` | Content Deep Dive › URL | list | `["https://stremio-content-deepdive-addon-dc8f7b513289.herokuapp.com"]` | Upstream URL(s) for the Content Deep Dive addon. |
| `DEFAULT_CONTENT_DEEP_DIVE_TIMEOUT` | Content Deep Dive › Default Timeout | number | — | Default timeout for the Content Deep Dive addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_CONTENT_DEEP_DIVE_USER_AGENT` | Content Deep Dive › Default User Agent | string | — | Default User-Agent for the Content Deep Dive addon. Supports `{version}`/`{random}` placeholders. |
Ai Companion [#ai-companion]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | --------------------------------- | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `AI_COMPANION_URL` | Ai Companion › URL | list | `["https://ai-companion.saladprecedestretch123.uk"]` | Upstream URL(s) for the AI Companion addon. |
| `DEFAULT_AI_COMPANION_TIMEOUT` | Ai Companion › Default Timeout | number | — | Default timeout for the AI Companion addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_AI_COMPANION_USER_AGENT` | Ai Companion › Default User Agent | string | — | Default User-Agent for the AI Companion addon. Supports `{version}`/`{random}` placeholders. |
Astream [#astream]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------- | ---------------------------- | ------ | ----------------------------------- | -------------------------------------------------------------------------------------------------- |
| `ASTREAM_URL` | Astream › URL | list | `["https://astream.stremiofr.com"]` | Upstream URL(s) for the AStream addon. |
| `DEFAULT_ASTREAM_TIMEOUT` | Astream › Default Timeout | number | — | Default timeout for the AStream addon (milliseconds). Falls back to the global default when unset. |
| `DEFAULT_ASTREAM_USER_AGENT` | Astream › Default User Agent | string | — | Default User-Agent for the AStream addon. Supports `{version}`/`{random}` placeholders. |
Built-ins [#built-ins]
Stremthru [#stremthru]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | --------------------------------------- | ------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `BUILTIN_STREMTHRU_URL` | Stremthru › URL | string | `https://stremthru.13377001.xyz` | Base URL of the StremThru instance used by the built-in addons. |
| `TORBOX_USENET_VIA_STREMTHRU` | Stremthru › Torbox Usenet Via Stremthru | boolean | `false` | Route Torbox usenet operations entirely through StremThru rather than the Torbox API. *(restart required)* |
Debrid [#debrid]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------------- | --------------------------------------- | -------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `BUILTIN_DEBRID_INSTANT_AVAILABILITY_CACHE_TTL` | Debrid › Instant Availability Cache TTL | duration | `1800` | Cache TTL for instant-availability checks. |
| `BUILTIN_DEBRID_PLAYBACK_LINK_CACHE_TTL` | Debrid › Playback Link Cache TTL | duration | `3600` | Cache TTL for resolved playback links. |
| `BUILTIN_DEBRID_ERROR_CACHE_TTL` | Debrid › Error Cache TTL | duration | `3600` | How long content-level failures (e.g. download status = failed/invalid) are cached globally to suppress retries. |
| `BUILTIN_DEBRID_LIBRARY_CACHE_TTL` | Debrid › Library Cache TTL | duration | `604800` | Cache TTL for library list results (listMagnets/listNzbs). |
| `BUILTIN_DEBRID_LIBRARY_STALE_THRESHOLD` | Debrid › Library Stale Threshold | duration | `600` | Time after which cached library data is treated as stale (background refresh while serving cached data). |
| `BUILTIN_DEBRID_LIBRARY_PAGE_LIMIT` | Debrid › Library Page Limit | number | `1` | Maximum pages fetched per listMagnets / listNzbs request. |
| `BUILTIN_DEBRID_LIBRARY_PAGE_SIZE` | Debrid › Library Page Size | number | `500` | Maximum items per page when listing library items. StremThru caps at 500, Torbox at 1000. |
| `BUILTIN_DEBRID_USE_TORRENT_DOWNLOAD_URL` | Debrid › Use Torrent Download URL | boolean | `true` | Prefer .torrent URLs over magnets for better private-tracker compatibility. |
| `BUILTIN_DEBRID_METADATA_STORE` | Debrid › Metadata Store | enum | — | Backend used to persist debrid metadata. Defaults to the platform-default when unset. *(restart required)* |
| `BUILTIN_DEBRID_FILEINFO_STORE` | Debrid › Fileinfo Store | json | `true` | Backend (or `true`/`false`) used for the debrid fileinfo store. *(restart required)* |
| `BUILTIN_PLAYBACK_LINK_VALIDITY` | Debrid › Playback Link Validity | duration | `86400` | How long a generated playback link is treated as valid (seconds). |
| `BUILTIN_DOWNLOAD_POLL_INTERVAL` | Debrid › Download Poll Interval | map | `{"nzbdav":2000,"altmount":2000,"stremthru_newz":2000,"*":10000}` | Per-service download-status poll interval. Env shape: `service:duration,...`. Wildcard `*` covers unlisted services. |
| `BUILTIN_DOWNLOAD_MAX_WAIT_TIME` | Debrid › Download Max Wait Time | map | `{"nzbdav":90000,"altmount":90000,"stremthru_newz":90000,"*":120000}` | Per-service maximum wait time before timing out a download check. Env shape: `service:duration,...`. |
Scrape [#scrape]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------------------- | --------------------------------------------- | ------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BUILTIN_SCRAPE_TITLE_LANGUAGES` | Scrape › Title Languages | map | — | Fine-grained alternative-title control, per indexer hostname, indexer name, or addon type. Format: `:[,...][,:...]`. Keys (checked in priority order): exact indexer hostname (e.g. `my-indexer.com`); auto-extracted indexer name (Jackett `/api/v2.0/indexers//...`, NZBHydra2 `?indexers=`); addon-id (`newznab`, `torznab`, `easynews`, `knaben`, `prowlarr`, `torrent-galaxy`); `*` wildcard fallback. Specs: `default` (primary/English-style title), `all` (all alternative titles up to BUILTIN\_SCRAPE\_TITLE\_LIMIT), `original` (TMDB original-language title), `scene` (scene-release titles from the scene mappings), `` (ISO 639-1 code, e.g. `de`, `fr`). Multiple specs under one key are combined (duplicates removed); only the highest-priority matching key applies; always falls back to the primary title. Examples: `*:default,original` — every indexer gets default + TMDB original-language title. `*:default,newznab:default,original,de` — newznab indexers query English + original + German, others English only. `*:default,germanindexer.com:de,default` — germanindexer.com queries German + English, all others English only. |
| `BUILTIN_SCRAPE_TITLE_LIMIT` | Scrape › Title Limit | number | `3` | Maximum alternative titles used per scrape. |
| `BUILTIN_SCRAPE_QUERY_CONCURRENCY` | Scrape › Query Concurrency | number | `5` | Maximum concurrent scrape queries. |
| `BUILTIN_SCRAPE_LATIN_QUERIES_ONLY` | Scrape › Latin Queries Only | boolean | `true` | Skip predominantly non-Latin-script titles (e.g. kanji) when building search queries, since scene releases are named in Latin script. Such titles are still used for matching results. Disable if you use native-language indexers (e.g. Cyrillic trackers). |
| `BUILTIN_SCRAPE_DATE_BASED_ENABLED` | Scrape › Date Based › Enabled | boolean | `true` | Detect date-based series (talk shows, WWE, game shows, soaps) and search/match their releases by episode air date instead of season/episode numbers. |
| `BUILTIN_SCRAPE_DATE_BASED_EPISODE_COUNT_THRESHOLD` | Scrape › Date Based › Episode Count Threshold | number | `40` | A season with at least this many episodes counts towards detecting a series as date-based. |
| `BUILTIN_SCRAPE_DATE_BASED_MIN_SEASONS` | Scrape › Date Based › Min Seasons | number | `4` | Minimum number of seasons required before the episode count threshold marks a series as date-based. Prevents long single-season dramas from being misdetected. |
| `BUILTIN_SCRAPE_ABSOLUTE_SEARCH_LANGUAGES` | Scrape › Absolute Search › Languages | list | `["ko","zh","ja","th"]` | Original languages (ISO 639-1) whose non-anime series also get absolute-episode search queries (e.g. Asian dramas released as "Show E40"). Requires a TMDB key for original language detection. Empty disables. |
Get Torrent [#get-torrent]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | ------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------- |
| `BUILTIN_GET_TORRENT_TIMEOUT` | Get Torrent › Timeout | number | `5000` | Timeout for fetching torrent files. |
| `BUILTIN_GET_TORRENT_CONCURRENCY` | Get Torrent › Concurrency | number | `100` | Maximum concurrent torrent fetches. |
| `BUILTIN_GET_TORRENT_LAZILY` | Get Torrent › Lazily | boolean | `true` | Fetch torrents lazily in the background. First search returns immediately with available results. |
Torrent [#torrent]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------------- | --------------------------------------------- | -------- | -------- | -------------------------------------------------------------------------------------------- |
| `BUILTIN_TORRENT_METADATA_CACHE_TTL` | Torrent › Metadata Cache TTL | duration | `604800` | Cache TTL for torrent metadata. |
| `BUILTIN_MINIMUM_BACKGROUND_REFRESH_INTERVAL` | Torrent › Minimum Background Refresh Interval | duration | `86400` | Minimum interval between background search-cache refreshes triggered during normal searches. |
Grab [#grab]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------------- | ------------------------------- | ---- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BUILTIN_NZB_GRAB_CACHE_BYTES` | Grab › NZB Cache Bytes | size | `64000000` | In-memory cache size for grabbed .nzb files. Accepts plain bytes or `64MB`-style strings. |
| `BUILTIN_NZB_GRAB_DISK_CACHE_BYTES` | Grab › NZB Disk Cache Bytes | size | `1000000000` | On-disk cache size for grabbed .nzb files (survives restarts). Set to `0` to disable the disk tier. Accepts plain bytes or `1GB`-style strings. |
| `BUILTIN_TORRENT_GRAB_CACHE_BYTES` | Grab › Torrent Cache Bytes | size | `64000000` | In-memory cache size for grabbed .torrent files. Accepts plain bytes or `64MB`-style strings. |
| `BUILTIN_TORRENT_GRAB_DISK_CACHE_BYTES` | Grab › Torrent Disk Cache Bytes | size | `512000000` | On-disk cache size for grabbed .torrent files (survives restarts). Set to `0` to disable the disk tier. Accepts plain bytes or `512MB`-style strings. |
GDrive [#gdrive]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | ------------------------ | ------ | --------- | ------------------------------------------------------------------- |
| `BUILTIN_GDRIVE_CLIENT_ID` | GDrive › Client ID | string | — | OAuth client ID for the Google Drive built-in addon. |
| `BUILTIN_GDRIVE_CLIENT_SECRET` | GDrive › Client Secret | string | *(unset)* | OAuth client secret for the Google Drive built-in addon. *(secret)* |
| `BUILTIN_GDRIVE_TIMEOUT` | GDrive › Timeout | number | — | Timeout for Google Drive requests. |
| `BUILTIN_GDRIVE_USER_AGENT` | GDrive › User Agent | string | — | User-Agent for Google Drive requests. |
| `BUILTIN_GDRIVE_PAGE_SIZE_LIMIT` | GDrive › Page Size Limit | number | `1000` | Maximum items per page from Google Drive API. |
Torbox Search [#torbox-search]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------------------- | -------------------------------------------- | -------- | --------- | --------------------------------------------------------------------------------- |
| `BUILTIN_TORBOX_SEARCH_TIMEOUT` | Torbox Search › Timeout | number | — | Timeout for Torbox Search requests. |
| `BUILTIN_TORBOX_SEARCH_USER_AGENT` | Torbox Search › User Agent | string | — | User-Agent for Torbox Search requests. |
| `BUILTIN_TORBOX_SEARCH_SEARCH_API_TIMEOUT` | Torbox Search › Search API Timeout | number | `30000` | Timeout for the Torbox /search API. |
| `BUILTIN_TORBOX_SEARCH_SEARCH_API_CACHE_TTL` | Torbox Search › Search API Cache TTL | duration | `604800` | Cache TTL for /search responses. |
| `BUILTIN_TORBOX_SEARCH_METADATA_CACHE_TTL` | Torbox Search › Metadata Cache TTL | duration | `1209600` | Cache TTL for Torbox Search metadata. |
| `BUILTIN_TORBOX_SEARCH_CACHE_PER_USER_SEARCH_ENGINE` | Torbox Search › Cache Per User Search Engine | boolean | `false` | Cache search results separately per user when they bring their own search engine. |
NAB [#nab]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ----------------------------------- | -------- | ------------------------------- | ---------------------------------------------------------------- |
| `BUILTIN_NAB_SEARCH_TIMEOUT` | NAB › Search Timeout | number | `30000` | Timeout for Newznab/Torznab search calls. |
| `BUILTIN_NAB_SEARCH_CACHE_TTL` | NAB › Search Cache TTL | duration | `604800` | Cache TTL for Newznab/Torznab search results. |
| `BUILTIN_NAB_CAPABILITIES_CACHE_TTL` | NAB › Capabilities Cache TTL | duration | `1209600` | Cache TTL for Newznab/Torznab capabilities responses. |
| `BUILTIN_NAB_MAX_PAGES` | NAB › Max Pages | number | `5` | Maximum pages to fetch when paginating Newznab/Torznab results. |
| `ZYCLOPS_HEALTH_PROXY_ENDPOINT` | NAB › Zyclops Health Proxy Endpoint | string | `https://zyclops.elfhosted.com` | Base URL of the Zyclops health proxy used by the Newznab preset. |
Zilean [#zilean]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | ---------------- | ------ | --------------------------------------------- | --------------------------------------- |
| `BUILTIN_ZILEAN_URL` | Zilean › URL | string | `https://zileanfortheweebs.midnightignite.me` | Base URL for the Zilean built-in addon. |
| `BUILTIN_DEFAULT_ZILEAN_TIMEOUT` | Zilean › Timeout | number | — | Timeout for Zilean requests. |
Animetosho [#animetosho]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | -------------------- | ------ | ----------------------------- | ------------------------------------------- |
| `BUILTIN_ANIMETOSHO_URL` | Animetosho › URL | string | `https://feed.animetosho.org` | Base URL for the AnimeTosho built-in addon. |
| `BUILTIN_DEFAULT_ANIMETOSHO_TIMEOUT` | Animetosho › Timeout | number | — | Timeout for AnimeTosho requests. |
Nekobt [#nekobt]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | ---------------- | ------ | ------------------------------- | --------------------------------------- |
| `BUILTIN_NEKOBT_URL` | Nekobt › URL | string | `https://nekobt.to/api/torznab` | Base URL for the NekoBT built-in addon. |
| `BUILTIN_DEFAULT_NEKOBT_TIMEOUT` | Nekobt › Timeout | number | — | Timeout for NekoBT requests. |
Seadex [#seadex]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------- | --------------------------------- | -------- | ---------------------- | --------------------------------------------------------------- |
| `BUILTIN_SEADEX_URL` | Seadex › URL | string | `https://releases.moe` | Base URL for the SeaDex built-in addon. |
| `BUILTIN_SEADEX_DATASET_REFRESH_INTERVAL` | Seadex › Dataset Refresh Interval | duration | `86400` | How often the SeaDex dataset is refreshed. *(restart required)* |
Bitmagnet [#bitmagnet]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ------------------- | ------ | ------- | ------------------------------------------ |
| `BUILTIN_BITMAGNET_URL` | Bitmagnet › URL | string | — | Base URL for the Bitmagnet built-in addon. |
| `BUILTIN_DEFAULT_BITMAGNET_TIMEOUT` | Bitmagnet › Timeout | number | — | Timeout for Bitmagnet requests. |
Jackett [#jackett]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------------------- | ----------------- | ------ | --------- | -------------------------------------------------- |
| `BUILTIN_JACKETT_URL` | Jackett › URL | string | — | Base URL for the Jackett built-in addon. |
| `BUILTIN_JACKETT_API_KEY` | Jackett › API Key | string | *(unset)* | API key for the Jackett built-in addon. *(secret)* |
| `BUILTIN_DEFAULT_JACKETT_TIMEOUT` | Jackett › Timeout | number | — | Timeout for Jackett requests. |
Nzbhydra [#nzbhydra]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ------------------ | ------ | --------- | --------------------------------------------------- |
| `BUILTIN_NZBHYDRA_URL` | Nzbhydra › URL | string | — | Base URL for the NZBHydra built-in addon. |
| `BUILTIN_NZBHYDRA_API_KEY` | Nzbhydra › API Key | string | *(unset)* | API key for the NZBHydra built-in addon. *(secret)* |
| `BUILTIN_DEFAULT_NZBHYDRA_TIMEOUT` | Nzbhydra › Timeout | number | — | Timeout for NZBHydra requests. |
Prowlarr [#prowlarr]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | ----------------------------- | -------- | --------- | --------------------------------------------------- |
| `BUILTIN_PROWLARR_URL` | Prowlarr › URL | string | — | Base URL for the Prowlarr built-in addon. |
| `BUILTIN_PROWLARR_API_KEY` | Prowlarr › API Key | string | *(unset)* | API key for the Prowlarr built-in addon. *(secret)* |
| `BUILTIN_PROWLARR_INDEXERS` | Prowlarr › Indexers | list | — | Comma-separated list of Prowlarr indexers to query. |
| `BUILTIN_DEFAULT_PROWLARR_TIMEOUT` | Prowlarr › Default Timeout | number | — | Default timeout for Prowlarr requests. |
| `BUILTIN_PROWLARR_SEARCH_TIMEOUT` | Prowlarr › Search Timeout | number | `30000` | Timeout for Prowlarr search requests. |
| `BUILTIN_PROWLARR_SEARCH_CACHE_TTL` | Prowlarr › Search Cache TTL | duration | `604800` | Cache TTL for Prowlarr search results. |
| `BUILTIN_PROWLARR_INDEXERS_CACHE_TTL` | Prowlarr › Indexers Cache TTL | duration | `1209600` | Cache TTL for the Prowlarr indexers list. |
Knaben [#knaben]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | -------------------------- | -------- | -------- | ---------------------------------------------------------------------------------- |
| `BUILTIN_DEFAULT_KNABEN_TIMEOUT` | Knaben › Default Timeout | number | — | Default timeout for Knaben requests. |
| `BUILTIN_KNABEN_SEARCH_TIMEOUT` | Knaben › Search Timeout | number | `30000` | Timeout for Knaben search requests. |
| `BUILTIN_KNABEN_DOWNLOAD_TORRENTS` | Knaben › Download Torrents | boolean | `true` | When true, attempt to fetch .torrent files for Knaben results without an infohash. |
| `BUILTIN_KNABEN_SEARCH_CACHE_TTL` | Knaben › Search Cache TTL | duration | `604800` | Cache TTL for Knaben search results. |
Easynews [#easynews-1]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | --------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------- |
| `BUILTIN_EASYNEWS_SEARCH_TIMEOUT` | Easynews › Search Timeout | number | `30000` | Timeout for Easynews search requests. |
| `BUILTIN_EASYNEWS_SEARCH_CACHE_TTL` | Easynews › Search Cache TTL | duration | `3600` | Cache TTL for Easynews search results. Defaults to 1h since Easynews content rotates more frequently. |
| `BUILTIN_EASYNEWS_SEARCH_MAX_PAGES` | Easynews › Max Pages | number | `8` | Maximum pages fetched when paginating Easynews search results. |
Torrent Galaxy [#torrent-galaxy]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------- | --------------------------------- | -------- | --------------------------- | ------------------------------------------------------------- |
| `BUILTIN_TORRENT_GALAXY_URL` | Torrent Galaxy › URL | string | `https://torrentgalaxy.one` | Base URL for the Torrent Galaxy built-in addon. |
| `BUILTIN_DEFAULT_TORRENT_GALAXY_TIMEOUT` | Torrent Galaxy › Default Timeout | number | — | Default timeout for Torrent Galaxy requests. |
| `BUILTIN_TORRENT_GALAXY_SEARCH_TIMEOUT` | Torrent Galaxy › Search Timeout | number | `30000` | Timeout for Torrent Galaxy search requests. |
| `BUILTIN_TORRENT_GALAXY_SEARCH_CACHE_TTL` | Torrent Galaxy › Search Cache TTL | duration | `604800` | Cache TTL for Torrent Galaxy search results. |
| `BUILTIN_TORRENT_GALAXY_PAGE_LIMIT` | Torrent Galaxy › Page Limit | number | `5` | Maximum pages fetched when paginating Torrent Galaxy results. |
Eztv [#eztv]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ----------------------- | -------- | ------------------ | --------------------------------------------------- |
| `BUILTIN_EZTV_URL` | Eztv › URL | string | `https://eztvx.to` | Base URL for the EZTV built-in addon. |
| `BUILTIN_DEFAULT_EZTV_TIMEOUT` | Eztv › Default Timeout | number | — | Default timeout for EZTV requests. |
| `BUILTIN_EZTV_SEARCH_TIMEOUT` | Eztv › Search Timeout | number | `30000` | Timeout for EZTV search requests. |
| `BUILTIN_EZTV_SEARCH_CACHE_TTL` | Eztv › Search Cache TTL | duration | `604800` | Cache TTL for EZTV search results. |
| `BUILTIN_EZTV_MAX_PAGES` | Eztv › Max Pages | number | `5` | Maximum pages fetched when paginating EZTV results. |
Posters [#posters]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | -------------------------- | ------ | -------- | --------------------------------------------------------------------- |
| `POSTER_API_KEY_VALIDITY_CACHE_TTL` | API Key Validity Cache TTL | number | `604800` | How long an RPDB / poster-API key validity check is cached (seconds). |
Resources [#resources]
| Environment Variable | UI Setting | Type | Default | Description |
| --------------------- | ------------------- | ---- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `STREAM_URL_MAPPINGS` | Stream URL Mappings | map | — | Origin-level rewrites applied to stream URLs returned to clients. JSON object of `{origin: replacement}` URLs. |
Timeouts [#timeouts]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------- | ----------------------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------- |
| `MANIFEST_TIMEOUT` | Timeouts › Manifest | number | `3000` | Timeout for `/manifest.json` fetches (milliseconds). Slower manifest operations use the increased timeout below. |
| `MANIFEST_INCREASED_TIMEOUT` | Timeouts › Manifest Increased | number | `10000` | Extended timeout used during slower manifest operations (milliseconds). |
| `META_TIMEOUT` | Timeouts › Meta | number | `30000` | Timeout for `/meta` requests (milliseconds). |
| `CATALOG_TIMEOUT` | Timeouts › Catalog | number | `30000` | Timeout for `/catalog/*` fetches (milliseconds). |
Precache [#precache]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------ | ------------------------------------ | -------- | ------- | --------------------------------------------------------------------------------------------------- |
| `PRECACHE_NEXT_EPISODE_MIN_INTERVAL` | Precache › Next Episode Min Interval | duration | `86400` | Minimum interval before re-attempting to precache the same next episode (accepts e.g. "30m", "1h"). |
Preload [#preload]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------- | ----------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------- |
| `PRELOAD_MIN_INTERVAL` | Preload › Min Interval | duration | `3600` | Minimum interval between preload operations for the same item per user (0 disables the cooldown). |
| `PRELOAD_STREAMS_CONCURRENCY` | Preload › Streams Concurrency | number | `5` | Maximum simultaneous stream preload requests. |
Background [#background]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------------- | --------------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BACKGROUND_RESOURCE_REQUESTS_ENABLED` | Background › Enabled | boolean | `true` | Issue resource requests in the background to keep caches warm. |
| `BACKGROUND_RESOURCE_REQUEST_TIMEOUT` | Background › Timeout | number | — | Timeout for background resource requests (milliseconds). When unset, the maximum HTTP timeout is used. |
| `MAX_BACKGROUND_RESOURCE_REQUESTS` | Background › Max Concurrent | number | `50` | Maximum resource requests allowed to keep running after the client has already timed out. Beyond this, an abandoned request is cancelled instead of being left to warm the cache, which bounds how many connections a client that retries without backoff can tie up. |
Cache [#cache]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------ | -------------------------------- | ------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT_MAX_CACHE_SIZE` | Cache › Default Max Size | number | `100000` | Default maximum number of items per cache instance. *(restart required)* |
| `SQL_CACHE_MAX_SIZE` | Cache › Sql Max Size | number | `100000` | Maximum number of items in the shared SQL cache. *(restart required)* |
| `MAX_CACHE_VALUE_BYTES` | Cache › Max Value Bytes | size | `2000000` | Largest single value written to the Redis or SQL cache (`0` disables the limit). Oversized entries are skipped rather than stored. A skipped entry is recomputed on each request, so raising this trades memory for CPU. Accepts plain bytes or `2MB`-style strings. |
| `MANIFEST_CACHE_TTL` | Cache › Manifest › TTL | map | `{"*":21600}` | Per-key cache TTL for manifest responses (seconds; -1 disables). Env shape: integer or `key:value,...`. |
| `MANIFEST_CACHE_MAX_SIZE` | Cache › Manifest › Max Size | number | — | Maximum number of cached manifests. *(restart required)* |
| `MANIFEST_FAILURE_CACHE_TTL` | Cache › Manifest › Failure TTL | number | `60` | How long a failed manifest fetch is remembered (seconds; 0 disables). Without this, an addon whose manifest has never succeeded is re-fetched on every request and costs a full manifest timeout each time. A successful fetch clears the entry immediately, and saving a configuration always re-checks. |
| `SUBTITLE_CACHE_TTL` | Cache › Subtitle › TTL | map | `{"*":300}` | Per-key cache TTL for subtitle responses (seconds; -1 disables). |
| `SUBTITLE_CACHE_MAX_SIZE` | Cache › Subtitle › Max Size | number | — | Maximum number of cached subtitle responses. *(restart required)* |
| `STREAM_CACHE_TTL` | Cache › Stream › TTL | map | `{"*":-1}` | Per-key cache TTL for stream responses (seconds; -1 disables, the default). |
| `STREAM_CACHE_MAX_SIZE` | Cache › Stream › Max Size | number | — | Maximum number of cached stream responses. *(restart required)* |
| `PIPELINE_CACHE_TTL` | Cache › Pipeline › TTL | number | `0` | TTL for the full-pipeline result cache (seconds). Caches the final processed response (streams, statistics and errors) for a whole request, keyed per user. Unlike the per-addon stream cache, this returns the previous response verbatim, including any partial errors, and does not re-fetch or retry failed addons within the TTL. Set to 0 to disable. |
| `PIPELINE_CACHE_MAX_SIZE` | Cache › Pipeline › Max Size | number | `1000` | Maximum number of cached pipeline results. *(restart required)* |
| `CATALOG_CACHE_TTL` | Cache › Catalog › TTL | map | `{"*":300}` | Per-key cache TTL for catalog responses (seconds; -1 disables). |
| `CATALOG_CACHE_MAX_SIZE` | Cache › Catalog › Max Size | number | `1000` | Maximum number of cached catalog responses. *(restart required)* |
| `META_CACHE_TTL` | Cache › Meta › TTL | map | `{"*":300}` | Per-key cache TTL for meta responses (seconds; -1 disables). |
| `META_CACHE_MAX_SIZE` | Cache › Meta › Max Size | number | — | Maximum number of cached meta responses. *(restart required)* |
| `ADDON_CATALOG_CACHE_TTL` | Cache › Addon Catalog › TTL | map | `{"*":300}` | Per-key cache TTL for addon-catalog responses (seconds; -1 disables). |
| `ADDON_CATALOG_CACHE_MAX_SIZE` | Cache › Addon Catalog › Max Size | number | — | Maximum number of cached addon-catalog responses. *(restart required)* |
Limits [#limits]
User Limits [#user-limits]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ----------------------------- | ------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `MAX_ADDONS` | Max Addons | number | `15` | Maximum number of addons a user configuration can install. |
| `MAX_KEYWORD_FILTERS` | Max Keyword Filters | number | `30` | Maximum number of keyword filters per keyword filter group. |
| `MAX_FORMATTER_TEMPLATE_LENGTH` | Max Formatter Template Length | number | `5000` | Maximum length (characters) of a single formatter template string. Enforced during config validation. |
| `MAX_FAILOVER_ATTEMPTS` | Max Failover Attempts | number | `5` | Maximum total failover attempts (after de-duplication) a user can configure. |
| `MAX_PARALLEL_ATTEMPTS` | Max Parallel Attempts | number | `2` | Maximum concurrent failover attempts a user can configure. Caps load on upstream providers. |
| `MAX_GROUPS` | Max Groups | number | `20` | Maximum number of stream groups in a user configuration. |
| `MAX_MERGED_CATALOG_SOURCES` | Max Merged Catalog Sources | number | `10` | Maximum source catalogs in a single merged catalog. |
| `MAX_BACKGROUND_PINGS` | Max Background Pings | number | `2` | Maximum streams pinged in a background preload/precache operation. |
Timeouts [#timeouts-1]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | ---------------------- | ------ | ------- | ------------------------------------------------------------------ |
| `MIN_TIMEOUT` | Timeouts › Min Timeout | number | `1000` | Lower bound (milliseconds) for any user-configurable HTTP timeout. |
| `MAX_TIMEOUT` | Timeouts › Max Timeout | number | `50000` | Upper bound (milliseconds) for any user-configurable HTTP timeout. |
Regex [#regex]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------------- | ---------------------------- | ------ | --------- | ---------------------------------------------------------------------------- |
| `REGEX_FILTER_ACCESS` | Regex › Access | enum | `trusted` | Who can use regex filters: "none", "trusted" (only trusted UUIDs), or "all". |
| `WHITELISTED_REGEX_PATTERNS` | Regex › Patterns | list | — | Regex patterns whitelisted for non-trusted users. |
| `WHITELISTED_REGEX_PATTERNS_URLS` | Regex › Patterns Urls | list | — | URLs from which to fetch additional whitelisted regex patterns periodically. |
| `WHITELISTED_REGEX_PATTERNS_DESCRIPTION` | Regex › Patterns Description | string | — | Free-form description shown alongside the whitelisted regex patterns. |
SEL [#sel]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------------- | ------------------------------- | ------ | --------- | ---------------------------------------------------------------------------------------------------- |
| `SEL_SYNC_ACCESS` | SEL › Access | enum | `trusted` | "all" = anyone can sync from any URL; "trusted" = non-trusted users limited to whitelisted SEL URLs. |
| `WHITELISTED_SEL_URLS` | SEL › Urls | list | — | Stream Expression Language sync URLs that non-trusted users may use. |
| `MAX_STREAM_EXPRESSIONS` | SEL › Max Expressions | number | `200` | Maximum total stream expressions across all filter types. |
| `MAX_STREAM_EXPRESSIONS_TOTAL_CHARACTERS` | SEL › Max Expression Characters | number | `50000` | Maximum total character count across all stream expressions. |
| `MAX_SEL_LENGTH` | SEL › Max Expression Length | number | `3000` | Maximum length (characters) of a single stream expression. Enforced during config validation. |
Variants [#variants]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------- | -------------------------------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `VARIANT_ACCESS` | Variants › Access | enum | `all` | Who may define config variants. "all" = everyone, "trusted" = trusted users only, "none" = the feature is disabled. |
| `MAX_VARIANTS` | Variants › Max | number | `10` | Maximum number of config variants a user may define. |
| `MAX_VARIANT_SCRIPT_LENGTH` | Variants › Max Script Length | number | `4000` | Maximum length (characters) of a single variant script. Enforced during config validation. |
| `MAX_VARIANT_TOTAL_SCRIPT_CHARACTERS` | Variants › Max Total Script Characters | number | `20000` | Maximum total character count across every variant script in a configuration. |
| `MAX_VARIANT_INSTRUCTIONS` | Variants › Max Instructions | number | `100` | Maximum number of instructions in a single variant script. |
| `MAX_ACTIVE_VARIANTS` | Variants › Max Active | number | `4` | Maximum number of variants that may be combined on a single request. |
| `MAX_VARIANT_DEPTH` | Variants › Max Depth | number | `5` | Maximum depth a variant may nest others through "use variant". |
| `MAX_VARIANT_PATH_MATCHES` | Variants › Max Path Matches | number | `200` | Maximum number of places a single variant instruction may write to. |
| `MAX_VARIANT_VALUE_DEPTH` | Variants › Max Value Depth | number | `10` | Maximum nesting depth of an object or array literal in a variant script. |
| `MAX_VARIANT_PATH_SEGMENTS` | Variants › Max Path Segments | number | `12` | Maximum number of segments in a single variant instruction path. |
Sync [#sync]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------------------- | ----------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `WHITELISTED_SYNC_REFRESH_INTERVAL` | Sync › Refresh Interval | duration | `86400` | How often whitelisted regex/SEL sync URLs are refreshed (accepts e.g. "5m", "1h"). *(restart required)* |
Disabled [#disabled]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------- | ------------------------- | ---- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `DISABLED_ADDONS` | Disabled › Addons | map | — | Map of disabled addon IDs to a reason. Env-supplied form: comma-separated "addon:reason" entries. |
| `REMOVED_ADDONS` | Disabled › Removed Addons | map | — | Map of removed addon IDs (hidden from marketplace; errors on save) to a reason. Env-supplied form: comma-separated "addon:reason" entries. |
| `DISABLED_SERVICES` | Disabled › Services | map | — | Map of disabled service IDs to a reason. Env-supplied form: comma-separated "service:reason" entries. |
| `DISABLED_HOSTS` | Disabled › Hosts | map | — | Map of disabled hostnames to a reason. Env-supplied form: comma-separated "host:reason" entries. |
| `DISABLED_STREAM_TYPES` | Disabled › Stream Types | list | — | Stream types that should never be returned to clients (e.g. p2p, http, live). |
Self Scraping [#self-scraping]
| Environment Variable | UI Setting | Type | Default | Description |
| ----------------------- | ------------------------ | ------- | ------- | ------------------------------------------------------------- |
| `DISABLE_SELF_SCRAPING` | Self Scraping › Disabled | boolean | `true` | When true, addons cannot scrape the same AIOStreams instance. |
Trusted [#trusted]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | --------------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `TRUSTED_UUIDS` | Trusted › Uuids | string | — | Comma-separated list of trusted user UUIDs. Trusted users may use regex filters and bypass certain access policies. |
Recursion [#recursion]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------- | ---------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `RECURSION_THRESHOLD_LIMIT` | Threshold Limit | number | `60` | Maximum number of requests to the same URL within the threshold window before marking the chain as recursive. |
| `RECURSION_THRESHOLD_WINDOW` | Threshold Window | duration | `10` | Time window for the recursion threshold (seconds; accepts e.g. "30s", "1m"). |
Tasks [#tasks]
Pruning [#pruning]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------ |
| `PRUNE_INTERVAL` | Pruning › Interval | duration | `86400` | How often to run the inactive-user pruning task (accepts e.g. "12h", "1d"). *(restart required)* |
| `PRUNE_MAX_DAYS` | Pruning › Max Days | number | `-1` | Days of inactivity before a user is pruned. Use -1 to disable pruning entirely. |
Other [#other]
Analytics [#analytics]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | ---------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `ANALYTICS_ENABLED` | Enabled | boolean | `true` | When false, all request analytics collection is disabled — zero events are written. |
| `USER_ANALYTICS_ENABLED` | User Analytics Enabled | boolean | `false` | When enabled, the configure-page "Stats" tab is available to every authenticated user. Adds one event per addon per stream request. |
| `ANALYTICS_EVENT_RETENTION_DAYS` | Event Retention Days | number | `7` | How many days of raw per-request analytics events to keep before the rollup task prunes them. The per-user Stats tab is limited to this window. |
| `ANALYTICS_DAILY_RETENTION_DAYS` | Daily Retention Days | number | `90` | How many days of aggregated daily analytics to keep. |
Release Blocklist [#release-blocklist]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------------------ | ---------------------- | ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RELEASE_BLOCKLIST_QUORUM` | Quorum | number | `2` | How many **corroborate**-trust sources must flag the same release before their shared verdict filters it. **full**-trust sources always filter on their own. If you have fewer corroborate sources than this number, their verdicts never filter anything. |
| `RELEASE_BLOCKLIST_BACKBONE_SCOPE` | Backbone Scope | enum | `overlap` | How shared dead-release verdicts are matched against the backbones your usenet providers use. A release missing on one backbone can still be intact on another, so **overlap** applies a verdict observed on at least one backbone you use, **covers** only applies one observed on every backbone you use, and **off** skips the check and applies every verdict. Verdicts recorded by this instance itself always apply. |
| `RELEASE_BLOCKLIST_BACKBONE_GROUPING` | Backbone Grouping | enum | `backbone` | How provider hosts are compared during the backbone scope check. **backbone** treats every provider reselling the same backbone as one group, which fits takedowns: those remove an article from the shared storage for all of them at once. **domain** only matches verdicts recorded on the same provider domain. Backbones enforce retention per reseller, so an old article can be expired on a short-retention brand yet intact on a full-retention one riding the same backbone; **domain** stops such verdicts from crossing brands, at the cost of far fewer shared verdicts applying to you. Backbone ids in the trusted backbones list keep working in either mode. |
| `RELEASE_BLOCKLIST_TRUSTED_BACKBONES` | Trusted Backbones | list | — | Extra backbones, as root domains or backbone ids (e.g. **omicron**), whose verdicts you accept as if your own providers used them. Useful when your providers are on a small or unrecognized backbone, where shared verdicts would otherwise never pass the backbone scope check. |
| `RELEASE_BLOCKLIST_PUBLIC_EXPORT` | Public Export | boolean | `false` | Serve this instance's blocklist at `/blocklist/export` so other instances can subscribe to it. The list contains only release digests and backbone root domains. |
| `RELEASE_BLOCKLIST_PUBLIC_EXPORT_SCOPE` | Public Export Scope | enum | `local` | The most the public export is allowed to serve. With **local**, subscribers only ever receive verdicts this instance recorded first-hand, even if they ask for more. **all** additionally allows `?scope=all` requests, which also serve everything collected from your subscribed and imported lists, including ones you do not trust yourself, so enable it only if you mean to re-publish them. |
| `RELEASE_BLOCKLIST_PUBLIC_EXPORT_PASSWORD` | Public Export Password | string | *(unset)* | When set, the public export requires `?key=` in the URL. Share the full URL with instances you want to allow; a missing or wrong key gets the same 404 as a disabled export, so the endpoint stays invisible. *(secret)* |
Streams [#streams]
| Environment Variable | UI Setting | Type | Default | Description |
| -------------------------------- | ---------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `STREAMS_HISTORY_RETENTION_DAYS` | History Retention Days | number | `7` | How long finished streams stay on the dashboard history list. Bandwidth totals are kept separately and are not affected by this. |
| `STREAMS_CLIENT_IP_RECORDING` | Client IP Recording | enum | `full` | How much of a viewer's IP address is kept. `full` shows the whole address on the dashboard and in stream history, `prefix` keeps only the network part (`203.0.113.x`, `2001:db8:1::/48`), and `none` records nothing. Server logs always carry the full address whatever this is set to. |
| `STREAMS_SESSION_IDLE_TIMEOUT` | Session Idle Timeout | duration | `90` | How long a stream may go without serving bytes before it is treated as finished and moved to history. Seeking and buffering pauses are shorter than this, so they keep the same entry; resuming after it counts as a new watch. Bandwidth totals are unaffected either way. |
| `STREAMS_CONNECTION_LIMITS` | Connection Limits | map | — | How many streams may run at once, counted across the built-in proxy and the usenet engine together. Use `**` for one pool everyone shares, `*` for what each unlisted user gets individually, or a username for that user alone. `0` means unlimited. |
Bandwidth [#bandwidth]
| Environment Variable | UI Setting | Type | Default | Description |
| ------------------------------- | ----------------------- | ------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `STREAMS_BANDWIDTH_PERIOD_MODE` | Bandwidth › Period Mode | enum | `rolling` | How the 30-day bandwidth figure (and any limits below) are measured. `rolling` counts the last 30 days at all times. `monthly` counts from a fixed reset day each month, matching how most providers meter a data cap. |
| `STREAMS_BANDWIDTH_RESET_DAY` | Bandwidth › Reset Day | number | `1` | Day of the month the bandwidth period restarts when the period is set to `monthly`. Capped at 28 so every month has one. |
| `STREAMS_BANDWIDTH_LIMITS` | Bandwidth › Limits | map | — | How many bytes may be served per accounting period. Use `**` for one pool everyone shares, `*` for what each unlisted user gets individually, or a username for that user alone. Sizes accept units (e.g. `500GB`); `0` means unlimited. Reaching a limit stops the streams it covers and refuses new ones. Only bytes served through AIOStreams count, so direct, unproxied links are invisible to it. |
Usenet [#usenet]
| Environment Variable | UI Setting | Type | Default | Description |
| ---------------------------------- | ------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `USENET_PROVIDERS` | Providers | json | *(unset)* | JSON array of NNTP provider objects: \{ id, name?, host, port, tls, tlsSkipVerify?, username?, password?, maxConnections, priority, isBackup?, enabled? }. *(secret)* |
| `USENET_PERFORMANCE_PROFILE` | Performance Profile | enum | `balanced` | How hard the engine works. **balanced** (the default) is right for most setups. **high** downloads more aggressively — best with a fast connection and a powerful machine. **conservative** uses less memory and CPU — best for small servers and NAS boxes. **custom** lets you tune the individual values below yourself. |
| `USENET_MAX_CONCURRENT_DOWNLOADS` | Max Concurrent Downloads | number | `0` | The most download requests the engine will run at the same time, across everything it does. **0** (the default) works this out automatically from your providers’ connection limits — leave it there unless AIOStreams is putting too much load on the machine it runs on, in which case set a lower number. |
| `USENET_MAX_CONCURRENT_INSPECTS` | Max Concurrent Inspects | number | `4` | How many NZB imports may run at the same time. Extra imports wait in a queue, and playback-triggered imports are served before background adds. **0** removes the limit. |
| `USENET_PREFETCH_SEGMENTS` | Prefetch Segments | number | `32` | How many pieces of the file each stream downloads ahead of the current playback position. Higher values give faster, smoother streaming on a good connection, but use more memory per stream. |
| `USENET_STREAMING_PRIORITY` | Streaming Priority | number | `0.8` | How strongly active playback is favoured over background work (like imports and health checks) when both want to download at once, from 0 to 1. **0.8** (the default) keeps playback smooth while background work still makes progress; **1** means playback always goes first. |
| `USENET_SEGMENT_DISK_CACHE_BYTES` | Segment Disk Cache Bytes | size | `2000000000` | How much disk space to use for keeping recently downloaded data. The cache survives restarts and makes seeking and re-watching faster. Set to **0** to disable it. |
| `USENET_SEGMENT_TIMEOUT` | Segment Timeout | duration | `30` | How long to wait for one piece of a download before giving up on it and retrying elsewhere. Set to **0** to never give up on a piece that is still downloading. |
| `USENET_SEGMENT_STALL_TIMEOUT` | Segment Stall Timeout | duration | `30` | How long to wait with no data arriving for a piece before dropping the connection and retrying. This catches connections that have hung or gone dead. |
| `USENET_DIAL_TIMEOUT` | Dial Timeout | duration | `15` | How long to wait when opening a connection to a provider before giving up. |
| `USENET_IDLE_CONNECTION` | Idle Connection | duration | `60` | How long to keep unused provider connections open. Keeping them around for a little while makes the next request start faster. |
| `USENET_STREAM_IDLE_TIMEOUT` | Stream Idle Timeout | duration | `3600` | Close a playback stream that has sent no data for this long, so abandoned connections cannot hold provider connections and memory forever. A paused player simply reconnects when it resumes. Set to **0** to disable. |
| `USENET_CIRCUIT_BREAKER_THRESHOLD` | Circuit Breaker Threshold | number | `5` | How many times in a row a provider can fail before the engine temporarily stops using it. |
| `USENET_CIRCUIT_BREAKER_COOLDOWN` | Circuit Breaker Cooldown | duration | `30` | How long a failing provider is rested before the engine tries it again. |
| `USENET_LAZY_RAR_RESOLUTION` | Lazy Rar Resolution | boolean | `true` | Makes importing large multi-part RAR releases (like season packs) much faster by reading some archive details on demand during playback instead of all up front. Leave this on unless you are troubleshooting a release that will not play. |
| `USENET_STRICT_ARCHIVE_MEMBERSHIP` | Strict Archive Membership | boolean | `false` | Some releases hide their real file names (“obfuscated” posts). Turning this on makes the engine identify every part of such split archives individually, which fixes rare cases of parts being matched up wrongly — at the cost of slower imports for those releases. Leave off unless an obfuscated release imports broken. |
| `USENET_VERIFY_MODE` | Verify Mode | enum | `census` | Whether to check that a release is actually complete on your providers when it is imported. **census** (the default) checks every part of the download without slowing the import down — badly damaged releases are rejected straight away, and slightly damaged ones are handled by the damage policy below. **none** skips the check; broken releases will then only fail once you try to play them. |
| `USENET_VERIFY_BUDGET_MS` | Verify Budget Ms | number | `0` | Extra time (in milliseconds) an import may spend waiting on the completeness check before finishing. **0** (the default) never delays imports — the check simply carries on in the background. Raise it to catch more damage before a stream is offered, at the cost of slower imports. |
| `USENET_CENSUS_SHADOW_CONCURRENCY` | Census Shadow Concurrency | number | `12` | How many checks run at the same time when a completeness check carries on in the background after an import. Lower is gentler on your provider connections while you are streaming; higher reaches the final verdict sooner. |
| `USENET_CENSUS_MAX_LIFETIME` | Census Max Lifetime | duration | `1800` | The longest a completeness check may keep running before it is stopped. Raise this if checks on very large releases are being cut off before they finish. |
| `USENET_DAMAGE_POLICY` | Damage Policy | enum | `strict` | What to do with slightly damaged releases (a few missing pieces). **strict** (the default) rejects them when the damage is caught in time, so another release is picked instead; damage that only shows up during playback is glitched over, and the release is not offered again afterwards. **tolerant** keeps damaged releases on offer, glitching over the gaps on every play. Heavy damage is always rejected and will stop playback if it only turns up mid-play. |
| `USENET_MATROSKA_HOLE_FILL` | Matroska Hole Fill | boolean | `true` | When part of an MKV release is missing on every provider, rewrite the gap into valid Matroska padding instead of raw zeros, so players skip the damaged seconds instead of stopping playback with an error. |
| `USENET_MAX_NZB_SIZE` | Max NZB Size | size | `150000000` | The largest NZB file the engine will accept — whether uploaded in the dashboard, grabbed from an indexer, or sent through the SABnzbd API. Raise it if large season packs are being rejected as too big. |
| `USENET_SABNZBD_API_ENABLED` | Sabnzbd API Enabled | boolean | `true` | Lets apps like Sonarr, Radarr and Prowlarr send downloads to AIOStreams as if it were a SABnzbd download client. Point them at `/api/v1/sabnzbd`, with an `AIOSTREAMS_AUTH` credential in `username:password` form as the API key. |
{/* END GENERATED ENV REFERENCE */}
# Configure Options
Description [#description]
This is the guide for v2 of AIOStreams, looking for the guide for v1? You can
find it [here](/migrations/v1-to-v2).
This addon lets you combine all of your addons into one and provide consistent formatting, sorting, and filtering of the results.
It is immensely configurable, allowing you to fine tune your catalogues too.
I will attempt to explain every single option available.
Configuration
Torrentio is disabled on the public ElfHosted instance.
To get started, you'll want to head to the configuration page by clicking the "Configure" button above. Then, you'll
need to click the big white `Configure` button on AIOStreams' configuration page.
Services
This is where you can enter credentials for, reorder, and toggle services like Real Debrid, Torbox etc.
The order of the services determines a couple things:
* The priority order when deduplicating streams - given that the deduplicator is enabled.
* The ranking of services used when sorting - given that the sorting uses service as a criterion.
Below all the services, there will be a field to enter your RPDB API Key. Add this if you would like to configure supported catalogue addons you add to use
posters from RPDB.
Addons
Here you can manage your installed addons, add new ones, and configure them. You can also manage your catalogues here, as well as using
the [Group](/guides/groups) feature to group addons together. I will not be covering the Groups feature here, as it is not
required and is a feature meant for advanced users. You may join the [AIOStreams Discord](https://discord.viren070.me) to view the community's configurations.
To install addons, head to the `Marketplace` tab. You can choose from a variety of addons, and filter by supported services and the type of content or streams they provide.
You may also use the `Custom Addon` feature to add any addon you want as long as you have the manifest URL. The addons provided in the Marketplace
are all provided for convenience's sake to ensure you can easily add addons without having to leave AIOStreams.
Upon clicking `Configure` on an addon, you will be presented with a list of options. In most cases, you don't have to change anything, and click `Install` on the popup
that appears. However, some addons may require you to enter credentials or API keys, and some may have additional options that you can configure.
As most options are generally explained well in the popup, I'm not going to cover them here. However, I will cover some of the more important ones that tend to appear
on every addon.
* `Name` - This lets you change the name of the addon as it appears in AIOStreams. This name is only used for stream results, it does not affect any catalog names.
* `Timeout` - This lets you change the time that AIOStreams waits for the addon to respond before it aborts the request to it. Used for all requests to the addon, including catalogues, streams, subtitles etc.
* `Resources` - This option allows you to override the resources that AIOStreams would use from the addon. This is useful if you want to disable catalogues from the addon, but keep its streams. So you would only select `stream` here. In most cases, its recommended to leave this blank, or at the defaults - as it is automatically determined by AIOStreams later.
* `URL` - In most cases, you will be able to override the URL of the addon here. Usually this is generated automatically by AIOStreams - A benefit of this is you never having to worry about other addon updates, as AIOStreams will always use the latest version of the addon. However, if you want to use a custom URL, you can do so here. This is useful for using a specific configuration of the addon. Note: you will need to configure any debrid services separately, debrid services configured in AIOStreams do not apply to overriden URLs.
Once you've installed your addons, you can head back to the `Installed` tab to view them. You'll be able to click
the edit icon to edit the configuration, the delete icon to remove the addon from your installed addons, and optionally toggle the addon on or off
to temporarily disable it without removing it.
Catalogues
Within the `Installed` tab of the `Addons` menu, you will find a section where you can control your catalogues (given that you have at least one addon installed).
You'll first want to click the refresh icon on the top right corner. This will fetch the catalogues from all your addons and display them here.
There are a few options available here to you to help you manage your catalogues. For each catalogue, you will see the following options:
* To the left of each catalogue card, you can drag the black bar to reorder your catalogues. The order here determines the order they are displayed in on Stremio or other clients.
* The edit icon next to the catalogue name allows you to edit a catalogues `name` and `type`. Both these values can be edited to your liking and those changes will be reflected in Stremio.
* To the right, you also have position controls to allow you to instantly move a catalogue to the top or bottom of the list.
* There is also a switch to toggle the catalogue on or off. This is useful if you want to hide specific catalogues.
* Below that, there is a settings section that you can expand which allows you to apply certain modifications to the catalogue. Each modification also has a icon button to provide quick access to toggling it.
Below the catalogue section, you will notice the Groups section. Like I mentioned earlier, this is an advanced feature that allows you to group addons together and I will not be covering it here.
The [Groups guide](/guides/groups) has more information on how to use this feature.
Filters
One of AIOStreams' most recongisable feature is its advanced filtering system. This menu may look a bit overwhelming, but it is neatly organised into sections
to try and make it easier to understand.
Cache
This section allows you to filter streams based on whether they are cached or not. There is a cached section and an uncached section.
Each section will have a toggle to completely disable cached/uncached streams, and optionally a more granular filter
to exclude cached/uncached streams from specific addons, services, or stream types.
The apply mode setting controls how your 3 filters (addons, services, and stream types) are applied.
The `OR` mode means that a cached/uncached stream will be excluded if it is either from an addon, service, or stream type that is excluded.
The `AND` mode means that a cached/uncached stream will be excluded only if it is from all of the addons, services, and stream types that are excluded.
The stream type setting allows you to exclude uncached results for debrid, while keeping uncached results for usenet. Useful for TorBox pro users.
Generic Stream Attributes
The next couple sections allow you to filter streams based on generic attributes:
* `Resolution` - The resolution of the stream, such as 1080p, 720p, etc.
* `Encode` - The encoding of the stream, such as HEVC, AVC etc.
* `Stream Type` - The type of the stream, such as p2p, debrid, http, usenet, etc.
* `Visual Tag` - The visual tag of the stream, such as HDR, Dolby Vision, etc.
* `Audio Tag` - The audio tag of the stream, such as Dolby Atmos, DTS, etc.
* `Audio Channel` - The audio channel of the stream, such as 5.1, 7.1, etc.
* `Language` - The language of the stream, such as English, Spanish, etc.
Each of these sections above have very similar options:
* `Required` - Excludes streams that don't have one of the selected attributes. For example, if you set `1080p` as a required resolution, any streams that are not 1080p will be excluded.
* `Excluded` - Excludes streams that have at least one of the selected attributes. For example, if you set `720p` as an excluded resolution, any streams that are 720p will be excluded.
* `Included` - Prevents streams that have at least one of the selected attributes from being filtered out by all other exclude/required filters. For example, if you set `HDR` as an included visual tag, any streams that are HDR will not be filtered out by the other filters, even if they don't match the required or excluded attributes.
* `Preferred` - This is a special filter that allows you to set a ranking for that specific attribute. This ranking is used when you enable the relevant sorting option in the `Sorting` section. For example, if you set `1080p` as a preferred resolution, any streams that are 1080p will be ranked higher than streams that are not 1080p when sorting by resolution.
Seeders
This section allows you to filter streams based on the number of seeders they have. You can set 3 ranges of seeders
* `Required` - Excludes streams whose number of seeders is not within the range.
* `Excluded` - Excludes streams whose number of seeders is within the range.
* `Included` - Prevents streams whose number of seeders is within the range from being filtered out by all other exclude/required filters.
You can also optionally set stream types for which the seeders filter should be applied. This is useful if you want to only apply these seeder filters to p2p streams, for example.
Matching
This section allows you to filter streams out based on whether they match with the requested content or not.
You can apply title matching and season/episode matching.
Title Matching
This option fetches titles from the TMDB API and compares them to the titles of the streams returned by your addons.
This allows you to filter streams based on whether they match the title of the requested content or not.
You can set the `Matching mode` to either `Exact` or `Partial`. The `Exact` mode will only match streams that have the exact same title as the requested content, while the `contains` mode will match streams that have the requested content's title anywhere in their title.
As the TMDB API is being used, it is required to provide a **TMDB API Key**. This can be obtained by creating an account on [TMDB](https://www.themoviedb.org/) and generating an API key in your account settings.
You can then enter the key in the `TMDB API Key` field.
You can use fake information when creating your application on TMDB
You can then optionally limit the title matching to specific addons or request types (e.g. movies, series, anime)
Season/Episode Matching
This section is the same as the above, except it doesn't require a TMDB API Key, simply enable it if you wish to use it.
Keyword
This section allows you to filter streams based on whether they contain specific keywords in their title or not. You can enter multiple words
or phrases in 4 different fields:
* `Required` - Excludes streams that do not contain at least one of the words or phrases in their title.
* `Excluded` - Excludes streams that contain at least one of the words or phrases in their title.
* `Included` - Prevents streams that contain at least one of the words or phrases in their title from being filtered out by all other exclude/required filters.
* `Preferred` - This is a special filter that allows you to set a ranking for a specific set of keyword. This ranking is used when you enable the relevant sorting option in the `Sorting` section.
For example, if you add `ilikethisterm` as a preferred keyword, any streams that contain `ilikethisterm` in their title will be ranked higher than streams that do not contain that term when sorting by keyword.
Regex
This filter is only available on self hosted instances, due to potential abuse
You will either need to add your UUID to **Trusted UUIDs**
(`TRUSTED_UUIDS`), or set **Regex filter access** (`REGEX_FILTER_ACCESS`) to
`all` — only recommended if your instance is access-controlled (login
required via `AIOSTREAMS_AUTH` / `AIOSTREAMS_AUTH_REQUIRED`) so no one
unauthorised can access it. These are runtime settings, configurable from the
dashboard Settings page or the matching environment variables.
This is essentially a more powerful version of the keyword filter, allowing you to use regular expressions to filter streams.
The only difference is in the preferred section. With regex, the order of the regexes you enter matters, and streams that match a regex higher up in the list
are ranked higher than streams that match a regex lower down in the list when sorting by regex.
Size
This section allows you to filter streams based on their size.
You can set separate global size limits for movies and series. Any streams whose size does not lie within the specified range will be excluded.
If you leave the two slider thumbs at opposite ends, no size filtering is applied.
You can also optionally set a specific size limit for movies and series per resolution. This is useful if you want to have different size limits for different resolutions.
Only one set of size limits will be applied at a time, meaning a resolution specific size limit will take precedence over the global size limits.
Result Limits
This section allows you to limit the number of results returned by AIOStreams.
You can set a maximum number of results to return, and optionally also limit the
number of results per: service, addon, resolution, quality, indexer, and release
group.
What this means is that AIOStreams will cut off streams after a specific group limit is reached. This means that it would keep streams
higher up in the list (which depends on your sorting options) and remove streams lower down in the list.
Deduplicator
The deduplicator is a powerful feature that allows you to remove duplicate streams from your results.
`Detection Methods` - Allows you to control what attributes are used to determine whether two streams are duplicates or not.
* `filename` - Uses fuzzy filename matching.
* `infoHash` - uses the hash of the torrent file (available for all torrent streams, and some debrid streams) - this is the most accurate method.
* `smartDetect` - This uses a hash generated using a combination of the streams metadata - including file size, resolution, audio tags, visual tags etc.
You can then control how duplicates are removed for specific types (cached, uncached, P2P), these are the 3 options you can choose:
* `Single Result` - This keeps only one result, ranking the duplicates based on the serivce its from, and then the addon its from.
* `Per Service` - This aims to keep the best result from each service, determined by the ranking of the addons.
* `Per Addon` - This aims to keep the best result from each addon, determined by the ranking of the services.
Sorting
The sorting system uses a hierarchical approach to determine how content is sorted:
1. Primary Sorts: Define the main sorting order for different content types:
* Global: Used as a fallback when no specific type sort is defined (You most likely just want to define this and ignore everything else.)
* Movies: Specific sorting for movies
* Series: Specific sorting for TV series
* Anime: Specific sorting for anime content
2. Cached/Uncached Sorting: If your primary sort has "cached" at the top and there is both an `uncached` and `cached` sort criteria defined:
* Content is split into cached and uncached groups
* Each group is sorted separately using its specific sort criteria
* Groups are then combined based on the cached sort direction
* Type-specific cached/uncached sorts (e.g., "Cached Movies") take priority over global cached/uncached sorts
Formatter
Here, you can customise the way your streams are displayed in Stremio.
You can either choose from a list of pre-defined formats, or create your own custom format using AIOStreams\` [Formatter Syntax](/reference/custom-formatter)
Join AIOStreams' [Discord](https://discord.viren070.me) to see the formats
created by the community, or to share your own formats!
There is a preview section that allows you to see how your streams will look like in Stremio. You can adjust specific values in the preview to see how they affect the display of the streams.
Proxy
You can use this section to optionally configure a stream proxy for AIOStreams.
You can choose from either [MediaFlow Proxy](https://github.com/mhdzumair/mediaflow-proxy) or [StremThru](https://github.com/MunifTanjim/stremthru).
The rest of the options are already explained well in the UI, so I will not be covering them here.
Miscellaneous
The options on this menu are already explained well in the UI, so I will not be covering them here.
Save & Install
Here, you will be able to create a new user on your AIOStreams instance, or update the existing user with the current configuration.
When creating a new user, you will need to set a reasonably strong password. You will also be given a unique UUID that you may need to access your configuration, should you lose your installation URL.
Once you create a user, you will be presented with a button to install the addon using your preferred method.
Make sure you save your UUID and password somewhere safe, as you will need them to access your configuration in the future.
You will **NOT** be able to recover your configuration, or reset your password if you lose them.
You can, however, keep exports of your configurations as backups, which I will discuss below.
Below the installation button, you will find a backup section that allows you to export your configuration as a JSON file and also import these configurations later.
You can use this to backup your configuration, or to share it with others. You can also use this to import configurations from other users.
You can expand the `Export Settings` to toggle the `Exclude Credentials` option, which will exclude any credentials you have entered in the configuration from the exported file.
Note that while this option filters out all sensitive information entered in AIOStreams, it will not filter out any URLs from custom addons or URLs that you have overriden.
These URLs can possible contain sensitive information, so make sure to double check the exported file before sharing it with others.
FAQ
How do I edit my configuration?
When you create a user, you are given a unique installation URL ending in `/manifest.json`
Most apps will display a `Configure` button next to the addon, which will take you to the configuration page.
If this does not exist, you can manually navigate to the URL by replacing `/manifest.json` with `/configure`.
You may also manually go to the configuration page by navigating to `https://your-instance-url/stremio/configure` where `your-instance-url` is the URL of your AIOStreams instance,
and then click the login icon on the bottom left corner, where you will be asked for your UUID and password.
How do I update my configuration?
You can update your configuration by going to the configuration page and clicking the `Update user` button on the `Save & Install` menu.
You don't have to reinstall the addon in Stremio, as the manifest URL will remain the same. **Note: This does not apply if you make changes that cause the manifest of the addon to change, such as reordering or editing catalogues or adding a type of addon (e.g. subtitle) that was not previously provided by your installation. In that case, you will need to reinstall the addon in Stremio.**
Need Help?
If you need help in configuring this addon, feel free to ask in the [AIOStreams Discord](https://discord.viren070.me) in the `#support` channel,
# Setup Guide
This guide walks you through a complete AIOStreams setup — from installing your media client to having a fully configured addon ready to use. It uses a curated community template that handles the bulk of the configuration so you only need to fill in your credentials.
**Template Credits**
This guide uses a template built and maintained by
[Tam-Taro](https://github.com/Tam-Taro/SEL-Filtering-and-Sorting)
([@tam.taro](https://discord.com) on Discord), who has spent countless hours
perfecting their Stream Expression Language filtering and sorting. It also
incorporates [Vidhin's Regex Patterns](https://github.com/Vidhin05/Releases-Regex),
which bring the Trash Guides release group rankings into AIOStreams.
If you find this useful, consider supporting them:
* Tam-Taro: [ko-fi.com/tamtaro](https://ko-fi.com/tamtaro)
* Vidhin: [ko-fi.com/vidhin](https://ko-fi.com/vidhin)
Just want a reference for every option? See
[Configure Options](/configuration/options).
Step 1 — Install a Media Client [#step-1--install-a-media-client]
AIOStreams is primarily designed and maintained for Stremio, but works with other Stremio addon compatible clients as well
Go to [stremio.com/downloads](https://www.stremio.com/downloads) and
download the version for your platform (Stremio supports most platforms, but I would recommend doing the initial setup on a PC or laptop).
Open Stremio and create a free account, or sign in.
Go to the **Addons** page (puzzle-piece icon) and uninstall
**Watchhub** — it serves ads and is not useful for most setups.
Stremio is account-based — your addons, watch history, and continue
watching all sync automatically. You can do this initial setup on your
PC or laptop and then just sign in to the same account on your TV,
phone, or any other device and everything will already be there.
The following clients also work with AIOStreams — most via Stremio addon
compatibility, and Chillio via its own direct protocol. This guide does not
cover their individual setup — refer to each client's own documentation /
Discord for help getting started.
| Client | Platforms | Discord |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| [Fusion](https://fusionapp.dev) | iPhone, iPad, Mac, Apple TV | [Join](https://discord.gg/CuwDRSKbCW) |
| [Omni](https://omni.stkc.win/) | iPhone, iPad, Apple TV | [Join](https://discord.gg/hdPAnA3fck) |
| [RealStream](https://rstream.app/) | Android TV, Desktop *(coming soon)* | [Join](https://discord.gg/pn6UBHwHDq) |
| [Nuvio](https://nuvioapp.space/) | Android, Android TV, iOS | [Join](https://discord.gg/JvAPmadRgX) |
| [Seanime](https://seanime.app/) | Windows, macOS, Linux,
(and Android, Android TV, iOS, iPadOS [coming soon](https://seanime.app/tenji)) | [Join](https://discord.gg/GRMQ8yG4NG) |
| [Chillio](https://chillio.app/) | iPhone, iPad, Apple TV | — |
**Chillio users:** when installing AIOStreams, select **Chill Link
Protocol** (not Stremio) in the install modal.
**Seanime users:** AIOStreams provides dedicated extensions for Seanime.
See the [Seanime Extensions guide](/guides/seanime) for setup instructions.
***
Step 2 — Get a Debrid Service (Optional) [#step-2--get-a-debrid-service-optional]
A **debrid service** is a paid service that maintains a large cache of torrents on its own servers. When a torrent is first requested, the debrid service downloads it once and keeps it cached — subsequent requests for the same torrent are served instantly from the cache at high speeds over HTTPS, with no P2P involved on your end.
In Stremio, streams from debrid-enabled addons are labelled as either **cached** (available instantly) or **uncached** (needs to be downloaded to the debrid service first before you can stream it). For popular content, the vast majority of results will already be cached.
**A debrid service is optional, but recommended.** P2P streams work without one — provided there are enough seeders — but in many countries (including Germany and the United States) torrenting without a VPN exposes you to ISP warnings, throttling, or fines. A debrid service eliminates this risk entirely, since you stream over HTTPS from the debrid provider's servers. Using one also gives you:
* Instant streaming of cached content at high speeds (4K, HDR, Dolby Atmos without buffering)
* No reliance on seeders
* No VPN required
If you choose to use P2P streams **without** a debrid service and without a
VPN, be aware that your ISP can see your torrent traffic. In some countries
this can result in warning letters, throttling, or fines. Check the rules in
your country before proceeding.
There are three services worth considering. Click each tab to read more and get set up.
**Recommended for most users.**
TorBox is the best value option: affordable, no IP restrictions (use on
as many devices as you want) and accurate cache indicators. The main trade-off is a smaller cache than Real
Debrid — very popular content is always cached, but obscure titles may
not be.
TorBox also offers a **free** 24-hour trial
of the Pro plan, so you can test it before committing.
**Referral links — get 7 free days per month purchased (first purchase only):**
Using a referral link gives you **7 free days per month purchased**, on your first
purchase only — subsequent purchases get nothing. For example, buying 12 months = 84 free days.
Either click the link and/or enter the code manually at the bottom of [torbox.app/subscription](https://torbox.app/subscription).
You can use either:
* [Tam's referral link](https://torbox.app/subscription?referral=f1cdd3f8-aeee-48f1-849b-64fc7e5aeb3c)
```
f1cdd3f8-aeee-48f1-849b-64fc7e5aeb3c
```
* [Viren's referral link](https://torbox.app/subscription?referral=9ca21adb-dbcb-4fb0-9195-412a5f3519bc)
```
9ca21adb-dbcb-4fb0-9195-412a5f3519bc
```
**Setup:**
Go to [torbox.app](https://torbox.app/) and click **Get Started
For Free Now** to create an account. Verify your email.
Open the
[Subscription page](https://torbox.app/subscription?referral=f1cdd3f8-aeee-48f1-849b-64fc7e5aeb3c)
(referral link — gives 7 free days per month on your first purchase)
and choose a plan. A yearly subscription offers the best value.
Go to **Settings → API** and copy your **API Key** — you'll need
it during AIOStreams configuration.
**Most popular choice.**
Real Debrid has the largest cache of any debrid service, meaning almost
anything popular will already be cached and ready to stream instantly.
It has a one-IP-at-a-time restriction (so you can only use it in one
location at once) and its cache indicators are less accurate since a
2024 API change — but for popular content this rarely matters.
**Setup:**
Go to [real-debrid.com](https://real-debrid.com/?id=9483829)
(referral link — supports the guide at no extra cost) and click
**Sign Up**.
Go to
**Premium Offers**
and choose a package.
Go to **[My Account → API](https://real-debrid.com/apitoken)** and copy your **API Key**.
**Most stable and reliable.**
Premiumize has a large cache (second only to Real Debrid), accurate
cache indicators, and allows several IPs simultaneously. It is more
expensive than the alternatives, but it is the most consistent
experience.
**Setup:**
Go to [premiumize.me](https://www.premiumize.me/) and click **Sign
Up**.
Select your payment method and plan.
Go to **Account → API** and copy your **API Key**.
***
Step 3 — Obtain API Keys [#step-3--obtain-api-keys]
The template uses two free API keys for title and season/episode matching — these filter out incorrectly matched streams.
TMDB (The Movie Database) [#tmdb-the-movie-database]
Go to [themoviedb.org](https://www.themoviedb.org/) and create a free
account (or sign in).
Open **Account Settings → API** and click **Create** to request a key.
Choose **Developer** when asked for the application type.
None of the information on the form needs to be accurate. You can use a
temporary email address, a fake name, and a made-up phone number — none
of it is verified.
Copy your **API Key** from the API settings page.
TVDB (The TV Database) [#tvdb-the-tv-database]
Go to [thetvdb.com](https://thetvdb.com/) and create a free account (or
sign in).
Navigate to [API Information](https://thetvdb.com/api-information) and
generate a new API key.
Copy the API key.
***
Step 4 — Open the Template [#step-4--open-the-template]
Select a public instance below and click **Open Template**. This takes you directly to that instance's configuration page with the template pre-loaded.
Not sure which instance to pick? See
[Public Instances](/getting-started/public-instances) for a comparison.
**Yeb's** is the recommended starting point for most users. **ElfHosted** is
a reliable and reputable option but the public instance disables P2P, HTTP,
and Live stream types — if you need those, consider a
[private ElfHosted instance](/getting-started/deployment#elfhosted-paid-managed)
(paid, no restrictions, relaxed rate limits, direct ElfHosted support, and
33% of your subscription goes to the developer).
This template requires the following environment variable to be set on your
instance so that its SEL sync and regex URLs are whitelisted:
```sh
TEMPLATE_URLS=["https://git.tamtaro.de/complete.json"]
```
Optionally, if your instance is access-controlled (login required via
`AIOSTREAMS_AUTH` / `AIOSTREAMS_AUTH_REQUIRED`) and you want to avoid
per-user URL restrictions, you can also set the **SEL sync access** and
**Regex filter access** to `all`. These (and `TEMPLATE_URLS`) are now
runtime settings configurable from the dashboard Settings page, or via the
matching environment variables:
```sh
SEL_SYNC_ACCESS=all
REGEX_FILTER_ACCESS=all
```
See [Environment Variables](/configuration/environment-variables) for details
***
Step 5 — Import the Template [#step-5--import-the-template]
After clicking **Open Template** you will be walked through a short import wizard.
**Select a template** — You will see two options: **Tamtaro Complete SEL
Setup** and **Tamtaro Partial SEL Setup**. Select **Complete**. The Partial
option is intended for users who already have an existing setup they want to
retain parts of. Click **Use Selected** to continue.
**Select services** — Choose the debrid service(s) you have (e.g. Real
Debrid, TorBox). If you are not using any debrid service, click **Skip**.
Otherwise select your service(s) and click **Next**.
**Template inputs** — This step lets you customise the template before it
is applied. Every option has a description — read through them and adjust
anything you want. Nothing here is required and the defaults are sensible
for most users. A few options worth noting:
* **Preferred Language** — Select your preferred stream language(s). These
are placed first in the language ranking. You can fine-tune the full
order in Filters → Language after import.
* **TorBox Tier** *(TorBox users only)* — Select your TorBox plan:
**Essential**, **Standard**, or **Pro**. This applies the correct size
limit for uncached downloads. If you are not on TorBox this option will
not appear.
* **Core Filtering Engine** — The default (**Standard SEL**) is tuned to
remove clutter and show a minimal but sufficient set of results across
all scenarios. If you prefer more results, switch to **Extended SEL**.
Once you are happy with the inputs, click **Next**.
**Credential inputs** — Enter the API keys for any debrid services you
selected, plus the TMDB and TVDB API keys from Step 3. Click **Load
Template** when done.
Want to understand what every option does after import? See
[Configure Options](/configuration/options).
***
Step 6 — Save and Install [#step-6--save-and-install]
After the template loads you will land on the **Save & Install** page.
Under **Create Configuration**, enter a password and click **Create**. If
successful you will see **Configuration created successfully** and a UUID
will be shown. **Save both your UUID and password somewhere safe — there is
no way to recover them if lost.**
If you see an error like **"Failed to fetch manifest for \"**,
go to the **Addons** menu and disable the addon mentioned in the error,
then try clicking **Create** again. This is usually a temporary issue
with that addon — you can re-enable it and save again later once it
recovers.
If you see **"Failed to validate TMDB API Key" / 401 Unauthorized**,
double-check your TMDB API key is correct. You can review it in the
**Services** menu under the **Metadata** section.
An **Installation Options** card will appear.
There will be a section for Stremio (and Stremio addon compatible clients), containing three methods of installation:
* **Install to Stremio** — Opens the Stremio app and installs the addon directly. Use
this if Stremio is installed on the device you are currently on.
* **Install to Stremio Web** — Installs via the Stremio web player. Only use this if
you are already signed in to Stremio Web — otherwise it will install to
a guest account and your progress won't sync.
* **Manifest URL** — A text box with a copy button. Use this to install
manually on Stremio or for other Stremio addon compatible clients.
If you are using a client that doesn't directly support Stremio addons (e.g Seanime, Chillio), there is an
`Other Apps` section next to the Stremio section. Find your client there and click on it for instructions.
A Stremio pop-up will ask whether to install the addon. Click **Install**.
You are now set up — click on any title in Stremio and results should
appear.
Click **Export** in the **Backups** section to download a JSON copy of your
configuration. You can use this to restore or transfer your setup later.
***
Making Changes [#making-changes]
To edit your configuration after installation:
* In **Stremio**, click **Configure** next to the AIOStreams addon.
* Or navigate to `https://your-instance/stremio/configure` and log in with your UUID and password.
Once you've made your changes, go to **Save & Install** and click **Save**.
Most changes apply immediately without reinstalling. Only reinstall if a
pop-up appears telling you to.
***
Getting Help [#getting-help]
Run into something not covered here? Come join the [Discord server](https://discord.viren070.me).
* **#support** — General AIOStreams questions and issues.
* **#setups** — Tam is active on the server and has a [dedicated forum](https://discord.com/channels/1225024298490662974/1485044483857846324) for questions specifically about this template.
If you're stuck on something with this setup, this is the best place to ask.
***
Next Steps [#next-steps]
# Anime
`GET /api/v1/anime`
Resolves an anime identifier from one database to its mappings across other supported databases. Uses the internal AIOStreams anime database (aggregated from AniDB, AniList, TVDB, TMDB, MAL, Kitsu, and more).
This endpoint does not require authentication.
***
Query Parameters [#query-parameters]
| Parameter | Type | Required | Description | Example |
| --------- | ---------------- | -------- | ------------------------------------------------------------------------------------------------ | ----------- |
| `idType` | string | ✅ | The identifier type you are querying with. See accepted values below. | `imdbId` |
| `idValue` | string \| number | ✅ | The value of the identifier. | `tt0388629` |
| `season` | number | ❌ | Season number (for series). Helps disambiguate entries with multiple seasons mapped differently. | `1` |
| `episode` | number | ❌ | Episode number (for series). | `5` |
Accepted idType values [#accepted-idtype-values]
| Value | Database |
| ------------------ | -------------- |
| `animePlanetId` | Anime-Planet |
| `animecountdownId` | AnimeCountdown |
| `anidbId` | AniDB |
| `anilistId` | AniList |
| `anisearchId` | AniSearch |
| `imdbId` | IMDb |
| `kitsuId` | Kitsu |
| `livechartId` | LiveChart |
| `malId` | MyAnimeList |
| `notifyMoeId` | Notify.moe |
| `simklId` | Simkl |
| `themoviedbId` | TMDB |
| `thetvdbId` | TVDB |
| `traktId` | Trakt |
***
Example Requests [#example-requests]
**Look up by IMDb ID:**
```http
GET /api/v1/anime?idType=imdbId&idValue=tt0388629
```
```bash
curl "https://your-instance.example.com/api/v1/anime?idType=imdbId&idValue=tt0388629"
```
**Look up a specific season of a series:**
```bash
curl "https://your-instance.example.com/api/v1/anime?idType=imdbId&idValue=tt0388629&season=2&episode=1"
```
***
Response [#response]
```json
{
"success": true,
"detail": "OK",
"error": null,
"data": {
/* AnimeDatabase mapping entry, or null if not found */
}
}
```
Returns `null` in `data` if no mapping entry was found for the provided identifier.
***
Error Response [#error-response]
```json
{
"success": false,
"detail": null,
"error": {
"code": "BAD_REQUEST",
"message": "Invalid idType"
},
"data": null
}
```
# Overview
AIOStreams exposes a REST API that lets you query streams programmatically, look up anime mappings, and more. Responses use a consistent JSON envelope.
**Base Path:** `/api/v1`
***
Response Envelope [#response-envelope]
All endpoints return a consistent top-level structure:
```json
{
"success": true,
"detail": "optional message",
"error": null,
"data": { ... }
}
```
| Field | Type | Description |
| --------- | -------------- | ---------------------------------------------- |
| `success` | boolean | `true` if the request succeeded |
| `detail` | string \| null | Optional informational message |
| `error` | object \| null | Error object present when `success` is `false` |
| `data` | any \| null | Response payload |
**Error object:**
| Field | Type | Description |
| --------- | ------ | --------------------------- |
| `code` | string | Machine-readable error code |
| `message` | string | Human-readable description |
***
Endpoints [#endpoints]
# Search
`GET /api/v1/search`
Search for streams across all enabled and configured sources in your AIOStreams instance. Results are processed through the same filtering and sorting pipeline as normal Stremio stream requests.
***
Query Parameters [#query-parameters]
| Parameter | Type | Required | Description | Example |
| ---------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `type` | string | ✅ | Type of content — usually `movie` or `series` | `movie` |
| `id` | string | ✅ | Media ID. Supports IMDb, TMDb, and other upstream identifiers. For series, include season/episode. | `tt1234567`, `tmdb:2131:1:3` |
| `requiredFields` | string\[] | ❌ | Fields from `SearchApiResult` that must be present. Results missing them are dropped. Repeat for multiple. | `requiredFields=infoHash&requiredFields=seeders` |
| `format` | boolean | ❌ | When `true`, each result is augmented with Stremio-compatible `name` and `description`. Same envelope; coerced from query (e.g. `true`, `1`). | `true` |
***
User Data [#user-data]
To retrieve personalised streams from your configured addons, you must provide user data via one of two methods:
Basic Auth [#basic-auth]
```http
Authorization: Basic
```
```bash
curl -u uuid:password "https://your-instance.example.com/api/v1/search?type=movie&id=tt1234567"
```
User Data Header [#user-data-header]
Pass a base64-encoded `UserData` JSON object (exportable from the config page):
```http
x-aiostreams-user-data:
```
If the instance is password-protected, the `addonPassword` field is still
required in the UserData object.
Top-level Properties [#top-level-properties]
| Field | Type | Description |
| --------- | -------------- | ---------------------------------- |
| `success` | boolean | `true` if the search succeeded |
| `detail` | string \| null | Optional detail message |
| `error` | object \| null | Error object if the request failed |
| `data` | object \| null | Data object containing results |
Error Object [#error-object]
| Field | Type | Description |
| --------- | ------ | ------------------------------------------------ |
| `code` | string | Machine-readable error code (e.g. `BAD_REQUEST`) |
| `message` | string | Human-readable error message |
Data Object [#data-object]
| Field | Type | Description |
| ------------ | --------------- | ------------------------------------------------------- |
| `results` | SearchResult\[] | Array of search results |
| `filtered` | number | Number of results removed by `requiredFields` filtering |
| `statistics` | object\[] | Statistic entries generated during search/filtering |
| `errors` | object\[] | Per-source errors that occurred during search |
Each error in `errors` has:
* `title` — short error title
* `description` — detailed description
Each entry in `statistics` has:
* `title` — short statistic title
* `description` — detailed statistic message
***
Error Response Example [#error-response-example]
***
Example Requests [#example-requests]
**Movie by IMDb ID (Basic Auth):**
```http
GET /api/v1/search?type=movie&id=tt1234567 HTTP/1.1
Authorization: Basic bXktdXVpZDpteS1wYXNzd29yZA==
```
```bash
curl -u uuid:password "https://your-instance.example.com/api/v1/search?type=movie&id=tt1234567"
```
**Series with Stremio-format response:**
```http
GET /api/v1/search?type=series&id=tmdb:2131&format=true HTTP/1.1
x-aiostreams-user-data: eyJ1c2VySWQiOiAiYWJjZCIsICJwYXNzd29yZCI6ICJzZWNyZXQifQ==
```
***
Response [#response]
Default [#default]
```json
{
"success": true,
"detail": null,
"error": null,
"data": {
"filtered": 0,
"results": [
/* SearchResult[] */
],
"statistics": [],
"errors": []
}
}
```
Data Object [#data-object-1]
| Field | Type | Description |
| ------------ | --------------- | ------------------------------------------------------- |
| `results` | SearchResult\[] | Filtered and sorted stream results |
| `filtered` | number | Number of results removed by `requiredFields` filtering |
| `statistics` | object\[] | Statistic entries (`title` + `description`) |
| `errors` | object\[] | Per-source errors (`title` + `description`) |
format=true [#formattrue]
When `format` is truthy, each item in `data.results` is augmented with Stremio-compatible `name` and `description`; the response envelope is unchanged.
***
Error Response [#error-response]
```json
{
"success": false,
"detail": null,
"error": {
"code": "BAD_REQUEST",
"message": "Invalid credentials"
},
"data": null
}
```
***
Schemas [#schemas]
* [UserData schema](https://github.com/Viren070/AIOStreams/blob/main/packages/core/src/db/schemas.ts) — the JSON object encoded in `x-aiostreams-user-data`
* [SearchResult schema](https://github.com/Viren070/AIOStreams/blob/main/packages/core/src/transformers/api.ts) — the shape of each result in `data.results`
# User
`/api/v1/user`
Manage AIOStreams user accounts and configurations. A **user** is identified by a UUID and protected by a password. These endpoints are the programmatic equivalent of the **Save & Install** flow in the UI.
All user API requests are rate-limited.
***
Authentication [#authentication]
All endpoints except `HEAD /api/v1/user` and `POST /api/v1/user` (create) authenticate via the standard **HTTP Basic** scheme:
```
Authorization: Basic base64(:)
```
The `` may be either the raw user password or the `encryptedPassword` token returned by `POST /api/v1/user` and `GET /api/v1/user` — the server transparently decrypts encrypted tokens.
***
UUID Aliases [#uuid-aliases]
Wherever a UUID is accepted (including as the username in the `Authorization` header), you may supply a configured **alias** (a human-readable name mapped to a UUID via the `ALIASED_CONFIGURATIONS` environment variable) instead. The alias is resolved to the real UUID before processing.
***
Check User Exists [#check-user-exists]
`HEAD /api/v1/user`
Checks whether a user exists without returning any sensitive data. Useful for validating a UUID before attempting a full fetch.
Query Parameters [#query-parameters]
| Parameter | Type | Required | Description |
| --------- | ------ | -------- | ------------------------------------ |
| `uuid` | string | ✅ | UUID (or alias) of the user to check |
Response 200 [#response-200]
```json
{
"success": true,
"detail": "User exists",
"error": null,
"data": {
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
```
Returns an error response if the UUID is not found.
Example [#example]
```bash
curl -X HEAD "https://your-instance.example.com/api/v1/user?uuid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```
***
Get User [#get-user]
`GET /api/v1/user`
Returns the full user configuration and an encrypted copy of the password for use in subsequent requests.
Authentication [#authentication-1]
Requires `Authorization: Basic base64(uuid:password)`.
Query Parameters [#query-parameters-1]
| Parameter | Type | Required | Description |
| --------- | ------- | -------- | ---------------------------------------------------------------------------- |
| `raw` | boolean | ❌ | When `true`, returns the stored config without merging the parent config in. |
Response 200 [#response-200-1]
```json
{
"success": true,
"detail": "User details retrieved successfully",
"error": null,
"data": {
"userData": { },
"encryptedPassword": ""
}
}
```
| Field | Type | Description |
| ------------------- | ------ | ------------------------------------------------------------------------------------------ |
| `userData` | object | Full user configuration object |
| `encryptedPassword` | string | Server-encrypted password — pass this back in the manifest URL instead of the raw password |
Example [#example-1]
```bash
curl "https://your-instance.example.com/api/v1/user" \
-H "Authorization: Basic $(echo -n 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:mypassword' | base64)"
```
***
Create User [#create-user]
`POST /api/v1/user`
Creates a new user and returns the assigned UUID and an encrypted password.
Request Body [#request-body]
```json
{
"config": { },
"password": "mypassword"
}
```
| Field | Type | Required | Description |
| ---------- | ------ | -------- | -------------------------------------- |
| `config` | object | ✅ | Full user configuration object |
| `password` | string | ✅ | Password to protect this configuration |
Response 201 [#response-201]
```json
{
"success": true,
"detail": "User was successfully created",
"error": null,
"data": {
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"encryptedPassword": ""
}
}
```
| Field | Type | Description |
| ------------------- | ------ | ----------------------------------------------------- |
| `uuid` | string | Assigned UUID — save this, it cannot be recovered |
| `encryptedPassword` | string | Server-encrypted password for use in the manifest URL |
Example [#example-2]
```bash
curl -X POST "https://your-instance.example.com/api/v1/user" \
-H "Content-Type: application/json" \
-d '{"config": { }, "password": "mypassword"}'
```
***
Update User [#update-user]
`PUT /api/v1/user`
Replaces the stored configuration for an existing user. The request must include the full updated `config` object — partial updates are not supported.
Authentication [#authentication-2]
Requires `Authorization: Basic base64(uuid:password)`.
Request Body [#request-body-1]
```json
{
"config": { }
}
```
| Field | Type | Required | Description |
| -------- | ------ | -------- | ------------------------------------- |
| `config` | object | ✅ | Full replacement configuration object |
Response 200 [#response-200-2]
```json
{
"success": true,
"detail": "User updated successfully",
"error": null,
"data": {
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"userData": { }
}
}
```
Example [#example-3]
```bash
curl -X PUT "https://your-instance.example.com/api/v1/user" \
-H "Authorization: Basic $(echo -n 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:mypassword' | base64)" \
-H "Content-Type: application/json" \
-d '{"config": { }}'
```
***
Delete User [#delete-user]
`DELETE /api/v1/user`
Permanently deletes a user and their configuration. This action is irreversible.
Authentication [#authentication-3]
Requires `Authorization: Basic base64(uuid:password)`.
Response 200 [#response-200-3]
```json
{
"success": true,
"detail": "User deleted successfully",
"error": null,
"data": null
}
```
Example [#example-4]
```bash
curl -X DELETE "https://your-instance.example.com/api/v1/user" \
-H "Authorization: Basic $(echo -n 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:mypassword' | base64)"
```
# Deployment
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
Docker [#docker]
[Docker](https://docs.docker.com/get-docker/) is the recommended way to run AIOStreams. Official images are published to both [ghcr.io](https://github.com/Viren070/AIOStreams/pkgs/container/aiostreams) and [Docker Hub](https://hub.docker.com/r/viren070/aiostreams).
Use the `latest` tag for the latest stable release, or `nightly` for the latest commit build.
Only **`BASE_URL`** and **`SECRET_KEY`** are required environment variables -
these (and a few other bootstrap variables) must be set in the environment.
Almost everything else is a runtime setting best configured from the
dashboard Settings page after first start. See the full list in [Environment Variables](/configuration/environment-variables).
Rather than running this on a personal device, consider hosting on a server or VPS.
You can use a [free Oracle VPS](https://guides.viren070.me/selfhosting/oracle) or find cheap options on [LowEndBox](https://lowendbox.com/).
Docker Compose (recommended) [#docker-compose-recommended]
Download the compose file and sample env:
```bash
curl -O https://raw.githubusercontent.com/Viren070/AIOStreams/main/compose.yaml
curl -o .env https://raw.githubusercontent.com/Viren070/AIOStreams/main/.env.sample
```
Edit `.env` to set at minimum a `SECRET_KEY` (64-character hex string). See [Environment Variables](/configuration/environment-variables).
Start the container:
```bash
docker compose up -d
```
Open `http://localhost:3000/stremio/configure` to complete setup.
Traefik (HTTPS) [#traefik-https]
Stremio requires HTTPS for addons hosted outside of `localhost`. Traefik is a convenient reverse proxy that can obtain Let's Encrypt certificates automatically.
**Requirements:**
* A domain or subdomain with a CNAME/A record pointing to your server's public IP
* Port 443 open on the server
Free domains are available from [DuckDNS](https://www.duckdns.org/) and
[Afraid.org](https://freedns.afraid.org/).
Merge the following into your `compose.yaml`, replacing `aiostreams.example.com` and `youremail@example.com`:
```yaml
services:
aiostreams:
image: ghcr.io/viren070/aiostreams:latest
container_name: aiostreams
restart: unless-stopped
ports:
- 3000:3000
env_file:
- .env
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.aiostreams.rule=Host(`aiostreams.example.com`)'
- 'traefik.http.routers.aiostreams.entrypoints=websecure'
- 'traefik.http.routers.aiostreams.tls.certresolver=letsencrypt'
volumes:
- ./data:/app/data
traefik:
image: traefik:v3
container_name: traefik
restart: unless-stopped
ports:
- 443:443
command:
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entryPoints.websecure.address=:443'
- '--certificatesresolvers.letsencrypt.acme.tlschallenge=true'
- '--certificatesresolvers.letsencrypt.acme.email=youremail@example.com'
- '--certificatesresolvers.letsencrypt.acme.storage=/config/acme.json'
volumes:
- '/var/run/docker.sock:/var/run/docker.sock'
- './traefik:/config'
```
Then run `docker compose up -d`.
Torrentio / strem.fun blocks [#torrentio--stremfun-blocks]
Torrentio and other `strem.fun` addons block certain server IP ranges (e.g. Oracle VPS), causing 403 errors. You can work around this with a VPN via [gluetun](https://github.com/qdm12/gluetun):
```bash
ADDON_PROXY=http://gluetun:8080
ADDON_PROXY_CONFIG=*:false,*.strem.fun:true
```
Multi-addon Compose [#multi-addon-compose]
For a full compose template that includes popular self-hosted addons (Jackett, Prowlarr, etc.), see [Viren070/stremio-addons-docker-compose-template](https://github.com/Viren070/stremio-addons-docker-compose-template).
Docker CLI [#docker-cli]
```bash
# GitHub Container Registry
docker run -p 8080:3000 \
-e BASE_URL=http://localhost:8080 \
-e SECRET_KEY=Your64CharacterHexKeyGoesHere \
-v aiostreams-data:/app/data \
ghcr.io/viren070/aiostreams:latest
# Docker Hub
docker run -p 8080:3000 \
-e BASE_URL=http://localhost:8080 \
-e SECRET_KEY=Your64CharacterHexKeyGoesHere \
-v aiostreams-data:/app/data \
viren070/aiostreams:latest
```
Pass additional environment variables with `-e`:
```bash
docker run -p 8080:3000 \
-e BASE_URL=http://localhost:8080 \
-e SECRET_KEY=... \
-e LOG_LEVEL=debug \
viren070/aiostreams:latest
```
To build from source instead of using a prebuilt image:
```bash
git clone https://github.com/Viren070/AIOStreams.git
cd AIOStreams
docker build -t aiostreams .
docker run -p 8080:3000 \
-e BASE_URL=http://localhost:8080 \
-e SECRET_KEY=Your64CharacterHexKeyGoesHere \
aiostreams
```
***
ElfHosted (paid, managed) [#elfhosted-paid-managed]
Using the link below supports the project — 33% of your AIOStreams
subscription goes to the author.
AIOStreams is available as a [paid product on ElfHosted](https://store.elfhosted.com/product/aiostreams/elf/viren070/). Private instances have Torrentio enabled, avoid rate-limits, and require no self-hosting knowledge.
***
Other free platforms [#other-free-platforms]
These platforms offer free tiers that can run AIOStreams, but they are **not permanent solutions** — they can be revoked or stop working at any time:
| Platform | Notes |
| -------------------------------------- | ------------------------------------------------------ |
| [Hugging Face](https://huggingface.co) | Requires a custom `Dockerfile` setting `ENV PORT=7860` |
| [Koyeb](https://koyeb.com/) | Free tier available |
| [Render](https://render.com) | Free tier available |
***
From source [#from-source]
Node.js v22 and pnpm v10.15 were used during development. Earlier versions may
not work.
Building from source compiles the **`yencode`** native addon (used by the
native usenet engine) from source with `node-gyp` — it ships no prebuilt
binaries — so you need a C/C++ toolchain and Python 3 installed:
* **Debian / Ubuntu:** `sudo apt-get install -y python3 make g++`
* **macOS:** `xcode-select --install`
* **Windows:** [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) ("Desktop development with C++") + [Python 3](https://www.python.org/downloads/)
To skip this entirely, deploy the prebuilt [Docker image](#docker) instead — it
already contains the compiled addon. The
[Development guide](/guides/development#native-module-build-tools) has more
detail.
Clone the repository:
```bash
git clone https://github.com/Viren070/AIOStreams.git
cd AIOStreams
```
Install dependencies:
```bash
pnpm install
```
Build the project:
```bash
pnpm run build
pnpm run metadata --channel=nightly
```
Start the server:
```bash
pnpm run start
```
Open `http://localhost:3000/stremio/configure`.
Change the `PORT` environment variable to use a different port.
# Introduction
AIOStreams is a single Stremio addon that sits in front of every source you use — your other addons, your debrid and Usenet services, and a suite of search addons built into AIOStreams itself. Everything comes back as one list, deduplicated, filtered, sorted and formatted to your rules.
How would you like to get started? [#how-would-you-like-to-get-started]
What is AIOStreams? [#what-is-aiostreams]
AIOStreams is a **super-addon**: one addon in Stremio in place of the pile you would otherwise install, configure and keep in sync separately. It fetches from every source you configure and returns a single, unified list with:
* **Deduplication** across all addons and debrid services
* **Consistent sorting** by quality, resolution, cache status, and more
* **Powerful filters** — resolution, size, visual tags, regex, stream expressions, and more
* **Custom formatting** so results look exactly how you want in Stremio
* **Built-in addons** exclusive to AIOStreams (Usenet indexers, SeaDex, AnimeTosho, TorrentGalaxy, and more)
* **A built-in Usenet engine** that streams straight from your own NNTP provider with no external tool in between — see the [Usenet guide](/guides/usenet)
Other Resources [#other-resources]
# Public Instances
An **instance** is a running deployment of the AIOStreams server. Because
AIOStreams is open-source, anyone can host their own copy — this is called
**self-hosting**. However, if you don't want to manage your own server, you
can use a **public instance** hosted by a community member instead.
Each instance is completely independent: it has its own URL, its own
database, and its own resource limits (such as a cap on the number of addons
per user) set by the host. Your configuration — addons, filters, API keys —
is stored on whichever instance you use.
You do not need to self-host AIOStreams. There are several **public instances** run by the community that work great for most users. They may have minor limitations (e.g. a cap on the number of addons per user) to ensure stability for all users.
All instances are available on both **Stable** (official release tags, recommended) and **Nightly** (bleeding-edge `main` branch, latest features but may have bugs) channels where noted.
A live status page for all public instances is available at
[uptime.ibbylabs.dev](https://uptime.ibbylabs.dev).
Stable vs. Nightly [#stable-vs-nightly]
| Channel | Image tag | Who it's for |
| ----------- | --------- | --------------------------------------------------- |
| **Stable** | `latest` | Recommended — tested release builds |
| **Nightly** | `nightly` | Early adopters — latest commits, minor risk of bugs |
***
Instances [#instances]
Join the [Discord Server](https://discord.viren070.me/) to report issues or
get the latest instance news.
# Profiles
import { Callout } from 'fumadocs-ui/components/callout';
Two unrelated logins exist in AIOStreams, and this guide keeps them apart:
* **Account sign-in** — a username and password from `AIOSTREAMS_AUTH`, or an SSO identity, entered at `/login`. It issues a session cookie and identifies who you are. The instance owner decides who gets an account; holding one does not make you an administrator.
* **Configuration password** — the UUID and password that open one configuration. The browser holds it only in memory, so refreshing the configure page means typing it again.
A profile trades the second for the first. Sign in to your account, save a configuration to it once, and afterwards pick it from a list instead of retyping its UUID and password.
**Profiles require an account sign-in**, so they exist only on an instance
that has accounts. Either configure SSO under **Dashboard → Settings → SSO /
OIDC** (see the [SSO guide](/guides/sso)), or set `AIOSTREAMS_AUTH` —
that one is an environment variable with no dashboard equivalent, because it
is read at startup. With neither there is nothing to save against, and the
profile controls do not appear anywhere in the UI.
**Turn on Require authentication for the config page** as well, under
**Dashboard → Settings → General** (`AIOSTREAMS_AUTH_REQUIRED`). Strictly it
is optional — profiles work whenever you hold a session, however you obtained
it. But with it off nothing ever asks you to sign in, so the only route to a
session is to visit `/login` yourself and then navigate back to the configure
page by hand.
With it on, opening the configure page redirects to `/login?next=…` and returns
you there once you are signed in, which is the flow profiles are designed
around.
Saving a profile [#saving-a-profile]
While signed in to your account, open **Save / Install** with a configuration loaded and use **Save as profile**, or tick **Save as a profile** in the Load Configuration dialog when you enter a UUID and password. Signed out, neither control is shown.
Saving is always explicit, because it stores the configuration password on the server. The stored value is the same encrypted blob that already appears in every install URL, encrypted with `SECRET_KEY`, so a copy of the database alone opens nothing.
You can save up to 50 profiles per account.
Reopening one [#reopening-one]
Open **Load Configuration**. Your profiles appear above the UUID and password fields; picking one loads it. While signed in, arriving at a `/stremio//.../configure` URL for a configuration you have saved opens it directly, without asking for its password. Signed out, that URL prompts for the UUID and password as it always did.
Profiles belong to the account you signed in with, not to the instance.
Anyone else signing in sees only their own, administrators included. No
permission is needed beyond being able to sign in, so an account mapped to
`none` in `AIOSTREAMS_AUTH_PERMISSIONS` can still use profiles in full.
Share aliases [#share-aliases]
A profile can be given an **alias**, which publishes the configuration at a short install URL:
```
https://your-instance.example/stremio/u//manifest.json
```
Set one and the **Manifest URL** on the Save / Install page switches to it, including the *Install to Stremio* buttons. The long UUID and password URL keeps working, so anything already installed is unaffected.
An alias is 2 to 64 characters of `a-z`, `0-9`, dot, dash or underscore, lowercase, and globally unique across the instance. It cannot look like a UUID, and it cannot reuse a name already published through the instance-wide **Aliased configurations** setting.
The alias stands in for the UUID and password: following the URL redirects to
the full one. Anyone who has it can install the configuration, so choose a
name that is not easy to guess.
Relationship to instance-wide aliases [#relationship-to-instance-wide-aliases]
**Aliased configurations**, under **Dashboard → Settings → General** (`ALIASED_CONFIGURATIONS`), still works and is unchanged. It is resolved **first**, so an alias defined there always wins over a profile's alias, and a profile can never take over an install URL you published from settings.
It is, however, unnecessary with profiles.
Account sign-in methods [#account-sign-in-methods]
Profiles are keyed to the account username, whether that came from `AIOSTREAMS_AUTH` or from your identity provider. The configuration password plays no part in which profiles you see.
If the same person signs in both ways, enable **Link to local users by username** under **Dashboard → Settings → SSO / OIDC** (`AIOSTREAMS_OIDC_LINK_BY_USERNAME`, covered in the [SSO guide](/guides/sso)) so both routes resolve to one username and therefore one list. Without it, an SSO login whose username collides with a local user is refused outright, so there is no way to end up with two lists by accident.
Because the key is the username, an identity provider that later issues the
same username to a different person hands that person the previous holder's
profiles, including the ability to open them. If usernames are recycled at
your provider, point **Username claim** at `sub`.
Sessions are stateless, so removing an account from `AIOSTREAMS_AUTH` does not end a session it already holds, and its profiles stay reachable until that session expires.
When a profile stops working [#when-a-profile-stops-working]
A profile is marked as needing its password again if the stored password no longer opens the configuration, which happens if `SECRET_KEY` is rotated or the database is restored from a backup taken before a password change. Changing a configuration's password through AIOStreams updates every profile pointing at it automatically.
Pick the entry and enter the configuration's current password to repair it in place.
Deleting one [#deleting-one]
**Delete profile** removes the saved entry. The configuration itself, its UUID, and its password are untouched, and install URLs for it keep working, apart from the alias. Deleting the configuration removes every profile pointing at it.
# Development
Prerequisites [#prerequisites]
* **Node.js** v22+
* **pnpm** v10.15+
* **git**
* A **C/C++ toolchain + Python 3** — needed to compile the `yencode` native addon at install time (see [Native module build tools](#native-module-build-tools) below)
Earlier versions of Node.js or pnpm may work, but are not guaranteed.
***
Native module build tools [#native-module-build-tools]
AIOStreams' **native usenet engine** uses [`yencode`](https://www.npmjs.com/package/yencode) — a native (C++) addon that decodes yEnc-encoded article bodies. It is published **without prebuilt binaries**, so `pnpm i` compiles it from source with `node-gyp`. That requires a C/C++ compiler and Python 3 on your machine:
```bash
# Debian / Ubuntu
sudo apt-get install -y python3 make g++
```
On other distributions, install the equivalent packages: `python3`, `make`, and a C++ compiler such as `gcc-c++` or `clang`.
```bash
xcode-select --install
```
This provides Clang and `make`. Recent macOS already ships Python 3; if not, install it with `brew install python`.
Install both:
* **[Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022)** with the **"Desktop development with C++"** workload
* **[Python 3](https://www.python.org/downloads/)**
The Node.js Windows installer can also set these up for you — tick **"Automatically install the necessary tools"** (Tools for Native Modules) during installation.
***
Setup [#setup]
Clone the repository [#clone-the-repository]
```bash
git clone https://github.com/Viren070/AIOStreams.git
cd AIOStreams
```
Install dependencies [#install-dependencies]
```bash
pnpm i
```
Build the project [#build-the-project]
```bash
pnpm run build
```
***
Running in Development Mode [#running-in-development-mode]
Start the addon in development mode with hot reload for the server package:
```bash
pnpm run start:dev
```
Only changes to the **server** package benefit from hot reload. Changes to the **core** package require a rebuild:
```bash
pnpm -F core run build
```
Changes to the **frontend** package also require a rebuild:
```bash
pnpm -F frontend run build
```
***
Frontend Hot Reload [#frontend-hot-reload]
For full hot reload including the frontend, use the [rsbuild](https://rsbuild.dev) dev server. It serves the SPA with hot module replacement and proxies API requests through to the backend.
Point the frontend at the backend [#point-the-frontend-at-the-backend]
The dev server proxies every `/api` request to the backend. Create a `.env.local` file in `packages/frontend/` and set the backend's URL:
```bash
PUBLIC_BACKEND_BASE_URL=http://localhost:3000
```
Match the port to your backend's `PORT` (default `3000`). If you omit this, the proxy falls back to `http://localhost:3001`.
Start all packages with hot reload [#start-all-packages-with-hot-reload]
```bash
pnpm dev
```
This runs `core`, `server` and `frontend` together. The rsbuild dev server serves the SPA with hot reload on port `21456` (override with `PORT`) and proxies `/api` to the backend URL above.
Open the app [#open-the-app]
Open the dev server URL that rsbuild prints — `http://localhost:21456` by default.
If you're only testing backend changes, use `pnpm run start:dev` instead — it
avoids starting the frontend dev server unnecessarily.
***
Contributing [#contributing]
Contributions are welcome! Please open an issue or pull request on [GitHub](https://github.com/Viren070/AIOStreams). For questions or discussion, join the [Discord Server](https://discord.viren070.me/).
# Groups
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
The **Grouping** feature lets you control *when* different sets of your addons are asked for streams. This is useful for making AIOStreams faster by only querying fallback addons when your primary sources don't find enough results.
**Why use groups?**
* Make your fastest or most reliable addons run first.
* Only query slower or backup addons when the primary ones don't deliver.
* Avoid waiting for all addons every time when most results come from one or two.
***
How It Works [#how-it-works]
**Parallel fetch start** — AIOStreams begins fetching from **all groups**
simultaneously.
**Evaluate as results arrive** — as soon as Group 1 responds, AIOStreams
evaluates Group 2's condition:
* If the condition is `false`, Group 2 (and all subsequent groups) are skipped immediately.
* If `true`, Group 2's results are included and Group 3's condition is evaluated next.
**Early exit** — the moment a group's condition is `false`, all pending
results beyond that point are discarded.
**Example:** AIOStreams fires off fetches for Groups 1, 2, and 3 concurrently.
* Group 1 returns 3 streams → check Group 2's condition.
* If you set Group 2 to run only when Group 1 found fewer than 5 streams, and Group 1 returned 3: the condition is `true` → Group 2 runs.
* If Group 1 returned 6: the condition is `false` → Groups 2 and 3 are skipped.
***
Building Group Conditions [#building-group-conditions]
Group conditions are written using the [Stream Expression Language (SEL)](/reference/stream-expressions). The condition must evaluate to `true` or `false`.
Available constants [#available-constants]
| Constant | Description |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `previousStreams` | List of streams found by the last group that ran |
| `totalStreams` | List of all streams found by all groups so far |
| `queryType` | What is being searched for (`"movie"`, `"series"`, `"anime.series"`, `"anime.movie"`) |
| `previousGroupTimeTaken` | How long the last group took (milliseconds) |
| `totalTimeTaken` | Total time spent across all groups so far (milliseconds) |
The most useful function for group conditions is `count()`, which counts how many streams are in a list:
```
count(cached(previousStreams)) > 0
```
***
Examples [#examples]
**Run Group 2 if Group 1 found fewer than 3 streams:**
```
count(previousStreams) < 3
```
**Run Group 2 if Group 1 found no cached Real-Debrid streams:**
```
count(cached(service(previousStreams, 'realdebrid'))) == 0
```
**Run Group 3 for series only when all previous groups together found fewer than 2 1080p streams:**
```
queryType == "series" and count(resolution(totalStreams, '1080p')) < 2
```
**Run Group 2 if Group 1 found no 4K streams OR took longer than 5 seconds:**
```
count(resolution(previousStreams, '2160p')) == 0 or previousGroupTimeTaken > 5000
```
***
Tips [#tips]
* **Always return `true` or `false`:** A condition that evaluates to neither will cause the group (and subsequent groups) to be skipped.
* **`previousStreams` vs `totalStreams`:** Use `previousStreams` for logic based on the immediately preceding group; use `totalStreams` for cumulative logic across all groups run so far.
* **Start simple:** Begin with basic `count()` conditions and expand from there.
* **Errors skip groups:** A malformed condition causes the group to be skipped as if the condition returned `false`.
For the complete list of available functions, see the [SEL reference](/reference/stream-expressions).
# Release Blocklist
import { Callout } from 'fumadocs-ui/components/callout';
The release blocklist is an instance-wide filter of known-bad releases. When the built-in usenet engine proves a release is gone from every provider, it records a verdict; verdicts from your own instance and from any lists you subscribe to are then used to drop those releases from stream results before anything is fetched, and the whole pool can be shared with other instances.
Release keys [#release-keys]
Every verdict is keyed by a credential-free release identity that is identical on any server:
* **Torrents**: `btih:`.
* **Usenet, by release**: `wd1:`, a hash of the indexer-reported size, poster and posting day. Computable at search time, so it powers list-time filtering, but it needs indexer metadata: only Newznab-based addons provide it (the Prowlarr addon's API exposes neither the poster nor a precise date), and weak metadata (no size, or neither poster nor date) yields no key.
* **Usenet, by post**: `nh1:`, a hash of the NZB's segment message-ids. This is the exact identity of one post: it is only known once the NZB has been fetched and parsed, so it cannot filter search results, but it exists for *every* parsed NZB — including Prowlarr results and manual imports — and it fails a doomed resolve fast, before any provider is contacted.
A usenet verdict is recorded under both keys when both are known, and under `nh1:` alone when there is no indexer metadata. Keys carry no titles, URLs or account information, so lists are safe to publish.
Verdicts [#verdicts]
| Verdict | Meaning | Recorded by |
| ------------ | ----------------------------------------------------- | ------------------ |
| `dead` | Articles missing from usenet on every provider | Automatic + manual |
| `defective` | Downloads but unusable (corrupt, encrypted, no video) | Manual |
| `fake` | Deliberately wrong content | Manual |
| `mislabeled` | Wrong title, quality or language | Manual |
Only `dead` is recorded automatically, and only from all-provider article-miss evidence: import verification, the background census, or playback hitting its padding caps. Torrent verdicts are always manual; a debrid cache miss proves nothing about a torrent.
`fake` and `mislabeled` differ in what is wrong: a **fake** release's content is not what it claims to be at all (malware, spam, padded junk uploaded under a real release's name) and is never worth keeping, while a **mislabeled** release is real content with wrong metadata (the wrong episode, language or an upscale sold as 2160p) that might be fine under its true name.
Verdicts self-heal: when a release marked `dead` later imports or plays successfully, the local verdict is removed and an **override** is written that suppresses remote verdicts for that release. Overrides are visible (and clearable) on the dashboard.
Sources and trust [#sources-and-trust]
The **Blocklists** dashboard page manages the sources verdicts come from:
* **local** — this instance's own verdicts plus manual marks. Always present and always trusted.
* **remote** — a subscribed URL, refetched on its own interval.
* **imported** — an uploaded list. Imports never merge into your local list.
Each remote or imported source has a trust level that decides how much weight its verdicts carry:
* **full** (default for new subscriptions) — the source is believed outright: any release it flags is filtered, on that source's word alone. Use this for lists you chose deliberately and trust, such as another instance you run yourself or a curated community list. Your local source is always full trust.
* **corroborate** — the source only gets a vote. A release flagged by corroborate sources is filtered once at least the configured **quorum** of distinct corroborate sources (`RELEASE_BLOCKLIST_QUORUM`, default 2) all flag it; below the quorum its verdicts do nothing. Use this to hedge against a single list being wrong or malicious by requiring independent agreement. Note the arithmetic: with only one corroborate source and the default quorum of 2, that source never filters anything, so either add a second independent list or lower the quorum.
* **observe** — the source never filters. Its entries are stored, counted and visible in the entries browser, but have no effect on streams. Use this to evaluate an unknown list (what would it block? does it overlap with sources you already trust?) before promoting it to corroborate or full.
Full-trust sources ignore the quorum entirely; it only ever applies to corroborate sources.
Backbone scope [#backbone-scope]
A usenet release can be dead on one backbone and fine on another, so shared `dead` verdicts record which provider backbones observed them and are matched against yours (`RELEASE_BLOCKLIST_BACKBONE_SCOPE`):
* `overlap` (default) — the verdict applies if it was observed on at least one backbone you use.
* `covers` — the verdict must have been observed on every backbone you use.
* `off` — verdicts apply regardless of backbone.
Verdicts record the full provider hostnames that observed them. How those hosts are compared is set by `RELEASE_BLOCKLIST_BACKBONE_GROUPING`:
* `backbone` (default) — comparisons run through a built-in reseller map: a verdict from `news.newshosting.com` matches a provider on `news.usenetserver.com` (both Omicron). Right for takedowns, which remove an article from the backbone's shared storage for every reseller at once.
* `domain` — hosts only match within the same provider domain. Backbones enforce retention per reseller, so an old article can be expired on a short-retention brand yet intact on a full-retention one riding the same backbone; domain grouping keeps a verdict recorded on one brand from filtering releases for another, at the cost of far fewer shared verdicts applying to you. Consider it only if you stream old content on a long-retention provider.
In both modes exact hostnames win over root domains, so providers whose servers sit on different backbones per subdomain (bonus servers and the like) are told apart; other hosts fall back to their root domain, which keeps different server aliases of one provider together. Verdicts with no recorded backbone apply everywhere, and `RELEASE_BLOCKLIST_TRUSTED_BACKBONES` can whitelist additional backbones (root domains, hostnames or backbone ids — ids expand to their known provider domains under `domain` grouping).
Enforcement [#enforcement]
Flagged releases are removed from stream lists before deduplication and show up in the filter statistics as **Blocklisted**. If every stream in a response is flagged, everything is shown instead so a viewer is never left with an empty list.
At playback the two usenet keys behave differently, on purpose:
* `wd1:` verdicts from remote lists never block a resolve — they already filtered the stream out at list time, so an explicit play is treated as deliberate: the attempt either proves the release works (which retracts the verdicts) or records a first-hand failure. Only your local `wd1:` verdicts short-circuit.
* `nh1:` verdicts block the resolve itself (under the same trust, quorum and backbone rules), right after the NZB is parsed and before any provider is contacted. A post identity can't appear in search results, so resolve is its only enforcement point — and since it names one exact post, a trusted verdict leaves nothing worth re-checking. Failover simply moves on to the next stream.
Sharing your list [#sharing-your-list]
* **Export/import**: the dashboard exports NDJSON in the native format or in the Warden format used by nzbdav-based tools, and imports either transparently (gzipped files included). The Warden dialect has no verdict field and only understands `wd1:` fingerprints, so it carries only that `dead` subset; `nh1:` content-hash keys travel in the native format.
* **Public endpoint**: enable `RELEASE_BLOCKLIST_PUBLIC_EXPORT` to serve your list at `/blocklist/export`, which other instances (or Warden consumers) can subscribe to directly. Set `RELEASE_BLOCKLIST_PUBLIC_EXPORT_PASSWORD` to require `?key=` in the URL, and share the full URL only with instances you want to allow.
* **Publish targets**: if your instance is not publicly reachable (or you'd rather not hand out its URL), the **Publishing** section of the Blocklists page pushes your list to remote destinations instead, on a per-target interval and on demand. Consumers then subscribe to the destination's URL, never to your instance.
Publish targets [#publish-targets]
A target is a destination plus a set of files to upload: any combination of format (native or Warden) and scope (**local** — your own verdicts, the default — or **all** — everything, deduplicated). Unchanged lists are skipped, so an interval push of a quiet list costs nothing.
One provider is supported:
* **GitHub Gist** — leave the gist ID blank and the first push creates the gist (secret by default; note that secret gists are unlisted, not private — anyone with the URL can read them). The shareable `gist.githubusercontent.com/.../raw/...` URL is shown next to each file and always serves the latest revision.
The classic `gist` OAuth scope grants read/write access to *all* of your
gists. Prefer a GitHub fine-grained personal access token scoped to just the
gist account the target writes to. Tokens are stored encrypted with your
instance's `SECRET_KEY`.
Publishing the **all** scope rebroadcasts lists you merely subscribe to. To
anyone consuming both your list and the original, one upstream verdict then
looks like two independent confirmations, which quietly defeats
corroborate-trust quorums. Publish **local** unless you are deliberately
running an aggregator, and say so if you are.
A refresh that returns an empty or corrupt payload never wipes a source's
existing entries. To empty a source, clear or delete it on the dashboard.
# Scored Sorting
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
**Scored Sorting** is an advanced alternative to index-based "Preferred" sorting. Instead of a stream matching only the first applicable rule, scored sorting lets a stream match **multiple ranked rules** and accumulates a total score from each match — giving you fine-grained, quantitative control over your results.
***
How It Works [#how-it-works]
There are two types of ranked filters:
1. **Ranked Regex Filters** — assign scores based on regex patterns matching stream attributes like the filename.
2. **Ranked Stream Expressions** — use the [SEL](/reference/stream-expressions) to create complex conditional scoring logic.
Unlike Preferred filters (where a stream matches only the *first* rule), a stream can match **multiple** ranked rules and the scores are summed.
***
Choosing Your Approach [#choosing-your-approach]
| Approach | Best for |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Ranked Regexes only | Text-pattern-based scoring (release groups, keywords in filenames) |
| Ranked Stream Expressions only | Property-based scoring (bitrate, resolution, `isAnime`, etc.) |
| Both combined | Maximum flexibility; note that `regexScore` and `seScore` are separate scores — you must add both sort criteria |
| Combined with Preferred | Use Preferred for must-have streams; Ranked for scoring the rest |
**Advanced strategy:** Use Ranked Regexes with a score of `0` to "tag" streams, then use `regexMatched()` in Ranked Stream Expressions to apply conditional scores. This consolidates your final score into `{stream.seScore}`, requiring only one sort criterion.
***
Setup [#setup]
**Create ranked rules:**
Navigate to **Filters → Regex** or **Filters → Stream Expression**, scroll to the **Ranked** section, and add rules. Each rule needs:
* **Name** (optional but recommended for formatter display)
* **Expression or Regex** — the logic to evaluate
* **Score** — points to add (can be negative)
For SEL expressions, names are extracted from comments: `/* My Rule Name */ expression`
**Update your sort configuration:**
Go to **Sorting**, add `Stream Expression Score` and/or `Regex Score` criteria, set the order to **descending** (higher scores first), and position them appropriately in your sort list.
***
Formatter Variables [#formatter-variables]
Scores [#scores]
| Variable | Description |
| ---------------------- | ---------------------------------------------------------- |
| `{stream.regexScore}` | Raw Regex Filter score |
| `{stream.seScore}` | Raw Stream Expression score |
| `{stream.nRegexScore}` | Regex score normalised 0–100 (highest-scored stream = 100) |
| `{stream.nSeScore}` | SE score normalised 0–100 |
Matched rule names [#matched-rule-names]
| Variable | Description |
| ----------------------------- | ---------------------------------------------------------------------------------------- |
| `{stream.regexMatched}` | Name of the highest-ranking *Preferred* regex matched (falls back to first Ranked match) |
| `{stream.seMatched}` | Name of the first *Preferred* SE expression matched |
| `{stream.rankedRegexMatched}` | Array of all *Ranked* regex names matched |
| `{stream.rseMatched}` | Array of all *Ranked* SE expression names matched |
Star ratings [#star-ratings]
The `::star` and `::pstar` modifiers display a star rating from a 0–100 input — perfect for normalised scores:
```
{stream.nSeScore::pstar}
```
A normalised score of `75` displays: `★★★★☆`
***
Examples [#examples]
For a practical example using [TRaSH Guides](https://trash-guides.info/) recommendations, see:
* [Vidhin05/Releases-Regex](https://github.com/Vidhin05/Releases-Regex/)
# Seanime Extensions
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
AIOStreams comes with three extensions that integrate directly with Seanime.
This guide covers what each extension does and how to install it.
Extensions [#extensions]
AIOStreams Plugin [#aiostreams-plugin]
The recommended extension for most users. It uses Seanime's plugin system to add a dedicated results panel that
preserves all your AIOStreams sorting and formatting exactly as configured. You can open the panel from a variety of
triggers; by default it adds an **AIOStreams** tab alongside the standard **Torrent Streaming** / **Online Streaming** /
**Debrid Streaming** tabs.
Supports any URL or P2P-based stream (debrid and usenet included). Stremio NNTP streams are not supported.
Requires the Search API. [Instances](/getting-started/public-instances) that disable it (e.g. the **Public ElfHosted Instance**) cannot be used with this extension.
AIOStreams Torrent Provider [#aiostreams-torrent-provider]
Adds AIOStreams as a torrent provider inside Seanime's **Torrent Streaming** / **Debrid Streaming** tab. Only the
torrents surfaced by AIOStreams are used - any debrid configured in AIOStreams itself is ignored. Seanime's own debrid
support handles debrid playback.
Requires the Search API. [Instances](/getting-started/public-instances) that disable it (e.g. the **Public ElfHosted Instance**) cannot be used with this extension.
This extension was developed before the Plugin system supported torrent streaming. Now that it does, the Torrent
Provider may be deprecated in a future release. The **AIOStreams Plugin** is preferred.
Stremio Custom Source [#stremio-custom-source]
Exposes a Stremio addon catalogue as a Seanime custom source — useful for anime catalogues as well as standard
movie/series ones. It does not provide streams on its own; pair it with the **AIOStreams Plugin**, which can serve
streams for any custom source.
***
Installation [#installation]
Configure AIOStreams as normal (e.g. using [the setup guide](/configuration/setup/)).
On the **Save & Install** page, under the **Other Apps** section, click **Seanime**.
Then follow the steps for your extension:
Copy the **Manifest URL** shown at the top of the Seanime pop-up.
In Seanime, go to **Extensions** → **Add Extensions**. Paste the URL next to **Install from URL** and click **Find**.
Once found, install it and then click **Grant** → **Grant permissions** to approve the required permissions.
To adjust settings, click the **Preferences** icon (to the left of the three-dot menu) on the extension card.
Available options include preferred player (built-in, desktop, or external link), search ID (IMDb or Kitsu),
auto-play behaviour, and which triggers to show.
Copy the **Manifest URL** for the `AIOStreams Torrent Provider` shown on the Seanime pop-up.
In Seanime, go to **Extensions** → **Add Extensions**. Paste the URL next to **Install from URL** and click **Find**, then install.
A single Custom Source Extension in Seanime is limited to one list, so each catalogue gets its own extension instance.
Under **Stremio Custom Source**, click **Configure**.
Enter a Stremio addon manifest URL to import catalogues from. If your catalogues are already in AIOStreams, click
**Continue** without changing the pre-filled URL. Otherwise, replace it with another addon's manifest URL (e.g. your
AIOMetadata URL) before continuing.
Review the detected catalogues, select the ones you want, optionally rename them, then click **Continue with X catalogues**.
Copy the resulting JSON (in the format `{"urls": ["url1", "url2", ...]}`).
In Seanime, go to **Extensions** → **Add Extensions**. Paste the JSON to the left of **Import from repository** and
click **Import all**. Scroll to the bottom and click **Install all**.
If you want to uninstall a lot of these custom sources at once, rather than doing this individually in Seanime, which can become quite
cumbersome, you can delete the relevant `stremio-custom-source-******.json` files under the `extensions` folder in your [Seanime data directory](https://seanime.app/docs/config#data-directory).
# SSO / OIDC Login
import { Callout } from 'fumadocs-ui/components/callout';
AIOStreams can delegate operator login to any OpenID Connect provider — Pocket ID, Authelia, Authentik, Keycloak, Entra ID, Google, and so on.
Instead of maintaining a second set of credentials in `AIOSTREAMS_AUTH`, operators sign in with the identity they already have, and what they are allowed to do is decided by a claim you map, usually group membership.
What SSO covers [#what-sso-covers]
SSO replaces the **operator login** only. That is the identity behind:
* the **dashboard** at `/dashboard`
* the **configuration page** when `AIOSTREAMS_AUTH_REQUIRED` is on
**No accounts are created.** Unlike most applications with SSO, AIOStreams
does not provision a user on first sign-in. A successful login issues a
session cookie and nothing else: no user record, no profile, nothing stored
against your identity. There is nothing to deprovision, and no "registration"
to disable.
Access is decided entirely by the group mapping below, and it **denies by
default** — an identity matching no group is refused. So the restrictive state
is the one you start in; you open it up per group, rather than closing it down
after the fact.
**SSO does not affect Stremio addon URLs.** Stremio fetches
`/stremio///manifest.json` as a headless HTTP client — there is
no browser, no cookie jar, and no way to follow a login redirect. That URL
remains a bearer credential regardless of how you sign in to the dashboard.
Stremio users, their passwords and their encrypted configs are a completely
separate identity system and are untouched by SSO.
**SSO is not a lock on your instance.** Whether strangers can create
configurations is governed by `AIOSTREAMS_AUTH_REQUIRED`, and by nothing else.
With that off, anyone who can reach `/stremio/configure` can create one,
signed in or not, exactly as they could before you enabled SSO. Enabling SSO
does not change it.
If you want the configuration page private, turn `AIOSTREAMS_AUTH_REQUIRED` on
as well. SSO then decides who may pass that gate; on its own it only decides who
may reach the dashboard.
There is one more limit worth planning around. Several features authenticate with HTTP Basic credentials, which an SSO identity does not have:
| Feature | Works with SSO |
| ------------------------------------------------------ | ------------------------------------ |
| Dashboard | Yes |
| Configuration page (`AIOSTREAMS_AUTH_REQUIRED`) | Yes |
| Built-in proxy | No, needs an `AIOSTREAMS_AUTH` entry |
| Built-in usenet engine (streaming and its SABnzbd API) | No, needs an `AIOSTREAMS_AUTH` entry |
Most operators keep one `AIOSTREAMS_AUTH` user for those, which conveniently doubles as the recovery account if SSO ever breaks.
Setup [#setup]
Every setting lives under **Dashboard → Settings → SSO / OIDC** and applies without a restart, so the dashboard is the easier place to get this working. Each one also has an environment variable if you prefer to declare your instance in a compose file.
**Register AIOStreams with your provider.** The redirect URI is your base URL plus the callback path:
```
/api/v1/auth/oidc/callback
```
For example, `https://aiostreams.example.com/api/v1/auth/oidc/callback`. It must match exactly, including the scheme.
Registering produces the **client ID** and **client secret** you will need in the next step.
Some may let you choose a client ID, while others generate one for you.
Whatever your provider ends up showing is the value AIOStreams needs.
**Point AIOStreams at the provider.** Only four settings are required. The issuer is usually the provider's root URL; `/.well-known/openid-configuration` is discovered automatically.
Copy the issuer exactly as your provider states it, **including any trailing slash**.
| Setting | Value |
| ---------------- | --------------------------------------------------------- |
| Enable SSO login | on |
| Issuer URL | your provider's root URL, e.g. `https://auth.example.com` |
| Client ID | the client ID from step 1 |
| Client secret | the client secret from step 1 |
```bash
AIOSTREAMS_OIDC_ENABLED=true
AIOSTREAMS_OIDC_ISSUER=https://auth.example.com
AIOSTREAMS_OIDC_CLIENT_ID=
AIOSTREAMS_OIDC_CLIENT_SECRET=
```
**Request the group claim.** Most providers only include group membership when you ask for it, so add `groups` to **Scopes**:
Set **Scopes** to `openid`, `profile`, `email`, `groups`.
```bash
AIOSTREAMS_OIDC_SCOPES='openid profile email groups'
```
The default omits `groups` because Google and Entra ID reject it as an unknown scope. If your provider exposes groups under a different claim, change **Groups claim**. If it has no groups at all, skip ahead to [Providers that have no groups](#providers-that-have-no-groups).
**Map groups to permissions.** Nothing is granted until you do:
Under **Group permissions**, add one row per group:
| Group | Permissions |
| ------------------- | ---------------- |
| `aiostreams-admins` | `admin` |
| `media-team` | `proxy\|sabnzbd` |
```bash
AIOSTREAMS_OIDC_GROUP_PERMISSIONS='aiostreams-admins=admin,media-team=proxy|sabnzbd'
```
Group names containing `,` or `=`, such as LDAP distinguished names, need the JSON form instead:
```bash
AIOSTREAMS_OIDC_GROUP_PERMISSIONS='{"cn=admins,ou=groups,dc=example,dc=com":"admin"}'
```
A "Sign in with SSO" button then appears on the login page.
Providers that have no groups [#providers-that-have-no-groups]
Some providers send no group claim at all. Google is the common one: a consumer Google account has only `sub`, `email`, `email_verified`, `name` and `picture`. There is no `groups`, and there is no scope you can add to get one.
Follow the steps above with such a provider and **nobody can sign in, including you**. Nothing matches, so everything is refused. That is the safe failure, but it is a dead end.
**Do not fix this with Default permissions.** Setting it to `admin` is the
obvious-looking escape and it is the one genuinely dangerous configuration in
this guide: it admits every identity the provider will authenticate. With
Google, that is every Google account in existence, as an administrator of your
instance.
The fix is to map a claim that identifies the person instead. **Groups claim** simply names which claim carries the values you map — nothing requires it to be group membership. Point it at `email` and the mapping becomes an allowlist:
| Setting | Value |
| ------------------- | ------------------------- |
| Username claim | `email` |
| Groups claim | `email` |
| Group permissions | `you@gmail.com` → `admin` |
| Default permissions | leave empty |
```bash
AIOSTREAMS_OIDC_USERNAME_CLAIM=email
AIOSTREAMS_OIDC_GROUPS_CLAIM=email
AIOSTREAMS_OIDC_GROUP_PERMISSIONS='you@gmail.com=admin'
# Default permissions stays empty
```
Listed addresses are admitted; every other account is refused exactly as before. Deny-by-default is preserved, and you have not had to widen anything.
For Google Workspace, map the `hd` (hosted domain) claim instead to admit your whole organisation and nobody else:
```bash
AIOSTREAMS_OIDC_GROUPS_CLAIM=hd
AIOSTREAMS_OIDC_GROUP_PERMISSIONS='mycompany.com=admin'
```
Note that with these providers you must also change **Username claim**, since there is no `preferred_username` to fall back on. Leaving it at the default gives every login `oidc_claims_invalid`.
Permissions [#permissions]
Permissions are resolved from the group claim at login and stored in the session. Valid values are `admin`, `createConfig`, `proxy`, `service` and `sabnzbd`, combined with `|`, or `none` for a login with no permissions. `admin` implies all of them.
| Permission | Grants |
| ----------------------------- | -------------------------------------------------------------------- |
| `admin` | Everything, including the dashboard |
| `createConfig` | Creating new configurations |
| `proxy`, `service`, `sabnzbd` | The matching Basic-auth features, which an SSO identity cannot reach |
Editing an existing configuration needs no permission — it is already gated by knowing the configuration's own password.
**An identity that matches no group is refused.** This is deliberate. Without
a mapping there is nothing to fall back on except the behaviour that grants
every unlisted user full admin, which would hand instance administration to
everyone in your directory the moment you enable SSO.
**Default permissions** (`AIOSTREAMS_OIDC_DEFAULT_PERMISSIONS`) is the fallback for an identity that matched nothing, and it applies **only** in that case. It is empty by default, which is what makes an unmatched identity refused rather than admitted.
Setting it to `admin` inverts the model: every identity your provider will authenticate becomes an instance admin. That is only safe when the provider itself decides who may reach AIOStreams — true of a private Authelia or Keycloak, and emphatically not true of Google or any other provider the public can sign up to.
Login-only access [#login-only-access]
`none` is a permission spec in its own right, and it is how you express "may sign in and use what they already have, may not administer anything". Such a session reaches the configuration page and can edit an existing configuration, but cannot create a new one and is refused everywhere `admin` is required, including the whole dashboard.
Grant `createConfig` instead if they should also be able to make new configurations.
| Setting | Result for an unmatched identity |
| -------------------------- | -------------------------------- |
| Default permissions empty | Refused, no session |
| Default permissions `none` | Admitted, no permissions |
| A group mapped to `none` | Admitted, no permissions |
This matches what `none` already does in `AIOSTREAMS_AUTH_PERMISSIONS`, so a login-only user behaves the same whether they arrive by password or SSO.
Today `admin` and `createConfig` are the permissions that change what a
signed-in session can do, because they are what the dashboard and
configuration creation check. Mapping a group to `proxy`, `service` or
`sabnzbd` is recorded on the session but has no effect, since those
features authenticate with Basic credentials rather than a session.
Because permissions are resolved at login, a group change in your provider takes effect on the user's next sign-in, not immediately. Sessions last as long as **Session lifetime** (`SESSION_TTL_SECONDS`), 24 hours by default.
Provider setup [#provider-setup]
Under **Settings → OIDC Clients → Add OIDC Client**:
* Callback URLs: `https://aiostreams.example.com/api/v1/auth/oidc/callback`
* Public Client: off, so a client secret is issued
The client ID and secret are shown once the client is saved. The issuer is your Pocket ID URL, exactly as its `APP_URL` is set:
```
https://id.example.com
```
Pocket ID supports the `groups` scope and emits both `preferred_username` and `groups`, so no claim settings need changing.
A new client admits nobody until you say otherwise. In Pocket ID's words, "no
user groups and therefore no users are allowed to access the client". Either
press **Unrestrict** to allow every user, or list the groups you mapped under
**Allowed User Groups**. Until you do, sign-in comes back as `oidc_denied`.
Add a client under `identity_providers.oidc.clients`:
```yaml
identity_providers:
oidc:
clients:
- client_id: aiostreams
client_name: AIOStreams
client_secret: '$pbkdf2-sha512$310000$...' # hashed, see below
public: false
authorization_policy: two_factor
require_pkce: true
pkce_challenge_method: S256
consent_mode: pre-configured
pre_configured_consent_duration: 1 month
redirect_uris:
- 'https://aiostreams.example.com/api/v1/auth/oidc/callback'
scopes: [openid, profile, email, groups]
userinfo_signed_response_alg: none
```
Authelia stores client secrets hashed. Generate the digest with:
```bash
docker run --rm authelia/authelia:latest \
authelia crypto hash generate pbkdf2 --variant sha512 --password ''
```
Give AIOStreams the **plaintext** secret and Authelia the digest. The issuer is your Authelia root URL, and groups come from the user's `groups` list in the file or LDAP backend.
`consent_mode` decides how often the permissions screen appears. There are 3 modes:
* `explicit` asks every time, which is the default. This means signing out of AIOStreams and back in shows it again each time.
* `pre-configured` asks once and remembers the answer for `pre_configured_consent_duration`, as long as the user ticks the remember option on that first screen.
* `implicit` never asks at all.
Create an **OAuth2/OpenID Provider**:
* Authorization flow: `default-provider-authorization-implicit-consent`, or `default-provider-authorization-explicit-consent` for a permissions screen
* Client type: **Confidential**
* Redirect URI: `https://aiostreams.example.com/api/v1/auth/oidc/callback`
* Scopes: the `openid`, `profile` and `email` mappings, plus a mapping that emits `groups`
Then create an Application bound to it. The issuer is shown on the provider page and looks like:
```
https://authentik.example.com/application/o//
```
Authentik often exposes `groups` only from the userinfo endpoint rather than in the ID token. AIOStreams handles that automatically — if the configured username or groups claim is missing from the ID token, it fetches userinfo and merges the result.
Under **Clients → Create client**:
* Client authentication: **On**
* Authentication flow: **Standard flow**
* Valid redirect URIs: `https://aiostreams.example.com/api/v1/auth/oidc/callback`
Take the secret from the **Credentials** tab. Keycloak does not emit groups by default — add a mapper under **Client scopes → \-dedicated → Add mapper → Group Membership**:
* Token Claim Name: `groups`
* **Full group path: Off** — otherwise groups arrive as `/admins` rather than `admins` and your mapping will not match
* Add to ID token and userinfo: On
The issuer is:
```
https://keycloak.example.com/realms/
```
In the [Google Cloud Console](https://console.cloud.google.com), create a project, then:
* **OAuth consent screen** — user type **External** (or **Internal** if you have Workspace, which restricts sign-in to your organisation)
* **Credentials → Create credentials → OAuth client ID → Web application**
* Authorized redirect URI: `https://aiostreams.example.com/api/v1/auth/oidc/callback`
The issuer is `https://accounts.google.com`. Google supports PKCE and discovery, so nothing else needs configuring on their side.
Google sends no groups, so configure AIOStreams as an email allowlist rather than a group mapping — see [Providers that have no groups](#providers-that-have-no-groups) above. In short:
```bash
AIOSTREAMS_OIDC_ISSUER=https://accounts.google.com
AIOSTREAMS_OIDC_SCOPES='openid email profile'
AIOSTREAMS_OIDC_USERNAME_CLAIM=email
AIOSTREAMS_OIDC_GROUPS_CLAIM=email
AIOSTREAMS_OIDC_GROUP_PERMISSIONS='you@gmail.com=admin'
```
Leaving the consent screen in **Testing** adds a second layer: only accounts
listed as test users can complete the flow, up to 100 of them. Testing mode
expires refresh tokens after seven days, which does not affect AIOStreams
since it never uses one.
Do not rely on that alone. Google prompts you to publish the app, and publishing
removes the test-user list without touching anything else. The email mapping is
the layer that does not quietly disappear.
Usernames [#usernames]
The username comes from **Username claim** (`AIOSTREAMS_OIDC_USERNAME_CLAIM`), `preferred_username` by default.
It is not displayed as your identity anywhere in the UI, so it mostly matters after the fact. It identifies you in the server log, which means it is visible on the dashboard **Logs** page against entries like `settings updated`, `user deleted` and `all cache cleared`, and it is recorded as **Added by** on NZBs you add from the usenet dashboard.
Library entries that arrive through the SABnzbd API instead show the
`AIOSTREAMS_AUTH` user that Sonarr or Radarr authenticated with. That path
uses Basic credentials and has no session, so it can never show an SSO name.
Keycloak and Authentik let users change their own `preferred_username`. If
yours does, set the claim to `sub` instead: it is immutable and always
present. Otherwise a rename splits a user's audit history and frees the old
name for someone else to take.
If an SSO username collides with an `AIOSTREAMS_AUTH` user, **the login is refused** rather than silently merged. The two identities would otherwise be indistinguishable in exactly the places above, so anyone who can set a `preferred_username` in your directory could have their actions logged as one of your local operators.
Using one identity for both [#using-one-identity-for-both]
You will usually want an `AIOSTREAMS_AUTH` entry anyway, for the proxy and the usenet engine. Turning on **Link to local users by username** (`AIOSTREAMS_OIDC_LINK_BY_USERNAME`) makes a matching SSO identity *be* that user rather than colliding with it, so `alice` is one person with two ways to sign in.
A linked session takes its permissions from `AIOSTREAMS_AUTH_PERMISSIONS`, and the group mapping is **not** consulted for it. That is the point: the person is configured once rather than once per login method.
This is off by default because it turns a username into proof of identity. Only
enable it if you control who can authenticate at your provider *and* who can
change their own username there — a self-service `preferred_username` at
Keycloak or Authentik is not proof of anything. Google's `email` is, since
Google verifies it.
Because permissions are linked too, an impersonator would inherit the local
user's full set, not just group-derived ones.
If you cannot make that assumption, set **Username prefix** (`AIOSTREAMS_OIDC_USERNAME_PREFIX`) to something like `sso:` instead, which makes collisions structurally impossible.
| Setting | Behaviour on a username collision |
| ------------------------------- | ----------------------------------------------------------- |
| Default | Refused |
| Link to local users by username | Same person, permissions from `AIOSTREAMS_AUTH_PERMISSIONS` |
| Username prefix set | Cannot occur |
Locking it down [#locking-it-down]
Once SSO works you can retire password login and send users straight to the provider:
| Setting | Environment variable | Effect |
| --------------------------------------- | ----------------------------------------- | -------------------------------- |
| Allow local login → off | `AIOSTREAMS_OIDC_ALLOW_LOCAL_LOGIN=false` | Hides the username/password form |
| Redirect to provider automatically → on | `AIOSTREAMS_OIDC_AUTO_REDIRECT=true` | Skips the login page entirely |
**Recovery.** Setting both means a provider outage or a bad group mapping
locks everyone out. Three ways back in, in increasing order of force:
1. `/login?local=1` suppresses the automatic redirect, so you can reach the
login page at all. The password form still only appears if local login is
enabled.
2. Set `AIOSTREAMS_OIDC_ALLOW_LOCAL_LOGIN=true` in the environment and restart.
3. Set `AIOSTREAMS_OIDC_ENABLED=false` in the environment and restart.
Steps 2 and 3 are environment variables rather than dashboard settings on
purpose: the environment always wins, so nothing stored in the database can
override them, and neither can a dashboard you are locked out of. Keep at least
one `AIOSTREAMS_AUTH` user for the same reason. You will need one for the proxy
and the usenet engine anyway.
Troubleshooting [#troubleshooting]
Failures return you to the login page with an `?error=` code and a toast. The precise cause is always in the server log.
| Code | Meaning | Usual fix |
| ------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oidc_disabled` | SSO is off | Turn on **Enable SSO login** |
| `oidc_not_configured` | Enabled without an issuer or client ID | Set both |
| `oidc_discovery_failed` | Provider unreachable, or its issuer does not match the configured one | Check the issuer URL and that AIOStreams can resolve it. If the log says the issuer does not match, copy the issuer the provider reports verbatim, including any trailing slash |
| `oidc_state_invalid` | Login expired or the state cookie was missing | Retry; if it always happens, check that the browser is not blocking cookies |
| `oidc_denied` | Provider refused the request | Usually the user cancelled consent, or the client is not authorised for them |
| `oidc_exchange_failed` | Provider rejected the code | Check the client secret and that the redirect URI matches exactly |
| `oidc_claims_invalid` | No usable username claim | Check **Username claim** against what your provider emits. Google and other providers with no `preferred_username` need it set to `email` or `sub` |
| `oidc_username_conflict` | Collides with a local user | Turn on **Link to local users by username** if they are the same person, otherwise set a **Username prefix** |
| `oidc_no_permissions` | Nothing matched | Check **Group permissions**, and that the claim you are mapping is actually arriving |
For `oidc_no_permissions`, the log line records the username **and the values the provider actually sent** for the claim you are mapping. The two usual causes are an empty list because the `groups` scope was not requested, and a provider that sends no groups at all — see [Providers that have no groups](#providers-that-have-no-groups).
One more note:
* **HTTP issuers are refused.** If your provider is on plain HTTP inside a trusted network, turn on **Allow insecure (http) issuer** (`AIOSTREAMS_OIDC_ALLOW_INSECURE_REQUESTS`). The client secret and tokens then travel in the clear.
# Usenet
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
AIOStreams has first-class Usenet support: addons that search your indexers, and playback services — including a fully built-in NNTP streaming engine — that turn the results into streams on any device.
This guide is organised as a journey: understand [how it works](#how-usenet-streaming-works), [pick a playback service](#1-pick-a-playback-service) and [set it up](#2-set-up-your-service), [add your indexers](#3-add-your-indexers), then [make playback resilient](#4-make-it-resilient-failover) and get familiar with [NZB grabs and indexer rules](#5-nzb-grabs-and-indexer-rules).
How Usenet Streaming Works [#how-usenet-streaming-works]
Usenet in AIOStreams is built on a simple foundation with two halves:
1. **Addons** find content — they search your indexers and return NZB results.
2. **A service** plays it — it turns an NZB into a video stream your player can handle.
Every setup in this guide is some combination of the two: pick a playback service, then feed it results from one or more addons.
Usenet in 60 Seconds [#usenet-in-60-seconds]
Skip ahead if you already know your backbones from your indexers.
* **Providers** sell access to Usenet servers (spoken to over the NNTP protocol). Content is stored as millions of small **articles** (segments), and each provider sits on a **backbone** — providers on the same backbone share the same underlying storage.
* **Indexers** catalogue that content and hand out **NZB** files. An NZB is essentially a map: it lists the articles that make up a release.
* Retention and takedowns differ per backbone, so a release can be complete on one backbone while **missing articles** on another. Adding a provider from a *different* backbone gives you a second chance at finding every article.
The [Usenet Tree](https://usenet.rexum.space/tree) shows which providers sit on which backbone, and the same site tracks current [provider deals](https://usenet.rexum.space/deals).
**Don't overdo providers for streaming.** Every extra provider is another
place to check when an article is missing — and pre-playback, that costs
time. A two-backbone combination (commonly **Omicron + Abavia**) already
covers the vast majority of content; if an article is missing on both, a
stack of smaller backbones rarely saves it and
[failover](#4-make-it-resilient-failover) to a different release is usually
the better option.
1. Pick a Playback Service [#1-pick-a-playback-service]
There are three flavours of Usenet playback in AIOStreams:
1. **NNTP streaming** — something downloads articles from your provider and converts them into an HTTP stream on the fly. That something is either **AIOStreams itself** (the [built-in engine](#the-built-in-engine-aiostreams)), an [external self-hosted tool](#nzbdav-altmount-and-stremthru-newz) (NzbDAV, AltMount, StremThru Newz), or — uniquely — the **Stremio client itself** ([Stremio NNTP](#stremio-nntp)). You bring your own provider account(s) and indexers.
2. **Easynews** — a provider with its own web search whose results are direct HTTP URLs served from Easynews' servers. No extra software, no hosting; accessed through the [Easynews Search addon](#easynews).
3. **TorBox** — AIOStreams hands TorBox an NZB URL; TorBox downloads the whole release to *their* servers, after which it is "cached" and plays back instantly from them, on any device. See [TorBox](#torbox).
All of the NNTP streamers pull articles straight from your provider — the difference is which piece of software does the streaming, and therefore where it has to run:
| Service | How it plays | Streaming runs on | You need |
| -------------------------------------- | ---------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **AIOStreams** (built-in engine) | Streams over NNTP from your provider | Your AIOStreams instance | Your own instance (self-hosted or a private [ElfHosted](#zero-setup-the-elfhosted-bundle) one), a provider, indexers |
| **NzbDAV / AltMount / StremThru Newz** | Streams over NNTP from your provider | The tool's server | A self-hosted tool, a provider, indexers |
| **Stremio NNTP** | Streams over NNTP from your provider | Inside the Stremio client | Stremio Desktop v5 or Android TV, a provider, indexers |
| **TorBox** | Full download to TorBox first, then instant playback | TorBox's servers | A TorBox Pro plan |
| **Easynews** | Direct HTTP from Easynews | Easynews' servers | An Easynews subscription |
**Which one?** If you run your own AIOStreams instance (or a private
ElfHosted one), the **built-in engine** is the simplest full-control option —
no extra software to install. If you can't host anything, **TorBox Pro** or
**Easynews** work from any device, and **Stremio NNTP** works with just a
provider if you're on a supported Stremio client.
2. Set Up Your Service [#2-set-up-your-service]
Jump straight to the service you picked. Every path ends the same way: once the service is configured, continue to [Add Your Indexers](#3-add-your-indexers).
The Built-In Engine (AIOStreams) [#the-built-in-engine-aiostreams]
AIOStreams ships its own NNTP streaming engine: give it your provider details and it downloads, unpacks and serves releases as HTTP streams directly from your instance — no NzbDAV, AltMount or any other companion software.
It appears in your configuration as the **AIOStreams** service (results tagged `AIO`), and requires your own instance — self-hosted, or a private [ElfHosted](#zero-setup-the-elfhosted-bundle) instance.
Dashboard Setup (once per instance) [#dashboard-setup-once-per-instance]
Set the `AIOSTREAMS_AUTH` environment variable with one or more
`username:password` pairs (e.g. `AIOSTREAMS_AUTH=alice:secret`). Optionally
control what each user may do with `AIOSTREAMS_AUTH_PERMISSIONS` (valid
permissions: `admin`, `proxy`, `service`, `sabnzbd`; users not listed
default to `admin`, which implies everything). Configuring the engine
requires `admin`; streaming through it requires `service`.
Log in to the dashboard (with a user that has the `admin` permission) and
open **Usenet → Providers** (`/dashboard/usenet`).
Add your NNTP provider(s): name, host, port, SSL/TLS, username, password
and max connections (respect your provider's connection limit). The list
order sets priority, and a provider can be marked as **Backup** so it is
only consulted when the primary providers are missing articles.
The **Settings** section holds the engine's tuning knobs (performance
profile, cache sizes, timeouts, …). The defaults are right for almost
everyone — leave them alone unless you have a reason not to. Each setting
is documented in the [Usenet environment
variables](/configuration/environment-variables#usenet).
User Setup (configuration page) [#user-setup-configuration-page]
Go to
**Services**
and add the
**AIOStreams**
service.
Enter the **AIOStreams Auth Token**: a `username:password` pair from
`AIOSTREAMS_AUTH` (the user needs the `service` permission).
[Add your indexers](#3-add-your-indexers) — their results play through the
engine automatically.
Streams are served directly from your instance under your `BASE_URL` — there is no separate WebDAV server to expose, no credentials embedded in URLs, and no proxy configuration needed.
The dashboard's **Usenet** menu also gives you a **Library** of imported
NZBs, a live **Streams** view and transfer **Stats**. The engine additionally
exposes a SABnzbd-compatible API (`/api/v1/sabnzbd`), so tools like Sonarr,
Radarr and Prowlarr can send NZBs to it as if it were a SABnzbd download
client.
Stremio NNTP [#stremio-nntp]
The one NNTP option that needs no server at all: here, the tool that converts Usenet articles into video **is the Stremio client itself**. AIOStreams sends your provider details and the NZB location with the stream, and Stremio connects to your provider and streams natively.
Only supported by **Stremio Desktop v5** and **Stremio Android TV**. Other
clients cannot play these results.
**Setup:**
In AIOStreams, go to
**Services**
and add
**Stremio NNTP**
.
Fill in your NNTP server details: host, port, username, password, SSL
toggle and connections (the default of `5` is fine).
Usenet addons now show results tagged `SN`. Playing one triggers native
Stremio streaming.
**Drawbacks:**
* Limited to the two clients above.
* Harder to debug — streaming happens entirely inside Stremio.
* [Failover](#4-make-it-resilient-failover) cannot protect these streams, since playback never touches AIOStreams.
TorBox [#torbox]
TorBox works differently from the NNTP streamers: AIOStreams passes an NZB URL to TorBox, TorBox downloads the **entire release to their servers**, and once complete it becomes *cached* — from then on it plays back instantly from TorBox, on any device. Usenet downloads on TorBox are generally fast and reliable compared to torrents.
Usenet on TorBox requires their **Pro** plan, which also bundles internal indexers (via [TorBox Search](#torbox-search-caveats)) and [an NNTP server](#the-torbox-nntp-server) — so it can be a zero-hosting, all-in-one option.
**Setup:**
Obtain a
**TorBox Pro**
account.
In AIOStreams, go to
**Services**
and add your TorBox API key.
Add Usenet addons — TorBox Search for their internal indexers, or Newznab
and friends for your own.
When using your own indexers with TorBox (not their internal ones), results
appear uncached even if another TorBox user has already cached the same file.
Internal TorBox indexer results do show as cached when previously downloaded.
Cache and Play [#cache-and-play]
Because TorBox must finish downloading before playback, an uncached result normally returns a short "downloading, come back later" video — you'd retry once it's cached. **Cache and Play** (under **Services → Built-in → Cache and Play**) changes that: AIOStreams polls TorBox until the download completes, then serves the playback link directly, so your player just buffers a little longer and starts on its own.
Cache and Play is limited by your player's timeout. The Stremio iOS app, for
example, gives up after \~30 seconds — if the download takes longer, restart
the stream manually once it's cached.
(The setting also applies to uncached debrid downloads, but it shines with Usenet since downloads usually finish quickly.)
TorBox Search Caveats [#torbox-search-caveats]
TorBox's Search API returns both torrent and Usenet results. You can use it through the dedicated **TorBox Search** addon, or point the built-in **Newznab**/**Torznab** addons at its Newznab/Torznab endpoints. Two important caveats either way:
**The TorBox Search API is IP-whitelisted** — it only responds to approved
IPs. In practice it will not work from a self-hosted AIOStreams instance;
you need to use one of the trusted public instances.
**TorBox Search Usenet results only work with the TorBox service.** The "NZB
URLs" it returns are not real NZB downloads — only TorBox can internally
resolve them and start a download. No other service or tool can fetch an NZB
from them, and that includes NNTP tools configured with TorBox's own NNTP
server.
The TorBox NNTP Server [#the-torbox-nntp-server]
TorBox Pro also includes access to a TorBox **NNTP server**. That makes it usable as a regular provider with any NNTP-based service — the built-in engine, NzbDAV, AltMount, StremThru Newz or Stremio NNTP. It appears to sit on the **Abavia** backbone, so pairing it with an Omicron provider follows the two-backbone advice from [the primer](#usenet-in-60-seconds).
NzbDAV, AltMount and StremThru Newz [#nzbdav-altmount-and-stremthru-newz]
Before the built-in engine existed, streaming NNTP through AIOStreams meant self-hosting a companion tool, and these remain fully supported:
* [**NzbDAV**](https://github.com/nzbdav-dev/nzbdav)
* [**AltMount**](https://github.com/javi11/altmount)
* [**StremThru**](https://github.com/MunifTanjim/stremthru) (the **StremThru Newz** service)
Like the built-in engine, these stream over NNTP straight from your provider — they mount NZBs as a virtual filesystem and expose a WebDAV server (or equivalent) that AIOStreams wraps into streamable URLs. The stream URL your player hits is served by the tool itself (unless you [proxy it through AIOStreams](#proxying-streams-through-aiostreams)). Choose one of these over the built-in engine if you already run it for other tooling, or want the Usenet machinery on a separate box from AIOStreams.
For free self-hosting, follow the [Oracle VPS
guides](https://guides.viren070.me/selfhosting) and the [Docker Compose
template](https://github.com/Viren070/docker-compose-template), which
includes pre-configured setups for all required software. Prefer managed?
See the [ElfHosted bundle](#zero-setup-the-elfhosted-bundle).
If you run several of these concurrently, be mindful of your provider's connection limits.
The NzbDAV and AltMount integrations were inspired by the
[UsenetStreamer](https://github.com/Sanket9225/UsenetStreamer) addon, which
was the first to serve NzbDAV streams through a Stremio addon.
Set Up the Tool [#set-up-the-tool]
Register your admin user on first launch.
Go to
**Settings → Usenet**
and add your Usenet provider.
Go to the
**SABnzbd**
tab and note the API key.
In the **WebDAV** tab, set a WebDAV username and password (these are
separate from your admin login).
Complete the onboarding to create your admin user.
Go to **Configuration → WebDAV** and set your username and password.
In
**NNTP Providers**
, add your Usenet provider details.
In **SABnzbd API**, turn **Enable SABnzbd API** ON. Do **not** change
the `Complete` directory (must stay `/complete`).
Go to the
**System**
tab and note your API key.
Install and run StremThru following the [official
documentation](https://docs.stremthru.13377001.xyz/getting-started/installation).
Set
[`STREMTHRU_AUTH`](https://docs.stremthru.13377001.xyz/configuration/#stremthru-auth)
and
[`STREMTHRU_VAULT_SECRET`](https://docs.stremthru.13377001.xyz/configuration/#stremthru-vault-secret)
in your environment.
Configure your Usenet providers in the StremThru dashboard (`/dash`)
under **Usenet → Servers**.
Do **not** configure your indexers in StremThru — that is only for
using StremThru standalone. With AIOStreams, add your indexers via the
[Newznab addon](#3-add-your-indexers) as usual. (If you'd rather
centralise indexers in StremThru anyway, take each indexer's Newznab
URL from the StremThru dashboard and add each one as a separate Newznab
addon.)
Add the Service in AIOStreams [#add-the-service-in-aiostreams]
Go to **Services** and add **NzbDAV**, **AltMount** or **StremThru Newz**.
Enter the required details.
* **NzbDAV / AltMount:** API key, WebDAV username, WebDAV password.
* **StremThru:** StremThru URL and Auth Token — a `user:password` string
(optionally base64-encoded) from your `STREMTHRU_AUTH` value.
Configure the
**URL**
and
**Public URL**
fields (see below).
URL vs. Public URL [#url-vs-public-url]
This is the most critical configuration step for NzbDAV and AltMount.
| Field | Purpose |
| -------------- | ---------------------------------------------------------------------------- |
| **URL** | Address AIOStreams uses to talk to the tool's API |
| **Public URL** | Publicly accessible address used to construct streaming URLs for your player |
The **Public URL** field does not apply to StremThru. Use the
[`STREMTHRU_BASE_URL`](https://docs.stremthru.13377001.xyz/configuration/#stremthru-base-url)
environment variable instead, and set **URL** to your local StremThru address
(e.g. `http://stremthru:8080`).
**URL:** Internal Docker/local address (e.g. `http://nzbdav:3000`)
**Public URL — Option A (Proxy, recommended):** Leave blank and provide
your **AIOStreams Auth Token** in the service settings (see
[below](#proxying-streams-through-aiostreams)). AIOStreams proxies the
stream — the most reliable method.
**Public URL — Option B (Direct):** Enter the publicly accessible URL
(e.g. `https://nzbdav.yourdomain.com`). Without it, your player receives an
unreachable local address.
**URL:** Public URL of your instance (e.g.
`https://altmount.yourdomain.com`)
**Public URL:** Leave blank.
Proxying Streams Through AIOStreams [#proxying-streams-through-aiostreams]
The NzbDAV and AltMount service configs have an optional **AIOStreams Auth Token** field (a `username:password` pair from your instance's `AIOSTREAMS_AUTH`). Providing it makes AIOStreams proxy the tool's stream URLs instead of handing them to your player directly. That helps in three ways:
* **Player compatibility** — NzbDAV/AltMount playback URLs are WebDAV URLs with embedded credentials, which some players refuse. The proxy produces a "normal" URL.
* **Keep the tool private** — only AIOStreams needs to reach NzbDAV/AltMount, so you can leave them on your local network and expose just AIOStreams.
* **No protocol mismatches** — mixed HTTP/HTTPS setups are a common cause of failed playback; the proxy sidesteps them entirely.
Note that this only changes the URL your player sees. It has no effect on what your Usenet provider or indexer sees — the tool still downloads the articles and grabs the NZB exactly as before.
Only the built-in proxy can do this — provide the token in the **service's**
own settings. External proxies (e.g. MediaFlow) will not work for these
URLs.
Easynews [#easynews]
Easynews is a Usenet provider with its own web search, and its results are plain HTTP URLs served from Easynews' servers — no hosting, no companion software, works on every device.
Go to **Services** and add **Easynews** with your username and password.
Add the **Easynews Search** addon — it is the recommended way to access Easynews'
web search results in AIOStreams.
Using Easynews Results with Other Services [#using-easynews-results-with-other-services]
The Easynews Search addon can also obtain **real NZB files** for its web search results. That means those results can be played through any of the traditional NZB-accepting services too — the built-in engine, NzbDAV, AltMount, StremThru Newz, Stremio NNTP or TorBox — not just the Easynews service.
Since your AIOStreams instance is what generates and serves those NZBs, this requires the addon's **AIOStreams Auth** option (a `username:password` pair from `AIOSTREAMS_AUTH`) — so using Easynews Search with any service other than Easynews means running your own instance (self-hosted or a private [ElfHosted](#zero-setup-the-elfhosted-bundle) one). Using it purely with the Easynews service needs no auth and works on any instance.
An Easynews subscription also includes regular **NNTP provider access** — the
HTTP downloads behind their web search are just one way in. You can use their
NNTP server details with any NNTP-based service in this guide, exactly like
any other provider.
Zero Setup: The ElfHosted Bundle [#zero-setup-the-elfhosted-bundle]
Don't want to host anything yourself? A **private [ElfHosted](https://store.elfhosted.com/elf/viren070/) instance** (ElfHosted is a project sponsor) is the managed alternative anywhere this guide says *self-host*: paid, private, pre-configured, no server administration — see the [deployment guide](/getting-started/deployment#elfhosted-paid-managed). Note that ElfHosted also runs a *public* AIOStreams instance; anything here that needs `AIOSTREAMS_AUTH` or the dashboard requires a private instance of your own, not the public one.
For Usenet specifically, their [pre-configured AIOStreams + NzbDAV bundle](https://store.elfhosted.com/product/aiostreams-nzbdav/) includes:
* A private AIOStreams instance (*yours to manage*)
* NzbDAV, secured behind ElfHosted SSO
* A bundled indexer to get started (*you can add more of your own later*)
* 150 Mbps proxy bandwidth (*boostable*), enough to happily stream a 4K REMUX (*also usable for proxying debrid streams*)
Pre-configured, fully supported, available in 5 minutes — the same software stack as the self-hosted method, just hosted and pre-wired, so the configuration steps in this guide apply unchanged.
3. Add Your Indexers [#3-add-your-indexers]
With a playback service in place, add addons to feed it results. AIOStreams provides several built-in addons that return Usenet results:
| Addon | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------- |
| **Newznab** | Connect indexers directly via their API URL and key |
| **NZBHydra2** | Connect your NZBHydra2 instance |
| **Prowlarr** | Query Prowlarr for both torrent and Usenet results |
| **TorBox Search** | Uses TorBox's internal indexers (torrents and Usenet) — IP-whitelisted, trusted public instances only |
| **Easynews Search** | Search Easynews; results stream directly or serve as NZBs |
These addons need at least one Usenet-capable service configured — each result is tagged with the service that will play it (e.g. `AIO`, `SN`, `TB`).
You are not limited to a single Newznab addon: add it once **per indexer**, each with that indexer's API URL and key. A handful of Newznab addon instances side by side is the normal setup.
With the Newznab addon, set the **Search Mode** to `both` if you have API
hits to spare. Some indexers only return results via query search — test both
modes to see what works best.
If you already manage your indexers through **NZBHydra2** or **Prowlarr**, their addons connect all of them at once — but be aware their NZB-grab behaviour differs; see [Indexer Managers](#indexer-managers).
4. Make It Resilient: Failover [#4-make-it-resilient-failover]
Missing articles are the most common way a Usenet stream fails to play — and normally that means a "Something went wrong" error video. **Failover** fixes this: when the result you clicked fails, AIOStreams automatically tries the next result in your sorted list and redirects your player there instead, so playback works instead of failing.
Enable it under **Services → Built-in → Failover** on the configuration page.
Where It Works [#where-it-works]
Failover can only act where AIOStreams controls playback — where your player's first request goes to an AIOStreams URL, letting AIOStreams resolve a healthy target and redirect. That covers:
* **Built-in addons** (marked with a *Built-in* ribbon in the addon marketplace), and
* **service-wrapped addons** (external addons whose results AIOStreams resolves through your debrid service).
Stremio NNTP results are excluded — playback happens entirely inside Stremio. Additionally, the **Include External Addon Targets** option lets failover *fall over to* results from external addons (e.g. a Comet debrid link) even though a direct click on those can't trigger failover; such targets are health-checked by probing before your player is redirected.
Key Settings [#key-settings]
* **Failover Content Types** — whether Usenet and/or debrid results may be used as targets.
* **Allow Cross-Type Failover** — let a failed Usenet result fall through to a debrid result (or vice versa) if that's what's next in the list. Off keeps failover within the clicked result's own kind.
* **Include External Addon Targets** — see above.
* **Max Failover Attempts** — how many fallback results to try before giving up.
* **Same-Release Failover Attempts** — see [below](#same-release-variants-deduplicator-merging).
* **Parallel Attempts** and its timing knobs (backup delay, preferred-item grace, …) — advanced: race several attempts and take the first healthy one.
Same-Release Variants (Deduplicator Merging) [#same-release-variants-deduplicator-merging]
The deduplicator (**Filters → Deduplicator**) removes duplicate copies of the same release from your list. Two settings turn those discarded copies into failover ammunition instead of waste:
* **Merge Duplicates** — rather than just discarding duplicates, their info is folded into the surviving result (e.g. a runtime that only one addon reported is kept on the winner).
* **Harvest Failover Variants** — additionally retains the duplicates' playback URLs on the winner as *same-release* failover targets: the same release from a different indexer or addon.
With variants harvested, the **Same-Release Failover Attempts** setting (in the Failover card) controls how many alternative copies of the *same* release are tried before failover moves on to a different release — often the ideal behaviour, since another indexer's NZB of the same release may be complete where the first wasn't.
Failover Position [#failover-position]
The last stages of the result pipeline are the required/excluded stream expression filters (**Filters → Stream Expression**) followed by result limits (**Filters → Result Limits**) — and depending on your configuration, both can remove a lot of results. **Failover Position** picks the point (always after sorting) from which the fallback chain is built:
| Position | Fallback chain drawn from |
| ------------------- | -------------------------------------------------------------------- |
| **Before SEL** | Results before stream expression filters are applied |
| **Before Limiting** | Results after expression filters, before result limits |
| **Last** (default) | The fully filtered list — exactly the results you see in your player |
Earlier positions give failover a larger pool to draw from, at the cost of possibly landing on a result your filters would have removed. The default is WYSIWYG.
5. NZB Grabs and Indexer Rules [#5-nzb-grabs-and-indexer-rules]
Streaming is a contentious use of Usenet, and some indexers actively restrict it: blocking the user agents of known streaming tools (AIOStreams, NzbDAV, …), blocking datacenter/VPS IP ranges, or — as **Treasure Maps** (formerly SceneNZBs) is known to — serving decoy NZBs to suspected streamers. Others flag a mismatch between the IP that *searched* and the IP that *grabbed* the NZB as a TOS violation.
This section covers how NZBs actually travel from your indexer to your playback service — and the options AIOStreams gives you around it. Always check your indexer's rules.
Search IP vs. Grab IP [#search-ip-vs-grab-ip]
Searches always come from your AIOStreams instance. Who performs the NZB grab depends on the playback service:
| Playback service | Search request | NZB grab |
| ---------------------------------- | ------------------- | ------------------------------------------ |
| Built-in engine (**AIOStreams**) | AIOStreams instance | AIOStreams instance — always matches |
| NzbDAV / AltMount / StremThru Newz | AIOStreams instance | The tool's server (matches when co-hosted) |
| Stremio NNTP | AIOStreams instance | Your Stremio device (home IP) |
| TorBox | AIOStreams instance | TorBox's servers |
This table assumes the Newznab addon pointed directly at your indexers. If you go through an indexer manager (NZBHydra2, Prowlarr), the search still originates from AIOStreams but goes to the manager, which then queries the indexer — so the indexer sees the *manager's* IP for searches.
Proxying NZB Grabs Through AIOStreams [#proxying-nzb-grabs-through-aiostreams]
The Newznab addon has an **AIOStreams Proxy Auth** option (a `username:password` pair from `AIOSTREAMS_AUTH`). With it set, AIOStreams hands the playback service a proxied AIOStreams URL instead of the indexer's raw NZB URL — so the grab is performed by AIOStreams itself, and the indexer sees the same IP for search and grab.
**When it helps:**
* **Stremio NNTP** and **TorBox** — the grab would otherwise come from your Stremio device or TorBox's servers. (Some indexers block TorBox's servers outright; proxying means the indexer only ever sees your AIOStreams instance.)
**When it's usually unnecessary:**
* The **built-in engine** — AIOStreams already grabs the NZB itself.
* **NzbDAV / AltMount / StremThru Newz co-hosted with AIOStreams** — the tool most likely shares AIOStreams' IP anyway, so proxying just adds a hop. The only reasons to still enable it here are to reuse AIOStreams' [NZB grab cache](#the-nzb-grab-cache) or to centrally apply the [outbound request controls](#outbound-request-controls) (header overrides, HTTP proxies) when the tool doesn't support them itself.
The NZB Grab Cache [#the-nzb-grab-cache]
AIOStreams keeps grabbed `.nzb` files in a two-tier cache (in-memory plus on-disk) so the same NZB is never fetched from an indexer twice. It applies both to proxied grabs and to grabs made by the built-in Usenet engine. The sizes are configurable in the dashboard under **Settings → Built-in → Grab** — the defaults (64 MB memory, 1 GB disk) are reasonably sized and rarely need changing.
Indexer Managers [#indexer-managers]
* **NZBHydra2** proxies grabs by default, so search and grab both come from Hydra. Note that Hydra force-disables proxying for certain indexers at those indexers' request, so this is not guaranteed for every indexer.
* **Prowlarr** only redirects NZB grabs — the downloading service hits the indexer directly, so the IP mismatch remains.
ElfHosted & Zyclops [#elfhosted--zyclops]
When running an ElfHosted AIOStreams instance, your search IP and grab IP are always the same, routed through a built-in NZBHydra/SABnzbd proxy on your instance, so usage patterns are identical to traditional Usenet access. ElfHosted also offers managed [NZBHydra2](https://store.elfhosted.com/product/nzbhydra/) and [Prowlarr](https://store.elfhosted.com/product/prowlarr/) instances ($1 trial) for augmenting an existing setup.
The public [Zyclops NewzNab Healthcheck Proxy](https://zyclops.elfhosted.com/) (*an ElfHosted project*) is another way to keep your search and grab IPs aligned, and Zyclops health checks are built into the AIOStreams Newznab addon. Review the highlighted implications regarding credential/NZB sharing and your indexer's TOS before enabling it.
Outbound Request Controls [#outbound-request-controls]
The dashboard has two settings (**Settings → HTTP**) that shape every outbound request your instance makes. Whether a given configuration complies with your indexer's or provider's terms is between you and them.
* **Request header overrides** — per-key User-Agent/header overrides. A key is a hostname (`host`, `*.host`, `*`) or a `[context]` label for a request purpose (`[nzb_grabs]`, `[torrent_grabs]`, `[newznab]`, `[torznab]`). A value is a literal User-Agent (supports the `{version}` / `{random}` placeholders) or a `{preset}` reference to a built-in header set: `{sabnzbd}`, `{nzbget}`, `{sonarr}`, `{radarr}`, `{prowlarr}`, `{nzbhydra2}`, `{chrome}`. When several keys match, the most specific wins — exact host, then wildcard host, then `[context]`, then global `*`. Example: `[nzb_grabs]:{sabnzbd},indexer.com:{prowlarr}`.
* **Addon proxy URL(s)** and **Addon proxy config** — route outbound requests through one or more HTTP proxies (e.g. a gluetun container for VPN routing). The config map uses the same key scheme as above, with `true`/`false` to enable/disable per key, or a 0-based index to pick a specific proxy from the list. Example: `[newznab]:true`.
Both are also settable via environment variables (`REQUEST_HEADER_OVERRIDES`, `ADDON_PROXY`, `ADDON_PROXY_CONFIG`) — see the [environment variable reference](/configuration/environment-variables).
# Migrating from v1 to v2
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
AIOStreams v2 is a complete rewrite with a new database system for managing configurations. This guide walks through the changes required to migrate an existing v1 installation.
v1 configurations are **not directly importable** into v2. Before proceeding,
take screenshots or detailed notes of your configured addons, filter
priorities, and custom formatters.
***
1. Deployment Configuration [#1-deployment-configuration]
Volume mount (essential) [#volume-mount-essential]
v2 requires a **persistent volume** to store its SQLite database and other data. Without it, your configuration is lost on every restart.
Mount `/app/data` inside the container to a persistent location on the host:
```yaml
services:
aiostreams:
image: ghcr.io/viren070/aiostreams:latest
container_name: aiostreams
restart: unless-stopped
ports:
- '3000:3000'
env_file:
- .env
healthcheck:
test: wget -qO- http://localhost:3000/health
interval: 1m
timeout: 10s
retries: 5
start_period: 10s
```
```yaml
services:
aiostreams:
image: ghcr.io/viren070/aiostreams:latest
container_name: aiostreams
restart: unless-stopped
ports:
- '3000:3000'
env_file:
- .env
volumes:
- ./data:/app/data # ← required
# Health check is now built-in; remove or update:
# healthcheck:
# test: wget -qO- http://localhost:3000/api/v1/status
# interval: 1m
# timeout: 10s
# retries: 5
```
Health check endpoint [#health-check-endpoint]
The old `/health` endpoint is **removed**. If you need a custom health check, use the new status endpoint:
```
http://:/api/v1/status
```
The Docker image includes a built-in health check; removing your custom one is the simplest option.
Migration steps [#migration-steps]
Stop your v1 AIOStreams container.
Update `compose.yaml` to add the volume mount and remove/update the health
check.
Update your environment variables (see below).
Start the v2 container.
***
2. Environment Variable Changes [#2-environment-variable-changes]
| v1 Variable | v2 Variable | Notes |
| ----------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------- |
| `API_KEY` | `ADDON_PASSWORD` | Renamed |
| `BRANDING` | `CUSTOM_HTML` | Now a runtime variable (was build-time in v1) |
| `DETERMINISTIC_ADDON_ID` | *(removed)* | v2 always uses deterministic IDs |
| `SECRET_KEY` | `SECRET_KEY` | **Now requires a 64-character hex string**. Generate with `openssl rand -hex 32` |
| `DATABASE_URI` | `DATABASE_URI` | New. Default: `sqlite:///app/data/db.sqlite` |
| `LOG_LEVEL` | `LOG_LEVEL` | Expanded: `http`, `verbose`, `silly` added |
| `SHOW_DIE` | *(removed)* | The die emoji is gone |
| `ALIASED_CONFIGURATIONS` (was `CUSTOM_CONFIGS`) | `ALIASED_CONFIGURATIONS` | New format: `alias:uuid:encryptedPassword` triplets |
| `DEFAULT_MEDIAFLOW_URL` etc. | `DEFAULT_PROXY_URL`, `DEFAULT_PROXY_ID`, etc. | Proxy variables unified |
`SECRET_KEY` is now strictly validated. If your v1 key was not a 64-character
hex string, you must generate a new one. AIOStreams will not start otherwise.
***
3. Custom Formatter Syntax [#3-custom-formatter-syntax]
If you used a custom stream formatter in v1, update the variable names:
| v1 Variable | v2 Variable | Notes |
| --------------------------------- | ------------------------------------ | ------------------------------------------------------------------------ |
| `{provider.cached}` | `{service.cached}` | The `provider` object is now `service` |
| `{stream.personal}` | `{stream.library}` | Renamed |
| `{stream.infoHash}` check for P2P | `{stream.type::=p2p["[P2P]"\|\|""]}` | `infoHash` may now exist on non-P2P streams; use `{stream.type}` instead |
Note your v1 formatter strings before stopping the container.
After v2 is running, go to the configuration page and recreate your
formatters using the updated variable names.
See the [Custom Formatter reference](/reference/custom-formatter) for a
full list of available variables.
***
If you run into issues not covered here, join the [AIOStreams Discord](https://discord.viren070.me) for help.
# v2.30 Changes
import { Callout } from 'fumadocs-ui/components/callout';
v2.30 is a large internal overhaul — a new admin dashboard, structured logging,
a rewritten database layer, and a move to UI-first configuration — released as a
**minor** update because the breaking changes are small and only affect
environment variables that were **already deprecated**.
**End users are not affected.** Existing Stremio install URLs and stored user
configurations keep working across the upgrade. No re-configuration is needed.
Self-hosters only need to act if they use the deprecated environment variables
listed below.
***
What's new [#whats-new]
* **Admin dashboard** — analytics, logs, system info, users, proxy, tasks,
cache, and a Settings page.
* **UI-first configuration** — almost every setting that used to be an
environment variable is now a **runtime setting** stored in the database and
editable from the dashboard. The matching environment variable still works,
but now acts as a **locked override** (the field becomes read-only in the
UI). See the
[Environment Variables reference](/configuration/environment-variables) for
the full bootstrap-vs-runtime split.
* **Structured logging** — cleaner, structured log output. Setting
`LOG_FORMAT=json` is recommended for production.
* **Rewritten database layer** — better abstraction and a real migration
system. Migrations run automatically on startup.
* **Faster frontend** — migrated to TanStack Router with rspack/rsbuild.
* **Addon statistics** — new menu to see addon performance.
***
Breaking changes [#breaking-changes]
These environment variables were already deprecated and are now removed.
Replace them as follows.
Service credentials [#service-credentials]
All `DEFAULT__*` and `FORCED__*` credential variables (e.g.
`DEFAULT_REALDEBRID_API_KEY`, `FORCED_EASYNEWS_USERNAME`) are removed. Use the
consolidated `DEFAULT_SERVICE_CREDENTIALS` / `FORCED_SERVICE_CREDENTIALS`
instead, one `serviceId.credentialId=value` entry per line (or `\n` separated if
multi-line env variables are not available in your environment):
You can find a list of all service IDs [here](https://github.com/Viren070/AIOStreams/blob/2f42d4723e036b941154819552165379fd4ce3dc/packages/core/src/utils/constants.ts#L228-L243)
and a list of credential IDs [here](https://github.com/Viren070/AIOStreams/blob/2f42d4723e036b941154819552165379fd4ce3dc/packages/core/src/utils/constants.ts#L329-L779).
```sh
# Before
DEFAULT_ALLDEBRID_API_KEY=xxx
DEFAULT_EASYNEWS_USERNAME=yyy
# After
DEFAULT_SERVICE_CREDENTIALS='alldebrid.apiKey=xxx
easynews.username=yyy'
# or
DEFAULT_SERVICE_CREDENTIALS='alldebrid.apiKey=xxx\neasynews.username=yyy'
```
Addon host/port/protocol rewrites [#addon-hostportprotocol-rewrites]
`FORCE__HOSTNAME`, `FORCE__PORT`, and `FORCE__PROTOCOL`
(e.g. `FORCE_COMET_HOSTNAME`, and the StremThru/Jackettio equivalents) are
removed. Use `STREAM_URL_MAPPINGS`:
```sh
# Before
FORCE_COMET_HOSTNAME=comet.example.com
FORCE_COMET_PROTOCOL=https
# After
STREAM_URL_MAPPINGS='{"http://comet:2020": "https://comet.example.com"}'
```
Public proxy URL rewrites [#public-proxy-url-rewrites]
`FORCE_PUBLIC_PROXY_HOST`, `FORCE_PUBLIC_PROXY_PORT`, and
`FORCE_PUBLIC_PROXY_PROTOCOL` are removed. Use the proxy's Public URL field /
`FORCE_PROXY_PUBLIC_URL`:
```sh
# Before
FORCE_PUBLIC_PROXY_HOST=proxy.example.com
FORCE_PUBLIC_PROXY_PROTOCOL=https
# After
FORCE_PROXY_PUBLIC_URL='https://proxy.example.com'
```
Other removed variables [#other-removed-variables]
| Removed | Replacement |
| --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `ALLOWED_REGEX_PATTERNS`, `ALLOWED_REGEX_PATTERNS_DESCRIPTION`, `ALLOWED_REGEX_PATTERNS_URLS`, `ALLOWED_REGEX_PATTERNS_URLS_REFRESH_INTERVAL` | `WHITELISTED_REGEX_PATTERNS`, `WHITELISTED_REGEX_PATTERNS_DESCRIPTION`, `WHITELISTED_REGEX_PATTERNS_URLS`, `WHITELISTED_SYNC_REFRESH_INTERVAL` |
| `LOG_CACHE_STATS_INTERVAL` | Removed (no replacement) |
| `PTT_PORT`, `PTT_SOCKET` | Removed (dead config) |
***
ADDON_PASSWORD migration [#addon_password-migration]
`ADDON_PASSWORD` is removed and replaced by session-based login and an
automatically-managed config access key. Existing configurations are migrated
automatically — no manual changes to stored configs are needed.
**You must keep `ADDON_PASSWORD` in your environment for at least one startup
after upgrading to v2.30** so the migration can run. After that, you can remove
it. The config access key will be set to your old `ADDON_PASSWORD` value so
existing configs continue to work.
Since the old password-based protection is removed, make sure you have logins
configured via `AIOSTREAMS_AUTH` — a comma-separated list of `user:password`
pairs (e.g. `admin:admin123,user1:pass1`). These are the credentials you will
be asked for when you next access the configuration page.
Only admin users from `AIOSTREAMS_AUTH` can access the dashboard. By default,
all users are admins. You can restrict this to a subset with
`AIOSTREAMS_AUTH_ADMINS` (comma-separated list of usernames, e.g.
`admin,user1`).
Rotating the config access key (changing `CONFIG_ACCESS_KEY` or the dashboard
setting) invalidates every existing config until it is re-saved while logged
in. Only rotate it deliberately.
***
Next steps [#next-steps]
Migrate settings to the dashboard [#migrate-settings-to-the-dashboard]
Most runtime settings can now be managed from the dashboard instead of `.env`.
Your existing environment variables continue to act as overrides — the dashboard
shows this and prevents you from changing overridden settings, so there's
nothing broken about leaving them in place.
When you're ready, keep only the
[bootstrap variables](/configuration/environment-variables) in `.env` and move
everything else via the built-in import. The updated
[`.env.sample`](https://github.com/Viren070/AIOStreams/blob/main/.env.sample)
reflects this split.
Go to the admin dashboard and navigate to the Settings page.
Click the 3-dot menu that appears on the top right of each tab.
Click **Import environment variables**. A modal will appear showing all
currently active environment variables and their values.
Click the **Import** button at the bottom of the modal to copy all
settings into the dashboard.
Remove all non-bootstrap environment variables from your configuration.
Keep the bootstrap variables — these cannot be set via the UI.
Restart/recreate AIOStreams with your updated environment configuration.
All runtime settings can now be managed from the dashboard.
Enable structured logging [#enable-structured-logging]
Set `LOG_FORMAT=json` in production for cleaner, structured log output that works better
with log viewers.
***
API changes [#api-changes]
These changes only apply if you use the User API via external tools. If you
don't, skip this section.
User API authentication moved to Authorization: Basic [#user-api-authentication-moved-to-authorization-basic]
The `/api/v1/user` endpoints (`GET`, `PUT`, `DELETE`, `POST /password`,
`POST /verify`, `GET /analytics`) no longer accept `uuid` / `password` as query
parameters or in the JSON body. They now require the standard HTTP Basic scheme:
```
Authorization: Basic base64(:)
```
The `` may be either the raw user password or the `encryptedPassword`
token returned by `POST /api/v1/user` and `GET /api/v1/user` — encrypted tokens
are transparently decrypted server-side.
`HEAD /api/v1/user` (existence check) and `POST /api/v1/user` (create) are
unchanged: `HEAD` still takes `?uuid=`, and create still takes
`{ config, password }` in the body since no UUID exists yet.
```sh
# Before
curl "https://your-instance.example.com/api/v1/user?uuid=$UUID&password=$PASSWORD"
# After
curl "https://your-instance.example.com/api/v1/user" \
-H "Authorization: Basic $(echo -n "$UUID:$PASSWORD" | base64)"
```
See the [User API reference](/apis/user) for the full updated request shapes.
# Config Expression Language
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
A **variant** is a named script that adjusts your configuration at request time. Variants live inside one config and are selected by the install URL, so a single UUID can serve several differently-behaving addons.
The scripts are written in the **Config Expression Language (CEL)**. Where the [Stream Expression Language](/reference/stream-expressions) is evaluated against a list of streams, CEL is declarative: a list of edits applied to your configuration, top to bottom.
***
Variants or a parent config? [#variants-or-a-parent-config]
| | Parent/child config | Variant |
| ------------------ | --------------------------------------- | ---------------------------- |
| Configs to set up | Two, each with its own UUID | One |
| Written as | Merge strategies chosen in the UI | A few lines of CEL |
| Seeing the result | After saving, by looking at the streams | A diff, before you save |
| Whose config it is | Theirs, with their own credentials | Yours, viewed through a lens |
Prefer a variant where you can. Most differences are a line or two, the preview shows the
diff before you save, and there is no second configuration to create and keep track of.
Sharing does not settle it either way. Giving your config to someone whose TV cannot handle
Dolby Vision is a variant: `add excludedVisualTags "DV Only"` and hand them `/v/no-dv`.
They get your config exactly, minus the one thing their hardware cannot do, and it stays
one config when you change it.
Use a parent/child config when the second configuration has to belong to somebody else — a
child config is editable by whoever holds it and carries their own credentials, which you
never see, where a variant only ever shows your config through a lens you wrote. The other
reason is preference: a parent config is configured by clicking, at the cost of not being
able to preview what a merge produces.
***
Quick start [#quick-start]
In **Miscellaneous → Variants** (Advanced mode), add a variant with the id `phone`.
Write the script:
```
set addonName = "AIOStreams (Phone)"
set excludedResolutions = ["2160p", "1440p"]
set size.global.movies = [0, 8000000000]
set resultLimits.global = 10
```
Hit preview to see exactly which fields change, then save.
On **Save/Install**, pick the variant. Every install link gains `/v/phone`:
```
https://your-instance/stremio///v/phone/manifest.json
```
***
Where the selector goes [#where-the-selector-goes]
**Selector location** on the install page switches between two forms:
```
https://your-instance/stremio///v/phone/manifest.json
https://your-instance/stremio///manifest.json?v=phone
```
They do the same thing and both work everywhere, so the path form is the default: it is the one that survives a client rebuilding its request URLs from a base you gave it rather than following the manifest URL. A client keeping the query string is what the other form needs, and not all of them do.
The Search and Newznab/Torznab APIs take the query form only, as `&v=`, since they are single endpoints rather than a base a client extends.
***
Selecting variants [#selecting-variants]
Comma-separate to combine. They apply left to right, so later instructions win:
```
.../v/phone,rd2/manifest.json
```
An unknown or disabled id is an error, surfaced as an error stream, rather than a silent fall back to the base config.
The selector also works on ChillLink and the Seanime extension URLs.
The configure page always edits the **base** config, even when a variant is selected.
***
Syntax [#syntax]
One instruction per line. `#` starts a comment, except inside a string. Object and array literals may span lines.
```
# Two comments and a multi-line literal
set resultLimits.global = 10
merge deduplicator = {
"enabled": true,
"keys": ["filename", "infoHash"]
}
```
Instructions [#instructions]
| Instruction | Effect |
| ---------------------------------- | ----------------------------------------------------------------------------------- |
| `set = ` | Assign a value. Missing intermediate objects are created. |
| `merge = { ... }` | Deep merge. Objects recurse, arrays are replaced, a `null` member deletes that key. |
| `unset ` | Delete a key, or remove a list element when the path ends in an index or selector. |
| `clear ` | Empty a list or object. |
| `add , ...` | Append to a list, skipping values already present. Creates the list if absent. |
| `prepend , ...` | The same, inserted at the front. |
| `remove , ...` | Remove matching values from a list. |
| `remove ` | With no values, the path itself selects the elements to remove. |
| `enable ` / `disable ` | Shorthand for `set .enabled = true` / `false`. |
| `use formatter ` | Load one of your saved formatters. |
| `use variant ` | Apply another variant's instructions at this point. |
Values [#values]
Strings (single or double quoted), numbers, `true`, `false`, `null`, arrays and objects. Strings support `\n`, `\t`, `\r`, `\b`, `\f`, `\\`, `\"`, `\'`, `\/` and `\uXXXX`.
```
set addonName = "Living room"
set resultLimits.global = 10
set excludeUncached = true
set sortCriteria.global = [
{ "key": "cached", "direction": "desc" },
{ "key": "resolution", "direction": "desc" },
{ "key": "size", "direction": "desc" }
]
```
Paths [#paths]
A path starts with a configuration field and drills in.
| Segment | Meaning | Example |
| --------------- | ---------------------------------------------- | ------------------------------------- |
| `.name` | An object property | `deduplicator.enabled` |
| `[0]` | A list element by position | `sortCriteria.global[0]` |
| `[-1]` | Counting back from the end | `sortCriteria.global[-1]` |
| `[*]` | Every element | `presets[*].options.timeout` |
| `[key=value]` | Elements whose property equals a value | `services[id=realdebrid]` |
| `[key!=value]` | Elements whose property does not equal it | `presets[type!=torrentio]` |
| `[key*=value]` | Elements whose property contains the text | `rankedRegexPatterns[name*="german"]` |
| `[key!*=value]` | Elements whose property does not contain it | `presets[type!*=torrent]` |
| `[=value]` | Plain list entries, comparing the entry itself | `excludedKeywords[="cam"]` |
A selector may match several elements, in which case the instruction applies to all of them: `disable presets[type=torrentio]` disables every Torrentio instance.
`=` and `!=` are exact and case-sensitive. `*=` and `!*=` are substring matches that ignore case.
Lists of plain values [#lists-of-plain-values]
Drop the key to compare the entry itself. This is how you reach a list of strings such as `excludedKeywords` or `syncedExcludedRegexUrls`, where there is no property to name. All four operators work this way:
```
remove excludedKeywords[="CAM"] # exactly "CAM", nothing else
remove excludedKeywords "CAM" # the same thing, shorter
remove excludedKeywords[*="cam"] # "CAM", "hdcam" and "camrip"
remove syncedExcludedRegexUrls[*="example.com"] # every URL from one host
```
***
Targeting one entry in a list [#targeting-one-entry-in-a-list]
Different lists identify their entries differently.
**Services** are keyed by `id`:
```
set services[id=realdebrid].credentials.apiKey = "SECOND_ACCOUNT_KEY"
```
**Addons** have an `instanceId`, but it is a short random hex string such as `8ae`, so prefer the addon's type or the name you gave it. A selector key may be dotted, which is how you reach the name inside `options`:
```
disable presets[type=torrentio] # every Torrentio instance
disable presets[options.name*="4K"] # by the name shown in the UI
disable presets[instanceId=8ae] # one exact instance
```
The editor autocompletes real instance ids after `presets[instanceId=`, annotated with each addon's name.
**Ranked regex patterns and regex overrides** carry a `name`:
```
set rankedRegexPatterns[name="HDR boost"].score = 500
remove rankedRegexPatterns[name*="cam"]
```
**Stream expressions** have no name field. Their display name lives inside the expression as a `/* Name */` comment, so match the expression text with `*=`:
```
disable excludedStreamExpressions[expression*="Low seeders"]
disable rankedStreamExpressions[expression*="4K bonus"]
```
Position works too, but breaks when you reorder the list:
```
disable excludedStreamExpressions[0]
```
`enable` and `disable` set an `enabled` property. `regexOverrides` and `selOverrides` use an inverted `disabled` flag instead, so for those write `set regexOverrides[name*="hdr"].disabled = true`.
***
Recipes [#recipes]
**Phone on mobile data.** Cap the file size, skip 4K, return fewer results.
```
set addonName = "AIOStreams (Phone)"
set excludedResolutions = ["2160p", "1440p"]
set size.global.movies = [0, 8000000000]
set size.global.series = [0, 3000000000]
set resultLimits.global = 10
```
**A second debrid account.**
```
set services[id=realdebrid].credentials.apiKey = "SECOND_ACCOUNT_KEY"
```
**Instant playback only.** Cached results, no failover attempts.
```
set excludeUncached = true
set failover.enabled = false
```
**A different formatter.** `use formatter` copies one of your saved formatters into the custom slot and switches to it, so a whole template never has to go in the script.
```
use formatter "Minimal"
```
Or pick a built-in one:
```
set formatter.id = "minimalisticgdrive"
```
**Debugging.** Show the statistics streams and stop hiding addon errors.
```
set hideErrors = false
set statistics = { "enabled": true, "position": "top", "statsToShow": ["addon", "filter", "timing"] }
```
**Only a couple of addons.**
```
disable presets[*]
enable presets[type=comet]
enable presets[type=easynews]
```
**A different language.** Language values are the ones the filter menu offers, plus `Original`, `Dual Audio`, `Multi`, `Dubbed` and `Unknown`.
```
set requiredLanguages = ["German", "Multi", "Dual Audio"]
prepend preferredLanguages "German"
```
**Reordering the sort.** Put resolution above cache status for a machine that does not mind waiting.
```
set sortCriteria.global = [
{ "key": "resolution", "direction": "desc" },
{ "key": "cached", "direction": "desc" },
{ "key": "streamExpressionScore", "direction": "desc" },
{ "key": "size", "direction": "desc" }
]
```
**Layering.** Either reference one variant from another:
```
use variant phone
add excludedVisualTags "3D"
```
or keep them independent and combine at install time with `/v/phone,no3d`.
***
Limits [#limits]
Operators can tune these; defaults shown.
| Limit | Default | Environment variable |
| ------------------------------- | -------- | ------------------------------------------- |
| Availability | everyone | `VARIANT_ACCESS` (`all`, `trusted`, `none`) |
| Variants per config | 10 | `MAX_VARIANTS` |
| Characters per script | 4000 | `MAX_VARIANT_SCRIPT_LENGTH` |
| Characters across all scripts | 20000 | `MAX_VARIANT_TOTAL_SCRIPT_CHARACTERS` |
| Instructions per script | 100 | `MAX_VARIANT_INSTRUCTIONS` |
| Variants per request | 4 | `MAX_ACTIVE_VARIANTS` |
| `use variant` nesting depth | 5 | `MAX_VARIANT_DEPTH` |
| Elements one instruction writes | 200 | `MAX_VARIANT_PATH_MATCHES` |
***
Behaviour notes [#behaviour-notes]
* **An instruction that matches nothing is skipped** and only logged. Remove Real-Debrid later and a variant that swapped its credentials keeps working instead of breaking every install URL you handed out. The editor preview shows these as warnings while you write.
* **Syntax errors and forbidden fields are rejected on save**, so they never reach a live request.
* **A variant's name is a label** for the configuration UI and the install page. Use `set addonName` to change what your client shows.
* **Variants are not inherited** from a parent config: they reference this config's own addon ids and saved formatter names.
# Custom Formatter
The **Custom Formatter** gives you full control over how each stream's name and description are displayed in Stremio (and other clients). You can live-preview your format on the configuration page using the **Preview** button.
Access a variable using:
```
{variableName.propertyName}
```
Modifiers are chained with `::`:
```
{variableName.propertyName::modifier}
```
***
Variables [#variables]
Config [#config]
| Variable | Type | Description |
| -------------------- | ------ | ------------------------------------------------------------------------- |
| `{config.addonName}` | string | The name of the AIOStreams instance (Branding → Addon name; `ADDON_NAME`) |
Stream [#stream]
Source [#source]
| Variable | Type | Description |
| -------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `{stream.type}` | string | Type: `debrid`, `usenet`, `http`, `live`, `youtube`, `p2p` |
| `{stream.proxied}` | boolean | Whether the stream is proxied (e.g. MediaFlow) |
| `{stream.library}` | boolean | Whether the file is already in your "library" e.g. debrid account |
| `{stream.idMatched}` | boolean | Whether the source identified the release by external ID rather than by searching its title as text |
| `{stream.indexer}` | string | Source indexer |
| `{stream.message}` | string | Additional status message |
| `{stream.infoHash}` | string | Torrent info hash |
File [#file]
| Variable | Type | Description |
| --------------------- | ------ | ------------------------------------------ |
| `{stream.filename}` | string | Filename of the stream or media file |
| `{stream.folderName}` | string | Folder name (usually only specific addons) |
| `{stream.size}` | number | File size in bytes |
| `{stream.folderSize}` | number | Folder/torrent size in bytes |
| `{stream.bitrate}` | number | Bitrate in bits per second |
| `{stream.duration}` | number | Media duration in seconds |
| `{stream.container}` | string | File container format (e.g. `mkv`, `mp4`) |
| `{stream.extension}` | string | File extension (e.g. `.mkv`, `.iso`) |
Video [#video]
| Variable | Type | Description |
| ---------------------- | --------- | ------------------------------------------- |
| `{stream.quality}` | string | Quality tag (e.g. `Bluray`, `WEB-DL`) |
| `{stream.resolution}` | string | Video resolution (e.g. `1080p`, `2160p`) |
| `{stream.visualTags}` | string\[] | Visual tags (e.g. `HDR`, `DV`, `HDR10+`) |
| `{stream.encode}` | string | Encoding format (e.g. `HEVC`, `AVC`, `AV1`) |
| `{stream.network}` | string | Source network (e.g. `Netflix`, `Disney+`) |
| `{stream.hasChapters}` | boolean | Whether the file contains chapters |
Audio [#audio]
| Variable | Type | Description |
| ------------------------ | --------- | --------------------------------------------- |
| `{stream.audioTags}` | string\[] | Audio tags (e.g. `Atmos`, `DTS-HD MA`, `DD+`) |
| `{stream.audioChannels}` | string\[] | Audio channels (e.g. `5.1`, `7.1`) |
Languages [#languages]
| Variable | Type | Description |
| ------------------------------ | --------- | ----------------------------------------------------------------------------------------------------- |
| `{stream.languages}` | string\[] | Languages extracted from filename or audio track info |
| `{stream.languageEmojis}` | string\[] | `languages` as emoji flags |
| `{stream.languageCodes}` | string\[] | `languages` as ISO 639 codes |
| `{stream.smallLanguageCodes}` | string\[] | `languages` as small-caps codes |
| `{stream.uLanguages}` | string\[] | `languages` filtered to those you have configured (preferred, required, or included but not excluded) |
| `{stream.uLanguageEmojis}` | string\[] | `uLanguages` as emoji flags |
| `{stream.uLanguageCodes}` | string\[] | `uLanguages` as ISO 639 codes |
| `{stream.uSmallLanguageCodes}` | string\[] | `uLanguages` as small-caps codes |
| `{stream.dubbed}` | boolean | Whether the release is dubbed |
Subtitles [#subtitles]
| Variable | Type | Description |
| ------------------------------ | --------- | ----------------------------------------------------------------------------------------------------- |
| `{stream.subtitles}` | string\[] | Embedded subtitle languages (when known — see note below) |
| `{stream.subtitleEmojis}` | string\[] | `subtitles` as emoji flags |
| `{stream.subtitleCodes}` | string\[] | `subtitles` as ISO 639 codes |
| `{stream.smallSubtitleCodes}` | string\[] | `subtitles` as small-caps codes |
| `{stream.uSubtitles}` | string\[] | `subtitles` filtered to those you have configured (preferred, required, or included but not excluded) |
| `{stream.uSubtitleEmojis}` | string\[] | `uSubtitles` as emoji flags |
| `{stream.uSubtitleCodes}` | string\[] | `uSubtitles` as ISO 639 codes |
| `{stream.uSmallSubtitleCodes}` | string\[] | `uSubtitles` as small-caps codes |
| `{stream.subbed}` | boolean | Whether the release has subtitles |
**`languages` vs `subtitles`**
`languages` and `subtitles` behave differently depending on what information is available for a stream.
**When accurate media info is available**, `languages` contains audio track languages and `subtitles` contains embedded subtitle languages. This is the case for:
* Debrid results from built-in and service-wrapped addons that have `media_info` from StremThru (crowdsourced/probed via FFmpeg)
* Torznab/Newznab results from indexers that provide separate audio/subtitle metadata
* nekoBT results (all results have accurate audio/subtitle info)
* Torrentio anime results (subtitles field may be populated)
**In all other cases**, `subtitles` is empty and `languages` contains every language found in the filename — including subtitle-only languages (e.g. a filename containing `Eng.Sub` will add `English` to `languages`, not `subtitles`). This is a known limitation of filename-only parsing.
Release [#release]
| Variable | Type | Description |
| ----------------------- | --------- | --------------------------------------------------- |
| `{stream.title}` | string | Media title extracted from filename |
| `{stream.year}` | string | Year extracted from filename |
| `{stream.country}` | string | Country tag from the release name (e.g. `UK`, `US`) |
| `{stream.date}` | string | Date extracted from filename |
| `{stream.releaseGroup}` | string | Name of the release group |
| `{stream.editions}` | string\[] | Special editions (e.g. `Director's Cut`) |
| `{stream.repack}` | boolean | Whether the release is a repack |
| `{stream.proper}` | boolean | Whether the release is a proper |
| `{stream.regraded}` | boolean | Whether the content is regraded |
| `{stream.uncensored}` | boolean | Whether the content is uncensored |
| `{stream.unrated}` | boolean | Whether the content is unrated |
| `{stream.upscaled}` | boolean | Whether the content has been upscaled |
Season / Episode [#season--episode]
| Variable | Type | Description |
| ---------------------------------- | --------- | ------------------------------------------------------------ |
| `{stream.seasonPack}` | boolean | `true` if part of a season pack |
| `{stream.seasons}` | number\[] | Detected season numbers |
| `{stream.formattedSeasons}` | string | Formatted season string (e.g. `S01` or `S01-05`) |
| `{stream.folderSeasons}` | number\[] | Seasons from folder name (when different from filename) |
| `{stream.formattedFolderSeasons}` | string | Formatted seasons from folder name |
| `{stream.episodes}` | number\[] | Detected episode numbers |
| `{stream.formattedEpisodes}` | string | Formatted episode string (e.g. `E01` or `E01-05`) |
| `{stream.folderEpisodes}` | number\[] | Episodes from folder name (when different from filename) |
| `{stream.formattedFolderEpisodes}` | string | Formatted episodes from folder name |
| `{stream.seasonEpisode}` | string\[] | Pre-formatted season/episode strings (e.g. `['S01', 'E05']`) |
| `{stream.episodeTitle}` | string | Episode title parsed from the release name |
P2P / Tracker [#p2p--tracker]
| Variable | Type | Description |
| -------------------- | ------- | ---------------------------------- |
| `{stream.seeders}` | number | Torrent seeder count |
| `{stream.private}` | boolean | `true` if from a private tracker |
| `{stream.freeleech}` | boolean | `true` if the torrent is freeleech |
| `{stream.age}` | string | Human-readable age since release |
| `{stream.ageHours}` | number | Age in hours |
Anime [#anime]
| Variable | Type | Description |
| --------------------- | ------- | -------------------------------------------- |
| `{stream.seadex}` | boolean | Whether listed as best/alt on SeaDex |
| `{stream.seadexBest}` | boolean | Whether listed as a *best* release on SeaDex |
Scoring [#scoring]
| Variable | Type | Description |
| ----------------------------- | --------- | ------------------------------------------------------------------------- |
| `{stream.regexMatched}` | string | Name of the highest-priority matched preferred regex |
| `{stream.rankedRegexMatched}` | string\[] | All matched Ranked Regex Filter names (sorted) |
| `{stream.regexScore}` | number | Score from matched Regex Filter |
| `{stream.nRegexScore}` | number | Regex score normalised to 0–100 |
| `{stream.seScore}` | number | Score from matched Stream Expression sort rule |
| `{stream.nSeScore}` | number | Stream Expression score normalised to 0–100 |
| `{stream.seMatched}` | string | Name of the Preferred Stream Expression that matched (from first comment) |
| `{stream.rseMatched}` | string\[] | Names of all Ranked Stream Expressions that matched |
Service [#service]
| Variable | Type | Description |
| --------------------- | ------- | -------------------------------------- |
| `{service.id}` | string | Service identifier (e.g. `realdebrid`) |
| `{service.shortName}` | string | Abbreviated name (e.g. `RD`) |
| `{service.name}` | string | Full name (e.g. `Real-Debrid`) |
| `{service.cached}` | boolean | Whether the stream is cached |
Addon [#addon]
| Variable | Type | Description |
| --------------------- | ------ | ------------------------------------------ |
| `{addon.presetId}` | string | The preset ID the addon was generated from |
| `{addon.name}` | string | Display name of the addon |
| `{addon.manifestUrl}` | string | The addon's manifest URL |
Metadata [#metadata]
These describe the item that was requested, not the release, so they are identical for every stream in the response.
| Variable | Type | Description |
| ------------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `{metadata.queryType}` | string | Media type being queried (`movie`, `series`, `anime.series`, or `anime.movie`) |
| `{metadata.type}` | string | Stremio type being queried (`movie` or `series`) |
| `{metadata.isAnime}` | boolean | Whether the requested item is anime |
| `{metadata.title}` | string | Title of the media being queried |
| `{metadata.titles}` | string\[] | All known titles, across sources and languages |
| `{metadata.year}` | number | Release year |
| `{metadata.yearEnd}` | number | Year the series ended |
| `{metadata.runtime}` | number | Movie runtime in minutes |
| `{metadata.episodeRuntime}` | number | Episode runtime in minutes |
| `{metadata.genres}` | string\[] | List of genres |
| `{metadata.originalLanguage}` | string | Language the item was originally produced in (e.g. `Japanese`); use `::languageCode` / `::languageEmoji` for its code or flag |
| `{metadata.country}` | string | Country of origin (e.g. `GB`), used to tell same-name series apart |
| `{metadata.season}` | number | Requested season number |
| `{metadata.episode}` | number | Requested episode number |
| `{metadata.absoluteEpisode}` | number | Requested episode as an absolute number across all seasons |
| `{metadata.relativeAbsoluteEpisode}` | number | Absolute episode number within the current AniDB entry (for split entries) |
| `{metadata.episodeTitle}` | string | Title of the requested episode |
| `{metadata.episodeTitles}` | string\[] | All known titles of the requested episode, across sources and languages |
| `{metadata.latestSeason}` | number | Highest season number the series has |
| `{metadata.daysSinceRelease}` | number | Days since the movie or requested episode was released |
| `{metadata.daysSinceFirstAired}` | number | Days since the series first aired |
| `{metadata.daysSinceLastAired}` | number | Days since the most recent episode aired |
| `{metadata.hasNextEpisode}` | boolean | Whether a further episode is scheduled |
| `{metadata.daysUntilNextEpisode}` | number | Days until the next episode airs |
| `{metadata.anilistId}` | number | AniList ID (anime only) |
| `{metadata.malId}` | number | MyAnimeList ID (anime only) |
| `{metadata.hasSeaDex}` | boolean | Whether the requested item has a SeaDex entry at all |
Debug [#debug]
| Variable | Type | Description |
| --------------- | ------ | -------------------------------------- |
| `{debug.json}` | string | Raw JSON of the stream data |
| `{debug.jsonf}` | string | Pretty-printed JSON of the stream data |
***
Modifiers [#modifiers]
Modifiers are applied left to right, and each one acts on the result of the last:
```
{stream.filename::lower::truncate(20)}
```
Field and modifier names are case-insensitive, so `{stream.filename}` and `{Stream.FileName}` are the same. Whitespace inside the braces is ignored.
Applying a modifier to a field that has no value renders nothing, so `{stream.size::bytes}` is empty rather than an error when the size is unknown. Applying one to the wrong *type* is an error and is shown inline, e.g. `{stream.filename::bytes}` renders `{unknown_string_modifier(bytes)}`.
Any Type [#any-type]
| Modifier | Description |
| ------------------- | ------------------------------------ |
| `::default('text')` | Use `text` when the value is missing |
| `::string` | Convert to string |
```
{stream.quality::default('Unknown')} -> BluRay, or Unknown when absent
{stream.library::string} -> true
```
A quoted value can be used wherever a field is expected, which is useful for piping fixed text through modifiers:
```
{'n/a'::upper} -> N/A
{'ready'::smallcaps} -> ʀᴇᴀᴅʏ
```
String Modifiers [#string-modifiers]
| Modifier | Description |
| ---------------------------------- | -------------------------------------------------------------- |
| `::upper` | Convert to UPPERCASE |
| `::lower` | Convert to lowercase |
| `::smallcaps` | Convert letters to ꜱᴍᴀʟʟ ᴄᴀᴘꜱ |
| `::subscript` | Convert digits to ₀₁₂₃₄₅₆₇₈₉ |
| `::superscript` | Convert digits to ⁰¹²³⁴⁵⁶⁷⁸⁹ |
| `::translate('from', 'to')` | Map characters by position |
| `::title` | Title Case (capitalise first letter of each word) |
| `::replace('find', 'replaceWith')` | Replace all occurrences of `find` |
| `::remove('a', 'b', …)` | Delete every occurrence of each argument |
| `::truncate(N)` | Truncate to `N` characters and append `…` |
| `::date('pattern')` | Reformat a date — see [Date patterns](#date-patterns) |
| `::length` | Return length of the string |
| `::reverse` | Reverse the string |
| `::base64` | Encode as base64 |
| `::languageCode` | A language name or code → its ISO 639 code (`Japanese` → `JA`) |
| `::languageEmoji` | A language name or code → its flag emoji (`Japanese` → 🇯🇵) |
`::smallcaps` only affects letters, so it combines with the digit modifiers:
```
{stream.resolution::smallcaps} -> 2160ᴘ
{stream.resolution::subscript::smallcaps} -> ₂₁₆₀ᴘ
```
`::translate` maps each character of `from` to the character at the same position in `to`. Characters without a counterpart are left unchanged, so a shorter `to` never drops text:
```
{stream.quality::translate('Bl','Яⅼ')} -> ЯⅼuRay
```
Number Modifiers [#number-modifiers]
| Modifier | Description |
| ------------------------- | ----------------------------------------------------------------- |
| `::bytes` / `::bytes10` | Format as base-10 bytes (KB, MB, GB) |
| `::sbytes` / `::sbytes10` | Concise base-10 byte format |
| `::bytes2` | Format as base-2 bytes (KiB, MiB, GiB) |
| `::sbytes2` | Concise base-2 byte format |
| `::rbytes` / `::rbytes10` | Like `::bytes` but rounded |
| `::rbytes2` | Like `::bytes2` but rounded |
| `::bitrate` | Format as bitrate (Kbps, Mbps) |
| `::rbitrate` | Like `::bitrate` but rounded |
| `::sbitrate` | Concise bitrate (e.g. `5.2 Mbps`) |
| `::time` | Format milliseconds as `1h:23m:45s` |
| `::time('pattern')` | Format milliseconds — see [Duration patterns](#duration-patterns) |
| `::star` | Star rating ★ out of 5 from a 0–100 score |
| `::pstar` | Padded star rating (always 5 stars e.g. ★★★☆☆) |
| `::hex` | Encode to hexadecimal |
| `::octal` | Encode to octal |
| `::binary` | Encode to binary |
Date & Duration Patterns [#date--duration-patterns]
`::date('…')` and `::time('…')` take a pattern string, so you can build any style you like instead of picking from a fixed list.
Both share the same rules:
* `%X` is a token, replaced with a value. Everything else is literal text.
* `%-X` is the unpadded form of `%X` (`7` instead of `07`).
* `[...]` marks an **optional group**: it disappears when every token inside it is zero. Use it to hide empty units.
* `%%`, `%[` and `%]` emit a literal `%`, `[` and `]`.
* An unrecognised token is left as-is, so typos show up in the output.
Duration patterns [#duration-patterns]
Used by `::time('…')` on `{stream.duration}`.
| Token | Description | Example |
| ----- | -------------------- | ------- |
| `%H` | Hours, zero padded | `01` |
| `%-H` | Hours | `1` |
| `%M` | Minutes, zero padded | `23` |
| `%-M` | Minutes | `23` |
| `%S` | Seconds, zero padded | `45` |
| `%-S` | Seconds | `45` |
The **largest unit in the pattern carries the overflow**, so a pattern with no `%H` reads minutes as the total:
| Pattern | 1h 23m 45s | 45s |
| ---------------------- | ------------ | ---------- |
| `'%H:%M:%S'` | `01:23:45` | `00:00:45` |
| `'%-Hh %-Mm'` | `1h 23m` | `0h 0m` |
| `'[%-Hh ]%-Mm'` | `1h 23m` | `0m` |
| `'[%-Hh ]%-Mm[ %-Ss]'` | `1h 23m 45s` | `0m 45s` |
| `'%-M min'` | `83 min` | `0 min` |
Date patterns [#date-patterns]
Used by `::date('…')` on `{stream.date}`. Dates are read in UTC; anything that isn't a valid `YYYY-MM-DD` is passed through untouched.
| Token | Description | Example |
| ----- | -------------------------------- | --------- |
| `%Y` | Full year | `2023` |
| `%y` | Two-digit year | `23` |
| `%m` | Month number, zero padded | `07` |
| `%-m` | Month number | `7` |
| `%B` | Month name | `July` |
| `%b` | Short month name | `Jul` |
| `%d` | Day of month, zero padded | `04` |
| `%-d` | Day of month | `4` |
| `%o` | Day of month with ordinal suffix | `4th` |
| `%A` | Weekday name | `Tuesday` |
| `%a` | Short weekday name | `Tue` |
| Pattern | `2023-07-04` |
| ------------- | ---------------- |
| `'%Y-%m-%d'` | `2023-07-04` |
| `'%-d %b %Y'` | `4 Jul 2023` |
| `'%B %o, %Y'` | `July 4th, 2023` |
| `'%d/%m/%y'` | `04/07/23` |
| `'%b %Y'` | `Jul 2023` |
| `'%A'` | `Tuesday` |
Patterns chain with the other modifiers as usual:
```
{stream.date::date('%B %o, %Y')::upper} -> JULY 4TH, 2023
```
`%o` already includes the day number, so write `%B %o` (`July 4th`) — not `%B %-d%o`, which would render `July 44th`.
Array Modifiers [#array-modifiers]
| Modifier | Description |
| --------------------- | ------------------------------------------------------ |
| `::join('separator')` | Join elements with separator |
| `::slice(start, end)` | Return a section (end is optional) |
| `::length` | Number of elements |
| `::first` | First element |
| `::last` | Last element |
| `::random` | Single random element |
| `::sort` | Sort (alphabetical for strings, numerical for numbers) |
| `::rsort` | Reverse sort |
| `::lsort` | Lexicographic sort (case-sensitive) |
| `::reverse` | Reverse order |
| `::languageCode` | Each language name/code → its ISO 639 code |
| `::languageEmoji` | Each language name/code → its flag emoji |
Conditional Modifiers [#conditional-modifiers]
Conditionals evaluate to `true` or `false` and control what text is shown:
```
{variable.property::conditionalModifier["trueString"||"falseString"]}
```
**Example:** Show seeders only if greater than 1:
```
{stream.seeders::>1["Seeders: {stream.seeders}"||""]}
```
| Modifier | Description | Types |
| ----------------- | ------------------------------------------------- | ----------------------------------------- |
| `istrue` | Value is `true` | `boolean` |
| `isfalse` | Value is `false` | `boolean` |
| `exists` | Not null, undefined, empty string, or empty array | `string`, `array`, `any` |
| `in('a', 'b', …)` | Value is one of the listed options | `string`, `number`, `boolean`, `string[]` |
| `$X` | Starts with `X` / first array element is `X` | `string`, `string[]` |
| `^X` | Ends with `X` / last array element is `X` | `string`, `string[]` |
| `~X` | Contains `X` | `string`, `string[]` |
| `=X` | Exactly equal to `X` | `string`, `number` |
| `>=X` | Greater than or equal to `X` | `number` |
| `<=X` | Less than or equal to `X` | `number` |
| `>X` | Greater than `X` | `number` |
| ` Cached
not cached -> Uncached
unknown -> Unknown
```
Without a third branch, a missing value renders nothing. `istrue` and `isfalse` both evaluate to `false` for a missing value, so use the third branch when the three states need distinguishing.
Branch text is taken literally. To include a `"` character in a branch, escape it as `\"`:
```
{stream.title::exists["say \"hi\""||""]} -> say "hi"
```
Nesting [#nesting]
A conditional can contain another conditional inside either branch, and the inner one's quotes need no special treatment: a branch only ends at a quote at the outermost brace level, and a nested conditional is always wrapped in `{…}`, so its quotes are never taken for the outer branch's delimiters. This holds at any depth.
```
{stream.resolution::exists["{stream.quality::exists["{stream.resolution} {stream.quality}"||"{stream.resolution}"]}"||"Unknown"]}
2160p + BluRay -> 2160p BluRay
2160p only -> 2160p
neither -> Unknown
```
Nesting is limited to five levels deep. Beyond that the branch is emitted as plain text.
***
Optional Groups [#optional-groups]
`{? ... ?}` renders only when every field inside it has a value. If any is missing, the whole group disappears, including its literal text.
```
{?📅 {stream.age} ?}
age known -> 📅 30d
age unknown -> (nothing, not even the icon)
```
Groups nest, so a separator can depend on its own field:
```
{?[{stream.resolution}{? · {stream.quality}?}]?}
2160p + BluRay -> [2160p · BluRay]
2160p only -> [2160p]
neither -> (nothing)
```
A group tests whether the **field** has a value, not whether the rendered text looks empty. `{?{stream.type::replace('debrid','')}?}` still renders, because `stream.type` was present — the modifier just blanked it. This matches `::exists`.
Conditionals [#conditionals]
Chain multiple conditions together using `and`, `or`, or `xor`:
```
{var1::cond1::or::var2::cond2["trueString"||"falseString"]}
```
Conditions are evaluated **left to right**: `(x and y or z)` → `((x and y) or z)`.
| Operator | Description |
| -------- | ------------------------------------ |
| `and` | Both expressions must be true |
| `or` | At least one expression must be true |
| `xor` | Exactly one expression must be true |
**Example:** Show seeders only when the stream is either uncached or a P2P torrent:
```
{service.cached::isfalse::or::stream.type::=p2p::and::stream.seeders::>0["Seeders: {stream.seeders}"||""]}
```
***
Tools [#tools]
Use formatting tools with `{tools.toolName}`:
| Tool | Description |
| -------------------- | ------------------------------------- |
| `{tools.newLine}` | Add a newline at this position |
| `{tools.removeLine}` | Remove the entire line wherever found |
`{tools.newLine}` works in the formatter preview but how it renders in Stremio
is platform-dependent. Many platforms do not respect newlines in the `name`
field. Avoid building your name template around `{tools.newLine}`.
***
Chillio [#chillio]
For **Chillio**, the `name` template maps to the ChillLink `title` field. The `description` template is split line-by-line into `metadata` strings — each line becomes a separate metadata entry.
***
Examples [#examples]
Community-created formats are shared on the [Discord Server](https://discord.viren070.me/).
You can also view the built-in formatter definitions at:
[`packages/core/src/formatters/predefined.ts`](https://github.com/Viren070/AIOStreams/blob/main/packages/core/src/formatters/predefined.ts)
# Stream Expression Language
The **Stream Expression Language (SEL)** lets you write custom logic to filter, rank, and select streams. It is used in **Groups**, **Stream Expression Filters**, **Precompute Selectors**, and more.
***
Operators [#operators]
| Operator | Description | Example |
| ----------------- | ------------------------------------- | ------------------------------------- |
| `==` | Equal | `queryType == 'movie'` |
| `!=` | Not equal | `queryType != 'series'` |
| `>` `<` `>=` `<=` | Comparison | `count(previousStreams) > 5` |
| `in` | Membership — value exists in sequence | `'Torrentio' in queriedAddons` |
| `+` `-` `*` `/` | Arithmetic | `totalTimeTaken / 1000` |
| `and` `or` `not` | Logical | `isAnime and season == 1` |
| `x ? y : z` | Ternary | `isAnime ? seadex(streams) : streams` |
| `()` | Grouping | `(a or b) and c` |
***
Nesting Functions [#nesting-functions]
Most functions accept a stream list as input and return a stream list as output. Chain them together by nesting:
```
count(resolution(cached(streams), '2160p'))
```
1. `cached(streams)` — get only cached streams
2. `resolution(..., '2160p')` — keep only 4K ones
3. `count(...)` — count how many remain
***
Context Constants [#context-constants]
The constants available to you depend on where the expression is used.
Group Conditions [#group-conditions]
| Constant | Type | Description |
| ------------------------ | ---------------- | ------------------------------------------------------ |
| `previousStreams` | `ParsedStream[]` | Streams found by the last group that ran |
| `totalStreams` | `ParsedStream[]` | All streams found by all groups so far |
| `queryType` | `string` | Media type being searched (e.g. `"movie"`, `"series"`) |
| `previousGroupTimeTaken` | `number` | Time the last group took (ms) |
| `totalTimeTaken` | `number` | Total time spent so far (ms) |
Dynamic Exit Conditions [#dynamic-exit-conditions]
| Constant | Type | Description |
| ---------------- | ---------------- | -------------------------------------------------------------------------- |
| `totalStreams` | `ParsedStream[]` | All streams found by all groups so far |
| `totalTimeTaken` | `number` | Total time spent so far (ms) |
| `queryType` | `string` | Media type (e.g. `"movie"`, `"series"`, `"anime.series"`, `"anime.movie"`) |
| `queriedAddons` | `string[]` | Addon names queried so far |
Stream Expression Filters & Precompute Selector [#stream-expression-filters--precompute-selector]
| Constant | Type | Default | Description |
| ---------------------- | ---------------- | ------- | ----------------------------------------------------- |
| `streams` | `ParsedStream[]` | — | All available streams |
| `queryType` | `string` | `''` | Media type |
| `isAnime` | `boolean` | `false` | Whether the media is anime |
| `season` | `number` | `-1` | Season number |
| `episode` | `number` | `-1` | Episode number |
| `absoluteEpisode` | `number` | `-1` | Absolute episode number |
| `genres` | `string[]` | `[]` | List of genres |
| `title` | `string` | `''` | Media title |
| `year` | `number` | `0` | Release year |
| `yearEnd` | `number` | `0` | End year (series) |
| `daysSinceRelease` | `number` | `-1` | Days since media was released |
| `runtime` | `number` | `0` | Runtime in minutes |
| `originalLanguage` | `string` | `''` | Original language (e.g. `"English"`) |
| `hasSeaDex` | `boolean` | `false` | Whether SeaDex results exist for this media |
| `hasNextEpisode` | `boolean` | `false` | Whether a next episode exists |
| `daysUntilNextEpisode` | `number` | `-1` | Days until next episode airs |
| `daysSinceFirstAired` | `number` | `-1` | Days since the first episode aired |
| `daysSinceLastAired` | `number` | `-1` | Days since the last episode aired |
| `latestSeason` | `number` | `-1` | Latest season number |
| `ongoingSeason` | `boolean` | `false` | Viewing the latest season with a next episode pending |
***
Naming Expressions [#naming-expressions]
Assign a name to a Stream Expression by adding a C-style comment. The name is used in the [Custom Formatter](/reference/custom-formatter).
```
/* 4K Dolby Vision */ merge(resolution(streams, '2160p'), visualTag(streams, 'DV'))
```
Multiple names (for Ranked Stream Expressions):
```
/* 4K */ resolution(streams, '2160p') /* High Quality */
```
Reference-only comment (not used as a name — start with `#`):
```
/*# This is just a note */ quality(streams, 'Bluray')
```
Preferred vs. Ranked [#preferred-vs-ranked]
| Type | Matching behaviour | Formatter variable |
| ------------- | ---------------------------------------------------------- | --------------------- |
| **Preferred** | Stream matches only the *first* expression it satisfies | `{stream.seMatched}` |
| **Ranked** | Stream can match *multiple* expressions; scores are summed | `{stream.rseMatched}` |
***
Function Reference [#function-reference]
Utility Functions [#utility-functions]
negate() [#negate]
Returns streams from `originalStreamList` that are **not** in `streamsToExclude`.
| Parameter | Type | Description |
| -------------------- | ---------------- | ------------------------------- |
| `streamsToExclude` | `ParsedStream[]` | Streams to exclude |
| `originalStreamList` | `ParsedStream[]` | Full stream list to filter from |
```
negate(quality(streams, 'CAM', 'TS'), streams)
```
***
merge() [#merge]
Combines multiple stream arrays into one, removing duplicates.
```
merge(visualTag(streams, 'DV'), audioTag(streams, 'Atmos'))
```
***
slice() [#slice]
Returns a section of the stream list. Negative indices count from the end.
| Parameter | Type | Description |
| --------- | ---------------- | --------------------------------------------- |
| `streams` | `ParsedStream[]` | Stream list to slice |
| `start` | `number` | Start index (inclusive) |
| `end` | `number?` | End index (exclusive). Omit to go to the end. |
```
slice(addon(streams, 'TorBox'), 0, 5) // first 5 TorBox streams
```
***
perGroup() [#pergroup]
Groups streams by an attribute, keeps up to **`n`** streams from each group, then **interleaves** the groups round-robin so results are balanced across all values.
| Parameter | Type | Description |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `attribute` | `string` | The stream property to group by (see below) |
| `n` | `number` | Maximum streams to take from each group (must be a positive integer) |
| `...filterValues` | `string?` | Optional group keys to include. If omitted, **all** groups are included. Matching is case-insensitive. |
**Supported attributes:**
| Attribute | Groups by |
| -------------- | ---------------------------------------------- |
| `resolution` | Video resolution (`2160p`, `1080p`, …) |
| `quality` | Quality tag (`Bluray`, `WEB-DL`, …) |
| `encode` | Encoding format (`H.265`, `AV1`, …) |
| `type` | Stream type (`debrid`, `p2p`, …) |
| `service` | Debrid service ID (`realdebrid`, `torbox`, …) |
| `indexer` | Indexer name (`Torrentio`, `Knightcrawler`, …) |
| `releaseGroup` | Release group name (`YIFY`, `FLUX`, …) |
| `visualTag` | Visual tag (`DV`, `HDR`, `HDR10+`, …) |
| `audioTag` | Audio format tag (`Atmos`, `DTS-HD MA`, …) |
| `audioChannel` | Audio channel layout (`5.1`, `7.1`, …) |
| `language` | Audio language (`English`, `Japanese`, …) |
| `subtitle` | Subtitle language (`English`, `Japanese`, …) |
Streams with no value for the chosen attribute are bucketed under `'Unknown'` (or `'none'` for `service`).
For multi-value attributes (`visualTag`, `audioTag`, `audioChannel`, `language`, `subtitle`) a stream is assigned to the **first** group key that matches the `filterValues` list, or to its first value when no filter is specified. A stream is never included more than once in the output.
**How interleaving works:**
Given streams `[A1, A2, B1, B2, C1]` grouped by resolution with `n=2`:
* Group `2160p`: `[A1, A2]`
* Group `1080p`: `[B1, B2]`
* Group `720p`: `[C1]`
Result: `[A1, B1, C1, A2, B2]` — one from each group per round.
```
// Up to 3 streams per resolution, all resolutions
perGroup(streams, 'resolution', 3)
// Up to 2 streams per resolution, 4K and 1080p only
perGroup(streams, 'resolution', 2, '2160p', '1080p')
// Up to 1 stream per debrid service
perGroup(type(streams, 'debrid'), 'service', 1)
// Top 2 streams from each addon, balanced
perGroup(streams, 'indexer', 2)
```
***
values() [#values]
Extracts a numeric property from each stream as an array of numbers. Used with [Math Functions](#math-functions).
| Parameter | Type | Description |
| ----------- | ---------------- | ------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `attribute` | `string` | Property to extract |
**Accepted attributes:** `'bitrate'` `'size'` `'folderSize'` `'age'` `'duration'` `'seeders'` `'seScore'` `'regexScore'`
```
avg(values(streams, 'bitrate'))
```
***
Filter Functions [#filter-functions]
All filter functions follow the pattern: `fn(streams, ...values)` → `ParsedStream[]`
***
indexer() [#indexer]
Filter by originating indexer name (case-sensitive).
```
indexer(streams, '1337x', 'RARBG')
```
***
resolution() [#resolution]
Filter by video resolution.
**Accepted values:** `'2160p'` `'1440p'` `'1080p'` `'720p'` `'576p'` `'480p'` `'360p'` `'240p'` `'144p'` `'Unknown'`
```
resolution(streams, '2160p', '1080p')
```
***
quality() [#quality]
Filter by quality tag.
**Accepted values:** `'Bluray REMUX'` `'Bluray'` `'WEB-DL'` `'WEBRip'` `'HDRip'` `'HC HD-Rip'` `'DVD REMUX'` `'DVDRip'` `'HDTV'` `'CAM'` `'TS'` `'TC'` `'SCR'` `'Unknown'`
```
quality(streams, 'Bluray', 'WEB-DL')
```
***
encode() [#encode]
Filter by video encoding format (e.g. `'H.264'`, `'H.265'`, `'HEVC'`, `'x265'`).
```
encode(streams, 'H.265', 'HEVC')
```
***
type() [#type]
Filter by stream type.
**Accepted values:** `'debrid'` `'usenet'` `'http'` `'live'` `'p2p'` `'external'` `'youtube'`
```
type(streams, 'debrid', 'p2p')
```
***
visualTag() [#visualtag]
Filter by visual tag (e.g. `'HDR'`, `'DV'`, `'HDR10'`, `'HDR10+'`, `'HLG'`).
```
visualTag(streams, 'DV', 'HDR')
```
***
audioTag() [#audiotag]
Filter by audio format tag (e.g. `'Atmos'`, `'DTS'`, `'DTS-HD MA'`, `'AC3'`, `'EAC3'`).
```
audioTag(streams, 'Atmos', 'DTS-HD MA')
```
***
audioChannels() [#audiochannels]
Filter by audio channel configuration (e.g. `'5.1'`, `'7.1'`, `'2.0'`).
```
audioChannels(streams, '5.1', '7.1')
```
***
language() [#language]
Filter by audio language (e.g. `'English'`, `'Spanish'`).
```
language(streams, 'English')
```
***
subtitle() / subtitles() [#subtitle--subtitles]
Filter by subtitle language (e.g. `'English'`, `'Spanish'`).
```
subtitle(streams, 'English')
subtitles(streams, 'English')
```
***
seeders() [#seeders]
Filter by torrent seeder count.
| Parameter | Type | Description |
| --------- | ---------------- | --------------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `min` | `number?` | Minimum seeders (inclusive) |
| `max` | `number?` | Maximum seeders (inclusive) |
```
seeders(streams, 5, 200)
```
***
age() [#age]
Filter by age in hours.
| Parameter | Type | Description |
| --------- | ---------------- | -------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `min` | `number?` | Minimum age in hours |
| `max` | `number?` | Maximum age in hours |
```
age(streams, 24, 8760) // 1 day to 1 year old
```
***
size() [#size]
Filter by file size. Accepts human-readable strings (`'1GB'`, `'500MB'`) or raw byte counts.
| Parameter | Type | Description |
| --------- | ------------------- | ------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `min` | `number \| string?` | Minimum size |
| `max` | `number \| string?` | Maximum size |
```
size(streams, '1GB', '10GB')
```
***
bitrate() [#bitrate]
Filter by bitrate. Accepts `'5Mbps'`, `'5000kbps'`, or raw bits-per-second.
| Parameter | Type | Description |
| --------- | ------------------- | --------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `min` | `number \| string?` | Minimum bitrate |
| `max` | `number \| string?` | Maximum bitrate |
```
bitrate(streams, '5Mbps')
```
***
service() [#service]
Filter by Debrid service.
**Accepted values:** `'realdebrid'` `'debridlink'` `'premiumize'` `'alldebrid'` `'torbox'` `'easydebrid'` `'debrider'` `'putio'` `'pikpak'` `'offcloud'` `'seedr'` `'easynews'` `'nzbdav'` `'altmount'` `'stremio_nntp'` `'stremthru_newz'` `'aiostreams'` `'torrin'`
```
service(streams, 'realdebrid', 'torbox')
```
***
cached() [#cached]
Filter for cached Debrid streams only.
```
cached(streams)
```
***
uncached() [#uncached]
Filter for non-cached streams only.
```
uncached(streams)
```
***
releaseGroup() [#releasegroup]
Filter by release group name (case-sensitive). Omitting names matches streams with *any* release group.
```
releaseGroup(streams, 'YIFY', 'FLUX')
```
***
seasonPack() [#seasonpack]
Filter for season pack streams.
| Parameter | Type | Description |
| --------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `mode` | `string?` | `'seasonPack'` — stream originates from a season pack. `'onlySeasons'` *(default)* — title contains only season info, no episode info (ambiguous pack). |
```
seasonPack(streams, 'seasonPack') // from any season pack
seasonPack(streams, 'onlySeasons') // ambiguous packs without episode reference
```
***
addon() [#addon]
Filter by addon name (case-sensitive).
```
addon(streams, 'Torrentio', 'Knightcrawler')
```
***
library() [#library]
Filter for streams from a personal Debrid library.
```
library(streams)
```
***
idMatched() [#idmatched]
Filter for streams whose source identified the release by **external ID** rather than by searching its
title as text.
This is the case when a built-in addon queries something like an `imdbid`/`tvdbid`/`tmdbid` against a
database that is actually indexed by those IDs — most commonly ID search on your own Newznab/Torznab
indexers. Such a result is already known to be the right release, so it is a good candidate to let
skip AIOStreams' own filename-derived matching filters via [`passthrough()`](#passthrough).
Streams are flagged by: **Newznab** and **Torznab** (when the search used an ID parameter rather than
falling back to a `q=` text query), **EZTV**, **SeaDex**, and the **Library** addon.
```
passthrough(idMatched(streams), 'title', 'year', 'episode')
```
Meta-indexers accept an ID search but silently fall back to a title search for any underlying
indexer that doesn't support that ID — so results from an **NZBHydra2** endpoint entered as a
custom Newznab/Torznab URL for example, get flagged without having been
ID-matched. If you use one, scope the expression to the addons you trust:
`passthrough(idMatched(addon(streams, 'DrunkenSlug')), 'title', 'episode')`.
The flag belongs to whichever copy of a release carried it, so it is lost if a duplicate from
another addon wins deduplication. To keep it, enable **Merge Duplicates** under **Filters →
Deduplicator** and add `idMatched` to **Merged Metadata Fields**.
***
message() [#message]
Filter by a stream's `message` property.
| Parameter | Type | Description |
| ------------- | ----------------------- | --------------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `mode` | `'exact' \| 'includes'` | Exact match or substring |
| `...messages` | `string` | One or more message strings |
```
message(streams, 'includes', 'cached')
```
***
seadex() [#seadex]
Filter for [SeaDex](https://releases.moe)-listed streams (best anime releases).
Streams are matched against SeaDex in two ways: by **info hash** (exact, the stream is one of the listed torrents) or, when the hash doesn't match, by **release group** (heuristic, the stream comes from a group listed on the SeaDex entry).
| Parameter | Type | Description |
| --------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `type` | `'best' \| 'alt' \| 'all'?` | `'best'` for only SeaDex "Best" entries; `'alt'` for listed-but-not-best entries; omit or `'all'` for all SeaDex matches |
| `method` | `'hash' \| 'group'?` | Restrict to a match method; omit for both. Use `'hash'` if you only want exact matches |
Only active when SeaDex integration is enabled under **Filters →
Miscellaneous**.
```
seadex(streams, 'best')
seadex(streams, 'all', 'hash')
seadex(streams, 'best', 'hash')
```
***
keyword() / keywords() [#keyword--keywords]
Filter streams by one or more keywords. Uses the same matching logic as the Keyword UI filters (Required / Excluded / Included / Preferred Keywords): keywords are escaped and joined into a single case-insensitive regex with the same word-boundary heuristics. You explicitly choose which stream attribute(s) the keywords are tested against.
Useful when you want fine-grained keyword filtering inside an SEL expression (e.g. inside a group, ranked stream expression, or precompute selector) without affecting the global keyword filters.
| Parameter | Type | Description |
| ------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `attributes` | `string` | Comma-separated attribute names that the keywords should be tested against. Use `'all'` or `'*'` to match every attribute. Case-insensitive. |
| `...keywords` | `string` | One or more keyword strings. Whitespace inside a keyword matches separators like `.`, `_`, `-`. |
**Accepted attribute names:** `filename`, `folderName`, `indexer`, `releaseGroup` — the same set the Keyword UI filters check.
```
keyword(streams, 'filename', 'remux') // match 'remux' only in filenames
keyword(streams, 'filename,releaseGroup', 'flux', 'ntb') // multiple attributes, multiple keywords
keywords(streams, 'all', 'extended cut') // match every attribute (= UI keyword filter behavior)
keyword(streams, '*', 'remux') // shorthand for 'all'
```
To exclude streams matching the keywords, combine with [`negate()`](#negate):
```
negate(keyword(streams, 'all', 'cam', 'hdcam'), streams)
```
***
regexMatched() [#regexmatched]
Filter for streams that matched a preferred or ranked regex filter.
| Parameter | Type | Description |
| ---------- | ---------------- | ------------------------------------------------------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `...names` | `string?` | Optional filter names. If omitted, matches any regex filter. |
Does not work in **Included Stream Expressions** — regex matching is computed
after filtering.
```
regexMatched(streams) // matched any regex
regexMatched(streams, 'HighQuality', 'Extras') // matched specific ones
```
***
regexMatchedInRange() [#regexmatchedinrange]
Filter for streams where the matched regex filter's index is within `[min, max]`.
Does not work in **Included Stream Expressions**. This only uses
`stream.regexMatched` (preferred regex result), so it does **not** work with
ranked regex matches.
```
regexMatchedInRange(streams, 0, 5)
```
***
seMatched() [#sematched]
Filter for streams that matched a preferred Stream Expression.
| Parameter | Type | Description |
| ---------- | ---------------- | ------------------------------------------------------------------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `...names` | `string?` | Optional expression names. If omitted, matches any preferred expression. |
```
seMatched(streams) // matched any preferred expression
seMatched(streams, '4K', 'Anime HQ') // matched specific expression names
```
***
seMatchedInRange() [#sematchedinrange]
Filter for streams where the matched preferred Stream Expression index is within `[min, max]`.
Does not work in **Included Stream Expressions**. This only uses
`stream.streamExpressionMatched` (preferred Stream Expression result), so it
does **not** work with ranked stream expressions.
```
seMatchedInRange(streams, 0, 3)
```
***
rseMatched() [#rsematched]
Filter for streams that matched one or more ranked Stream Expressions.
| Parameter | Type | Description |
| ---------- | ---------------- | --------------------------------------------------------------------- |
| `streams` | `ParsedStream[]` | Stream list |
| `...names` | `string?` | Optional ranked expression names. If omitted, matches any ranked hit. |
```
rseMatched(streams) // matched any ranked expression
rseMatched(streams, 'BD T1', 'Web T1') // matched specific ranked names
```
***
streamExpressionScore() / seScore() [#streamexpressionscore--sescore]
Filter by Ranked Stream Expression score.
Does not work in
**Included Stream Expressions**
.
```
streamExpressionScore(streams, 100, 500)
```
***
regexScore() [#regexscore]
Filter by Ranked Regex score.
Does not work in
**Included Stream Expressions**
.
```
regexScore(streams, 50)
```
***
passthrough() [#passthrough]
Flags streams to bypass specific pipeline stages. Best used in **Included Stream Expressions** or rarely in **Excluded Stream Expressions**.
While any stream selected by an Included Stream Expression (ISE) is already protected from normal filters, it still passes through stages like deduplication and result limiting. Wrapping your ISE with `passthrough()` expands which stages those streams skip.
When used in an **Excluded Stream Expression**: flagged streams won't be removed by *subsequent* excluded expressions. Since ESE filters run last, most stage values are not applicable here — only `excluded` is relevant.
| Parameter | Type | Description |
| ----------- | ---------------- | ------------------------------------------ |
| `streams` | `ParsedStream[]` | Stream list |
| `...stages` | `string?` | Stages to skip. Omit to bypass all stages. |
**Accepted stage values:** `filter` `language` `subtitle` `dedup` `limit` `excluded` `required` `title` `year` `episode` `digitalRelease`
```
passthrough(streams, 'language', 'limit')
passthrough(streams) // bypass all stages
// let results your indexer matched by ID skip filename-based matching
passthrough(idMatched(streams), 'title', 'year', 'episode')
```
***
pin() [#pin]
Pins streams to the very top or bottom of the final results list, overriding sort order. Typically used in **Excluded Stream Expressions**. Can also be used in **Required Stream Expressions** — but since unselected streams get filtered out, set `returnMatched: true` when doing so.
| Parameter | Type | Default | Description |
| --------------- | ------------------- | ------- | ---------------------------------------------------------------------------------- |
| `streams` | `ParsedStream[]` | — | Streams to pin |
| `position` | `'top' \| 'bottom'` | `'top'` | Where to pin them |
| `returnMatched` | `boolean` | `false` | `true` → return pinned streams; `false` → return empty array (useful for chaining) |
```
pin(seadex(streams, 'best'), 'top')
```
***
Math Functions [#math-functions]
Math functions operate on arrays of numbers, typically from [`values()`](#values).
Most accept either an array (`avg(values(streams, 'bitrate'))`) or individual arguments (`avg(1, 2, 3)`).
| Function | Alias | Description |
| ------------------------ | ------ | ------------------------------------------- |
| `count(arr)` | — | Number of items in the array |
| `max(numbers)` | — | Largest value |
| `min(numbers)` | — | Smallest value |
| `avg(numbers)` | `mean` | Arithmetic mean |
| `sum(numbers)` | — | Sum of all values |
| `median(numbers)` | `q2` | Middle value (50th percentile) |
| `mode(numbers)` | — | Most frequently occurring value |
| `range(numbers)` | — | `max - min` |
| `variance(numbers)` | — | Spread from the average |
| `stddev(numbers)` | — | Standard deviation |
| `percentile(numbers, p)` | — | Value below which `p`% of observations fall |
| `q1(numbers)` | — | First quartile (25th percentile) |
| `q3(numbers)` | — | Third quartile (75th percentile) |
| `iqr(numbers)` | — | Interquartile range (`q3 - q1`) |
| `skewness(numbers)` | — | Asymmetry of the distribution |
| `kurtosis(numbers)` | — | "Tailedness" of the distribution |
# Templates
import { Callout } from 'fumadocs-ui/components/callout';
import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';
A template is a shareable JSON file that applies a partial AIOStreams configuration to a user's instance. Templates support dynamic expressions, user inputs, and service-conditional logic — making them powerful enough to cover most configuration scenarios without hardcoding anything.
***
Template Structure [#template-structure]
```json
{
"metadata": { ... },
"config": { ... }
}
```
* **`metadata`** — describes the template (name, author, inputs, etc.)
* **`config`** — a **partial** `UserData` object. Only the keys you include are applied; everything absent is left unchanged in the user's existing config.
All dynamic expressions inside `config` are evaluated at load time, before the config is merged.
***
metadata Fields [#metadata-fields]
| Field | Type | Required | Description |
| ---------------------- | ----------------------------------------- | -------- | -------------------------------------------------------------------------------------- |
| `id` | string (1–100) | No | Unique ID. Auto-generates a UUID if omitted. Use namespaced form: `author.my-template` |
| `name` | string (1–100) | **Yes** | Display name shown in the template browser |
| `description` | string (1–1000) | **Yes** | Supports Markdown (links, bold, lists) |
| `author` | string (1–20) | **Yes** | Author name or handle |
| `source` | `"builtin"` \| `"custom"` \| `"external"` | No | Defaults to `"builtin"`. Use `"external"` for user-imported remote templates |
| `version` | semver string | No | Defaults to `"1.0.0"`. Used for update comparisons |
| `category` | string (1–20) | **Yes** | Shown in the browser filter bar (e.g. `"Debrid"`, `"Usenet"`) |
| `services` | `ServiceId[]` | No | Controls the service selection screen — see [Service Handling](#service-handling) |
| `serviceRequired` | boolean | No | Whether a service must be selected before applying |
| `setToSaveInstallMenu` | boolean | No | Redirects the UI to Save & Install after loading. Defaults to `true` |
| `sourceUrl` | URL string | No | URL the template was fetched from — enables auto-update |
| `changelog` | `ChangelogEntry[]` | No | Inline version history. See [Changelog](#changelog) |
| `changelogUrl` | URL string | No | URL to a remote `CHANGELOG.md`. See [Changelog](#changelog) |
| `inputs` | `InputDefinition[]` | No | User-fillable options shown before loading. See [Template Inputs](#template-inputs) |
***
Service Handling [#service-handling]
The `services` field controls whether (and how) the user is asked to select a debrid service.
| Scenario | Behaviour |
| ---------------------------------------- | ------------------------------------------------------------------------------ |
| `services` not set | All services are shown in the selection screen |
| `services: []` | Service selection is skipped entirely |
| `services: ["realdebrid", "torbox"]` | Only the listed services are shown |
| Single service + `serviceRequired: true` | Selection is skipped; user is prompted for that service's credentials directly |
| `serviceRequired` absent or `false` | A "Skip" button is shown |
At load time, selected services are available via `services.`. The bare `services` reference (no service ID) is truthy when at least one service is selected and falsy when none are — making it the canonical way to distinguish debrid mode from P2P/no-service mode.
***
Template Inputs [#template-inputs]
Inputs are defined in `metadata.inputs`. They appear in a dialog before the template loads. Values are accessible throughout `config` via `inputs.`.
Input Fields [#input-fields]
| Field | Type | Required | Description |
| ------------------ | ---------------------------- | --------------------------- | --------------------------------------------------------------------------- |
| `id` | string | **Yes** | Identifier used in `inputs.` references |
| `name` | string | **Yes** | Label shown in the UI |
| `description` | string | **Yes** | Help text (supports Markdown) |
| `type` | see below | **Yes** | Controls the UI widget |
| `required` | boolean | No | Prevents proceeding if the field is empty |
| `default` | any | No | Pre-filled value |
| `options` | `{ value, label }[]` | For `select`/`multi-select` | List of choices |
| `showInSimpleMode` | boolean | No | Set `false` to hide in Simple mode. Defaults to `true` |
| `advanced` | boolean | No | Shorthand for `showInSimpleMode: false` |
| `constraints` | `{ min?, max?, forceInUi? }` | No | Min/max for strings (length) or numbers (value) |
| `__if` | string | No | Hide this input when the condition is false. Only `services.` supported |
| `intent` | see Alert | For `alert` | Controls the colour/icon of an alert banner |
| `socials` | `{ id, url }[]` | For `socials` | Social links to render |
Input Types [#input-types]
| Type | Widget | Notes |
| -------------------- | ---------------- | -------------------------------------------------------- |
| `string` | Text input | |
| `password` | Masked input | Not stored in plaintext after import |
| `number` | Number input | Returns a number; blank returns `undefined` |
| `boolean` | Toggle | Returns `true` or `false` |
| `select` | Dropdown | Requires `options` |
| `select-with-custom` | Dropdown + text | Same as `select` + "Custom" option with free-text |
| `multi-select` | Multi-select | Requires `options`. Returns an array |
| `url` | URL text input | Client-side format validation |
| `alert` | Styled banner | Displays info/warning/error. See Alert section |
| `socials` | Icon row | Renders social media icon links. See Socials section |
| `subsection` | Button → modal | Groups sub-inputs. See Subsections section |
| `nab-endpoint` | URL + key + test | Newznab/Torznab endpoint pair. See Nab endpoints section |
Nab endpoints [#nab-endpoints]
A `nab-endpoint` renders the URL and API key of a Newznab/Torznab indexer together, with a button that probes the endpoint and reports whether it works. Like a `subsection` it holds its value as an object keyed by `subOptions`, accessed with dot notation.
It expects exactly two `subOptions`: `url` (the **full** endpoint including its path, usually ending in `/api`) and `apiKey`. Supply `options` on the `url` sub-option to offer a dropdown of known indexers; omit it for a plain text input.
The `nab` field tells the test button what to probe — `namespace` picks the protocol, and `preset` lets the server fall back to a preconfigured instance when the URL is left blank.
```json
{
"id": "api",
"name": "Newznab Endpoint",
"type": "nab-endpoint",
"nab": { "namespace": "newznab", "preset": "newznab" },
"subOptions": [
{
"id": "url",
"name": "Newznab URL",
"type": "select-with-custom",
"required": true,
"options": [{ "label": "NZBgeek", "value": "https://api.nzbgeek.info/api" }]
},
{ "id": "apiKey", "name": "API Key", "type": "password" }
]
}
```
Use in config as `{{inputs.api.url}}` and `{{inputs.api.apiKey}}`.
The test runs on the server rather than in the browser: most indexers send no CORS headers, and self-hosted ones are often plain `http`, so a direct fetch from the page would fail for reasons unrelated to the config being correct.
Subsections [#subsections]
A `subsection` renders as a trigger that opens a modal containing `subOptions`. Values are accessed with dot notation: `inputs..`.
```json
{
"id": "proxy",
"name": "Proxy Settings",
"description": "Optional proxy configuration.",
"type": "subsection",
"subOptions": [
{ "id": "url", "name": "Proxy URL", "type": "url" },
{ "id": "id", "name": "Proxy Service", "type": "select", "options": [...] }
]
}
```
Use in config as `{{inputs.proxy.url}}`.
The optional `subsectionIntent` field controls the visual appearance of the trigger:
| Value | Appearance |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"default"` (or omitted) | Gray card with a gear icon button on the right |
| `"block"` | Full-width clickable panel — name in bold, description in grey below |
| `"inline"` | Name and description on the left, compact **Open** button on the right — matches the style of other option types |
| `"pill"` | Full-width rounded pill button showing the option name — description rendered below |
| `"link"` | Brand-coloured underlined text link with a small arrow — no card or background. Good for optional/secondary subsections that should not draw attention |
| `"banner"` | Full-width card with a brand-coloured left accent border and a chevron on the right. Visually prominent without being as heavy as a button |
For intents that render a `Button` (`"inline"` and `"pill"`), the optional `buttonIntent` field overrides the button's colour/style. Accepts any Button intent value (e.g. `"primary"`, `"gray-outline"`, `"white-subtle"`). Defaults to `"white"` for `inline` and `"white-subtle"` for `pill`.
```json
{
"id": "advanced",
"name": "Advanced Settings",
"type": "subsection",
"subsectionIntent": "pill",
"buttonIntent": "primary-subtle",
"subOptions": [...]
}
```
```json
{
"id": "proxy",
"name": "Proxy Settings",
"description": "Optional proxy configuration.",
"type": "subsection",
"subsectionIntent": "inline",
"subOptions": [...]
}
```
Alert [#alert]
Displays a styled banner — useful for important notes before the user fills in options. Captures no value.
```json
{
"id": "notice",
"name": "Important",
"description": "This template requires an active RealDebrid subscription.",
"type": "alert",
"intent": "warning"
}
```
`intent` values: `info`, `success`, `warning`, `alert` (filled background), `info-basic`, `success-basic`, `warning-basic`, `alert-basic` (neutral card with coloured icon).
Socials [#socials]
Renders a row of social icon links. Captures no value.
```json
{
"id": "credits",
"name": "Credits",
"type": "socials",
"socials": [
{ "id": "github", "url": "https://github.com/yourname" },
{ "id": "ko-fi", "url": "https://ko-fi.com/yourname" }
]
}
```
Supported `id` values: `website` · `github` · `discord` · `ko-fi` · `patreon` · `buymeacoffee` · `github-sponsors` · `donate`
***
Dynamic Expressions [#dynamic-expressions]
{{}} — String Interpolation [#--string-interpolation]
Use inside any string value to substitute at load time:
```json
{
"addonLogo": "{{inputs.logoUrl}}",
"preferredLanguages": ["{{inputs.languages}}", "Original", "Unknown"]
}
```
**Type preservation:** When the entire string value is a single `{{...}}` token, the raw value is returned (array stays array, boolean stays boolean). Arrays are spread into the parent array.
**Credential refs:** `{{services..}}` injects a credential entered by the user during service setup. Resolved at the final save step.
```json
{ "apiKey": "{{services.torbox.apiKey}}" }
```
`{{services}}` resolves to the array of all selected service IDs (or comma-joined string when embedded in a larger string).
__if — Conditional Array Items [#__if--conditional-array-items]
Add `"__if": ""` to any **object inside an array**. The object is included when the condition is true; omitted when false.
```json
{
"excludedStreamExpressions": [
{
"__if": "inputs.torboxTier == nonPro",
"expression": "/*TB Non-Pro Download Limit*/ size(uncached(streams), '200GB')",
"enabled": true
}
]
}
```
Condition Syntax [#condition-syntax]
| Form | Example | Meaning |
| ------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- |
| Bare reference | `inputs.enableFeature` | Truthy if not `false`, `null`, `""`, or `[]` — `0` **is truthy** |
| Negation | `!inputs.enableFeature` | Logical NOT |
| Equality | `inputs.tier == premium` | String equals `"premium"` |
| Inequality | `inputs.tier != none` | String not equal to `"none"` |
| Array includes | `inputs.optionalFilters includes dvPassthrough` | Array contains the string |
| Numeric `>` / `>=` / `<` / `<=` | `inputs.count > 5` | Numeric comparison |
| Any service (debrid) | `services` | True when any service is selected |
| No service (P2P) | `!services` | True when no service is selected |
| Service check | `services.realdebrid` | Specific service is enabled |
| Nested subsection | `inputs.proxy.url` | Sub-option is filled |
| Compound `and` | `inputs.flag and inputs.tier == pro` | Both sub-conditions true |
| Compound `or` | `services.torbox or services.realdebrid` | At least one sub-condition true |
| Compound `xor` | `inputs.a xor inputs.b` | Exactly one sub-condition true |
**Precedence:** `and` > `xor` > `or`
At the Object-Key Level [#at-the-object-key-level]
`{ "__if": "cond", "__value": X }` as the *value* of a config key conditionally includes or removes the key:
```json
{
"formatter": {
"__if": "!inputs.retainFormatter",
"__value": { "id": "tamtaro" }
}
}
```
* Condition true → key is set to `X`
* Condition false → key is absent from applied config (user's value is kept)
__switch — Object Replacement [#__switch--object-replacement]
Replaces an entire object with a different object depending on an input value:
```json
"formatter": {
"__switch": "inputs.formatterStyle",
"cases": {
"torrentio": { "id": "torrentio" },
"gdrive": { "id": "gdrive" }
},
"default": { "id": "prism" }
}
```
Use `__switch: "services"` with `""` as the P2P case key (the resolved value is a comma-joined service ID string):
```json
"preferredStreamTypes": {
"__switch": "services",
"cases": { "": ["p2p"] },
"default": ["cached", "usenet"]
}
```
__value — Conditional Array Values [#__value--conditional-array-values]
Inject values directly into a parent array rather than inserting an object:
```json
{
"excludedVisualTags": [
"3D",
{ "__if": "inputs.excludeDV", "__value": "DV" },
{ "__if": "inputs.excludeHdr", "__value": ["HDR", "HDR10", "HDR10+"] }
]
}
```
__remove — Drop a Config Key [#__remove--drop-a-config-key]
Remove a key from the applied config unconditionally (leaves the user's existing value untouched):
```json
{ "formatter": { "__remove": true } }
```
Most useful as a `__switch` case value when "leave unchanged" is a selectable outcome.
***
Template Placeholders [#template-placeholders]
For fields the user should fill in themselves after loading:
| Placeholder | Meaning |
| ----------------------------------- | ---------------------------- |
| `""` | Required — must be filled |
| `""` | Same as above |
| `""` | Optional — can be left blank |
The frontend highlights unfilled placeholder values after the template is applied.
***
Validation Rules [#validation-rules]
Errors (block loading) [#errors-block-loading]
* `metadata.name`, `description`, `author`, `category` must be non-empty strings
* `metadata.version` must be a valid semver string
* `metadata.source` must be `"builtin"`, `"custom"`, or `"external"`
* `__if` condition must be a non-empty string
* `__if` namespace must be `inputs` or `services`
* `__switch` reference must start with `inputs.` or `services.`
Warnings (allowed but flagged) [#warnings-allowed-but-flagged]
* `{{inputs.}}` references an ID not declared in `metadata.inputs`
* `__if` references an `inputs.` not in `metadata.inputs`
* `__switch` references an input not in `metadata.inputs`
* `__switch` is missing a `cases` object
***
Changelog [#changelog]
Templates can expose a version history shown to users and used for update notifications.
Inline (changelog) [#inline-changelog]
```json
"changelog": [
{
"version": "1.2.0",
"date": "2026-03-01",
"content": "- Added support for multi-audio streams\n- Fixed sort order bug"
},
{
"version": "1.1.0",
"date": "2025-12-01",
"content": "- Initial release"
}
]
```
List entries in **reverse-chronological order** (newest first).
Remote (changelogUrl) [#remote-changelogurl]
```json
"metadata": {
"sourceUrl": "https://raw.githubusercontent.com/you/repo/main/template.json",
"changelogUrl": "https://raw.githubusercontent.com/you/repo/main/CHANGELOG.md"
}
```
When `changelogUrl` is present it takes full precedence — the inline `changelog` is ignored.
CHANGELOG.md Format [#changelogmd-format]
AIOStreams parses this file with a strict format:
```markdown
# Changelog
## 1.3.0 (2026-03-04)
### What's new
- Rewrote sort logic for better performance
## 1.2.0 (2026-01-20)
- Added language passthrough option
```
Rules:
* File must start with `# Changelog` (h1)
* Version headings must be `## ()` — nothing else on that line
* Use `###` or deeper for sub-headings inside entry content; **never `##`** inside entries
* Entries must be in **reverse-chronological order**
***
Sharing Templates [#sharing-templates]
Adding to an instance [#adding-to-an-instance]
Instance hosters can add templates via:
* The `templates` folder in the data directory (at `/app/data/templates` in the Docker container)
* The `TEMPLATE_URLS` environment variable — a JSON array of URLs pointing to template JSON files
Deep link [#deep-link]
Send a user directly to the template import flow by appending query parameters to any AIOStreams URL:
```
https://your-aiostreams.example.com/stremio/configure?template=https://example.com/my-template.json
```
With a specific template pre-selected from a multi-template file:
```
https://your-aiostreams.example.com/stremio/configure?template=https://example.com/templates.json&templateId=author.my-template
```
Users are shown a warning when a template is imported via deep link. Remind
users in your documentation to only import templates from sources they trust.