docs: apply simplified technical english across docs and docstrings.

This commit is contained in:
imSp4rky
2026-08-19 16:31:06 -06:00
parent 4177cb2267
commit 66971230c3
135 changed files with 2651 additions and 2570 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Config API
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
`unshackle` configuration file. For a human-friendly walkthrough of every config key, see the
[Configuration Reference](../configuration/index.md).
::: unshackle.core.config
+15 -15
View File
@@ -1,10 +1,10 @@
# 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.
The `directories` config key is a **dict** mapping directory names to filesystem paths.
unshackle honours only the names listed below. A handful are **protected** and you cannot
move them, and unshackle silently ignores an override of one. unshackle computes all
defaults relative to the installed package, and passes every user-settable path through
`Path(...).expanduser()`, so `~` works.
```yaml title="unshackle.yaml"
directories:
@@ -38,9 +38,9 @@ directories:
!!! 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.
`.git`) or `owner/repo` shorthand. unshackle clones and updates repo specs automatically,
and uses plain paths as they are. **List order is priority**: the first source to define a
service tag wins.
```yaml
directories:
@@ -51,11 +51,11 @@ directories:
```
!!! 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
unshackle's use of git is **read-only on the remote**: it only ever does a shallow
`clone`, `fetch`, `pull`, and a local `reset`. **unshackle never pushes anything**. Your
existing git credential helper authenticates private repos, 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
(or the bundled `unshackle/services`), and **unshackle writes nothing 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.
@@ -74,9 +74,9 @@ directories:
## 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.
The `filenames` config key is a **dict** of templated file/name patterns. unshackle uses
each value verbatim (no path processing). It fills in braced fields like `{time}` and
`{service}` at runtime.
| Key | Type | Default | Notes |
|-----|------|---------|-------|
+22 -21
View File
@@ -4,13 +4,14 @@
- **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
Persistent defaults for the `dl` command. unshackle uses the block as Click's `default_map`,
so you can give any `dl` flag 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:
**Config key naming.** A config key is the flag's long name with dashes replaced by
underscores (`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few
config keys use the flag's internal destination and not its visible name. Set these exact
config keys:
| Flag | Config key |
|------|-----------|
@@ -27,7 +28,7 @@ after the flag's internal destination rather than its visible name; set these ex
[`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):
Common config keys. This is a useful subset, and every `dl` flag works:
| Key | Type | Default | Sets |
|-----|------|---------|------|
@@ -89,28 +90,28 @@ dl:
!!! 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`).
to set server-wide download defaults for every request it handles. The API ignores the
config keys it does not know there, `speed_limit` for example. `serve` has its own
`global_speed_limit`.
## `continue_downloads`
- **Type:** `bool` &nbsp;·&nbsp; **Default:** `false`
Keep a failed download's completed segment files so the next attempt resumes from where it
Keep a failed download's completed segment files so the next download resumes from where it
stopped instead of restarting from byte zero.
Only whole, fully downloaded segments are reused; partially written segments are always
discarded, and no partial-byte (`Range`) resume across runs takes place. Reuse also
requires proof that the remote content is unchanged: the manifest URL, segment count, and
byte ranges must all match the previous run, otherwise the download silently restarts
clean.
unshackle reuses only whole, fully downloaded segments. It always discards partially written
segments, and no partial-byte (`Range`) resume across runs takes place. Reuse is possible only
with proof that the segmentation did not change. The manifest URL, segment count, and byte
ranges must all match the previous download, or the download silently restarts clean.
!!! note "What can and cannot resume"
- Segmented DASH, ISM, and HLS tracks resume.
- Single-file (non-segmented) downloads and DASH tracks collapsed to a single ranged
download never produce segment files, so this option does not apply to them (the
download never make segment files, so this option does not apply to them (the
downloader has its own in-flight partial-file handling for those).
- HLS resumes Widevine/PlayReady and unencrypted content only; AES-128 content always
- HLS resumes Widevine/PlayReady and unencrypted titles only. An AES-128 title always
restarts. An HLS failure during the post-download merge step also restarts.
For a one-off resume without changing config, pass `dl --continue-downloads` on the
@@ -124,8 +125,8 @@ continue_downloads: true
- **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.
Subtitle handling: SDH stripping, format conversion, and whether unshackle muxes the subtitles
in or writes them as sidecar files. See [Subtitles](../../guide/subtitles.md) for the full guide.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
@@ -168,7 +169,7 @@ subtitle:
| 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.
unshackle removes nothing, 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
@@ -245,8 +246,8 @@ muxing:
- **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:
`rules` list is present, unshackle evaluates each rule in order and uses the first match's
`tag`. A rule can examine:
| Condition | Meaning |
|-----------|---------|
+15 -15
View File
@@ -31,13 +31,13 @@ cdm:
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`.
Quality keys can hold `1080`, `>=1080`, `>720`, `<=576`, `<480` style comparisons. DRM keys
are `widevine` / `playready`. unshackle matches the quality keys first, but only when a video
height is known. If the value is still a dict after that, unshackle uses
`widevine`/`playready` when present, or else 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
unshackle finds the resolved name in [`remote_cdm`](#remote_cdm) by `name` first. If no entry
matches, unshackle loads it as a local device file, in this order: `<name>.prd` in
`directories.prds`, `<name>.prd` in `directories.wvds`, then `<name>.wvd` in
`directories.wvds`.
@@ -45,7 +45,7 @@ matches, it is loaded as a local device file, in this order: `<name>.prd` in
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]`
A list of remote CDM server definitions. Each entry is matched by its `name` (referenced from
A list of remote CDM definitions. unshackle matches each entry by its `name` (referenced from
[`cdm`](#cdm)), and its `type` selects the backend:
| `type` | Backend | Key fields |
@@ -89,7 +89,7 @@ styles: `Device Type`/`device_type`, `System ID`/`system_id`, `Security Level`/`
| `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`.
unshackle uses PlayReady mode when `device_type` is `PLAYREADY` or `device_name` starts with `SL`.
### `custom_api` fields { #remote-cdm-custom-api }
@@ -105,8 +105,8 @@ PlayReady mode is used when `device_type` is `PLAYREADY` or `device_name` starts
| `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`.
Transforms cover base64/hex/JSON encoding and `kid:key` parsing. unshackle uses PlayReady mode
when `device.type` is `PLAYREADY` or the device name is `SL2`/`SL3`.
```yaml
remote_cdm:
@@ -135,12 +135,12 @@ remote_cdm:
- **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.
Selects the tool that physically decrypts CENC-encrypted tracks. unshackle compares the value
case-insensitively. `"mp4decrypt"` selects Bento4's mp4decrypt, and **anything else**
(including `"shaka"`) selects shaka-packager. When the value is a dict, unshackle compares the
service tag case-insensitively and uses the `default` entry as the fallback.
HLS AES-128 ClearKey is decrypted in-process and ignores this setting.
unshackle decrypts HLS AES-128 ClearKey in-process and ignores this setting.
=== "Global"
+1 -1
View File
@@ -1,6 +1,6 @@
# A complete annotated example
The following config sets a handful of common keys. Every key not shown keeps its default.
The following config sets a handful of common config keys. Every config key not shown keeps its default.
```yaml title="unshackle.yaml"
directories:
+6 -6
View File
@@ -1,17 +1,17 @@
# Configuration Reference
Every key you can set in `unshackle.yaml`, with its name, type, default value, and behaviour,
Every config 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
Every config key is optional except [`output_template`](output.md), which `dl` requires.
Without it, `dl` aborts with an error. If you do not write any other config key, unshackle
uses the built-in default listed here. There is **no schema validation**: unshackle
silently ignores unknown config keys (they never become settings), and reads the file with
`yaml.safe_load`. An empty or missing file means every other config key falls back to its
default.
!!! warning "Two keys behave specially on load"
+18 -18
View File
@@ -5,7 +5,7 @@ Smaller configuration areas: external metadata API keys, caching and update chec
## External API keys { #external-api-keys }
All are unset by default and enable optional metadata/geolocation features. For which fields
each metadata provider can actually answer with, see
each metadata provider can answer with, see
[What each metadata provider supplies](../../guide/downloading.md#what-each-metadata-provider-supplies).
| Key | Type | Default | Description |
@@ -23,7 +23,7 @@ each metadata provider can actually answer with, see
| `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
A lookup goes to each metadata provider in turn and stops at the first good match, so the order
decides which source wins. `metadata_providers` sets that order:
```yaml
@@ -37,8 +37,8 @@ each metadata provider can actually answer with, see
metadata_providers: [imdb, omdb, simkl, tmdb, tvdb, anilist]
```
Whatever the order, unshackle skips a provider that has no API key. `imdb` and
`anilist` need no key, so they are always available. Available names are `imdb`, `omdb`,
Whatever the order, unshackle skips a metadata provider that has no API key. `imdb` and
`anilist` need no API key, so they are always available. Available names are `imdb`, `omdb`,
`simkl`, `tmdb`, `tvdb`, and `anilist`. The old name `imdbapi` still works and is read as
`imdb`.
@@ -63,7 +63,7 @@ each metadata provider can actually answer with, see
!!! note "What `disable_metadata` turns off"
With `disable_metadata: true`, unshackle never contacts a metadata provider on its own.
The title search is dead, and that includes the keyless `imdb` provider. Titles, years and
The title search is dead, and that includes the keyless `imdb` metadata provider. Titles, years and
languages then come only from the service.
```yaml
@@ -72,18 +72,18 @@ each metadata provider can actually answer with, see
An ID you give with `--tmdb`, `--imdb`, `--tvdb`, or `--anilist` is your permission to use
that ID. That lookup still works. unshackle looks the ID up directly, cross references the other
IDs from it, and `--enrich` still reads that source. Only the lookups you did not ask for
are stopped.
IDs from it, and `--enrich` still reads that source. unshackle stops only the lookups you
did not ask for.
!!! note "The `imdb` provider needs no key"
It replaces the earlier `imdbapi` provider, which read a third-party mirror
It replaces the earlier `imdbapi` metadata provider, which read a third-party mirror
(api.tiffara.com) and needed the `imdb_api_enabled` switch. That option is gone: delete it
from your config, where it is now ignored.
!!! tip "A different order per title kind"
Give a map instead of one list when the best source differs by kind. TVDB is strongest on
series, TMDB on films. There are two kinds, `tv` and `movie`, and this example sets both
of them and names every provider, so nothing is left to the default:
of them and names every metadata provider, so nothing is left to the default:
```yaml
metadata_providers:
@@ -91,9 +91,9 @@ each metadata provider can actually answer with, see
movie: [tmdb, imdb, omdb, simkl, tvdb]
```
Each list is independent: it both orders and filters that kind on its own, so a provider
you leave out of `tv` is still used for `movie`. Here TVDB is tried first for a series and
last for a film.
Each list is independent: it both orders and filters that kind on its own, so unshackle
still uses for `movie` a metadata provider you leave out of `tv`. Here unshackle tries TVDB
first for a series and last for a film.
You do not need to give both keys, or a full list in each. A kind you leave out uses the
default order:
@@ -104,11 +104,11 @@ each metadata provider can actually answer with, see
```
`tv` covers every episode and `movie` covers every film. unshackle only looks up
metadata for those two kinds, so any other key, such as `anime` or `music`, is ignored.
metadata for those two kinds, so unshackle ignores any other config key, such as `anime` or `music`.
!!! 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.
unshackle sends the token only to `api.ipinfo.io`, as a per-request `Authorization` header.
It **never** goes on your service HTTP session, so it cannot leak to a streaming service.
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.
@@ -160,19 +160,19 @@ calls.
| `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
When on, unshackle appends the theme's background colour 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
With `debug_keys` on, unshackle writes the content keys 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.
setting, and KIDs, content key counts, and other metadata are always logged either way.
---
+8 -8
View File
@@ -4,7 +4,7 @@
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
TLS-fingerprinting and HTTP client settings for the rnet-based session.
TLS-fingerprinting and HTTP client settings for the rnet-based HTTP session.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
@@ -43,7 +43,7 @@ network:
- **Type:** `dict` &nbsp;·&nbsp; **Default:** `{}`
Default HTTP headers merged into every session unshackle creates.
Default HTTP headers merged into every HTTP session unshackle makes.
```yaml
headers:
@@ -52,17 +52,17 @@ headers:
!!! 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.
its browser-impersonation profile. If you override them, you break 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:
Proxy/VPN provider configuration. Each sub-key names a proxy provider, and unshackle passes its
block straight to that provider's constructor. See [Proxies & VPN](../../guide/proxies-and-vpn.md)
for the full proxy provider guide. Recognised proxy providers and their exit ports:
| Provider | Config key | Credentials | Proxy scheme/port |
|----------|-----------|-------------|-------------------|
+15 -14
View File
@@ -23,7 +23,7 @@ output_template:
series: "{title} ({year})"
```
**The nested `folder` sub-key** controls output subfolders and is popped out of
**The nested `folder` sub-key** controls output subfolders, and unshackle pops it out of
`output_template`:
- A **dict** sets per-kind folder templates (kinds: `movies`, `series`, `songs`, `albums`).
@@ -53,7 +53,7 @@ folder-kind keys.
!!! tip "`{absolute}` counts episodes across seasons"
`absolute` holds the absolute episode number, zero-padded to 3 digits (`007`), and is
empty on titles that have none. It is added to the name and never replaces `{season}`
empty on titles that have none. You add it to the name yourself, and it never replaces `{season}`
or `{episode}`. Any series with a TVDB absolute order can use it, anime most often.
Services can supply it, and
[`--enrich`](../../guide/downloading.md#metadata-and-tagging) fills it in from TVDB.
@@ -74,8 +74,8 @@ folder-kind keys.
- **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.
dict of conditions with the `tag` to use when they hold. unshackle evaluates the rules in
order and the first match wins. If no rule matches, the global `tag` stays.
```yaml title="unshackle.yaml"
tag_rules:
@@ -88,30 +88,31 @@ tag_rules:
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.
- A value can 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:
- The keys of `when` are the filename template variables for 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.
and `lang_tag`. Only these are available. unshackle adds the title's own fields (`title`,
`year`, `season`, `episode`, and so on) after it evaluates the rules, so a condition cannot
use them.
- 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.
Put the more specific rules first, because unshackle stops at the first match.
!!! 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
does **not** match those. Name 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:
A condition value can start with a comparison operator, which unshackle applies to the rest of
the value:
```yaml title="unshackle.yaml"
tag_rules:
+33 -33
View File
@@ -1,12 +1,12 @@
# Post-download scripts { #post-scripts }
Run your own script when a download finishes, with unshackle's metadata passed in as
Operate your own script when a download finishes, with unshackle's metadata passed in as
`{variable}` placeholders. Use it to hand a finished file to an uploader, send a
notification, or start a library scan.
## `post_scripts`
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]` (no scripts run)
- **Type:** `list[dict]` &nbsp;·&nbsp; **Default:** `[]` (unshackle operates no scripts)
```yaml title="unshackle.yaml"
post_scripts:
@@ -17,7 +17,7 @@ post_scripts:
```
Every entry that matches the event and mode runs, so one download can feed an uploader and
a notifier without a wrapper script. An entry may also be written as a plain string, which
a notifier without a wrapper script. You can also write an entry as a plain string, which
is the same as `{command: "...", event: success, mode: file}`.
| Key | Default | Meaning |
@@ -26,15 +26,15 @@ is the same as `{command: "...", event: success, mode: file}`.
| `event` | `success` | `success` when a file was written, `failure` when the download failed |
| `mode` | `file` | `file`, `season`, or `run`. See [Modes](#modes) |
An entry whose `event` or `mode` is not one of the listed values is skipped and logs a
warning naming the bad value, once per command. An entry whose `command` cannot be split
into arguments, such as one with an unbalanced quote, is skipped with a warning too. Neither
stops the download or the other entries.
unshackle skips an entry whose `event` or `mode` is not one of the listed values, and logs a
warning that names the bad value, once per command. unshackle also skips an entry whose
`command` it cannot split into arguments, such as one with an unbalanced quote, and logs a
warning. Neither stops the download or the other entries.
## How the command runs
The command is split into arguments **first**, and variables are substituted into those
arguments afterwards. The process runs without a shell.
unshackle splits the command into arguments **first**, and then substitutes the variables
into those arguments. The process runs without a shell.
This is what keeps a title like `Bob"; rm -rf ~` harmless: a value can never turn into an
extra argument or a shell operator, and you never have to think about quoting. It also
@@ -45,12 +45,12 @@ means a path containing spaces always arrives as a single argument on every OS.
first stops a value becoming a *new* argument, but a value on its own can still be a
whole argument. A bare `{title}` whose value is `--upload-file=/etc/passwd` would arrive
as an option to your script, not as text. Write `--title={title}`, not a bare `{title}`,
so a service can never forge a flag. unshackle refuses to run a command when a
so a service can never forge a flag. unshackle refuses to operate a command when a
substituted value would become an option-like token (one that starts with `-`), and logs
a warning instead.
!!! warning "No shell features, and name your interpreter"
Pipes, `&&`, `>` redirection, `~` and globs do not work, because no shell is involved.
Pipes, `&&`, `>` redirection, `~` and globs do not work, because unshackle uses no shell.
If you want them, make the shell explicit: `bash -c "..."`.
For the same reason there is no interpreter lookup. Write `python /opt/upload.py`
@@ -58,7 +58,7 @@ means a path containing spaces always arrives as a single argument on every OS.
Python or the one in its own virtual environment.
On Windows, when a **fixed** part of an argument contains a space, quote the whole
argument (`"--out=C:\My Dir\{filename}"`), not just the value. Substituted values
argument (`"--out=C:\My Dir\{filename}"`), not only the value. Substituted values
never need quoting on any OS.
On Windows, do not target a `.bat` or `.cmd` file directly. Windows runs batch files
@@ -79,7 +79,7 @@ already started.
## Variables
Metadata comes from the same naming context that produced the file's name, built per output
file. So `{quality}` and `{hdr}` always describe the file being passed. Downloading
file. So `{quality}` and `{hdr}` always give the values for the file unshackle passes in. Downloading
`-q 1080,2160 -r HDR10,SDR` produces four files, each with its own invocation and its own
values.
@@ -104,11 +104,11 @@ Every variable from your `output_template` is available here as well, including
!!! warning "Empty variables become an empty string"
`--tmdb={tmdb}` with no ID resolved hands your script `--tmdb=`, so treat an empty
string as absent. Write your script to handle every variable being empty.
string as absent. Write your script to accept an empty value for every variable.
Empty is common, and not only when data is missing:
- Nothing was resolved. `{tmdb}` is empty when no metadata provider found the title,
- No metadata provider found an ID. `{tmdb}` is empty when no metadata provider found the title,
the matching API key is missing, or the title is a movie you gave no ID for.
- The title has no such field. Movies have no `{season}` or `{episode}`, and music has
neither.
@@ -153,10 +153,9 @@ track arrived in, such as `.flac`, because a music download is not muxed.
### Tagging IDs { #tagging-ids }
`{tmdb}`, `{imdb}` and `{tvdb}` carry the IDs unshackle is tagging the file with, read at
the moment the hook is dispatched rather than snapshotted at the start of the run. An
episode's TMDB ID is resolved by the title search inside the download loop, so a hook fires
with it already filled in.
`{tmdb}`, `{imdb}` and `{tvdb}` carry the IDs unshackle tags the file with. unshackle reads
them at the moment it sends the hook, and not at the start of the run. The title search inside
the download loop finds an episode's TMDB ID, so a hook fires with it already filled in.
They hold an ID you passed with `--tmdb`, `--imdb` or `--tvdb`, plus a TMDB ID the episode
search resolved. No such search runs for a movie, so a movie hook gets them empty unless you
@@ -165,14 +164,14 @@ passed one.
### Sidecar files
`{sidecars}` holds the sidecar subtitle files written beside the output, which are the only
extra files a download leaves next to it. Attachments such as fonts are muxed into the
container and temporary artwork is deleted, so neither appears. Expect `{sidecars}` to be
extra files a download leaves next to it. unshackle muxes attachments such as fonts into the
container and deletes temporary artwork, so neither appears. Expect `{sidecars}` to be
empty unless [`subtitle.output_mode`](download.md#subtitle) is `sidecar` or `both`, which is
not the default. A music download writes no sidecars at all.
Paths are absolute and separated by a newline, because a newline is the only character that
cannot appear in a filename on any supported OS. The whole list arrives as a single
argument, so no quoting is needed in your template.
argument, so your template needs no quoting.
When one download writes several outputs (`-q 1080,720`), each output's folder gets its own
copy of the sidecar files, and each invocation lists the copies sitting beside its own
@@ -201,7 +200,7 @@ subs = [p for p in sys.argv[i].removeprefix("--subs=").split("\n") if p]
hook fires after that point. This applies to every title type. If one episode of S01
fails, the S01 hook does not fire at all.
The hook is keyed to the folder, not to the season, so one season landing in several
unshackle attaches the hook to the folder, not to the season, so one season landing in several
folders (`-q 1080,720` with a quality-dependent folder template) fires it once per folder,
each with its own `{folder}`. A movie is a group of its own: a `season` hook fires for a
movie's folder as soon as the movie finishes.
@@ -269,32 +268,33 @@ entries, which have no `--postscript` equivalent.
## The REST API
The API never accepts a post-script command. `postscript`, `post_script` and `post_scripts`
in a `POST /api/download` body are rejected with `400 INVALID_PARAMETERS`, because a command
arriving from an HTTP caller would be remote code execution.
in a `POST /api/download` body get `400 INVALID_PARAMETERS`. A command that arrives from an
HTTP caller would be remote code execution.
Scripts defined in `unshackle.yaml` do run for API jobs, music jobs included. Only the
unshackle does operate the scripts defined in `unshackle.yaml` for API jobs, music jobs
included. Only the
command itself may not cross the network.
## Logging
The command that was run is logged twice: at `DEBUG` level on the console, and as a
unshackle logs the command it operated twice: at `DEBUG` level on the console, and as a
`post_script_dispatch` entry in the
[structured JSON debug log](../../guide/troubleshooting.md#the-structured-json-debug-log)
when that log is enabled.
when that log is on.
Both are masked the same way, honouring [`redact_paths`](misc.md), which is on by default.
unshackle masks both the same way, and honours [`redact_paths`](misc.md), which is on by default.
Debug logs get shared in issue reports, and a hook command line is mostly paths. URL
credentials and `token=` / `api_key=` query parameters are also masked.
!!! warning "Do not hardcode secrets in the command"
An arbitrary secret in the command line, such as `-H "Authorization: Bearer <key>"`,
cannot be recognised and is logged verbatim. Read the secret from a file or an
unshackle cannot recognise an arbitrary secret in the command line, such as
`-H "Authorization: Bearer <key>"`, and logs it verbatim. Read the secret from a file or an
environment variable inside your script rather than putting it in the `command`.
## Limitations
- No `success` hooks run under `--no-mux` or `--skip-dl`, because neither writes a muxed
- No `success` hook operates under `--no-mux` or `--skip-dl`, because neither writes a muxed
output. A `failure` hook still runs under `--no-mux` if the download itself fails.
- `failure` hooks only run in `file` mode.
- `failure` hooks only operate in `file` mode.
- There is no webhook sender. Call `curl` from a script instead.
- Script output is not captured. Redirect it inside your own script if you want a log.
+31 -30
View File
@@ -7,19 +7,19 @@
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.
- **`proxy_map`**: remaps the region query you passed to `-p/--proxy` for this service.
A config key is that query, or `"provider:query"` when the flag also named a proxy provider. The value is
the query asked of the proxy provider instead. It has no effect when you give `-p` 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`.
Individual services may read any additional keys they define. unshackle hands the merged result
(the service's own `config.yaml` with this block layered on top) 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
rather than in the service's own `config.yaml`, which holds shared defaults. See
[creating a service](../../dev/creating-a-service.md).
```yaml
@@ -38,10 +38,10 @@ services:
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.
name to either of those. With the dict form, `-p/--profile` selects the entry, and unshackle
uses the `default` config key when you give no profile, or when the named profile is missing.
unshackle parses these into `Credential` objects. It also uses the credential's SHA-1 as an
account hash for cache keys.
```yaml
credentials:
@@ -52,22 +52,22 @@ credentials:
```
!!! tip "Cookies vs credentials"
Cookies are stored as files under [`directories.cookies`](directories.md), not in this key.
unshackle stores cookies as files under [`directories.cookies`](directories.md), not in this config 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.
Settings for extracting cookies directly from a local Firefox profile. A service block must give
`hosts`, a list of cookie hostnames. unshackle ignores an entry shorter than 3 characters, to
prevent dumping the whole cookie store. With an optional `local_storage` boolean, unshackle also
pulls 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
directory, so the copy holds the writes Firefox has not yet flushed to the main DB. Extraction
fails only if Firefox holds an exclusive write lock at the instant of the copy. The live
profile is not modified.
@@ -81,21 +81,21 @@ keep auth tokens in localStorage rather than in HTTP cookies need. Extraction is
- **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
named by you (pick it with `--server`, or do not write that flag when you configure only one) and gives
the server's `url` (required), an optional `api_key`, an optional `auth_headers` list, an
optional `server_cdm` boolean, and an optional `services` sub-dict of per-service local
overrides such as `title_map`.
`auth_headers` lists extra header names to send the API key in, tried before the defaults
`X-Secret-Key` and `X-Api-Key`, which are always appended as fallbacks. unshackle sends the
first name; if the server answers `401`, it retries the same request with the next name, and
keeps the one that works for the rest of the session. Names you list keep your spelling and are
not repeated in the fallbacks, so `auth_headers: ["Authorization", "x-secret-key"]` is tried as
`Authorization`, `x-secret-key`, `X-Api-Key`.
first name. If the server answers `401`, it retries the same request with the next name, and
keeps the one that works for the rest of the HTTP session. Names you give keep your spelling and
are not repeated in the fallbacks, so unshackle tries
`auth_headers: ["Authorization", "x-secret-key"]` as `Authorization`, `x-secret-key`, `X-Api-Key`.
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. Each synthetic command carries the server-side service's options and
commands that operate against it, falling back to the tags in that `services` sub-dict when
unshackle cannot fetch the list. Each synthetic command carries the server-side service's options and
documentation, so `unshackle dl --remote <TAG> -h` shows the same help text as it does on the
server. See [remote sessions](../../dev/rest-api/remote-sessions.md) for the
full setup.
@@ -105,7 +105,7 @@ full setup.
- **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.
is the [REST API](../../dev/rest-api/index.md) section. These are the config keys.
| Sub-key | Type | Default | Description |
|---------|------|---------|-------------|
@@ -123,15 +123,16 @@ is the [REST API](../../dev/rest-api/index.md) section; these are the config key
| `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`,
Each entry under `users` uses that user's API key as its name, and can 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.
`username` used as the log label for that API key (defaults to a truncated form of the API key).
A user with no `playready_devices` config key gets no PlayReady access at all, not the global
list.
`server_cdm` decides whether the server runs the CDM licensing for that key. It is `false`
`server_cdm` decides whether the server runs the CDM licensing for that API key. It is `false`
unless the entry sets it, so a remote client configured with `server_cdm: true` is told to
license with its own local CDM instead, and a client that asks anyway gets a `FORBIDDEN`
error. Because a download job always licenses with the server's CDM, a key without
error. Because a download job always licenses with the server's CDM, an API key without
`server_cdm` also cannot submit or retry `/api/download` jobs. Keys that have no `users`
entry, such as `api_secret`, keep server CDM access.
+6 -6
View File
@@ -6,9 +6,9 @@ The full guide is at [Vaults](../../guide/vaults.md). Two keys configure them.
- **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.
An ordered list of key-vault backends. unshackle queries them in order and reuses the
content keys instead of licensing them again. Each entry needs a `type` (the backend module
name) and a `name`, plus backend-specific keys.
| `type` | Purpose | Required keys | Notes |
|--------|---------|--------------|-------|
@@ -32,12 +32,12 @@ key_vaults:
```
!!! note "Per-entry options"
- `no_push: true` makes a vault read-only (keys are fetched but never written to it).
- `no_push: true` makes a vault read-only (unshackle fetches content keys from it but never writes 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.
- unshackle treats an all-zero content key (32 zeros) as "no key" everywhere and never
stores it.
## `vault_timeout`