mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-15 13:35:05 +00:00
docs: restructure documentation into MkDocs Material site
This commit is contained in:
@@ -0,0 +1,730 @@
|
||||
# Configuration Reference
|
||||
|
||||
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.
|
||||
|
||||
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 |
|
||||
|------|------|
|
||||
| [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) | `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 · **Default:** `{}`
|
||||
|
||||
Per-service configuration, keyed by the canonical service tag (for example `AMZN`, `NF`,
|
||||
`DSNP`). 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:
|
||||
AMZN:
|
||||
proxy_map:
|
||||
default: us
|
||||
title_map:
|
||||
"The Grand Tour: The Movie": "The Grand Tour Presents The Movie"
|
||||
```
|
||||
|
||||
### `credentials`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **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:
|
||||
NF: user@example.com:hunter2
|
||||
AMZN:
|
||||
- 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` · **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` · **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` · **Default:** `{}`
|
||||
|
||||
Configuration for the `serve` command (the built-in REST API server).
|
||||
|
||||
!!! tip "Declaring `dl` defaults under `serve`"
|
||||
You can nest `dl` flags (`downloads`, `workers`, `best_available`, and so on) 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` · **Default:** `{}`
|
||||
|
||||
Default options forwarded to the `dl` command. Anything you would otherwise pass as a `dl`
|
||||
flag can be given a persistent default here. See [Downloading](../guide/downloading.md) for
|
||||
the flags themselves.
|
||||
|
||||
### `subtitle`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Subtitle handling options. See [Subtitles](../guide/subtitles.md).
|
||||
|
||||
### `audio`
|
||||
|
||||
- **Type:** `dict` · **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` · **Default:** `{}`
|
||||
|
||||
Matroska (MKV) muxing options.
|
||||
|
||||
### `language_tags`
|
||||
|
||||
- **Type:** `dict` · **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` · **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` · **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
|
||||
NF: android_l1
|
||||
```
|
||||
|
||||
A value may also be a **nested dict** for advanced selection by quality, DRM system, or
|
||||
profile:
|
||||
|
||||
```yaml
|
||||
cdm:
|
||||
AMZN:
|
||||
">=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]` · **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` · **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` · **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. |
|
||||
| `pool_max_idle_per_host` | int | *(unset)* | Connection-pool tuning. |
|
||||
| `pool_max_size` | int | *(unset)* | Connection-pool tuning. |
|
||||
| `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` · **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` · **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` | browser cookies / 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 cookie file 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]` · **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` · **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 |
|
||||
|-----|------|---------|-------------|
|
||||
| `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
|
||||
NF: android_l1
|
||||
|
||||
decryption: shaka
|
||||
|
||||
credentials:
|
||||
NF: 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
|
||||
|
||||
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).
|
||||
Reference in New Issue
Block a user