Merge branch 'dev' into feat/downloader-speed

Catch the branch up with dev (45 commits) so testers can run the perf work
against current dev.

Conflict resolutions:
- downloaders/requests.py: keep the branch's racing-gated read1, adaptive
  controller speed reporting and byte-range slice guards; take dev's
  content-encoding checks (raw reads are skipped on an encoded body) and its
  rolling SpeedWindow for the no-controller speed fallback. The single-URL
  ranged-parallel gate now requires no speed limit and no caller Range.
- service.py / session.py: keep the branch's TimeoutHTTPAdapter and retry
  config, raise POOL_MAX_SIZE to 64 so dev's pool-starvation fix survives
  (pool_block=True stalls threads on a pool smaller than the in-flight count).
- docs: dev split configuration.md into per-area pages, so the branch's rnet
  network option rows moved to reference/configuration/network.md.
This commit is contained in:
imSp4rky
2026-08-07 10:45:47 -06:00
104 changed files with 8567 additions and 1375 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
Auto-generated reference for the [`Config`][unshackle.core.config.Config] object that models the
`unshackle` configuration file. For a human-friendly walkthrough of every key, see the
[Configuration Reference](../configuration.md).
[Configuration Reference](../configuration/index.md).
::: unshackle.core.config
options:
+15 -888
View File
@@ -1,891 +1,18 @@
# Configuration Reference
# Configuration Reference (moved)
This page is the exhaustive, authoritative reference for **every** key you can set in
`unshackle.yaml`. For each key you will find its name, expected type, default value, and
what it does. Keys are grouped by area so you can jump straight to the part of unshackle
you are configuring.
This page has moved. The configuration reference is now split by area under
[`docs/reference/configuration/`](configuration/index.md), or on the docs site at
[docs.unshackle.dev/reference/configuration](https://docs.unshackle.dev/reference/configuration/).
If you are new to the config file (where it lives, how unshackle discovers it, and how to
edit it), start with [The Configuration File](../getting-started/configuration-file.md).
This page assumes you already know the basics and want precise details.
!!! info "How defaults work"
Every key is optional. If you omit a key, unshackle uses the built-in default listed
here. There is **no schema validation**: unknown keys are silently ignored (they never
become settings), and the file is parsed with `yaml.safe_load`. An empty or missing
file means *all* defaults apply.
!!! warning "Two keys behave specially on load"
- `curl_impersonate` is a **deprecated** alias of [`network`](#network-proxy) and emits a
`DeprecationWarning`.
- `scene_naming` has been **removed**. If it is present at all, unshackle exits
immediately with an error telling you to use [`output_template`](#output-naming)
instead.
See [Deprecated & removed keys](#deprecated-removed-keys) at the end of this page.
---
## Quick index
| Area | Keys |
| Area | Page |
|------|------|
| [Directories](#directories) | `directories`, `filenames` |
| [Services & authentication](#services-auth) | `services`, `credentials`, `firefox_cookies`, `remote_services`, `serve` |
| [Download & processing](#download-processing) | `dl`, `subtitle`, `audio`, `muxing`, `language_tags` |
| [Output & naming](#output-naming) | `output_template`, `tag`, `tag_group_name`, `tag_imdb_tmdb`, `chapter_fallback_name`, `unicode_filenames` |
| [DRM & CDM](#drm-cdm) | `cdm`, `remote_cdm`, `decryption` |
| [Network & proxy](#network-proxy) | `network`, `headers`, `proxy_providers` |
| [Key vaults](#key-vaults) | `key_vaults`, `vault_timeout` |
| [External API keys](#external-api-keys) | `imdb_api_enabled`, `omdb_api_key`, `tmdb_api_key`, `simkl_client_id`, `decrypt_labs_api_key`, `ipinfo_api_key` |
| [Caching & updates](#caching-updates) | `title_cache_enabled`, `title_cache_time`, `title_cache_max_retention`, `update_checks`, `update_check_interval` |
| [Logging, privacy & debug](#logging-privacy-debug) | `redact_paths`, `debug`, `debug_keys`, `debug_requests`, `set_terminal_bg` |
| [Deprecated & removed](#deprecated-removed-keys) | `curl_impersonate`, `downloader`, `scene_naming` |
---
## Directories { #directories }
The `directories` key is a **dict** mapping directory names to filesystem paths. Only the
names listed below are honoured; a handful are **protected** and cannot be moved (attempts
to override them are silently skipped). All defaults are computed relative to the installed
package, and every user-settable path is passed through `Path(...).expanduser()`, so `~`
works.
```yaml title="unshackle.yaml"
directories:
downloads: ~/Media/unshackle
temp: /mnt/fast-scratch/unshackle-temp
wvds: ~/.unshackle/WVDs
prds: ~/.unshackle/PRDs
```
| Key | Type | Default | Overridable | Purpose |
|-----|------|---------|:-----------:|---------|
| `downloads` | path | `<repo>/downloads` | ✅ | Default output directory for finished files. |
| `temp` | path | `<repo>/temp` | ✅ | Temporary working files during download/decrypt/mux. |
| `cache` | path | `<data>/cache` | ✅ | Generic cache, title cache, and the update-check store. |
| `cookies` | path | `<data>/cookies` | ✅ | Per-service cookie files (and VPN cookie/token files). |
| `logs` | path | `<data>/logs` | ✅ | Log files. |
| `exports` | path | `<data>/exports` | ✅ | Export JSON files. |
| `wvds` | path | `<data>/WVDs` | ✅ | Widevine device files (`.wvd`). |
| `prds` | path | `<data>/PRDs` | ✅ | PlayReady device files (`.prd`). |
| `dcsl` | path | `<data>/DCSL` | ✅ | DCSL data. |
| `commands` | path | `unshackle/commands` | ✅ | CLI command modules. |
| `services` | list \| path | `[unshackle/services]` | ✅ | Service search paths and/or remote repo specs (see below). |
| `vaults` | path | `unshackle/vaults` | ✅ | Vault backend modules. |
| `fonts` | path | `unshackle/fonts` | ✅ | Bundled fonts. |
| `user_configs` | path | `unshackle/` | ❌ protected | Where `unshackle.yaml` lives. |
| `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. |
| `core_dir` | path | `unshackle/core` | ❌ protected | Package core. |
| `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. |
| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. |
!!! note "The `services` directory is special"
`services` may be a **list**, and each entry can be either a local directory or a
**repository spec**: a git URL (`https://...`, `ssh://...`, `git@...`, or anything ending in
`.git`) or `owner/repo` shorthand. Repo specs are cloned and updated automatically; plain
paths are used as-is. **List order is priority**: the first source to define a service
tag wins, so put your own local service directories **last** to make them fallbacks.
```yaml
directories:
services:
- unshackle-dl/community-services # cloned from GitHub
- https://example.com/private.git@stable
- ~/my-services # local, lowest priority
```
!!! info "How git-backed service repos are handled"
unshackle's use of git is **read-only on the remote**: it only ever performs a shallow
`clone`, `fetch`, `pull`, and a local `reset`; **nothing is ever pushed**. Private repos
are authenticated through your existing git credential helper, and unshackle stores no
tokens of its own. Clones live under `<first-local-services-dir>/_repos/<repo-name>/` (or
the bundled `unshackle/services`), and **nothing is written to the cache directory**. After
the first clone unshackle re-pulls **at most once every 24 h**, so it does not touch the
network on every run.
!!! warning "The two refresh paths diverge on purpose"
The **automatic** 24 h-TTL refresh that happens during a normal `dl`/`search` run will
deliberately **refuse to refresh and exit** (naming the offending clone) if that clone
has uncommitted changes to tracked files or unpushed local commits, so a background pull can
never clobber work in progress. Untracked files (new service folders, `__pycache__`) never
block it. The **manual** `unshackle util refresh-services` command does the opposite: it
**hard-resets** the clone to upstream, discarding any local edits. Reach for the manual
command only when you actually want to throw local changes away.
!!! warning "Read-only installs and reinstalls"
If the services directory lives inside the installed package, a reinstall can delete the
`_repos` clones. They are simply re-cloned on next use. On **read-only installs** you must
point `services` at a writable path, or cloning will fail.
### Filenames { #filenames }
The `filenames` key is a **dict** of templated file/name patterns. Each value is used
verbatim (no path processing). Braced fields like `{time}` and `{service}` are filled in at
runtime.
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `log` | str | `"unshackle_{name}_{time}.log"` | Written under `directories.logs`. |
| `debug_log` | str | `"unshackle_debug_{service}_{time}.jsonl"` | Structured debug log, under `directories.logs`. |
| `config` | str | `"config.yaml"` | Per-service config file, under that service's directory. |
| `root_config` | str | `"unshackle.yaml"` | The main config filename itself. |
| `chapters` | str | `"Chapters_{title}_{random}.txt"` | Under `directories.temp`. |
| `subtitle` | str | `"Subtitle_{id}_{language}.srt"` | Under `directories.temp`. |
---
## Services & authentication { #services-auth }
### `services`
- **Type:** `dict` keyed by service tag &nbsp;·&nbsp; **Default:** `{}`
Per-service configuration, keyed by the canonical service tag (for example `EXAMPLE1`, `EXAMPLE2`,
`EXAMPLE3`). unshackle reads several well-known sub-keys out of each service's block:
- **`proxy_map`**: a dict mapping `"provider:query"` or `"query"` to a proxy value, used to
auto-select a proxy for that service.
- **`title_map`**: an exact-match rename map applied to fetched titles (source name →
desired name), so a service that names a title differently from your library still matches.
Individual services may read any additional keys they define. The whole block is handed to
the service as `self.config`.
!!! tip "Why put secrets here and not in the service's own `config.yaml`?"
A service ships a per-service `config.yaml` inside its own folder, and that file is
typically shared/committed and **not** meant to be edited by you. So anything user- or
device-specific (API keys, account IDs, device attributes) belongs in this `services`
block of `unshackle.yaml` instead, where it is **merged over** the service's `config.yaml`
at load time. That is why the two config locations exist: one for shared service defaults,
one for your private overrides.
```yaml
services:
EXAMPLE1:
proxy_map:
default: us
title_map:
"My Show: The Movie": "My Show Presents The Movie"
```
### `credentials`
- **Type:** `dict` keyed by service tag &nbsp;·&nbsp; **Default:** `{}`
Per-service login credentials. Each value is a `username:password[:extra]` string (a list or
dict form is also accepted where a service supports it). These are parsed into `Credential`
objects; the credential's SHA-1 is also used as an account hash for cache keys.
```yaml
credentials:
EXAMPLE2: user@example.com:hunter2
EXAMPLE1:
- primary@example.com:pw1
- secondary@example.com:pw2
```
!!! tip "Cookies vs credentials"
Cookies are stored as files under [`directories.cookies`](#directories), not in this key.
A service's `authenticate()` accepts cookies, credentials, or both.
### `firefox_cookies`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Settings for extracting cookies directly from a local Firefox profile. A service block is
expected to provide `hosts` (a list of cookie hostnames; entries shorter than 3 characters
are ignored to prevent dumping the whole cookie store) and an optional `local_storage`
boolean to also pull matching local-storage entries. Extraction is read-only.
!!! note "Firefox does not need to be closed"
The extractor copies **both** `cookies.sqlite` **and** its WAL file into a `0700` temp
directory; the WAL copy exists to capture writes Firefox has not yet flushed to the main
DB, so the copy is current even against a running browser. The only real constraint is that
Firefox must not hold an exclusive write lock at the exact instant of the copy. The live
profile is otherwise left untouched.
!!! tip "When you actually need `local_storage: true`"
Enable it only for services that keep their auth tokens in the browser's **localStorage**
(`webappsstore.sqlite`) rather than in HTTP cookies. If a service authenticates with plain
cookies, leaving it off is correct.
!!! warning "Extraction fails silently to file cookies"
If extraction yields no cookies or fails for **any** reason, unshackle silently falls back
to the normal file-based cookie path (`cookies/<SERVICE>.txt` or
`cookies/<SERVICE>/<profile>.txt`). This is a safety net, but it also means that if you
expected Firefox cookies and file cookies are being used instead, extraction quietly failed.
Check that first when troubleshooting.
### `remote_services`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Definitions of remote unshackle service servers, used by the `--remote` mode. Each entry
carries a `services` sub-dict of remote service tags, which unshackle turns into synthetic
CLI commands that run against the remote server.
### `serve`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Configuration for the `serve` command (the built-in REST API server). The full server guide
is the [REST API](../dev/rest-api/index.md) section; these are the config keys.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `api_secret` | str | *(unset)* | Master secret accepted in the `X-Secret-Key` header. |
| `users` | dict | `{}` | Per-user API keys and their allowlists (see below). |
| `services` | list | *(unset)* | Global service allowlist. Omit to allow all. |
| `remote_only` | bool | `false` | Run without a local `output_template` (CORS/Cloudflare-friendly). |
| `session_ttl` | int (s) | `300` | Lifetime of an interactive auth session. |
| `max_sessions` | int | `100` | Maximum concurrent sessions. |
| `history_limit` | int | `100` | How many finished jobs to retain in history. |
| `compression_level` | int | `1` | gzip level for responses. |
| `cdm_overrides` | dict | *(unset)* | Allowed per-request CDM overrides. |
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
| `country` / `locations` | str / dict | *(unset)* | Advertised region metadata. |
Each entry under `users` is keyed by that user's API key and may set its own `services`,
`devices`, and `playready_devices` allowlists, narrowing the global ones.
```yaml
serve:
api_secret: change-me
remote_only: true
services: [EXAMPLE1, EXAMPLE2]
# server-wide download defaults (same keys as the `dl:` block)
downloads: 3
best_available: true
users:
a1b2c3d4: # this user's API key
services: [EXAMPLE1] # may only use EXAMPLE1
```
!!! tip "Declaring `dl` defaults under `serve`"
You can set any [`dl`](#dl) flag key (`downloads`, `workers`, `best_available`, and so on)
directly inside `serve`. The point is to raise concurrency and behaviour defaults
**server-wide** (every request the server handles picks them up) without having to change
each client call individually.
---
## Download & processing { #download-processing }
### `dl`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Persistent defaults for the `dl` command. This block is used as Click's `default_map`, so
**any** `dl` flag can be given a default here and it applies to every download without you
typing it. See [Downloading](../guide/downloading.md) and the
[CLI Reference](../guide/cli-reference.md) for what each flag does.
**Key naming.** A key is the flag's long name with dashes replaced by underscores
(`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few keys are named
after the flag's internal destination rather than its visible name; set these exact keys:
| Flag | Config key |
|------|-----------|
| `-r` / `--range` | `range` |
| `--list` | `list` |
| `--tmdb` | `tmdb_id` |
| `--imdb` | `imdb_id` |
| `--animeapi` | `animeapi_id` |
| `-naa` / `--noatmos` | `no_atmos` |
| `-o` / `--output` | `output_dir` |
| `--cdm-only` / `--vaults-only` | `cdm_only` |
**Precedence** (highest first): an explicit CLI flag or environment variable → a per-service
[`services.<TAG>.dl`](#services-auth) override → this global `dl:` block → the built-in
default. In other words, config only fills in options you did not set on the command line.
Common keys (a useful subset; every `dl` flag works):
| Key | Type | Default | Sets |
|-----|------|---------|------|
| `lang` | list | `["orig"]` | Video/audio language(s); `orig` = original language. |
| `a_lang` / `v_lang` | list | `[]` | Audio- / video-only language override. |
| `s_lang` | list | `["all"]` | Subtitle language(s). |
| `quality` | list | `[]` (best) | Resolution(s), e.g. `[1080]`. |
| `vcodec` | list | `[]` (any) | Video codec(s), e.g. `[H265]`. |
| `acodec` | list | `[]` (any) | Audio codec(s), e.g. `[EC3]`. |
| `range` | list | `["SDR"]` | Colour range(s): `SDR`, `HDR10`, `HDR10+`, `DV`. |
| `channels` | float | *(unset)* | Audio channels, e.g. `6` for 5.1. |
| `sub_format` | str | *(unset)* | Convert subtitles to this format (`srt`, `vtt`, `original`, ...). |
| `forced_subs` | bool | `false` | Include forced subtitle tracks. |
| `no_subs` / `no_audio` / `no_chapters` | bool | `false` | Skip that track type. |
| `downloads` | int | `1` | Tracks downloaded concurrently. |
| `workers` | int | *(downloader default)* | Threads per track. |
| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. |
| `best_available` | bool | `false` | Fall back to best quality if the request is unavailable. |
| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. |
| `no_folder` | bool | `false` | Do not create a per-title folder. |
```yaml title="Global download defaults"
dl:
lang: [en]
quality: [1080]
range: [HDR10]
vcodec: [H265]
sub_format: srt
downloads: 2
```
!!! tip "Per-service overrides"
Nest a `dl` block under a service in [`services`](#services-auth) to override these defaults
for just that service. It uses the same keys and wins over the global `dl:` block (but still
loses to an explicit CLI flag).
```yaml
dl:
lang: [en] # global default
services:
EXAMPLE1:
dl:
lang: [en, ja] # EXAMPLE1 downloads both; everything else stays English
range: [DV]
```
!!! note "`serve` uses these same keys"
The [`serve`](#serve) block accepts the same `dl` option keys at its top level to set
server-wide download defaults for every request it handles.
### `subtitle`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Subtitle handling: SDH stripping, format conversion, and whether subtitles are muxed in or
written as sidecar files. See [Subtitles](../guide/subtitles.md) for the full guide.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `strip_sdh` | bool | `true` | Strip SDH/CC cues (hearing-impaired annotations) into a clean track. |
| `sdh_method` | str | `"auto"` | SDH-stripping backend to use. |
| `preserve_formatting` | bool | `true` | Keep styling/positioning when converting or stripping. |
| `convert_before_strip` | bool | `true` | Convert to the working format before stripping SDH. |
| `conversion_method` | str | `"auto"` | Subtitle conversion backend. |
| `preferred_conversion_method` | str | *(unset)* | Preferred converter when several are available. |
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files. |
| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode: sidecar`. |
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH) that sorts the variants within each language. Types you leave out fall to the end. |
```yaml
subtitle:
strip_sdh: true
output_mode: sidecar
sidecar_format: srt
```
!!! note "`type_priority` also picks the default subtitle"
`type_priority` sorts the tracks within each language, and at mux time the first track
in the default subtitle language gets the `default` flag. The built-in order is `forced`,
`normal`, `sdh`, which means a forced track becomes default whenever one exists. If you
want the full dialogue track as default instead, set:
```yaml
subtitle:
type_priority: [normal, sdh, forced]
```
### `audio`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Audio handling options. The most notable sub-key is **`codec_priority`**, an ordered list of
audio codecs used to break ties in track selection.
```yaml
audio:
codec_priority:
- EAC3
- AC3
```
!!! note "`codec_priority` is a soft priority: nothing is dropped"
`codec_priority` only breaks ties **among tracks that share the same bitrate and language**.
Codecs not in the list keep their normal bitrate-based ordering and are simply placed after
all listed codecs, and none are ever removed. Two higher-level rules still override it: **Atmos
tracks always take precedence over `codec_priority`**, and **audio-description tracks are
always forced to the end** regardless of it. When `codec_priority` is unset, tracks sort by
bitrate alone, with those same Atmos/descriptive rules still applied.
### `muxing`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Matroska (MKV) muxing options.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `set_title` | bool | `true` | Write a human-readable title into the MKV container. |
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. |
| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. |
| `merge_video` | bool | `false` | Merge all video tracks into one file. `--merge-video` on the CLI flips this on. |
```yaml
muxing:
set_title: true
default_language:
audio: en
subtitle: en
```
### `language_tags`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Language-tag remapping and the rule engine behind the `{lang_tag}` filename variable. When a
`rules` list is present, each rule is evaluated in order and the first match's `tag` is used.
A rule may test:
| Condition | Meaning |
|-----------|---------|
| `audio` | Matches against the audio track languages. |
| `subs_contain` | At least one subtitle language matches. |
| `subs_contain_all` | Every listed language must be present (scalar or list). |
```yaml
language_tags:
rules:
- audio: [en]
subs_contain_all: [en]
tag: "" # native English release, no tag
- audio: [ja]
subs_contain: [en]
tag: "SUBBED"
```
---
## Output & naming { #output-naming }
For the full picture of how templates render, see
[Output & naming](../guide/output-and-naming.md). The keys are summarised here.
### `output_template`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Filename templates keyed by media kind, using `{variable}` placeholders. Recognised kinds are
`movies`, `series`, and `songs` (and `albums` for folder templates). A trailing `?` on a
variable (e.g. `{quality?}`) marks it optional: if empty, it and one adjacent separator are
dropped.
```yaml title="unshackle.yaml"
output_template:
movies: "{title}.{year}.{quality?}.{source}-{tag}"
series: "{title}.{season_episode}.{episode_name?}.{quality?}.{source}-{tag}"
songs: "{track_number}. {title}"
folder: # special nested key, see below
movies: "{title} ({year})"
series: "{title} ({year})"
```
**The nested `folder` sub-key** controls output subfolders and is popped out of
`output_template`:
- A **dict** sets per-kind folder templates (kinds: `movies`, `series`, `songs`, `albums`).
- A **str** sets a single folder template for everything.
unshackle validates templates on load and emits warnings for unknown variables, non-string
values, filesystem-unsafe characters (`< > : " / \ | ? *`), empty templates, and unknown
folder-kind keys.
??? example "All valid template variables"
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `date`,
`quality`, `resolution`, `source`, `tag`, `track_number`, `artist`, `album_artist`,
`album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`, `explicit`,
`isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`, `dual`,
`multi`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`.
### Tagging & naming keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `tag` | str | `""` | Release-group tag appended to filenames and written into MKV tags. |
| `tag_group_name` | bool | `true` | Include the group name (`tag`) in the MKV `Group` tag. |
| `tag_imdb_tmdb` | bool | `true` | Write IMDb/TMDB/TVDB external-ID tags into the MKV (needs metadata providers). |
| `chapter_fallback_name` | str | `""` | Fallback chapter-name template, e.g. `"Chapter {i:02}"`. |
| `unicode_filenames` | bool | `false` | Allow Unicode in filenames; when `false`, names are ASCII-sanitised. |
---
## DRM & CDM { #drm-cdm }
The full conceptual guide lives at [DRM & CDM](../guide/drm-and-cdm.md). These are the config
keys. Device files themselves live in [`directories.wvds`](#directories) (`.wvd`) and
`directories.prds` (`.prd`).
### `cdm`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a `default` fallback.
Resolution is case-insensitive: an explicit CLI override wins, then the per-service entry,
then `default`.
```yaml
cdm:
default: chromecdm_l3
EXAMPLE2: android_l1
```
A value may also be a **nested dict** for advanced selection by quality, DRM system, or
profile:
```yaml
cdm:
EXAMPLE1:
">=1080": android_l1 # by track height
"<1080": chromecdm_l3
widevine: android_l1 # by DRM system
playready: sl3000
default: chromecdm_l3
```
Quality keys support `1080`, `>=1080`, `>720`, `<=576`, `<480` style comparisons; DRM keys
are `widevine` / `playready`.
### `remote_cdm`
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]`
A list of remote CDM server definitions. Each entry is matched by its `name` (referenced from
[`cdm`](#cdm)), and its `type` selects the backend:
| `type` | Backend | Key fields |
|--------|---------|-----------|
| `decrypt_labs` | Decrypt Labs KeyXtractor | `host` (default `https://keyxtractor.decryptlabs.com`), `device_name`, `secret` (falls back to `decrypt_labs_api_key`) |
| `custom_api` | Fully YAML-configurable remote API | `device`, `auth`, `endpoints`, `request_mapping`, `response_mapping`, `caching`, `timeout` |
| *(none)*, `Device Type: PLAYREADY` | pyplayready `RemoteCdm` | `host`, `secret`, `device_name`, `security_level` (default 3000) |
| *(none)*, otherwise | pywidevine `RemoteCdm` | `host`, `secret`, `device_name`, `device_type`, `system_id`, `security_level` (default 3000) |
```yaml
remote_cdm:
- name: keyxtractor
type: decrypt_labs
device_name: L1
- name: my_wv_server
host: https://cdm.example.com
secret: s3cr3t
device_name: android_l1
device_type: ANDROID
system_id: 26830
security_level: 1
```
### `decryption`
- **Type:** `str` or `dict` &nbsp;·&nbsp; **Default:** `"shaka"`
Selects the tool used to physically decrypt CENC content. The value is compared
case-insensitively; `"mp4decrypt"` selects Bento4's mp4decrypt, and **anything else**
(including `"shaka"`) selects shaka-packager.
=== "Global"
```yaml
decryption: shaka
```
=== "Per-service"
```yaml
decryption:
default: shaka
SOMESERVICE: mp4decrypt
```
---
## Network & proxy { #network-proxy }
### `network`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
TLS-fingerprinting and HTTP client settings for the rnet-based session.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `browser` | str | `"Chrome131"` | Impersonation preset. Must be an exact preset name (e.g. `Chrome131`, `Firefox135`, `Edge101`, `Safari18`, `OkHttp4_12`, `OkHttp5`, `Opera118`); unknown names raise an error. |
| `http1_only` | bool | *(unset)* | Force HTTP/1.1. |
| `http2_only` | bool | *(unset)* | Force HTTP/2. |
| `connect_timeout` | int | `10` | Connect timeout in seconds. |
| `read_timeout` | int | `30` | Read timeout in seconds (max gap between response chunks). |
| `timeout` | int | *(unset)* | Total request timeout in seconds. |
| `allow_redirects` | bool | *(unset)* | Follow redirects automatically (rnet client default when unset). |
| `pool_idle_timeout` | int | `55` | Seconds an idle pooled connection is kept (default stays under typical ~60s CDN idle kills). |
| `pool_max_idle_per_host` | int | `16` | Max idle connections kept per host. |
| `pool_max_size` | int | *(unset)* | Max total connections in the pool. |
| `tcp_keepalive` | int | `30` | TCP keepalive time in seconds. |
| `tcp_keepalive_interval` | int | *(unset)* | TCP keepalive probe interval in seconds. |
| `tcp_keepalive_retries` | int | *(unset)* | TCP keepalive probe retry count. |
| `tcp_nodelay` | bool | *(unset)* | Disable Nagle's algorithm. |
```yaml
network:
browser: Firefox135
http2_only: true
```
!!! note "When to force `http1_only`"
HTTP/2 is the right default on large CDNs, so leave it on. Force HTTP/1.1 only for a host that
throttles per-connection, or a slow origin stuck behind HTTP/2 flow control. In benchmarks
this won 30 to 50% on such hosts but **cost up to 27% on fast CDNs**, so enable it only when
your own measurements justify it.
!!! warning "Renamed from `curl_impersonate`"
The old `curl_impersonate` section is a deprecated alias. If you still use it, unshackle
honours it (only when `network` is absent) but emits a `DeprecationWarning`. Rename it to
`network`.
### `headers`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Default HTTP headers merged into every session unshackle creates.
```yaml
headers:
Accept-Language: en-US,en;q=0.9
```
!!! warning "Don't set `Accept-Encoding` (and similar) here"
Keep this block to sane cross-service defaults such as `Accept-Language` and `User-Agent`.
Compatibility headers like `Accept-Encoding` are set for you by the rnet HTTP backend as
part of its browser-impersonation profile, and overriding them can break the impersonation
fingerprint. Anything useful to only some services belongs in that service's config, not
here.
### `proxy_providers`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Proxy/VPN provider configuration. Each sub-key names a provider, and its block is passed
straight to that provider's constructor. See [Proxies & VPN](../guide/proxies-and-vpn.md) for
the full provider guide. Recognised providers and their exit ports:
| Provider | Config key | Credentials | Proxy scheme/port |
|----------|-----------|-------------|-------------------|
| Basic (static) | `basic` | country → URI(s) | as specified |
| NordVPN | `nordvpn` | 48-char **service** credentials | `https://...:89` |
| Surfshark | `surfsharkvpn` | 48-char **service** credentials | `https://...:443` |
| Windscribe | `windscribevpn` | service credentials | `https://...:443` |
| ExpressVPN | `expressvpn` | device login (`enable: true`) / token cache | `https://cat:...@...:443` |
| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://...:4443` (or `:443` Secure Core) |
| Gluetun | `gluetun` | per-VPN keys/creds | `http://localhost:{port}` (local Docker) |
| Hola | *(none, auto)* | none | `http://...:{peer}` |
```yaml
proxy_providers:
basic:
us: http://user:pass@1.2.3.4:8080
de:
- http://a.example:8080
- socks5://b.example:1080
nordvpn:
username: <48-char service username>
password: <48-char service password>
```
!!! note "Provider loading differs between CLI and REST server"
The `dl` CLI loads all providers, including `windscribevpn` and `gluetun`. The REST API /
remote-client path uses a separate resolver that does **not** load `windscribevpn` or
`gluetun`. ExpressVPN and ProtonVPN also auto-load when their cached session exists, and Hola
auto-loads whenever the `hola-proxy` binary is present.
---
## Key vaults { #key-vaults }
The full guide is at [Vaults](../guide/vaults.md). Two keys configure them.
### `key_vaults`
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]`
An ordered list of key-vault backends. unshackle queries them in order and reuses content
keys instead of re-licensing. Each entry needs a `type` (the backend module name) and a
`name`, plus backend-specific keys.
| `type` | Purpose | Required keys | Notes |
|--------|---------|--------------|-------|
| `SQLite` | Local SQLite database | `name`, `path` | Loaded **critically**; a failure aborts the run. |
| `MySQL` | Remote MySQL database | `name`, `host`, `database`, `username` | Extra keys (e.g. `password`, `port`) forwarded to pymysql. |
| `API` | RESTful JSON API | `name`, `uri`, `token` | Honours `vault_timeout`. |
| `HTTP` | HTTP API with modes | `name`, `host`, one of `password`/`api_key` | `api_mode`: `query` (default), `json`, `decrypt_labs`. |
```yaml
key_vaults:
- type: SQLite
name: Local
path: ~/.unshackle/keys.db
- type: MySQL
name: Team
host: db.example.com
database: keys
username: unshackle
password: hunter2
no_push: false
```
!!! tip "Common per-entry options"
- `no_push: true` makes a vault read-only (keys are fetched but never written to it).
- A vault of `type: api` whose `name` contains `decrypt_labs` auto-fills its `token` from
[`decrypt_labs_api_key`](#external-api-keys) when not set inline.
- An all-zero content key (32 zeros) is treated as "no key" everywhere and is never
stored.
### `vault_timeout`
- **Type:** `float` &nbsp;·&nbsp; **Default:** `10.0`
Timeout in seconds for vault operations. Injected automatically into any backend whose
constructor accepts a `timeout` parameter (a per-vault `timeout` still wins).
---
## External API keys { #external-api-keys }
All default to an empty string and enable optional metadata/geolocation features.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `imdb_api_enabled` | bool | `false` | Use the free IMDxAPI (api.tiffara.com) metadata provider, which needs no API key. Off by default since the site has been unreliable. |
| `omdb_api_key` | str | `""` | [OMDb](https://www.omdbapi.com/) API key for IMDb metadata lookups; a more reliable alternative to IMDxAPI. Free keys are available on the OMDb site. |
| `tmdb_api_key` | str | `""` | TMDB API key for metadata enrichment and external-ID tags. |
| `simkl_client_id` | str | `""` | SIMKL client ID for metadata lookups; an alternative/fallback source to TMDB. |
| `decrypt_labs_api_key` | str | `""` | Global Decrypt Labs API key (used by remote CDM / vault). |
| `ipinfo_api_key` | str | `""` | ipinfo.io API key for IP/region lookups. |
!!! tip "SIMKL is a fallback for TMDB"
SIMKL is positioned as an alternative metadata source, and it is most valuable precisely
**when no `tmdb_api_key` is configured**. It improves title-matching and tagging
reliability in that case rather than replacing TMDB where TMDB is available.
!!! note "`ipinfo_api_key` never touches your service sessions"
The token is only ever sent to `api.ipinfo.io` as a per-request `Authorization` header; it
is **never** attached to your service session, so it cannot leak to a streaming provider.
Lookups degrade gracefully through a fallback chain: the authenticated Lite endpoint (higher
rate limits and richer fields) → anonymous ipinfo → `ip-api.in` as a last resort.
---
## Caching & updates { #caching-updates }
### Title cache
unshackle caches fetched title metadata (region- and account-aware) to avoid repeat API
calls.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `title_cache_enabled` | bool | `true` | Master switch for the title cache. |
| `title_cache_time` | int (seconds) | `1800` (30 min) | Lifetime of fresh cached titles. |
| `title_cache_max_retention` | int (seconds) | `86400` (24 h) | How long stale cached data may still be served as a fallback when a live fetch fails. |
### Update checks
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `update_checks` | bool | `true` | Whether to check for new unshackle releases. |
| `update_check_interval` | int (hours) | `24` | Minimum hours between update checks. |
!!! note
Update checks query the GitHub releases API with a fixed 5-second timeout and cache the
result in `directories.cache/update_check.json`.
---
## Logging, privacy & debug { #logging-privacy-debug }
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `redact_paths` | bool | `true` | Mask install-root / venv / home prefixes in logged paths (`<unshackle>`, `<venv>`, `~`). Set `false` to show full paths. |
| `debug` | bool | `false` | Global debug mode. |
| `debug_keys` | bool | `false` | Log decryption keys. |
| `debug_requests` | bool | `false` | Log HTTP requests. |
| `set_terminal_bg` | bool | `false` | Append the theme's background colour to output styling. |
!!! tip "When to enable `set_terminal_bg`"
When on, the theme's background colour is appended to the foreground styles so the full
colour palette renders correctly on terminals whose *default* background differs from
unshackle's theme. Turn it on if the ASCII-art banner or coloured output **looks off** on
your terminal. Otherwise leave it off.
!!! danger "Key exposure"
Decryption content keys are written to the structured debug log at INFO level during the
DRM handshake. Treat `debug`, `debug_keys`, and the `unshackle_debug_*.jsonl` files as
sensitive, and keep `redact_paths` enabled when sharing logs.
`debug_keys` affects **only** content-encryption keys (the `content_key`/`key` fields).
Passwords, tokens, cookies, and session tokens are **always** redacted regardless of this
setting, and KIDs, key counts, and other metadata are always logged either way.
---
## Deprecated & removed keys { #deprecated-removed-keys }
| Key | Status | Behaviour |
|-----|--------|-----------|
| `curl_impersonate` | **Deprecated** → use [`network`](#network-proxy) | Emits a `DeprecationWarning`; still honoured only if `network` is absent. |
| `downloader` | **Deprecated** | Any value other than `"requests"` emits a `DeprecationWarning`; the value is otherwise ignored (the unified requests downloader is always used). |
| `scene_naming` | **Removed** | If present at all, unshackle exits with an error directing you to configure [`output_template`](#output-naming) instead. |
---
## A complete annotated example
The following config sets a handful of common keys. Every key not shown keeps its default.
```yaml title="unshackle.yaml"
directories:
downloads: ~/Media/unshackle
temp: /mnt/scratch/unshackle
network:
browser: Chrome131
headers:
Accept-Language: en-US,en;q=0.9
cdm:
default: chromecdm_l3
EXAMPLE2: android_l1
decryption: shaka
credentials:
EXAMPLE2: user@example.com:hunter2
output_template:
movies: "{title}.{year}.{quality?}.{source}-{tag}"
series: "{title}.{season_episode}.{quality?}.{source}-{tag}"
folder:
movies: "{title} ({year})"
series: "{title} ({year})"
tag: MYGRP
key_vaults:
- type: SQLite
name: Local
path: ~/.unshackle/keys.db
proxy_providers:
basic:
us: http://user:pass@1.2.3.4:8080
omdb_api_key: "your-omdb-key"
tmdb_api_key: "your-tmdb-key"
title_cache_time: 3600
redact_paths: true
```
For how these interact at runtime, continue to [Downloading](../guide/downloading.md),
[DRM & CDM](../guide/drm-and-cdm.md), and [Output & naming](../guide/output-and-naming.md).
| Overview & quick index | [configuration/index.md](configuration/index.md) |
| Directories & filenames | [configuration/directories.md](configuration/directories.md) |
| Services & authentication | [configuration/services.md](configuration/services.md) |
| Download & processing | [configuration/download.md](configuration/download.md) |
| Output & naming | [configuration/output.md](configuration/output.md) |
| DRM & CDM | [configuration/drm.md](configuration/drm.md) |
| Network & proxy | [configuration/network.md](configuration/network.md) |
| Key vaults | [configuration/vaults.md](configuration/vaults.md) |
| Other settings | [configuration/misc.md](configuration/misc.md) |
| Complete annotated example | [configuration/example.md](configuration/example.md) |
@@ -0,0 +1,88 @@
# Directories { #directories }
The `directories` key is a **dict** mapping directory names to filesystem paths. Only the
names listed below are honoured; a handful are **protected** and cannot be moved (attempts
to override them are silently skipped). All defaults are computed relative to the installed
package, and every user-settable path is passed through `Path(...).expanduser()`, so `~`
works.
```yaml title="unshackle.yaml"
directories:
downloads: ~/Media/unshackle
temp: /mnt/fast-scratch/unshackle-temp
wvds: ~/.unshackle/WVDs
prds: ~/.unshackle/PRDs
```
| Key | Type | Default | Overridable | Purpose |
|-----|------|---------|:-----------:|---------|
| `downloads` | path | `<repo>/downloads` | ✅ | Default output directory for finished files. |
| `temp` | path | `<repo>/temp` | ✅ | Temporary working files during download/decrypt/mux. |
| `cache` | path | `<data>/cache` | ✅ | Generic cache, title cache, and the update-check store. |
| `cookies` | path | `<data>/cookies` | ✅ | Per-service cookie files (and VPN cookie files). |
| `logs` | path | `<data>/logs` | ✅ | Log files. |
| `exports` | path | `<data>/exports` | ✅ | Export JSON files. |
| `wvds` | path | `<data>/WVDs` | ✅ | Widevine device files (`.wvd`). |
| `prds` | path | `<data>/PRDs` | ✅ | PlayReady device files (`.prd`). |
| `dcsl` | path | `<data>/DCSL` | ✅ | DCSL data. |
| `commands` | path | `unshackle/commands` | ✅ | CLI command modules. |
| `services` | list \| path | `[unshackle/services]` | ✅ | Service search paths and/or remote repo specs (see below). |
| `vaults` | path | `unshackle/vaults` | ✅ | Vault backend modules. |
| `fonts` | path | `unshackle/fonts` | ✅ | Bundled fonts. |
| `user_configs` | path | `unshackle/` | ❌ protected | Where `unshackle.yaml` lives. |
| `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. |
| `core_dir` | path | `unshackle/core` | ❌ protected | Package core. |
| `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. |
| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. |
!!! note "The `services` directory is special"
`services` may be a **list**, and each entry can be either a local directory or a
**repository spec**: a git URL (`https://...`, `ssh://...`, `git@...`, or anything ending in
`.git`) or `owner/repo` shorthand. Repo specs are cloned and updated automatically; plain
paths are used as-is. **List order is priority**: the first source to define a service
tag wins.
```yaml
directories:
services:
- you/your-services # cloned from GitHub
- https://example.com/private.git@stable
- ~/my-services # local, lowest priority
```
!!! info "How git-backed service repos are handled"
unshackle's use of git is **read-only on the remote**: it only ever performs a shallow
`clone`, `fetch`, `pull`, and a local `reset`; **nothing is ever pushed**. Private repos
are authenticated through your existing git credential helper, and unshackle stores no
tokens of its own. Clones live under `<first-local-services-dir>/_repos/<host>__<owner>__<repo>/`
(or the bundled `unshackle/services`), and **nothing is written to the cache directory**. After
the first clone unshackle re-pulls **at most once every 24 h**, so it does not touch the
network on every run.
!!! warning "Automatic and manual refresh behave differently"
The **automatic** 24 h-TTL refresh that happens during a normal `dl`/`search` run
**refuses to refresh and exits** (naming the offending clone) if that clone has
uncommitted changes to tracked files or unpushed local commits. Untracked files (new
service folders, `__pycache__`) never block it. The **manual**
`unshackle util refresh-services` command instead **hard-resets** the clone to upstream,
discarding any local edits.
!!! warning "Read-only installs and reinstalls"
If the services directory lives inside the installed package, a reinstall can delete the
`_repos` clones. They are re-cloned on next use. On **read-only installs** you must
point `services` at a writable path, or cloning will fail.
## Filenames { #filenames }
The `filenames` key is a **dict** of templated file/name patterns. Each value is used
verbatim (no path processing). Braced fields like `{time}` and `{service}` are filled in at
runtime.
| Key | Type | Default | Notes |
|-----|------|---------|-------|
| `log` | str | `"unshackle_{name}_{time}.log"` | Written under `directories.logs`. |
| `debug_log` | str | `"unshackle_debug_{service}_{time}.jsonl"` | Structured debug log, under `directories.logs`. |
| `config` | str | `"config.yaml"` | Per-service config file, under that service's directory. |
| `root_config` | str | `"unshackle.yaml"` | The main config filename itself. |
| `chapters` | str | `"Chapters_{title}_{random}.txt"` | Under `directories.temp`. |
| `subtitle` | str | `"Subtitle_{id}_{language}.srt"` | Under `directories.temp`. |
+253
View File
@@ -0,0 +1,253 @@
# Download & processing { #download-processing }
## `dl`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Persistent defaults for the `dl` command. The block is used as Click's `default_map`, so any
`dl` flag can be given a default here. See [Downloading](../../guide/downloading.md) and the
[CLI Reference](../../guide/cli-reference.md) for what each flag does.
**Key naming.** A key is the flag's long name with dashes replaced by underscores
(`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few keys are named
after the flag's internal destination rather than its visible name; set these exact keys:
| Flag | Config key |
|------|-----------|
| `-r` / `--range` | `range` |
| `--list` | `list` |
| `--tmdb` | `tmdb_id` |
| `--imdb` | `imdb_id` |
| `--animeapi` | `animeapi_id` |
| `-naa` / `--noatmos` | `no_atmos` |
| `-o` / `--output` | `output_dir` |
| `--cdm-only` / `--vaults-only` | `cdm_only` |
**Precedence** (highest first): an explicit CLI flag → a per-service
[`services.<TAG>.dl`](services.md) override → this global `dl:` block → the built-in
default. In other words, config only fills in options you did not set on the command line.
Common keys (a useful subset; every `dl` flag works):
| Key | Type | Default | Sets |
|-----|------|---------|------|
| `lang` | list | `["orig"]` | Video/audio language(s); `orig` = original language. |
| `a_lang` / `v_lang` | list | `[]` | Audio- / video-only language override. |
| `s_lang` | list | `["all"]` | Subtitle language(s). |
| `quality` | list | `[]` (best) | Resolution(s), e.g. `[1080]`. |
| `vcodec` | list | `[]` (any) | Video codec(s), e.g. `[H265]`. |
| `acodec` | list | `[]` (any) | Audio codec(s), e.g. `[EC3]`. |
| `range` | list | `["SDR"]` | Colour range(s): `SDR`, `HLG`, `HDR10`, `HDR10P`, `DV`, `HYBRID`. |
| `channels` | float | *(unset)* | Audio channels, e.g. `6` for 5.1. |
| `sub_format` | str | *(unset)* | Convert subtitles to this format (`srt`, `vtt`, `original`, ...). |
| `forced_subs` | bool | `false` | Include forced subtitle tracks. |
| `forced_s_lang` | list | `[]` | Forced subtitle language(s); implies `forced_subs`. |
| `no_subs` / `no_audio` / `no_chapters` | bool | `false` | Skip that track type. |
| `downloads` | int | `1` | Tracks downloaded concurrently. |
| `workers` | int | *(downloader default)* | Threads per track. |
| `speed_limit` | str | *(unlimited)* | Total download speed cap across all downloads combined, e.g. `500k`, `5M`, `1.5G` or plain bytes/sec. Values are bytes, not bits (`5M` = 5.0 MB/s). |
| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. |
| `best_available` | bool | `false` | Fall back to best quality if the request is unavailable. |
| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. |
| `no_folder` | bool | `false` | Do not create a per-title folder. |
```yaml title="Global download defaults"
dl:
lang: [en]
quality: [1080]
range: [HDR10]
vcodec: [H265]
sub_format: srt
downloads: 2
```
!!! note "Per-service overrides"
A `dl` block nested under a service in [`services`](services.md) takes the same keys and
applies to that service only.
```yaml
dl:
lang: [en]
services:
EXAMPLE1:
dl:
lang: [en, ja]
range: [DV]
```
!!! note "`serve` uses these same keys"
The [`serve`](services.md#serve) block accepts most of the same `dl` option keys at its top level
to set server-wide download defaults for every request it handles. Keys the API does not know
are ignored there (`speed_limit`, for example; `serve` has its own `global_speed_limit`).
## `subtitle`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Subtitle handling: SDH stripping, format conversion, and whether subtitles are muxed in or
written as sidecar files. See [Subtitles](../../guide/subtitles.md) for the full guide.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `strip_sdh` | bool | `true` | Strip SDH/CC cues (hearing-impaired annotations) into a clean track. |
| `sdh_method` | str | `"auto"` | SDH-stripping backend to use. |
| `preserve_formatting` | bool | `true` | Keep styling/positioning when converting or stripping. |
| `convert_before_strip` | bool | `true` | Convert to the working format before stripping SDH. |
| `conversion_method` | str | `"auto"` | Subtitle conversion backend. |
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files; `both` writes sidecars and still muxes. |
| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode` is `sidecar` or `both`. |
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH). Types you leave out fall to the end. |
| `group_by` | str | `"type"` | Which key groups the subtitle tracks. `type` puts all forced tracks together, then all normal, then all SDH. `language` keeps each language next to its own variants. |
| `language_priority` | list | *(unset)* | Languages to sort to the top, in the order you give. The rest keep the default order: original language first, then alphabetical. Nothing is removed. |
```yaml
subtitle:
strip_sdh: true
output_mode: sidecar
sidecar_format: srt
```
!!! note "`group_by` track order"
| `group_by` | Result |
|---|---|
| `type` | Danish, Finnish, French, … Danish SDH, Finnish SDH, French SDH |
| `language` | Danish, Danish SDH, Finnish, Finnish SDH, French, French SDH |
`type_priority` still sets the order of the variants, so with `group_by: language` it
controls the order inside each language.
!!! note "`language_priority` sorts languages, it does not select them"
```yaml
subtitle:
group_by: language
language_priority: [en, es, fr]
```
| | Result for an English title |
|---|---|
| Default | English, Arabic, Bulgarian, Spanish, French, Japanese |
| With the list above | English, Spanish, French, Arabic, Bulgarian, Japanese |
Nothing is removed, unlike `-sl`, which drops every subtitle language it does not name.
Use `orig` in the list for the title's original language. A base tag also covers its
regional forms, so `en` sorts `en-US` and `en-GB`, but `zh` does **not** cover
`zh-Hant`, which you must name. The sort ignores entries that match no track, and it
ignores entries that are not valid language tags.
`--exact-lang` applies here too: with it, `en` sorts only `en`, and `en-US` falls to
the alphabetical tail with the rest. Name each tag you want when you use that flag.
!!! note "`type_priority` picks the default subtitle only with `muxing.default_language`"
At mux time the `default` flag goes to every forced subtitle whose language matches the
first audio track, so a full dialogue track never becomes default on its own.
`type_priority` alone does not change that. Name a subtitle language in
[`muxing.default_language`](#muxing) and the first track in that language gets the flag
instead, which is where `type_priority` decides which track that is:
```yaml
muxing:
default_language:
subtitle: en
subtitle:
type_priority: [normal, sdh, forced]
```
## `audio`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Audio handling options. The most notable sub-key is **`codec_priority`**, an ordered list of
audio codecs used to break ties in track selection. **`language_priority`** is an ordered list
of languages that sort to the top of the audio tracks.
```yaml
audio:
codec_priority:
- EAC3
- AC3
language_priority:
- orig
- en
```
!!! note "`codec_priority` is a soft priority: nothing is dropped"
`codec_priority` outranks bitrate: any listed codec sorts above every unlisted one, and among
listed codecs the list order decides. Bitrate order survives only within a single codec rank.
Nothing is ever removed. Two higher-level rules still override it: **Atmos
tracks always take precedence over `codec_priority`**, and **audio-description tracks are
always forced to the end** regardless of it. When `codec_priority` is unset, tracks sort by
bitrate alone, with those same Atmos/descriptive rules still applied.
## `muxing`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Matroska (MKV) muxing options.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `set_title` | bool | `true` | Write a human-readable title into the MKV container. |
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. |
| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. |
| `merge_video` | bool | `false` | Merge video tracks that share height, range, and codec into one file, so only language varies inside a file. `--merge-video` on the CLI flips this on. |
```yaml
muxing:
set_title: true
default_language:
audio: en
subtitle: en
```
## `language_tags`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Language-tag remapping and the rule engine behind the `{lang_tag}` filename variable. When a
`rules` list is present, each rule is evaluated in order and the first match's `tag` is used.
A rule may test:
| Condition | Meaning |
|-----------|---------|
| `audio` | Matches against the audio track languages. |
| `subs_contain` | At least one subtitle language matches. |
| `subs_contain_all` | Every listed language must be present (scalar or list). |
```yaml
language_tags:
rules:
- audio: [ja]
subs_contain: [en]
tag: "SUBBED"
```
## `dual_multi_mode`
- **Type:** `str` &nbsp;·&nbsp; **Default:** `strict`
Rule set for the `{dual}`, `{multi}`, and `{dubbed}` filename variables.
=== "`strict` (default)"
| Variable | Set when |
|----------|----------|
| `{dual}` | Exactly two audio languages **and one is the title's original language**. |
| `{multi}` | Three or more audio languages, even without the original. |
| `{dubbed}` | A single audio language that is not the title's original language (requires a known original). |
=== "`count` (legacy)"
| Variable | Set when |
|----------|----------|
| `{dual}` | Exactly two audio languages (original language ignored). |
| `{multi}` | Three or more audio languages. |
| `{dubbed}` | Never set. |
```yaml
dual_multi_mode: strict
```
!!! note "Dialects count as one language"
Regional variants of the same base language (e.g. `en-US` and `en-GB`) collapse to a single
language in both modes, so they never trigger `{dual}` on their own.
+157
View File
@@ -0,0 +1,157 @@
# DRM & CDM { #drm-cdm }
These are the config keys. For concepts, device provisioning, and the `wvd`/`prd` commands see
the [DRM & CDM guide](../../guide/drm-and-cdm.md). Device files themselves live in
[`directories.wvds`](directories.md) (`.wvd`) and `directories.prds` (`.prd`).
## `cdm`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a `default` fallback.
Resolution is case-insensitive: a per-request API override wins, then the per-service entry,
then `default`.
```yaml
cdm:
default: chromecdm_l3
EXAMPLE2: android_l1
```
A value may also be a **nested dict** for advanced selection by quality, DRM system, or
profile:
```yaml
cdm:
EXAMPLE1:
">=1080": android_l1 # by track height
"<1080": chromecdm_l3
widevine: android_l1 # by DRM system
playready: sl3000
default: chromecdm_l3
```
Quality keys support `1080`, `>=1080`, `>720`, `<=576`, `<480` style comparisons; DRM keys
are `widevine` / `playready`. Quality keys are matched first, but only when a video height is
known; if the value is still a dict after that, `widevine`/`playready` are used when present,
otherwise the credential profile name, then `default`.
The resolved name is looked up in [`remote_cdm`](#remote_cdm) by `name` first. If no entry
matches, it is loaded as a local device file, in this order: `<name>.prd` in
`directories.prds`, `<name>.prd` in `directories.wvds`, then `<name>.wvd` in
`directories.wvds`.
## `remote_cdm`
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]`
A list of remote CDM server definitions. Each entry is matched by its `name` (referenced from
[`cdm`](#cdm)), and its `type` selects the backend:
| `type` | Backend | Key fields |
|--------|---------|-----------|
| `decrypt_labs` | Decrypt Labs KeyXtractor | `host`, `device_name`, `secret` ([fields](#remote-cdm-decrypt-labs)) |
| `custom_api` | Fully YAML-configurable remote API | `host` (required), `device`, `auth`, `endpoints`, `request_mapping`, `response_mapping`, `caching`, `timeout` ([fields](#remote-cdm-custom-api)) |
| *(none)*, `Device Type: PLAYREADY` | pyplayready `RemoteCdm` | `host`, `secret`, `device_name`, `security_level` (default 3000) |
| *(none)*, otherwise | pywidevine `RemoteCdm` | `host`, `secret`, `device_name`, `device_type`, `system_id`, `security_level` (default 3000) |
```yaml
remote_cdm:
- name: keyxtractor
type: decrypt_labs
device_name: L1
- name: my_wv_server
host: https://cdm.example.com
secret: s3cr3t
device_name: android_l1
device_type: ANDROID
system_id: 26830
security_level: 1
```
For the two pywidevine/pyplayready backends, field names are read case-insensitively in both
styles: `Device Type`/`device_type`, `System ID`/`system_id`, `Security Level`/`security_level`,
`Host`/`host`, `Secret`/`secret`, `Device Name`/`device_name`.
!!! warning "PlayReady `host` needs a `/playready` suffix"
pyplayready's `RemoteCdm` treats `host` as a base URL and appends its own endpoint paths,
so a PlayReady entry's `host` must include the trailing `/playready` segment (for example
`https://cdm.example.com/playready`). Without it the server returns `404` rather than a
configuration error.
### `decrypt_labs` fields { #remote-cdm-decrypt-labs }
| Field | Default | Notes |
|-------|---------|-------|
| `host` | `https://keyxtractor.decryptlabs.com` | |
| `device_name` | `ChromeCDM` | `ChromeCDM`, `L1`, `L2` (Widevine) or `SL2`, `SL3` (PlayReady) |
| `secret` | from [`decrypt_labs_api_key`](misc.md#external-api-keys) | Sent as the `decrypt-labs-api-key` header. An error is raised if neither is set. |
| `system_id` | `26830` (Widevine), `0` (PlayReady) | |
| `security_level` | Widevine `3`; PlayReady `2000` for `SL2`, else `3000` | |
PlayReady mode is used when `device_type` is `PLAYREADY` or `device_name` starts with `SL`.
### `custom_api` fields { #remote-cdm-custom-api }
| Field | Type | Notes |
|-------|------|-------|
| `host` | str | Required. Base URL of the API. |
| `device` | dict | `name`, `type` (`CHROME`, `ANDROID`, `PLAYREADY`), `system_id`, `security_level` |
| `auth` | dict | `type` (default `header`): `header` uses `header_name` (default `Authorization`) and `key`; `bearer` uses `bearer_token` or `key`; `basic` uses `username` and `password`. `custom_headers` is merged in for any type. |
| `endpoints` | dict | `get_request` and `decrypt_response`, each `{path, method, timeout}` |
| `request_mapping` | dict | Per endpoint: `param_names`, `static_params`, `conditional_params`, `transforms`, `nested_params`, `exclude_params` |
| `response_mapping` | dict | Per endpoint: `fields`, `transforms`, `response_types`, `success_conditions`, `error_fields`, `key_fields` |
| `caching` | dict | `enabled`, `use_vaults`, `check_cached_first` |
| `legacy` | dict | Legacy mode options. |
| `timeout` | int | Default `30`. Request timeout in seconds. |
Transforms cover base64/hex/JSON encoding and `kid:key` parsing. PlayReady mode is used when
`device.type` is `PLAYREADY` or the device name is `SL2`/`SL3`.
```yaml
remote_cdm:
- name: my_custom
type: custom_api
host: https://cdm.example.com
device:
name: ChromeCDM
type: CHROME
system_id: 26830
security_level: 3
auth:
type: bearer
key: your-token
endpoints:
get_request:
path: /get-challenge
method: POST
decrypt_response:
path: /get-keys
method: POST
timeout: 30
```
## `decryption`
- **Type:** `str` or `dict` &nbsp;·&nbsp; **Default:** `"shaka"`
Selects the tool used to physically decrypt CENC content. The value is compared
case-insensitively; `"mp4decrypt"` selects Bento4's mp4decrypt, and **anything else**
(including `"shaka"`) selects shaka-packager. When the value is a dict, the service key is
matched case-insensitively and the `default` entry is the fallback.
HLS AES-128 ClearKey is decrypted in-process and ignores this setting.
=== "Global"
```yaml
decryption: shaka
```
=== "Per-service"
```yaml
decryption:
default: shaka
EXAMPLE1: mp4decrypt
```
+51
View File
@@ -0,0 +1,51 @@
# A complete annotated example
The following config sets a handful of common keys. Every key not shown keeps its default.
```yaml title="unshackle.yaml"
directories:
downloads: ~/Media/unshackle
temp: /mnt/scratch/unshackle
network:
browser: Chrome131
headers:
Accept-Language: en-US,en;q=0.9
cdm:
default: chromecdm_l3
EXAMPLE2: android_l1
decryption: shaka
credentials:
EXAMPLE2: user@example.com:hunter2
output_template:
movies: "{title}.{year}.{quality?}.{source}-{tag}"
series: "{title}.{season_episode}.{quality?}.{source}-{tag}"
folder:
movies: "{title} ({year})"
series: "{title} ({year})"
tag: MYGRP
key_vaults:
- type: SQLite
name: Local
path: ~/.unshackle/keys.db
proxy_providers:
basic:
us: http://user:pass@1.2.3.4:8080
omdb_api_key: "your-omdb-key"
tmdb_api_key: "your-tmdb-key"
tvdb_api_key: "your-tvdb-key"
title_cache_time: 3600
redact_paths: true
```
For how these interact at runtime, continue to [Downloading](../../guide/downloading.md),
[DRM & CDM](../../guide/drm-and-cdm.md), and [Output & naming](../../guide/output-and-naming.md).
+42
View File
@@ -0,0 +1,42 @@
# Configuration Reference
Every key you can set in `unshackle.yaml`, with its name, type, default value, and behaviour,
split into one page per area. A worked config is on the [annotated example](example.md) page.
Where the file lives, how unshackle discovers it, and how to edit it:
[The Configuration File](../../getting-started/configuration-file.md).
!!! info "How defaults work"
Every key is optional except [`output_template`](output.md), which `dl` requires.
Without it, `dl` aborts with an error. If you omit any other key, unshackle uses
the built-in default listed here. There is **no schema validation**: unknown keys are
silently ignored (they never become settings), and the file is parsed with
`yaml.safe_load`. An empty or missing file means every other key falls back to its
default.
!!! warning "Two keys behave specially on load"
- `curl_impersonate` is a **deprecated** alias of [`network`](network.md) and emits a
`DeprecationWarning`.
- `scene_naming` has been **removed**. If it is set to any value, unshackle exits
immediately with an error telling you to use [`output_template`](output.md)
instead.
See [Deprecated & removed keys](misc.md#deprecated-removed-keys).
---
## Quick index
| Area | Keys |
|------|------|
| [Directories](directories.md) | `directories`, `filenames` |
| [Services & authentication](services.md) | `services`, `credentials`, `firefox_cookies`, `remote_services`, `serve` |
| [Download & processing](download.md) | `dl`, `subtitle`, `audio`, `muxing`, `language_tags`, `dual_multi_mode` |
| [Output & naming](output.md) | `output_template`, `tag`, `tag_rules`, `tag_group_name`, `tag_imdb_tmdb`, `chapter_fallback_name`, `unicode_filenames` |
| [DRM & CDM](drm.md) | `cdm`, `remote_cdm`, `decryption` |
| [Network & proxy](network.md) | `network`, `headers`, `proxy_providers` |
| [Key vaults](vaults.md) | `key_vaults`, `vault_timeout` |
| [External API keys](misc.md#external-api-keys) | `imdb_api_enabled`, `omdb_api_key`, `tmdb_api_key`, `tvdb_api_key`, `tvdb_pin`, `tvdb_order`, `simkl_client_id`, `metadata_providers`, `decrypt_labs_api_key`, `ipinfo_api_key` |
| [Caching & updates](misc.md#caching-updates) | `title_cache_enabled`, `title_cache_time`, `title_cache_max_retention`, `update_checks`, `update_check_interval` |
| [Logging, privacy & debug](misc.md#logging-privacy-debug) | `redact_paths`, `debug`, `debug_keys`, `debug_requests`, `set_terminal_bg` |
| [Deprecated & removed](misc.md#deprecated-removed-keys) | `curl_impersonate`, `downloader`, `scene_naming` |
+109
View File
@@ -0,0 +1,109 @@
# Other Settings
Smaller configuration areas: external metadata API keys, caching and update checks, logging and debug flags, and deprecated keys.
## External API keys { #external-api-keys }
All are unset by default and enable optional metadata/geolocation features.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `imdb_api_enabled` | bool | `false` | Use the free IMDxAPI (api.tiffara.com) metadata provider, which needs no API key. Off by default since the site has been unreliable. |
| `omdb_api_key` | str | `""` | [OMDb](https://www.omdbapi.com/) API key for IMDb metadata lookups; a more reliable alternative to IMDxAPI. Free keys are available on the OMDb site. |
| `tmdb_api_key` | str | `""` | TMDB API key for metadata enrichment and external-ID tags. |
| `simkl_client_id` | str | `""` | SIMKL client ID for metadata lookups; an alternative/fallback source to TMDB. |
| `tvdb_api_key` | str | `""` | [TheTVDB v4](https://thetvdb.com/api-information) API key; a fallback source to TMDB, strongest on TV series. Free keys are available on the TVDB site. |
| `tvdb_pin` | str | `""` | Subscriber PIN, only needed for a user-supported TVDB key. Leave empty for a normal project key. |
| `tvdb_order` | str | `""` | Default for `--tvdb-order`: renumber episodes to a TVDB season order (`official`, `dvd`, `absolute`, `alternate`, `regional`). Empty keeps the numbering the service gives. |
| `metadata_providers` | list | *(see below)* | Metadata providers to use, in the order they are tried. |
| `decrypt_labs_api_key` | str | `""` | Global Decrypt Labs API key (used by remote CDM / vault). |
| `ipinfo_api_key` | str | `""` | ipinfo.io API key for IP/region lookups. |
!!! note "How `metadata_providers` is applied"
A lookup goes to each provider in turn and stops at the first good match, so the order
decides which source wins. `metadata_providers` sets that order:
```yaml
metadata_providers: [tvdb, tmdb, simkl]
```
Names not in the list are never used, so this both orders and filters. Unknown names are
ignored with a warning. Leave it unset for the default order:
```yaml
metadata_providers: [imdbapi, omdb, simkl, tmdb, tvdb]
```
Whatever the order, a provider is skipped when its API key is missing (or, for
`imdbapi`, when `imdb_api_enabled` is `false`). Available names are
`imdbapi`, `omdb`, `simkl`, `tmdb`, and `tvdb`.
!!! note "`ipinfo_api_key` never touches your service sessions"
The token is only ever sent to `api.ipinfo.io` as a per-request `Authorization` header; it
is **never** attached to your service session, so it cannot leak to a streaming provider.
Lookups degrade gracefully through a fallback chain: the authenticated Lite endpoint (higher
rate limits, and ASN/continent data, but no city or region) → anonymous ipinfo →
`ip-api.in` as a last resort.
---
## Caching & updates { #caching-updates }
### Title cache
unshackle caches fetched title metadata (region- and account-aware) to avoid repeat API
calls.
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `title_cache_enabled` | bool | `true` | Master switch for the title cache. |
| `title_cache_time` | int (seconds) | `1800` (30 min) | Lifetime of fresh cached titles. |
| `title_cache_max_retention` | int (seconds) | `86400` (24 h) | Total retention window for a cached title, counted from when it was cached. Past `title_cache_time` the stale copy is only served as a fallback when a live fetch fails. |
### Update checks
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `update_checks` | bool | `true` | Whether to check for new unshackle releases. |
| `update_check_interval` | int (hours) | `24` | Minimum hours between update checks. |
!!! note
Update checks query the GitHub releases API with a fixed 5-second timeout and cache the
result in `directories.cache/update_check.json`.
---
## Logging, privacy & debug { #logging-privacy-debug }
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `redact_paths` | bool | `true` | Mask install-root / venv / home prefixes in logged paths (`<unshackle>`, `<venv>`, `~`). Set `false` to show full paths. |
| `debug` | bool | `false` | Global debug mode. |
| `debug_keys` | bool | `false` | Log decryption keys. |
| `debug_requests` | bool | `false` | Log HTTP requests. |
| `set_terminal_bg` | bool | `false` | Append the theme's background colour to output styling. |
!!! note "Effect of `set_terminal_bg`"
When on, the theme's background colour is appended to the foreground styles, which makes
the full colour palette render correctly on terminals whose *default* background differs
from unshackle's theme. When off, such terminals can render the ASCII-art banner and
coloured output incorrectly.
!!! danger "Key exposure"
With `debug_keys` on, decryption content keys are written to the structured debug log at
INFO level during the DRM handshake. Treat `debug`, `debug_keys`, and the `unshackle_debug_*.jsonl` files as
sensitive, and keep `redact_paths` enabled when sharing logs.
`debug_keys` affects **only** content-encryption keys (the `content_key`/`key` fields).
Passwords, tokens, cookies, and session tokens are **always** redacted regardless of this
setting, and KIDs, key counts, and other metadata are always logged either way.
---
## Deprecated & removed keys { #deprecated-removed-keys }
| Key | Status | Behaviour |
|-----|--------|-----------|
| `curl_impersonate` | **Deprecated** → use [`network`](network.md) | Emits a `DeprecationWarning`; still honoured only if `network` is absent. |
| `downloader` | **Deprecated** | Any value other than `"requests"` emits a `DeprecationWarning`; the value is otherwise ignored (the unified requests downloader is always used). |
| `scene_naming` | **Removed** | If set to any non-null value, unshackle exits with an error directing you to configure [`output_template`](output.md) instead. |
+94
View File
@@ -0,0 +1,94 @@
# Network & proxy { #network-proxy }
## `network`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
TLS-fingerprinting and HTTP client settings for the rnet-based session.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `browser` | str | `"Chrome131"` | Impersonation preset. Must be an exact preset name (e.g. `Chrome131`, `Firefox135`, `Edge101`, `Safari18`, `OkHttp4_12`, `OkHttp5`, `Opera118`); unknown names raise an error. |
| `http1_only` | bool | *(unset)* | Force HTTP/1.1. |
| `http2_only` | bool | *(unset)* | Force HTTP/2. |
| `connect_timeout` | int | `10` | Connect timeout in seconds. |
| `read_timeout` | int | `30` | Read timeout in seconds (max gap between response chunks). |
| `timeout` | int | *(unset)* | Total request timeout in seconds. |
| `allow_redirects` | bool | *(unset)* | Follow redirects automatically (rnet client default when unset). |
| `pool_idle_timeout` | int | `55` | Seconds an idle pooled connection is kept (default stays under typical ~60s CDN idle kills). |
| `pool_max_idle_per_host` | int | `64` | Max idle connections kept per host. |
| `pool_max_size` | int | *(unset)* | Max total connections in the pool. |
| `tcp_keepalive` | int | `30` | TCP keepalive time in seconds. |
| `tcp_keepalive_interval` | int | *(unset)* | TCP keepalive probe interval in seconds. |
| `tcp_keepalive_retries` | int | *(unset)* | TCP keepalive probe retry count. |
| `tcp_nodelay` | bool | *(unset)* | Disable Nagle's algorithm. |
```yaml
network:
browser: Firefox135
http2_only: true
```
!!! note "Measured effect of `http1_only`"
Both `http1_only` and `http2_only` are unset by default. In benchmarks, forcing HTTP/1.1
gained 30 to 50% on hosts that throttle per-connection or stall behind HTTP/2 flow control,
and **cost up to 27% on fast CDNs**.
!!! warning "Renamed from `curl_impersonate`"
The old `curl_impersonate` section is a deprecated alias. If you still use it, unshackle
honours it (only when `network` is absent) but emits a `DeprecationWarning`. Rename it to
`network`.
## `headers`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Default HTTP headers merged into every session unshackle creates.
```yaml
headers:
Accept-Language: en-US,en;q=0.9
```
!!! warning "Don't set `Accept-Encoding` (and similar) here"
Compatibility headers such as `Accept-Encoding` are set by the rnet HTTP backend as part of
its browser-impersonation profile; overriding them breaks the impersonation fingerprint.
This block is for cross-service defaults only (for example `Accept-Language`,
`User-Agent`); per-service headers belong in that service's own config.
## `proxy_providers`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Proxy/VPN provider configuration. Each sub-key names a provider, and its block is passed
straight to that provider's constructor. See [Proxies & VPN](../../guide/proxies-and-vpn.md) for
the full provider guide. Recognised providers and their exit ports:
| Provider | Config key | Credentials | Proxy scheme/port |
|----------|-----------|-------------|-------------------|
| Basic (static) | `basic` | country → URI(s) | as specified |
| NordVPN | `nordvpn` | **service** credentials (48 chars combined) | `https://...:89` |
| Surfshark | `surfsharkvpn` | **service** credentials (48 chars combined) | `https://...:443` |
| Windscribe | `windscribevpn` | service credentials | `https://...:443` |
| ExpressVPN | `expressvpn` | device login (`enable: true`) / token cache | `https://cat:...@...:443` |
| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://...:4443` (or `:443` Secure Core) |
| Gluetun | `gluetun` | per-VPN keys/creds | `http://localhost:{port}` (local Docker) |
| Hola | *(none, auto)* | none | `http://...:{peer}` |
```yaml
proxy_providers:
basic:
us: http://user:pass@1.2.3.4:8080
de:
- http://a.example:8080
- socks5://b.example:1080
nordvpn:
username: <service username>
password: <service password>
```
!!! note "Provider loading differs between CLI and REST server"
The `dl` CLI loads all providers, including `windscribevpn` and `gluetun`. The REST API /
remote-client path uses a separate resolver that does **not** load `windscribevpn` or
`gluetun`. ExpressVPN and ProtonVPN also auto-load when their cached session exists, and Hola
auto-loads whenever the `hola-proxy` binary is present.
+159
View File
@@ -0,0 +1,159 @@
# Output & naming { #output-naming }
Keys that control the output filename, the per-title subfolder, release-group tagging, and
filename character handling. For template cookbook material and worked naming examples, see
the [Output & naming guide](../../guide/output-and-naming.md).
## `output_template`
- **Type:** `dict` &nbsp;·&nbsp; **Required** (no usable default; `dl` exits with an error if this is missing)
Filename templates keyed by media kind, using `{variable}` placeholders. Recognised kinds are
`movies`, `series`, and `songs` (and `albums` for folder templates). A trailing `?` on a
variable (e.g. `{quality?}`) marks it optional: if empty, it and one adjacent separator are
dropped.
```yaml title="unshackle.yaml"
output_template:
movies: "{title}.{year}.{quality?}.{source}-{tag}"
series: "{title}.{season_episode}.{episode_name?}.{quality?}.{source}-{tag}"
songs: "{track_number}. {title}"
folder: # special nested key, see below
movies: "{title} ({year})"
series: "{title} ({year})"
```
**The nested `folder` sub-key** controls output subfolders and is popped out of
`output_template`:
- A **dict** sets per-kind folder templates (kinds: `movies`, `series`, `songs`, `albums`).
- A **str** sets a single folder template for everything.
unshackle validates templates on load and emits warnings for unknown variables, non-string
values, filesystem-unsafe characters (`< > : " / \ | ? *`), empty templates, and unknown
folder-kind keys.
??? example "All valid template variables"
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `part`,
`date`, `quality`, `resolution`, `source`, `tag`, `track_number`, `artist`,
`album_artist`, `album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`,
`explicit`, `isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`,
`dual`, `multi`, `dubbed`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`,
`title_type`.
!!! warning "`{part?}` is already inside `{season_episode}`"
`part` holds the bare part index of a
[split episode](../../guide/output-and-naming.md#split-episodes) (`2`) and is empty on
every other title. unshackle folds it into `{episode}` and `{season_episode}`, so the
shipped `series` template names split episodes correctly with no edit. Putting
`{part?}` next to either of them renders the part twice (`S01E01.Part.2.2`). Use the
standalone variable only in a template that names neither `{episode}` nor
`{season_episode}`, and note that it is always empty in a folder template, since
folders do not carry the part.
## Tagging & naming keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `tag` | str | `""` | Release-group tag. Fills the `{tag}` template variable and is written into MKV/audio tags. |
| `tag_group_name` | bool | `true` | Include the group name (`tag`) in the MKV `Group` tag. |
| `tag_imdb_tmdb` | bool | `true` | Write IMDb/TMDB/TVDB external-ID tags into the MKV (needs metadata providers). |
| `chapter_fallback_name` | str | `""` | Fallback chapter-name template, e.g. `"Chapter {i:02}"`. |
| `unicode_filenames` | bool | `false` | Allow Unicode in filenames; when `false`, names are ASCII-sanitised. |
## `tag_rules`
- **Type:** `list` &nbsp;·&nbsp; **Default:** `[]`
Conditionally replaces [`tag`](#tagging-naming-keys) for a given download. Each rule pairs a `when`
dict of conditions with the `tag` to use when they hold. Rules are evaluated in order and the
first match wins; if no rule matches, the global `tag` is kept.
```yaml title="unshackle.yaml"
tag_rules:
- when: { title_type: movie, lang_tag: SUBBED }
tag: SUBMOVIES
- when: { quality: 2160p, hdr: [DV, DV.HDR, DV.HDR10P, HDR, HDR10P] }
tag: UHDGROUP
```
Rule semantics:
- Every condition in `when` must hold (they are AND-ed).
- A value may be a scalar or a list; a list matches if **any** entry matches.
- Comparison is case-insensitive string equality.
- The keys of `when` are the filename template variables that describe the release itself:
`title_type`, `quality`, `resolution`, `source`, `video`, `hdr`, `hfr`, `audio`,
`audio_channels`, `audio_full`, `atmos`, `dual`, `multi`, `dubbed`, `edition`, `repack`,
and `lang_tag`. Only these are available. The title's own fields (`title`, `year`, `season`,
`episode`, and so on) are added after the rules run and cannot be matched.
- An unknown key in `when` is a mistake: unshackle logs a warning that names the key and skips
that rule. A rule without conditions or without a `tag` is also warned about and skipped.
Put the more specific rules first, because the first match ends the search.
!!! warning "Match `hdr` on the exact value"
Matching is exact, and `hdr` is a composite label for Dolby Vision titles: a DV title with
an HDR10 base layer gives `DV.HDR`, and with an HDR10+ base layer `DV.HDR10P`. Plain `DV`
does **not** match those. List every variant you want, as the example above does. Note
also that HDR10 gives `HDR`, not `HDR10`. The full set of values is `HDR`, `HDR10P`, `DV`,
`DV.HDR`, `DV.HDR10P`, and `HLG`, as listed in the
[variable table](../../guide/output-and-naming.md#template-variables).
### Operators
A condition value may start with a comparison operator, which is applied to the rest of the
value:
```yaml title="unshackle.yaml"
tag_rules:
- when: { resolution: "<1080" }
tag: SDGROUP
- when: { quality: ">=2160" }
tag: UHDGROUP
- when: { video: "!=H.265" }
tag: AVCGROUP
```
- `>`, `<`, `>=`, `<=` compare numbers. unshackle reads the first number out of the actual
value, so `">=2160"` matches a quality of `2160p`. If the actual value has no number, the
condition fails.
- `=`, `==`, and `!=` compare text, case-insensitively, when the operand is not a number.
With a numeric operand they compare numbers like the operators above. A value with no number
in it is never equal to a number, so `"!=2160"` holds for it and `"==2160"` does not.
- An ordering operator with a text operand (for example `">DV"`) is a mistake: unshackle logs
a warning and the condition fails. So does an operator with nothing after it (`">="`).
- A value with no operator keeps the plain equality behaviour described above.
!!! warning "Quote operator values in YAML"
Two of these operators break an unquoted YAML value, and both stop the config from
loading:
- `>` opens a folded block scalar, so `quality: >=2160` fails with a "while scanning a
block scalar" error.
- `!` starts a tag, so `hdr: !=DV` fails with a "could not determine a constructor"
error.
Quote every operator value: `quality: ">=2160"`, `resolution: "<1080"`, `video: "!=H.265"`.
!!! note "Keep a negation out of lists"
A list matches if any entry matches, so two or more `!=` entries in one list are always
true. Give a negation as a single value, not as a list entry.
`title_type` is the media kind of the title: `movie`, `series`, or `music`. It is useful to
give each kind its own group:
```yaml
tag_rules:
- when: { title_type: movie }
tag: MOVIEGRP
- when: { title_type: series }
tag: TVGRP
```
The resolved tag applies to the `{tag}` filename variable only. The `Group` metadata tag
written into MKV and music files always uses [`tag`](#tagging-naming-keys).
!!! note "`--tag` wins"
An explicit `dl --tag` on the command line disables the rules completely and uses the
given tag.
+137
View File
@@ -0,0 +1,137 @@
# Services & authentication { #services-auth }
## `services`
- **Type:** `dict` keyed by service tag &nbsp;·&nbsp; **Default:** `{}`
Per-service configuration, keyed by the canonical service tag (for example `EXAMPLE1`, `EXAMPLE2`,
`EXAMPLE3`). unshackle reads several well-known sub-keys out of each service's block:
- **`proxy_map`**: remaps the region query you passed to `-p/--proxy` for this service. A key is
that query, or `"provider:query"` when the flag also named a provider; the value is the query
asked of the proxy provider instead. It has no effect when `-p` was given a full proxy URL.
- **`title_map`**: an exact-match rename map applied to fetched titles (source name →
desired name), so a service that names a title differently from your library still matches.
- **`dl`**: per-service download defaults, using the same keys as the global
[`dl`](download.md#dl) block.
Individual services may read any additional keys they define. The merged result (the service's
own `config.yaml` with this block layered on top) is handed to the service as `self.config`.
User- or device-specific values (API keys, account IDs, device attributes) belong in this block
rather than in the service's own `config.yaml`, which holds shared defaults; see
[creating a service](../../dev/creating-a-service.md).
```yaml
services:
EXAMPLE1:
proxy_map:
us: us-nyc
"nordvpn:ca": ca-toronto
title_map:
"My Show: The Movie": "My Show Presents The Movie"
```
## `credentials`
- **Type:** `dict` keyed by service tag &nbsp;·&nbsp; **Default:** `{}`
Per-service login credentials. Each value is a `username:password[:extra]` string, the same
data as a `[username, password]` (or `[username, password, extra]`) list, or a dict of profile
name to either of those. With the dict form, `-p/--profile` selects the entry, and the
`default` key is used when no profile was given or the named profile is missing. These are
parsed into `Credential` objects; the credential's SHA-1 is also used as an account hash for
cache keys.
```yaml
credentials:
EXAMPLE2: user@example.com:hunter2
EXAMPLE1:
default: primary@example.com:pw1
second: secondary@example.com:pw2
```
!!! tip "Cookies vs credentials"
Cookies are stored as files under [`directories.cookies`](directories.md), not in this key.
A service's `authenticate()` accepts cookies, credentials, or both.
## `firefox_cookies`
- **Type:** `dict` keyed by service tag &nbsp;·&nbsp; **Default:** `{}`
Settings for extracting cookies directly from a local Firefox profile. A service block is
expected to provide `hosts` (a list of cookie hostnames; entries shorter than 3 characters
are ignored to prevent dumping the whole cookie store) and an optional `local_storage`
boolean to also pull matching entries from `webappsstore.sqlite`, which only services that
keep auth tokens in localStorage rather than in HTTP cookies need. Extraction is read-only.
!!! note "Firefox does not need to be closed"
The extractor copies **both** `cookies.sqlite` **and** its WAL file into a `0700` temp
directory, so writes Firefox has not yet flushed to the main DB are included. Extraction
fails only if Firefox holds an exclusive write lock at the instant of the copy. The live
profile is not modified.
!!! warning "Extraction falls back silently to file cookies"
If extraction yields no cookies or fails for **any** reason, unshackle silently falls back
to the normal file-based cookie path (`cookies/<SERVICE>.txt` or
`cookies/<SERVICE>/<profile>.txt`), with no error reported.
## `remote_services`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Definitions of remote unshackle service servers, used by the `--remote` mode. Each entry is
named by you (pick it with `--server`, or omit that flag when only one is configured) and gives
the server's `url` (required), an optional `api_key` sent as `X-Secret-Key`, an optional
`server_cdm` boolean, and an optional `services` sub-dict of per-service local overrides such
as `title_map`. In `--remote` mode unshackle turns the server's service list into synthetic CLI
commands that run against it, falling back to the tags in that `services` sub-dict when the
list cannot be fetched. See [remote sessions](../../dev/rest-api/remote-sessions.md) for the
full setup.
## `serve`
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Configuration for the `serve` command (the built-in REST API server). The full server guide
is the [REST API](../../dev/rest-api/index.md) section; these are the config keys.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
| `api_secret` | str | *(unset)* | Master secret accepted in the `X-Secret-Key` header. Required unless the server is started with `--no-key`. |
| `users` | dict | `{}` | Per-user API keys and their allowlists (see below). |
| `services` | list | *(unset)* | Global service allowlist. Omit to allow all. |
| `remote_only` | bool | `false` | Expose only the remote service session endpoints (health, services, search, session) and disable the rest of the REST API. |
| `session_ttl` | int (s) | `300` | Lifetime of an interactive auth session. |
| `max_sessions` | int | `100` | Maximum concurrent sessions. |
| `history_limit` | int | `100` | How many finished jobs to retain in history. |
| `compression_level` | int | `1` | gzip level for responses. |
| `global_speed_limit` | str | *(unlimited)* | Server-wide download speed cap, e.g. `10M`, `1.5G` or plain bytes/sec (same format as `speed_limit`). One shared budget across all concurrent jobs; per-job speed limits are ignored while it is set. |
| `cdm_overrides` | list or bool | *(unset)* | Allowed per-request CDM overrides: a list of permitted device names, or `true` for any. Unset rejects every override. |
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
Each entry under `users` is keyed by that user's API key and may set its own `services`,
`devices`, and `playready_devices` allowlists, narrowing the global ones, plus an optional
`username` used as the log label for that key (defaults to a truncated form of the key). A
user with no `playready_devices` key gets no PlayReady access at all, not the global list.
```yaml
serve:
api_secret: change-me
remote_only: true
services: [EXAMPLE1, EXAMPLE2]
# server-wide download defaults (same keys as the `dl:` block)
downloads: 3
best_available: true
users:
a1b2c3d4: # this user's API key
services: [EXAMPLE1] # may only use EXAMPLE1
```
!!! note "`dl` keys inside `serve`"
Most [`dl`](download.md#dl) flag keys (`downloads`, `workers`, `best_available`, and so on)
can be set directly inside `serve`, where they apply to every request the server handles.
The server recognises a fixed subset of download parameters, so a few CLI-only flags are
ignored here.
+47
View File
@@ -0,0 +1,47 @@
# Key vaults { #key-vaults }
The full guide is at [Vaults](../../guide/vaults.md). Two keys configure them.
## `key_vaults`
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]`
An ordered list of key-vault backends. unshackle queries them in order and reuses content
keys instead of re-licensing. Each entry needs a `type` (the backend module name) and a
`name`, plus backend-specific keys.
| `type` | Purpose | Required keys | Notes |
|--------|---------|--------------|-------|
| `SQLite` | Local SQLite database | `name`, `path` | Loaded **critically**; a failure aborts the run. |
| `MySQL` | Remote MySQL database | `name`, `host`, `database`, `username` | Extra keys (e.g. `password`, `port`) forwarded to pymysql. |
| `API` | RESTful JSON API | `name`, `uri`, `token` | Optional `headers` map is sent with every request. Honours `vault_timeout`. |
| `HTTP` | HTTP API with modes | `name`, `host`, one of `password`/`api_key`, and `username` in query mode | `api_mode`: `query` (default), `json`, `decrypt_labs`. Honours `vault_timeout`. |
```yaml
key_vaults:
- type: SQLite
name: Local
path: ~/.unshackle/keys.db
- type: MySQL
name: Team
host: db.example.com
database: keys
username: unshackle
password: hunter2
no_push: false
```
!!! note "Per-entry options"
- `no_push: true` makes a vault read-only (keys are fetched but never written to it).
- A vault of `type: API` whose `name` contains `decrypt_labs` auto-fills its `token` from
[`decrypt_labs_api_key`](misc.md#external-api-keys) when not set inline. Vault `type` values are
case-sensitive module names.
- An all-zero content key (32 zeros) is treated as "no key" everywhere and is never
stored.
## `vault_timeout`
- **Type:** `float` &nbsp;·&nbsp; **Default:** `10.0`
Timeout in seconds for vault operations. Injected automatically into any backend whose
constructor accepts a `timeout` parameter (a per-vault `timeout` still wins).