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
+41 -41
View File
@@ -1,6 +1,6 @@
# CLI Reference
A complete reference for every `unshackle` command, subcommand, and flag. Commands are grouped by purpose:
A complete reference for every `unshackle` command, subcommand, and flag. This page groups the commands by purpose:
- **[`dl`](#dl)**: the download pipeline (the command you use most).
- **[`search`](#search)**: find titles on a service.
@@ -8,11 +8,11 @@ A complete reference for every `unshackle` command, subcommand, and flag. Comman
- **[`cfg`](#cfg)**, **[`env`](#env)**: manage configuration and the environment.
- **[`kv`](#kv)**: Key Vault operations.
- **[`wvd`](#wvd)**, **[`prd`](#prd)**: Widevine / PlayReady device management.
- **[`serve`](#serve)**: run the local CDM + REST API server.
- **[`serve`](#serve)**: operate the local CDM and REST API server.
- **[`util`](#util)**: helper media utilities.
!!! tip "Getting help on any command"
Every command accepts `-h`, `--help`, or `-?`. For example `unshackle dl --help` or `unshackle wvd new --help`. Service-specific arguments for `dl` and `search` (such as a title ID or search query) are defined by each service, so check `unshackle dl SERVICE --help` for those.
Every command accepts `-h`, `--help`, or `-?`. For example `unshackle dl --help` or `unshackle wvd new --help`. Each service defines its own arguments for `dl` and `search` (such as a title ID or a query), so examine `unshackle dl SERVICE --help` for those.
---
@@ -24,7 +24,7 @@ unshackle [OPTIONS] COMMAND [ARGS]...
> unshackle: Modular Movie, TV, and Music Archival Software.
Every invocation prints a banner and, when `update_checks` is enabled in your config, checks for a newer release.
Every invocation prints a banner and, when you enable `update_checks` in your config, checks for a newer release.
| Option | Description |
|---|---|
@@ -45,7 +45,7 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
```
- `OPTIONS`: every flag below, parsed at the `dl` level.
- `SERVICE`: a service tag (e.g. `EXAMPLE1`, `EXAMPLE2`). Tags are case-insensitive and honour each service's aliases (`example+`, `EXAMPLE2`, etc. all resolve to the same tag).
- `SERVICE`: a service tag (for example `EXAMPLE1` or `EXAMPLE2`). Tags are case-insensitive and honour each service's aliases (`example+`, `EXAMPLE2`, and the other alias forms all give the same tag).
- `SERVICE ARGS`: the title, URL, or ID, plus any service-specific options. These belong to the service, not to `dl`.
!!! example "Typical downloads"
@@ -61,7 +61,7 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
```
!!! note "Config-driven defaults"
Any `dl` flag can be given a default under the `dl:` section of `unshackle.yaml`, and per-service under `services.<TAG>.dl`. Explicit command-line values (and environment values) always win over both; service defaults only fill in options you did not set. See the [configuration file](../getting-started/configuration-file.md) guide.
You can give any `dl` flag a default under the `dl:` section of `unshackle.yaml`, and per-service under `services.<TAG>.dl`. Explicit command-line values (and environment values) always win over both. Service defaults only fill in options you did not set. See the [configuration file](../getting-started/configuration-file.md) guide.
### Quality, codec, bitrate & range
@@ -106,7 +106,7 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
[`subtitle.language_priority`](../reference/configuration/download.md#subtitle) in your
configuration file.
The special language tokens `orig`, `all`, and `best` are honoured everywhere a language is expected.
unshackle honours the special language tokens `orig`, `all`, and `best` everywhere it expects a language.
### Title & episode selection
@@ -160,21 +160,21 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
| `--tvdb-order` | Renumber episodes to a TVDB season order: `official` (aired), `dvd`, `absolute`, `alternate`, or `regional`. Needs `tvdb_api_key`. |
!!! note "One ID at a time"
`--tmdb`, `--imdb` and `--tvdb` cannot be combined. Give one and unshackle resolves the
others from it, writing all three to the tags. `--anilist` is outside that rule and pairs
You cannot combine `--tmdb`, `--imdb` and `--tvdb`. Give one and unshackle finds the
others from it, and writes all three to the tags. `--anilist` is outside that rule and pairs
with one of them, which is how you attach a western ID to an anime title.
An ID with no provider that can resolve it, such as `--tmdb` with no `tmdb_api_key`, fails
before the download starts. `--anilist` needs no key, so it never fails this check.
An ID with no metadata provider that can find it, such as `--tmdb` with no `tmdb_api_key`,
fails before the download starts. `--anilist` needs no API key, so it never fails this check.
#### Episode ordering
A service does not always number a series the way TVDB's aired order does. Some services list
Futurama in TVDB's `alternate` (Streaming) order, for example. `--tvdb-order` works out which
A service does not always number a series the way TVDB's aired order does. Some services use
TVDB's `alternate` (Streaming) order for Futurama, for example. `--tvdb-order` works out which
order the service used, then renumbers the episodes into the order you asked for.
!!! note "Orders that do not cover the whole series"
An order can omit episodes the service carries; TVDB's `dvd` order for Futurama leaves out
the four movies. Those episodes keep their original numbering. If that would give two
An order can ignore episodes the service carries. TVDB's `dvd` order for Futurama leaves
out the four movies. Those episodes keep their original numbering. If that would give two
episodes the same season/episode slot, and so the same filename, unshackle logs an error
and keeps the service's numbering unchanged. Pick an order that covers the whole series.
@@ -212,19 +212,19 @@ order the service used, then renumbers the episodes into the order you asked for
| `--slow` | - | Inter-title delay. Bare `--slow` = 60-120s; `--slow 20-40` = custom range (minimum 20s). |
!!! warning "Some flags cannot be combined"
`--require-subs` and `--s-lang`; `--select-titles` and `--wanted`; `--worst` requires `--quality`; `--vbitrate` and `--vbitrate-range` (and the audio equivalents) are mutually exclusive.
`--require-subs` and `--s-lang` are mutually exclusive. `--select-titles` and `--wanted` are mutually exclusive. `--worst` needs `--quality`. `--vbitrate` and `--vbitrate-range` (and the audio equivalents) are mutually exclusive.
---
## `search`
Search a service for titles. Like `dl`, `search` is a group whose subcommands are the installed services, and it reuses `dl`'s authentication, cookie, and proxy machinery.
Find titles on a service. Like `dl`, `search` is a group whose subcommands are the installed services, and it reuses `dl`'s authentication, cookie, and proxy machinery.
```
unshackle search [OPTIONS] SERVICE [QUERY...]
```
The query syntax is defined per service. Results are printed as a tree of titles with their service IDs. Feed an ID straight into `dl`.
Each service defines its own query syntax. unshackle prints the results as a tree of titles with their service IDs. Feed an ID straight into `dl`.
| Option | Description |
|---|---|
@@ -249,7 +249,7 @@ Reconstruct a download (download → decrypt → mux) from an `--export` JSON fi
unshackle import EXPORT_FILE [DL_ARGS...]
```
Any `dl` options placed after the file are forwarded verbatim, so you can override quality, range, proxy, and so on.
unshackle forwards any `dl` options after the file verbatim, so you can override quality, range, proxy, and so on.
!!! example
```shell
@@ -257,13 +257,13 @@ Any `dl` options placed after the file are forwarded verbatim, so you can overri
unshackle import export.json -r HDR10 --proxy US
```
The export file must be a valid v2 export (created by a current build of unshackle via `dl --export`) and must contain a `service` tag.
The export file must be a valid v2 export from a current version of unshackle, made with `dl --export`, and must contain a `service` tag.
---
## `cfg`
Read, set, delete, or list configuration values in `unshackle.yaml` without hand-editing YAML.
Read, set, delete, or show configuration values in `unshackle.yaml` without hand-editing YAML.
```
unshackle cfg [KEY] [VALUE] [OPTIONS]
@@ -277,7 +277,7 @@ unshackle cfg [KEY] [VALUE] [OPTIONS]
| `--list` | List all set configuration values. |
!!! warning "Comments are stripped"
Writing a value through `cfg` rewrites the YAML file and **removes all comments** from it. If your config relies on comments, edit it by hand instead. Setting a value and using `--unset` together is an error.
Writing a value through `cfg` rewrites the configuration file and **removes all comments** from it. If your config relies on comments, edit it by hand instead. Setting a value and using `--unset` together is an error.
!!! example
```shell
@@ -305,7 +305,7 @@ unshackle env COMMAND [ARGS]...
unshackle env check
```
Prints a dependency table (Category / Tool / Status / Required / Purpose) showing which external tools resolve on your `PATH`, and a summary of how many required tools are installed.
Prints a dependency table (Category / Tool / Status / Required / Purpose) that shows which external tools unshackle finds on your `PATH`. It also gives a summary of how many required tools are present.
| Category | Tools |
|---|---|
@@ -316,7 +316,7 @@ Prints a dependency table (Category / Tool / Status / Required / Purpose) showin
| Player | FFplay, MPV |
| Network | HolaProxy, Caddy, Docker, git |
<small>* required; all others are optional.</small>
<small>* required. All others are optional.</small>
### `env info`
@@ -324,7 +324,7 @@ Prints a dependency table (Category / Tool / Status / Required / Purpose) showin
unshackle env info
```
Shows where the config was loaded from (or lists the candidate config locations if none was found) and prints a table of every configured directory (downloads, temp, cache, cookies, logs, exports, WVDs, PRDs, services, and more).
Shows the location unshackle loaded the configuration file from. If it found no file, it shows the candidate locations instead. It then prints a table of every configured directory (downloads, temp, cache, cookies, logs, exports, WVDs, PRDs, services, and more).
### `env theme`
@@ -332,11 +332,11 @@ Shows where the config was loaded from (or lists the candidate config locations
unshackle env theme
```
Prints a sample of every available CLI theme: colour swatches, help text with option rows, a track listing with video/audio/subtitle entries, log lines, a progress bar, and the gradient pulse bar. The active theme is marked and each theme's aliases are listed. Set your choice with the [`theme`](../reference/configuration/misc.md#appearance) config key.
Prints a sample of every available CLI theme. Each sample has colour swatches, help text with option rows, and a track listing with video, audio and subtitle entries. It also has log lines, a progress bar, and the gradient pulse bar. unshackle marks the active theme and shows each theme's aliases. Set your choice with the [`theme`](../reference/configuration/misc.md#appearance) config key.
### `env clear`
Clear an environment directory. The directory is emptied and recreated, and the number of files and bytes freed is reported.
Clear an environment directory. unshackle empties the directory, makes it again, and reports the number of files and bytes it freed.
| Command | Description |
|---|---|
@@ -357,7 +357,7 @@ Clear an environment directory. The directory is emptied and recreated, and the
## `kv`
Manage Key Vaults. Vaults are configured under `key_vaults` in `unshackle.yaml`, each with a `name`, a `type` (`sqlite`, `mysql`, `http`, `api`), and type-specific options. Service tags are normalised automatically.
Manage Key Vaults. You configure vaults under `key_vaults` in `unshackle.yaml`, each with a `name`, a `type` (`sqlite`, `mysql`, `http`, `api`), and type-specific options. unshackle normalises service tags automatically.
```
unshackle kv COMMAND [ARGS]...
@@ -365,7 +365,7 @@ unshackle kv COMMAND [ARGS]...
### `kv copy TO_VAULT FROM_VAULT...`
Copy keys from one or more source vaults into a destination vault. Rows with matching KIDs are skipped unless the existing row has no key; existing data is never altered or deleted.
Copy content keys from one or more source vaults into a destination vault. unshackle skips rows whose KIDs match, unless the existing row has no content key. unshackle never alters or deletes existing data.
| Option | Description |
|---|---|
@@ -374,15 +374,15 @@ Copy keys from one or more source vaults into a destination vault. Rows with mat
### `kv sync VAULT...`
Ensure two or more vaults hold the same set of keys, essentially a chained bidirectional copy. Requires more than one vault. Accepts the same `--service` / `--local-only` options as `copy`.
Make sure that two or more vaults hold the same set of content keys, essentially a chained bidirectional copy. More than one vault is necessary. Accepts the same `--service` / `--local-only` options as `copy`.
### `kv add FILE SERVICE VAULT...`
Add content keys to one or more vaults for a service. `FILE` contains one `KID:KEY` pair per line (32 hex : 32 hex, UTF-8). Lines that don't match are skipped.
Add content keys to one or more vaults for a service. `FILE` contains one `KID:KEY` pair per line (32 hex : 32 hex, UTF-8). unshackle skips every line that does not match that form.
### `kv search KID`
Search configured vaults for a KID (32 hex characters, no dashes) and report any matching key.
Find a KID (32 hex characters, no dashes) in the configured vaults and report its content key.
| Option | Description |
|---|---|
@@ -390,11 +390,11 @@ Search configured vaults for a KID (32 hex characters, no dashes) and report any
| `-v`, `--vault` | Limit the search to a specific configured vault by name. |
!!! note
Remote vaults cannot be enumerated without a service, so pass `--service` when searching them.
unshackle cannot enumerate remote vaults without a service, so pass `--service` when you examine them.
### `kv prepare VAULT...`
Create service tables on vaults that use tables, for every installed service, where they don't already exist.
Make service tables on vaults that use tables, for every installed service, where they do not already exist.
!!! example
```shell
@@ -452,7 +452,7 @@ unshackle prd COMMAND [ARGS]...
### `prd new PATHS...`
Create a new `.prd`. Provide either a single folder containing `zgpriv.dat` (group key) and `bgroupcert.dat` (group certificate), or two file paths (group key, then group certificate).
Make a new `.prd`. Give either a single folder that contains `zgpriv.dat` (group key) and `bgroupcert.dat` (group certificate), or two file paths (group key, then group certificate).
| Option | Description |
|---|---|
@@ -464,11 +464,11 @@ Create a new `.prd`. Provide either a single folder containing `zgpriv.dat` (gro
### `prd reprovision PRD_PATH`
Reprovision an existing device by replacing its leaf certificate and keys. The device must support reprovisioning (version 3 or higher). Accepts the same `-e` / `-s` / `-o` options; defaults to overwriting the device in place.
Reprovision an existing device by replacing its leaf certificate and keys. Only a device of version 3 or higher can do reprovisioning. Accepts the same `-e` / `-s` / `-o` options. By default it overwrites the device in place.
### `prd test DEVICE`
Test a device against the Microsoft PlayReady demo server and print the returned keys.
Do a test of a device against the Microsoft PlayReady demo server and print the returned content keys.
| Option | Default | Description |
|---|---|---|
@@ -509,7 +509,7 @@ unshackle serve [OPTIONS]
WVD files in the WVDs directory and PRD files in the PRDs directory are auto-loaded. The REST API lives under `http://<host>:<port>/api/`, with Swagger UI at `/api/docs/` and a health check at `/api/health` (exempt from auth).
!!! warning "Configure `api_secret` first"
Unless you pass `--no-key`, `serve.api_secret` must be set in your config. Requests authenticate with the `X-Secret-Key` header. Running with `--no-key` disables authentication entirely and should only be used on a trusted, private network.
Unless you pass `--no-key`, `serve.api_secret` must be set in your config. Requests authenticate with the `X-Secret-Key` header. `--no-key` disables authentication entirely. Only use it on a trusted, private network.
```yaml title="unshackle.yaml"
serve:
@@ -534,7 +534,7 @@ WVD files in the WVDs directory and PRD files in the PRDs directory are auto-loa
## `util`
Various helper media utilities. When a command is given a directory, it processes every `.mkv`/`.mp4` inside it in natural (S01E01 before S01E10) order.
Various helper media utilities. When you give a command a directory, it processes every `.mkv`/`.mp4` inside it in natural (S01E01 before S01E10) order.
```
unshackle util COMMAND [ARGS]...
@@ -569,11 +569,11 @@ Losslessly set the video range flag to full or limited at the bitstream level.
### `util test PATH`
Decode an entire video with FFmpeg and report any corruptions or errors. All streams are tested by default; subtitles cannot be tested.
Decode an entire video with FFmpeg and report any corruptions or errors. By default it does a test of every track. It cannot do a test of subtitles.
| Option | Default | Description |
|---|---|---|
| `-m`, `--map` | `0` | Test specific streams via FFmpeg's `-map`, e.g. `0:v:0` or `0:a`. |
| `-m`, `--map` | `0` | Do a test of specific tracks with FFmpeg's `-map`, for example `0:v:0` or `0:a`. |
!!! example
```shell
+129 -122
View File
@@ -9,23 +9,23 @@ the download flow itself.
!!! note "Where the full flag list lives"
This page covers the flags you will use most and explains how they interact. It is
not an exhaustive enumeration. For the complete, always-current list of every option
and its exact wording, run `unshackle dl --help`. Every flag shown here can also be
and its exact wording, use `unshackle dl --help`. Every flag shown here can also be
given a default in your configuration file. See
[Configuration file](../getting-started/configuration-file.md).
## Command shape
A download is always three layers: the root CLI, the `dl` command (which is where all
the flags below are parsed), and a **service tag** subcommand that carries the title
argument.
A download is always three layers: the root CLI, the `dl` command (which is where
unshackle parses all the flags below), and a **service tag** subcommand that carries the
title argument.
```shell
unshackle dl [OPTIONS] SERVICE [SERVICE ARGUMENTS]
```
- `unshackle`: the root command.
- `dl`: the download command. **All of the options on this page are parsed here**, so
they go *before* the service tag.
- `dl`: the download command. **unshackle parses all of the options on this page here**,
so they go *before* the service tag.
- `SERVICE`: a service tag such as `EXAMPLE1`, `EXAMPLE2`, `EXAMPLE3`. Service tags are
case-insensitive and honour each service's aliases (for example `example+` resolves to
its real tag).
@@ -41,15 +41,15 @@ unshackle dl -q 1080 -v H.265 -r HDR10 --lang en EXAMPLE 'https://www.example.co
```
!!! tip "See what a service accepts"
Because the positional argument is defined by each service, run
`unshackle dl SERVICE --help` (e.g. `unshackle dl EXAMPLE --help`) to see that service's
own argument and any service-specific options.
Because each service defines its own positional argument, use
`unshackle dl SERVICE --help` (for example `unshackle dl EXAMPLE --help`) to see that
service's own argument and any service-specific options.
## The download flow
When you run a download, `dl` moves through the pipeline below. Understanding the order
helps explain why, for example, `--list` stops early and why keys are fetched even with
`--skip-dl`.
When you start a download, `dl` moves through the pipeline below. The order tells you
why, for example, `--list` stops early, and why unshackle fetches the content keys even
with `--skip-dl`.
1. **Setup**: load the DRM CDM, key vaults, proxy providers, cookies, and credentials.
2. **Authenticate**: sign in to the service with your profile's cookies/credentials.
@@ -60,7 +60,7 @@ helps explain why, for example, `--list` stops early and why keys are fetched ev
6. **Select tracks**: narrow each type down using your quality, codec, range, language,
and bitrate flags.
7. **Download**: pull segments for the selected tracks (concurrently per `--downloads`).
8. **License & decrypt**: acquire content keys (vault → CDM), decrypt each track.
8. **License & decrypt**: get the content keys (vault → CDM), decrypt each track.
9. **Post-process**: extract closed captions, convert subtitles, repack, mux with
`mkvmerge`, and move the finished file to your downloads directory.
@@ -84,8 +84,8 @@ unshackle dl -q 2160,1080,720 EXAMPLE 81234567
```
!!! note "16:9 canvas matching"
Resolution matching is by track **height** first. If no track has an exact matching
height, unshackle falls back to a 16:9-canvas match, so `-q 1080` will also match an
Resolution matching is by track **height** first. If no track height matches exactly,
unshackle falls back to a 16:9-canvas match, so `-q 1080` also selects an
anamorphic `1920×804` track (computed as `int(width × 9 / 16)`).
### Best-available and worst
@@ -94,18 +94,18 @@ By default a missing requested resolution is an error. Two flags change that:
| Flag | Behaviour |
| --- | --- |
| `--best-available` | If the requested resolution(s) aren't present, continue with the best that *is* available instead of failing. Also softens missing video/audio/subtitle languages and hybrid fallbacks. |
| `--best-available` | If the requested resolution(s) are not present, continue with the best that *is* available instead of failing. Also softens missing video/audio/subtitle languages and hybrid fallbacks. |
| `--worst` | Within the specified quality, pick the **lowest** bitrate rendition. **Requires `-q/--quality`.** |
```shell title="Never fail on a missing resolution"
unshackle dl -q 2160 --best-available EXAMPLE 0ABC123
```
## Video codec and color range
## Video codec and colour range
### Codec
`-v` / `--vcodec` selects one or more video codecs; the default is any codec. It accepts
`-v` / `--vcodec` selects one or more video codecs. The default is any codec. It accepts
either enum **names** or their **values**, comma-separated.
| Name | Value |
@@ -123,9 +123,9 @@ unshackle dl -v H.265 EXAMPLE 81234567
unshackle dl -v hevc,avc EXAMPLE 81234567
```
### Color range
### Colour range
`-r` / `--range` selects one or more color ranges; the default is `SDR`.
`-r` / `--range` selects one or more colour ranges. The default is `SDR`.
| Range | Meaning |
| --- | --- |
@@ -141,13 +141,13 @@ unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...'
```
!!! warning "HYBRID requires dovi_tool"
`-r HYBRID` produces a single hybrid stream by injecting the Dolby Vision RPU
metadata onto an HDR10/HDR10+ base layer using **dovi_tool**.
`-r HYBRID` makes a single hybrid track by injecting the Dolby Vision RPU
metadata onto an HDR10/HDR10+ base layer with **dovi_tool**.
It requires the `dovi_tool` binary, resolved from unshackle's `binaries/` folder or
your `PATH`. The normal case is a DV track plus an HDR10 or HDR10+ base. With HDR10+
and no DV, unshackle converts the HDR10+ metadata to DV instead, which also needs
`hdr10plus_tool`. With no DV and no HDR10+, the title fails.
When HDR10+ is present it is preferred over HDR10 as the base layer.
When HDR10+ is present, unshackle prefers it over HDR10 as the base layer.
To keep an HDR10+ deliverable *alongside* the hybrid, request both ranges:
`-r HYBRID,HDR10P`. `-r HYBRID` on its own muxes only the merged hybrid.
@@ -165,13 +165,13 @@ resolution/codec/range/language combination. You can constrain this.
| `-ab-range` / `--abitrate-range` | Audio bitrate range in kbps, e.g. `128-256`. |
!!! warning "Exact vs range are mutually exclusive"
`--vbitrate` cannot be combined with `--vbitrate-range`, and likewise for the audio
You cannot combine `--vbitrate` with `--vbitrate-range`, and likewise for the audio
pair. Pick one form.
### Real bitrate probing
Manifest-declared bitrates are sometimes rounded or wrong. Services often advertise a
**peak** or **nominal** bandwidth that is far from the stream's real average. This matters
**peak** or **nominal** bandwidth that is far from the track's real average. This matters
because a track's bitrate drives the track listing, the sort order, *and* the
`--vbitrate`/`--vbitrate-range` selection above. A bogus declared value therefore makes
unshackle pick the wrong track. These flags probe the actual media size to compute a true
@@ -182,26 +182,26 @@ bitrate for the top renditions, overriding the manifest value:
language). Slower than the video variant because there are more renditions to probe.
!!! note "Reading the probed numbers"
A single-file track (DASH `SegmentBase`/`BaseURL`) is measured **exactly**. A
unshackle measures a single-file track (DASH `SegmentBase`/`BaseURL`) **exactly**. A
multi-segment track (most HLS) is a **sampled estimate**, normally within a few percent
of the true value. For MPEG-TS HLS the probed figure also reads a few percent *above*
the demuxed elementary stream, because the segment bytes include container overhead.
That is the real *delivered* size, not an over-report or a bug.
!!! tip "Why not every rendition is probed"
Probing does not touch every rendition. Only the five highest declared-bitrate
renditions of each quality tier are probed in parallel (video grouped by codec and
range, audio by codec, channels, language, and descriptive flag), and unshackle
extends a group downward while a lower unprobed rendition could still outrank a
probed one. This keeps probing fast even when a service exposes dozens of renditions.
Tracks whose duration can't be determined fall back to `ffprobe`, and a probe failure
is non-fatal: the manifest value is simply left in place.
Probing does not touch every rendition. unshackle probes only the five highest
declared-bitrate renditions of each quality tier, in parallel (video grouped by codec
and range, audio by codec, channels, language, and descriptive flag). It extends a
group downward while a lower unprobed rendition could still outrank a probed one.
This keeps probing fast even when a service exposes dozens of renditions. A track
whose duration unshackle cannot find falls back to `ffprobe`. A probe failure is
non-fatal: the manifest value stays in place.
## Audio codec, channels, and Atmos
### Codec
`-a` / `--acodec` selects one or more audio codecs (comma-separated); the default is any.
`-a` / `--acodec` selects one or more audio codecs (comma-separated). The default is any.
| Name | Value | Codec |
| --- | --- | --- |
@@ -215,8 +215,8 @@ bitrate for the top renditions, overriding the manifest value:
| `ALAC` | `ALAC` | Apple Lossless |
| `FLAC` | `FLAC` | FLAC |
Names, values, and a few aliases are accepted: `eac3` and `ddp` both resolve to `EC3`,
and `vorbis` resolves to `OGG`.
unshackle accepts names, values, and a few aliases: `eac3` and `ddp` both give `EC3`,
and `vorbis` gives `OGG`.
```shell title="Prefer Dolby Digital Plus, fall back to AAC"
unshackle dl -a EC3,AAC EXAMPLE 81234567
@@ -224,9 +224,9 @@ unshackle dl -a EC3,AAC EXAMPLE 81234567
### Channels and Atmos
- `-c` / `--channels`: desired channel layout, e.g. `5.1` or `2.0`. Matching is by
- `-c` / `--channels`: desired channel layout, for example `5.1` or `2.0`. Matching is by
ceiling, so `5.1` implicitly matches a `6.0`-reported layout.
- `-naa` / `--noatmos`: exclude Dolby Atmos audio tracks from selection.
- `-naa` / `--noatmos`: exclude Atmos audio tracks from selection.
```shell title="5.1 audio, no Atmos"
unshackle dl -c 5.1 --noatmos EXAMPLE 0ABC123
@@ -250,7 +250,7 @@ unshackle dl -a AAC -ab 320 EXAMPLE 81234567
!!! tip "Two FLAC tracks in one release"
A service can offer both a CD FLAC and a hi-res FLAC. The two have the same codec, so
the bitrate is what separates them. Run `--list` to read the bitrates, then give `-ab`
the bitrate is what separates them. Use `--list` to read the bitrates, then give `-ab`
the one you want.
## Languages
@@ -264,8 +264,8 @@ unshackle dl -a AAC -ab 320 EXAMPLE 81234567
unshackle dl -l orig,en EXAMPLE 81234567
```
The special token `orig` is resolved to the title's actual original language everywhere
it is used. You can override each stream type independently:
unshackle changes the special token `orig` to the title's own original language
everywhere you use it. You can override each track type independently:
- `-vl` / `--v-lang`: language for **video only** (overrides `-l` for video). Useful
when the burned-in video language differs from the audio you want.
@@ -284,8 +284,8 @@ Put a `-` in front of a language to remove it from the selection. This works on
unshackle dl -sl all,-es EXAMPLE 81234567
```
The exclusions are subtracted from the languages the flag would otherwise select, and the
order of the values does not matter. If you give only exclusions, the flag keeps its own
unshackle subtracts the exclusions from the languages the flag would otherwise select,
and the order of the values does not matter. If you give only exclusions, the flag keeps its own
default: `-sl -es` is the same as `-sl all,-es`, and `-l -es` is the same as `-l orig,-es`.
```shell title="Only exclusions"
@@ -297,7 +297,7 @@ unshackle dl -l -es EXAMPLE 81234567
```
`-vl` and `-al` have no default of their own: they cascade to `-l`. An override that names
a language replaces `-l` for that stream type, exclusions included. An override that gives
a language replaces `-l` for that track type, exclusions included. An override that gives
only exclusions adds them to the ones from `-l`.
```shell title="Cascade"
@@ -306,13 +306,13 @@ unshackle dl -l en,-es -vl ja EXAMPLE 81234567
```
!!! note "Rules for exclusion tokens"
- `orig` is resolved per title, so `-sl -orig` drops the subtitles in the title's
original language.
- `orig` becomes the title's own original language, so `-sl -orig` drops the
subtitles in that language.
- `--exact-lang` applies to exclusions with the same matching rules as selection:
`-es` removes exactly the tracks that `es` would select. Without it, `-es` also
removes `es-419` and `es-ES`.
- Tracks with no language tag are never excluded.
- `-all` is rejected. Name the languages you do not want instead.
- unshackle rejects `-all`. Name the languages you do not want instead.
- The comma form is the documented style. `-sl -es` also works, but a shell can read
a lone `-es` as another option.
@@ -330,17 +330,17 @@ subtitle:
```
Subtitles then start with English, Spanish, and French. The languages you leave out follow
alphabetically, after the title's original language. Nothing is removed. Audio works the
same way, but the languages you leave out keep their bitrate and codec order instead of an
alphabetically, after the title's original language. unshackle removes nothing. Audio
works the same way, but the languages you leave out keep their bitrate and codec order instead of an
alphabetical one.
See [`subtitle.language_priority`](../reference/configuration/download.md#subtitle) for the details.
### Exact vs fuzzy matching
By default language matching is fuzzy: `-l en` also accepts `en-US`, `en-GB`, etc. (up
to a small distance). Pass `--exact-lang` to require an exact match: with it, `-l es-419`
matches only `es-419`, not `es-ES`.
By default language matching is fuzzy: `-l en` also accepts `en-US`, `en-GB`, and the
other regional forms (up to a small distance). Pass `--exact-lang` when only an exact
match is acceptable: with it, `-l es-419` selects only `es-419`, not `es-ES`.
!!! tip "The `all` and `best` tokens"
For audio, the tokens `all` and `best` bypass the usual one-track-per-language pick
@@ -351,8 +351,8 @@ matches only `es-419`, not `es-ES`.
### Selecting subtitle languages
`-sl` / `--s-lang` sets the wanted subtitle language(s). The default is `all`, meaning
every available subtitle language is downloaded.
`-sl` / `--s-lang` sets the wanted subtitle language(s). The default is `all`, so
unshackle downloads every available subtitle language.
```shell title="Only English and Spanish subtitles"
unshackle dl -sl en,es EXAMPLE 81234567
@@ -364,9 +364,9 @@ except Spanish, forced Spanish subtitles included.
### Requiring subtitles
`--require-subs` takes a list of languages that **must** exist. If they all exist, *all*
subtitles are downloaded; if any is missing, the title fails. This is useful for gating a
download on the presence of a specific subtitle track.
`--require-subs` takes a list of languages that **must** exist. If they all exist,
unshackle downloads *all* subtitles. If any is missing, the title fails. This is useful
when you want a download only if a specific subtitle track is present.
!!! warning "Cannot combine with `--s-lang`"
`--require-subs` and `--s-lang` are mutually exclusive. Use one or the other.
@@ -374,14 +374,14 @@ download on the presence of a specific subtitle track.
### Forced subtitles and output format
- `-fs` / `--forced-subs`: include forced subtitle tracks (signs/foreign dialogue).
Without this flag, forced subs are dropped from selection.
Without this flag, unshackle drops forced subtitle tracks from the selection.
- `-fsl` / `--forced-s-lang`: keep forced subtitles only in these languages (implies
`-fs`). Works independently of `--s-lang`, so `-sl all -fsl en` grabs every full
subtitle but only the English forced track. It accepts exclusions too: `-fsl all,-es`
keeps every forced subtitle except the Spanish one, and `-fsl -es` means the same.
- `--sub-format`: set the output subtitle format, converting only when necessary.
Accepts codec names/values and common aliases (`srt`, `vtt`, `ass`, `ssa`, `ttml`,
etc.), or the literal `original` to keep the source format.
and the other codec aliases), or the literal `original` to keep the source format.
| Value | Format |
| --- | --- |
@@ -397,15 +397,16 @@ unshackle dl --sub-format srt EXAMPLE 81234567
```
!!! note "SDH stripping happens by default"
When a subtitle track is marked SDH (for the deaf/hard-of-hearing) and there is no
plain same-language subtitle, unshackle produces a stripped, non-SDH variant
automatically. This behaviour is governed by the `subtitle.strip_sdh` config option
When a subtitle track carries the SDH flag (for the deaf/hard-of-hearing) and there
is no plain same-language subtitle, unshackle makes a stripped, non-SDH track
automatically. The `subtitle.strip_sdh` config option controls this behaviour
(default on).
## Selecting episodes and tracks
For series, several flags control which episodes are downloaded. The same flags select the
tracks of a music release. By default, **all** episodes and **all** tracks are downloaded.
For series, several flags control which episodes unshackle downloads. The same flags
select the tracks of a music release. By default, unshackle downloads **all** episodes and
**all** tracks.
### Wanted ranges
@@ -508,10 +509,11 @@ track on disc 1, which is the number the track list shows for a single-disc rele
### Daily and date-based content
Talk shows, news and sports have no official episode numbering, so unshackle names them
by air date. An episode that carries an air date is written as `Show.YYYY.MM.DD` instead of
`SxxExx`, the `{date}` token holds the ISO date, and the season folder becomes the year.
by air date. unshackle writes an episode that carries an air date as `Show.YYYY.MM.DD`
instead of `SxxExx`, the `{date}` token holds the ISO date, and the season folder becomes
the year.
A service that only carries this kind of content sets `DAILY = True` on its class, and a
A service that only carries this kind of title sets `DAILY = True` on its class, and a
service can set `air_date` on each episode itself. Add `--daily` to mark any other title
as date-based:
@@ -520,14 +522,14 @@ unshackle dl --daily --tvdb 73871 --enrich EXAMPLE 81234567
```
With `--enrich` and a TVDB ID, `--daily` fills in the air date of every episode that has
none. An air date the service already set is kept. Dates before 1970 and dates in the
future are skipped, because TVDB carries placeholder schedule dates for episodes that
none. An air date the service already set is kept. unshackle skips dates before 1970 and
dates in the future, because TVDB carries placeholder schedule dates for episodes that
have not aired. Without `--enrich` unshackle has no source to fill from, and says so.
A dated episode answers to its air date in `-w`, as well as to its `SxxExx` key. A date
A dated episode answers to its air date in `-w`, as well as to its `SxxExx` token. A date
token is a plain ISO date (`2026-08-11`). A date range uses a colon (`2026-08-01:2026-08-31`),
because the dashes in a date are part of the date. A range cannot span more than 1000 days.
Date tokens and `SxxExx` tokens can be mixed in one `-w`.
You can mix date tokens and `SxxExx` tokens in one `-w`.
### Split episodes
@@ -545,14 +547,14 @@ A season token such as `S01` covers every episode and every part, so you only ne
when you want a part on its own.
!!! warning "A part range stays inside one episode"
`S01E01.1-S01E01.3` is valid. `S01E01.1-S01E02.3` is rejected, because the parser
cannot know how many parts episode 1 has and so cannot work out where the range ends.
To span episodes, list the parts you want as separate tokens:
`S01E01.1-S01E01.3` is valid. unshackle rejects `S01E01.1-S01E02.3`, because the
parser cannot know how many parts episode 1 has and so cannot work out where the range
ends. To span episodes, write the parts you want as separate tokens:
`-w S01E01.2,S01E02.1`.
!!! note "A part of an unsplit episode selects nothing"
`-w S01E02.2` on an episode that was never split is deliberately empty: it tells you
the episode has no parts, rather than quietly handing you the whole episode. Run
the episode has no parts, rather than quietly handing you the whole episode. Use
`--list-titles` to see which episodes carry parts.
### Other selection flags
@@ -589,7 +591,7 @@ or negatively (everything but these).
Additional track-type flags:
- `-ad` / `--audio-description`: include descriptive (audio-description) tracks, which
are dropped by default.
unshackle drops by default.
- `--skip-subtitle-errors`: if a subtitle fails to download, skip it and continue rather
than aborting the whole title. Video and audio failures remain fatal.
@@ -603,7 +605,7 @@ unshackle dl -nc EXAMPLE 81234567
!!! note
You can combine an `*-only` flag with `no-*` flags to fine-tune: `*-only` chooses the
starting set of categories, then `no-*` subtracts from it. Attachments (e.g. fonts)
starting set of categories, then `no-*` subtracts from it. Attachments (for example fonts)
are always kept.
## Listing and dry runs
@@ -624,9 +626,9 @@ unshackle dl -q 1080 -v H.265 -r HDR10 --list EXAMPLE 81234567
### Output location and folders
- `-o` / `--output`: override the output directory for this run (otherwise the
configured downloads directory is used).
- `--no-folder`: do not create a per-show folder for TV downloads.
- `-o` / `--output`: override the output directory for this run (otherwise unshackle
uses the configured downloads directory).
- `--no-folder`: do not make a per-show folder for TV downloads.
- `--no-source`: remove the service source tag from the filename and path.
```shell title="Send this download somewhere specific"
@@ -635,8 +637,8 @@ unshackle dl -o ~/Videos/incoming EXAMPLE 81234567
### Muxing behaviour
By default, the tracks of a movie or an episode are muxed into a single Matroska (`.mkv`)
file with `mkvmerge`. These flags change how the output is assembled:
By default, `mkvmerge` muxes the tracks of a movie or an episode into a single Matroska
(`.mkv`) file. These flags change how unshackle assembles the output:
| Flag | Behaviour | Default source |
| --- | --- | --- |
@@ -645,7 +647,7 @@ file with `mkvmerge`. These flags change how the output is assembled:
| `--merge-video` | Mux video tracks that share a height, range, and codec into one file, so only language varies inside a file. | config `muxing.merge_video` (off) |
!!! note "Music is never muxed"
A music track skips the muxer. unshackle keeps the container that the service
A song skips the muxer. unshackle keeps the container that the service
delivered, such as `.flac`, `.m4a`, or `.mp3`, and writes the metadata into that file.
Read [Music output files](output-and-naming.md#music-output-files).
@@ -653,14 +655,14 @@ file with `mkvmerge`. These flags change how the output is assembled:
`--postscript "<command>"` runs your own command once per output file, with unshackle's
metadata substituted into `{variable}` placeholders. It is repeatable, and it replaces the
`post_scripts` config for that run. No `success` hook runs under `--no-mux`, because that
run writes no muxed output. A `failure` hook still runs if the download itself fails.
`post_scripts` config for that run. No `success` hook operates under `--no-mux`, because
that run writes no muxed output. A `failure` hook still operates if the download fails.
```shell title="Hand each finished file to an uploader"
unshackle dl --postscript "python /opt/upload.py {filepath} --service={service}" EXAMPLE 81234567
```
For hooks that persist across runs, for the `season` and `run` modes, for `failure` hooks
For hooks that continue across runs, for the `season` and `run` modes, for `failure` hooks
and for the full variable list, see
[Post-download scripts](../reference/configuration/post-scripts.md).
@@ -688,8 +690,8 @@ Two related flags:
- `--no-proxy`: force-disable all proxy use for this run.
- `--no-proxy-download`: bypass the proxy for **all downloads**. The manifest,
license, and authentication requests still go through the proxy. This is useful when the
proxy is only needed to satisfy geo-checks, not to move the bulk of the data.
license, and authentication requests still go through the proxy. This is useful when you
need the proxy only to satisfy geo-checks, not to move the bulk of the data.
## Performance and caching
@@ -715,10 +717,11 @@ unshackle dl -w S01 --slow 30-60 EXAMPLE 81234567
## Keys, vaults, and export
By default, unshackle checks your **key vaults** first and only asks a **CDM** to license
a key when the vault misses. You can force one side or the other:
a content key when the vault misses. You can force one side or the other:
- `--cdm-only`: only use the CDM (skip vaults).
- `--vaults-only`: only use key vaults (never license via the CDM); a missing key fails.
- `--vaults-only`: only use key vaults and never license through the CDM. A missing
content key fails.
```shell title="Fetch keys only, no download"
unshackle dl --skip-dl EXAMPLE 81234567
@@ -733,8 +736,8 @@ unshackle dl --skip-dl --export EXAMPLE 81234567
```
!!! note "Region is recorded only with a proxy"
When `--proxy` is used, the export records the region so an import can reproduce the
correct geofence. Without a proxy, no region is stored.
When you use `--proxy`, the export records the region so an import can reproduce the
correct geofence. Without a proxy, the export stores no region.
!!! warning "Some DASH and Smooth exports need a title language"
An import re-fetches the DASH or ISM manifest and parses it again. Most manifests label
@@ -749,7 +752,7 @@ unshackle looks up metadata automatically, but you can override the identifiers
tagging and naming. An ID you give is authoritative: unshackle looks it up directly instead of
searching by title, and the ID stays in the tags. An ID only settles *which* title this is.
To also take that source's title, year and original language, add `--enrich`. Give at most one
of `--tmdb`, `--imdb` and `--tvdb`; unshackle works the rest out from it. To stop the automatic
of `--tmdb`, `--imdb` and `--tvdb`, and unshackle works the rest out from it. To stop the automatic
lookups but keep the IDs you give, set
[`disable_metadata`](../reference/configuration/misc.md#external-api-keys) in your config.
@@ -772,8 +775,9 @@ unshackle dl --imdb tt1375666 --enrich EXAMPLE 81234567
### What each metadata provider supplies
Providers differ in what they answer with, so the fields `--enrich` can replace depend on
which ID you gave. Every provider except `imdb` only runs when its key is configured:
Metadata providers differ in what they answer with, so the fields `--enrich` can replace
depend on which ID you gave. Every metadata provider except `imdb` and `anilist` only
operates when you set its config key:
| Provider | Config key | Title and year | Original language | External IDs it returns |
| --- | --- | --- | --- | --- |
@@ -788,11 +792,11 @@ Whatever the tag looks like, unshackle normalises it before use, so `ko`, `kor`
all end up as the same language.
!!! note "AniList only answers for anime"
It is last in the default provider order and returns nothing for a title that is not
anime. The only cost is one search that misses. Put `anilist` earlier in
It is last in the default metadata provider order and returns nothing for a title
that is not anime. The only cost is one search that misses. Put `anilist` earlier in
[`metadata_providers`](../reference/configuration/misc.md#external-api-keys) if you mostly
download anime. Which of the three AniList title variants is used is set by
[`anilist_title_language`](../reference/configuration/misc.md#external-api-keys).
download anime. [`anilist_title_language`](../reference/configuration/misc.md#external-api-keys)
sets which of the three AniList title variants unshackle uses.
A service can also mark its titles as anime. For those titles unshackle tries AniList
first. When AniList has no match, it falls through to the normal order.
@@ -801,31 +805,33 @@ Each ID goes to the providers that read that kind of ID, in
[`metadata_providers`](../reference/configuration/misc.md#external-api-keys) order:
- `--tmdb` reads TMDB.
- `--imdb` reads IMDb, then falls back to OMDb. IMDb needs no key, so this path works out of
the box, and an OMDb key only matters when IMDb has no answer.
- `--imdb` reads IMDb, then falls back to OMDb. IMDb needs no API key, so this path works
out of the box, and an OMDb API key only matters when IMDb has no answer.
- `--tvdb` reads TVDB. It needs `tvdb_api_key`.
- `--anilist` reads AniList, which needs no key. It supplies the title, the year and the
- `--anilist` reads AniList, which needs no API key. It supplies the title, the year and the
original language, and writes an AniList tag. It knows no TMDB, IMDB or TVDB ID, so pair it
with `--tmdb`, `--imdb` or `--tvdb` when you want those tags as well.
Give **one** of `--tmdb`, `--imdb` and `--tvdb`. They cannot be combined, because one ID does
the job on its own: unshackle resolves the other two from it and writes all three to the tags.
Give **one** of `--tmdb`, `--imdb` and `--tvdb`. You cannot combine them, because one ID
does the job on its own: unshackle finds the other two from it and writes all three to the
tags.
Passing two is an error rather than a silent choice between them. `--anilist` is the exception
and still pairs with one of the three, which is how you tag an anime title with a western ID.
The ID you give must have a provider that can actually resolve it, so unshackle checks that
before it downloads anything. `--tmdb` without `tmdb_api_key`, or an ID whose providers your
`metadata_providers` list leaves out, fails immediately with a message naming what to set.
`--imdb` needs no key, since the `imdb` provider is keyless and in the default order.
The ID you give must have a metadata provider that can find it, so unshackle makes sure of
that before it downloads anything. `--tmdb` without `tmdb_api_key`, or an ID whose metadata
providers your `metadata_providers` list leaves out, fails immediately with a message naming
what to set. `--imdb` needs no API key, because the `imdb` metadata provider is keyless
and in the default order.
With `--enrich`, the title, year and original language are all replaced with the external
source's values, whether or not the service already filled them in. A field the source does
not answer with is left alone, and unshackle logs which fields those were, so a provider with
a thin record cannot blank out what the service told you.
not answer with is left alone, and unshackle logs which fields those were, so a metadata provider
with a thin record cannot blank out what the service told you.
`--enrich` also fills in the absolute episode number of each episode from TVDB's absolute
order, for any series that has one and where the service did not supply it. Anime is the
usual beneficiary, but no part of this is limited to anime. This only adds the
usual beneficiary, but nothing here applies only to anime. This only adds the
[`{absolute}`](../reference/configuration/output.md#output_template) naming variable. The
season and episode numbers are never changed.
@@ -836,12 +842,13 @@ With `--daily`, `--enrich` also fills in the air date of each episode from TVDB.
Track selection reads the original language, so replacing it changes which audio is
treated as the original. That is the point when a service mislabels it, but it means a
wrong ID can pick the wrong audio track and not only write a wrong name. Without
`--enrich` the service's own value is kept and track selection is untouched.
`--enrich`, unshackle keeps the service's own value and does not change track
selection.
### Episode ordering
A service does not always number a series the way TVDB's aired order does. Some services list
Futurama in TVDB's `alternate` (Streaming) order, for example, while TVDB's `official` order
A service does not always number a series the way TVDB's aired order does. Some services use
TVDB's `alternate` (Streaming) order for Futurama, for example, while TVDB's `official` order
holds back four season-one episodes to the start of season two. `--tvdb-order` works out which
order the service used, then renumbers the episodes into the order you asked for:
@@ -853,26 +860,26 @@ Available orders are `official` (aired), `dvd`, `absolute`, `alternate`, and `re
`tvdb_order` in your config to apply one by default.
!!! warning "Orders that do not cover the whole series"
An order can leave out episodes the service carries. TVDB's `dvd` order does not list
Futurama's four movies. Those episodes keep their original numbering. If that would give
An order can leave out episodes the service carries. TVDB's `dvd` order does not
contain Futurama's four movies. Those episodes keep their original numbering. If that would give
two episodes the same season/episode slot, and so the same filename, unshackle logs an
error and leaves the numbering untouched. Pick an order that covers the whole series.
## Configuration defaults
Every flag on this page can be set as a default in your configuration file so you don't
have to type it each time, and defaults can be scoped per service. See
You can set every flag on this page as a default in your configuration file so you do not
have to type it each time, and you can scope defaults per service. See
[Configuration file](../getting-started/configuration-file.md) for the `dl:` block and
per-service overrides.
!!! tip "Explicit flags always win"
A value you pass on the command line (or via an environment variable) always takes
A value you pass on the command line (or through an environment variable) always takes
precedence over both the global `dl:` defaults and any per-service `dl:` overrides.
Config only fills in options you didn't set explicitly.
Config only fills in options you did not set explicitly.
## Quick reference
A condensed lookup of the most-used options. Run `unshackle dl --help` for the complete,
A condensed lookup of the most-used options. Use `unshackle dl --help` for the complete,
authoritative list.
| Flag | Short | Meaning |
@@ -880,7 +887,7 @@ authoritative list.
| `--quality` | `-q` | Target resolution(s), e.g. `1080,720`. |
| `--vcodec` | `-v` | Video codec(s). |
| `--acodec` | `-a` | Audio codec(s). |
| `--range` | `-r` | Color range(s); default `SDR`. |
| `--range` | `-r` | Colour range(s). Default `SDR`. |
| `--channels` | `-c` | Audio channel layout. |
| `--noatmos` | `-naa` | Exclude Atmos audio. |
| `--lang` | `-l` | Video + audio language(s); default `orig`. `-` excludes, e.g. `all,-es`. |
+88 -79
View File
@@ -1,16 +1,16 @@
# DRM & CDM Setup
Most commercial streaming content is protected by a Digital Rights Management (DRM)
system. To download and play it back, unshackle needs to obtain the **content keys**
A Digital Rights Management (DRM) system protects most commercial streaming titles.
To download a title and play it back, unshackle needs to get the **content keys**
for the encrypted tracks and then decrypt them. This page explains the DRM systems
unshackle supports, how to provide a device so it can talk to a license server, the
unshackle supports, how to install a device so it can talk to a license server, the
difference between local and remote CDMs, and how to choose a decryption backend.
!!! note "Who this page is for"
This is a practical configuration guide for end users of the unshackle CLI.
A few sections are marked **Developer**. They describe internal behaviour that is
useful when writing a service, but you do not need them to download protected
content.
A few sections have a **Developer** mark. They give internal behaviour that is
useful when you write a service, but you do not need them to download a protected
title.
## Key concepts
@@ -18,8 +18,8 @@ Before configuring anything, it helps to know the vocabulary. unshackle draws a
line between two things:
- A **CDM** (Content Decryption Module) is the trusted "black box" that turns a
*license challenge* into *content keys*. It is backed by a **device**, a file
containing the cryptographic identity of a real client (a phone, a browser, etc.).
*license challenge* into *content keys*. A **device** backs it: a file that carries
the cryptographic identity of a real client, such as a phone or a browser.
- A **DRM system** is the wrapper around a CDM that parses the protection metadata,
drives the challenge → license → keys handshake, and finally invokes an external
tool to physically decrypt the downloaded file.
@@ -42,7 +42,7 @@ then decrypts the file with those keys.
## Supported DRM systems
unshackle implements five DRM systems. Only the first two need a device file and a
license-server handshake; the rest are special cases.
license-server handshake. The rest are special cases.
| System | Device file | License flow | Decryption |
|--------|-------------|--------------|------------|
@@ -54,10 +54,10 @@ license-server handshake; the rest are special cases.
!!! warning "Two different ClearKeys"
"ClearKey (HLS)" and "ClearKey (CENC)" are entirely different systems that happen
to share a name. The HLS variant is the AES-128 key referenced by an `EXT-X-KEY`
tag and is decrypted in-process. The CENC variant is the W3C EME
`org.w3.clearkey` scheme, delivers keys as a JWK Set, and is decrypted through
shaka-packager or mp4decrypt just like Widevine. Neither involves a device file.
to share a name. The HLS variant is the AES-128 content key that an `EXT-X-KEY`
tag references, and unshackle decrypts it in-process. The CENC variant is the W3C
EME `org.w3.clearkey` scheme. It delivers keys as a JWK Set, and shaka-packager or
mp4decrypt decrypts it like Widevine. Neither involves a device file.
### Widevine vs PlayReady
@@ -65,20 +65,20 @@ These are the two "real" DRMs, and in unshackle they behave almost identically:
same PSSH/KID handling, the same decryption backends, and the same key-caching flow.
The differences that matter to you are:
- **Device file**: Widevine uses a `.wvd`; PlayReady uses a `.prd`.
- **Device file**: Widevine uses a `.wvd`. PlayReady uses a `.prd`.
- **Availability**: some services offer only one system, some offer both. When a
manifest carries both, unshackle picks the appropriate CDM for each track (see
[Widevine and PlayReady on the same title](#widevine-and-playready-on-the-same-title)).
To download a Widevine-protected title you need a `.wvd` device; for PlayReady you
To download a Widevine-protected title you need a `.wvd` device. For PlayReady you
need a `.prd` device. You can hold both, and unshackle automatically selects the
appropriate one for each track.
!!! info "Obtaining device files"
A `.wvd` or `.prd` encodes the private keys of a real client device. unshackle
does **not** ship with one and cannot generate the underlying secret material for
does **not** ship with one and cannot make the underlying secret material for
you. You must supply your own. The `wvd` and `prd` commands below manage and
provision device files once you have the raw key material.
provision device files once you have the raw cryptographic material.
## Providing a device with the `wvd` and `prd` commands
@@ -107,8 +107,8 @@ The `wvd` command group manages `.wvd` files.
$ unshackle wvd add ./nexus_6p.wvd
```
The file is validated and moved into the WVDs directory. You can add several at
once by passing multiple paths.
unshackle validates the file and moves it into the WVDs directory. You can add
several at once by passing multiple paths.
=== "Inspect a device"
@@ -117,8 +117,8 @@ The `wvd` command group manages `.wvd` files.
```
Prints the System ID, Security Level, Type, Flags, and whether a Private Key,
Client ID, and VMP are present. A bare name (no extension) is resolved inside the
WVDs directory; a path with an extension is used as-is.
Client ID, and VMP are present. unshackle finds a bare name (no extension) inside
the WVDs directory. It uses a path with an extension as-is.
=== "Create from raw material"
@@ -166,7 +166,7 @@ The `prd` command group creates and maintains `.prd` files.
$ unshackle prd new ./device_folder/
```
A fresh leaf certificate is generated and the device is written to the PRDs
unshackle makes a fresh leaf certificate and writes the device to the PRDs
directory (or to `-o/--output`). Use `-e/--encryption_key` and `-s/--signing_key`
to supply your own ECC keys instead of generating them.
@@ -177,8 +177,8 @@ The `prd` command group creates and maintains `.prd` files.
```
Regenerates the leaf certificate and encryption/signing keys of an existing
device in place. The device must support reprovisioning (a group key must be
present, i.e. a version 3 or higher device).
device in place. The device must be able to reprovision (a group key must be
present, that is, a version 3 or higher device).
=== "Test against Microsoft's demo server"
@@ -188,12 +188,12 @@ The `prd` command group creates and maintains `.prd` files.
Runs a full challenge/license/keys cycle against Microsoft's public PlayReady
test server and prints the returned keys, confirming the device works.
`-sl/--security-level` accepts `150`, `2000`, or `3000` (default `2000`);
`-sl/--security-level` accepts `150`, `2000`, or `3000` (default `2000`).
`-c/--ckt` accepts `aesctr` or `aescbc`.
## Selecting which device to use: the `cdm` config
Having a device on disk is not enough; you must tell unshackle which device to use
A device on disk is not enough. You must tell unshackle which device to use
for which service. That is the job of the `cdm` section, which maps a service tag to
a device name (the file's name without extension). A `default` entry acts as the
fallback:
@@ -221,8 +221,9 @@ profile.
=== "By quality"
Keys are matched against the video height. You can use exact heights or
comparisons (`>=1080`, `>720`, `<=576`, `<480`). More specific matches win.
unshackle compares each config key against the video height. You can use exact
heights or comparisons (`>=1080`, `>720`, `<=576`, `<480`). The most specific one
wins.
```yaml
cdm:
@@ -246,7 +247,8 @@ profile.
=== "By profile"
Keys are matched against the active profile, with a `default` fallback.
unshackle compares each config key against the active profile, with a `default`
fallback.
```yaml
cdm:
@@ -257,15 +259,15 @@ profile.
## Local vs remote CDMs
A CDM can run in two places:
A CDM can operate in two places:
- A **local CDM** loads a `.wvd` or `.prd` device from your machine and performs the
challenge/license/keys handshake locally. This is what the `cdm` mapping above
resolves to by default.
- A **remote CDM** delegates the CDM operations to an HTTP API. Your device (and its
secrets) live on the remote server; unshackle sends it PSSH/challenge data and
secrets) live on the remote CDM host. unshackle sends it PSSH/challenge data and
receives keys back. This is useful for sharing a high-security-level device, using
a hosted key service, or keeping device secrets off your download machine.
a hosted content key service, or keeping device secrets off your download machine.
The device name in your `cdm` mapping resolves to a remote CDM whenever it matches the
`name` of an entry in the `remote_cdm` list. Otherwise it falls back to a local
@@ -273,13 +275,13 @@ The device name in your `cdm` mapping resolves to a remote CDM whenever it match
## Remote CDMs
Remote CDMs are declared as a list under `remote_cdm`. Each entry has a `name` (which
You declare remote CDMs as a list under `remote_cdm`. Each entry has a `name` (which
you then reference from the `cdm` mapping) and a shape that depends on its `type`.
There are four kinds.
### Widevine remote CDM (pywidevine serve)
With no `type` field and a non-PlayReady device type, the entry is treated as a
With no `type` field and a non-PlayReady device type, unshackle treats the entry as a
standard pywidevine "serve" endpoint:
```yaml title="unshackle.yaml"
@@ -296,13 +298,14 @@ cdm:
default: my_wv_remote
```
`security_level` defaults to `3000` if omitted. Field names are read
case-insensitively, so `Device Type`/`device_type`, `System ID`/`system_id`, etc. are
all accepted.
`security_level` defaults to `3000` if omitted. unshackle reads field names
case-insensitively, so `Device Type`/`device_type`, `System ID`/`system_id`, and the
other field names are all accepted.
### PlayReady remote CDM
If the device type is `PLAYREADY`, the entry is routed to the pyplayready remote CDM:
If the device type is `PLAYREADY`, unshackle routes the entry to the pyplayready
remote CDM:
```yaml title="unshackle.yaml"
remote_cdm:
@@ -337,11 +340,11 @@ remote_cdm:
decrypt_labs_api_key: your-decrypt-labs-key
```
The API key is sent as the `decrypt-labs-api-key` header. If you omit `secret` on the
entry, unshackle falls back to the global `decrypt_labs_api_key` value; if neither is
present it raises an error. The `host` defaults to
unshackle sends the API key as the `decrypt-labs-api-key` header. If you do not write
`secret` on the entry, unshackle falls back to the global `decrypt_labs_api_key` value.
If neither is present it raises an error. The `host` defaults to
`https://keyxtractor.decryptlabs.com`. PlayReady devices (`SL2`/`SL3`) default to
security level `2000`/`3000` respectively; Widevine devices default to
security level `2000`/`3000` respectively. Widevine devices default to
`system_id: 26830`, `security_level: 3`.
### Custom API (`custom_api`)
@@ -373,25 +376,30 @@ remote_cdm:
The `custom_api` type supports much more: request/response field remapping,
transforms (base64/hex/JSON encoding, `kid:key` parsing), conditional parameters,
success/error detection, and vault-backed caching. Its full schema is aimed at
developers integrating a bespoke key server; see the developer note below.
success/error detection, and vault-backed caching. Its full schema is for developers
who integrate a bespoke CDM API. See the developer note below.
!!! note "Developer: full `custom_api` schema"
A `custom_api` entry accepts these sub-objects: `device` (`name`, `type`,
`system_id`, `security_level`); `auth` (`type` of `header`/`bearer`/`basic` with
the matching credential fields, plus `custom_headers`); `endpoints`
(`get_request` and `decrypt_response`, each `{path, method, timeout}`);
per-endpoint `request_mapping` (`param_names`, `static_params`,
`conditional_params`, `transforms`, `nested_params`, `exclude_params`) and
`response_mapping` (`fields`, `transforms`, `response_types`,
`success_conditions`, `error_fields`, `key_fields`); `caching` (`enabled`,
`use_vaults`, `check_cached_first`); and `legacy`/`timeout`. PlayReady mode is
inferred when `device.type == PLAYREADY` or the device name is `SL2`/`SL3`.
A `custom_api` entry accepts these sub-objects:
- `device` (`name`, `type`, `system_id`, `security_level`)
- `auth` (`type` of `header`/`bearer`/`basic` with the matching credential fields,
plus `custom_headers`)
- `endpoints` (`get_request` and `decrypt_response`, each `{path, method, timeout}`)
- per-endpoint `request_mapping` (`param_names`, `static_params`,
`conditional_params`, `transforms`, `nested_params`, `exclude_params`) and
`response_mapping` (`fields`, `transforms`, `response_types`,
`success_conditions`, `error_fields`, `key_fields`)
- `caching` (`enabled`, `use_vaults`, `check_cached_first`)
- `legacy` and `timeout`
unshackle infers PlayReady mode when `device.type == PLAYREADY` or the device name
is `SL2`/`SL3`.
## Decryption backends
Once the content keys are known, the encrypted file (for Widevine, PlayReady, and
ClearKey-CENC) is decrypted by one of two external tools:
Once unshackle knows the content keys, one of two external tools decrypts the
encrypted file (for Widevine, PlayReady, and ClearKey-CENC):
- **shaka-packager** is the default.
- **mp4decrypt** is part of the Bento4 tools.
@@ -401,7 +409,7 @@ Both must be discoverable in unshackle's bundled `binaries/` directory or on you
"executable not found but is required" error. See [dependencies](../getting-started/installation.md) for
installation.
You choose the backend with the `decryption` key. It can be a single value applied
You choose the backend with the `decryption` config key. It can be a single value applied
globally, or a per-service mapping:
=== "Global default"
@@ -419,31 +427,32 @@ globally, or a per-service mapping:
```
!!! info "How the value is interpreted"
The default is `shaka`. Only the exact value `mp4decrypt` selects mp4decrypt;
**anything else** (including `shaka`) selects shaka-packager. Per-service lookups
The default is `shaka`. Only the exact value `mp4decrypt` selects mp4decrypt.
**Anything else** (including `shaka`) selects shaka-packager. Per-service lookups
are case-insensitive, with the `default`/`DEFAULT` entry used as the fallback.
The two backends produce equivalent output for standard CENC content. shaka-packager
is generally preferred; switch a specific service to `mp4decrypt` if you hit a track
that shaka-packager cannot handle.
The two backends give equivalent output for a standard CENC title. In most
conditions shaka-packager is the better backend. Switch a specific service to
`mp4decrypt` if you find a track that shaka-packager cannot decrypt.
!!! note "ClearKey (HLS) does not use these tools"
HLS AES-128 ClearKey content is decrypted in-process using AES-CBC and ignores the
`decryption` setting entirely. There is nothing to configure.
unshackle decrypts an HLS AES-128 ClearKey title in-process with AES-CBC, and
ignores the `decryption` setting entirely. You do not need to configure anything.
## ClearKey
### ClearKey over HLS (AES-128)
When an HLS playlist references a key with an `EXT-X-KEY` tag, unshackle fetches the
key directly (from the given URI or an inline `data:` URL) and decrypts each segment
in-process with AES-CBC. There is no device, no CDM, and no license challenge, so
there is nothing to set up. Such tracks are handled automatically during download.
When an HLS playlist references a content key with an `EXT-X-KEY` tag, unshackle
fetches the content key directly (from the given URI or an inline `data:` URL) and
decrypts each segment in-process with AES-CBC. There is no device, no CDM, and no
license challenge, so you do not set anything up. unshackle decrypts such tracks
automatically during download.
### ClearKey over CENC (W3C EME `org.w3.clearkey`)
The CENC ClearKey scheme is a proper license flow, but the "license server" simply
returns the keys as a JWK Set rather than a DRM-wrapped blob. unshackle builds a W3C
The CENC ClearKey scheme is a proper license flow, but the "license server"
returns the keys as a JWK Set and not a DRM-wrapped blob. unshackle builds a W3C
EME request of the form:
```json
@@ -451,14 +460,14 @@ EME request of the form:
```
It sends this to the manifest's license URL (or to whatever a service's ClearKey
license hook returns) and reads the JSON Web Key set back. No device file is needed.
The decrypted content still goes through shaka-packager or mp4decrypt like any other
CENC track, so the `decryption` setting applies.
license hook returns) and reads the JSON Web Key set back. You do not need a device
file. The decrypted file still goes through shaka-packager or mp4decrypt like any
other CENC track, so the `decryption` setting applies.
## Widevine and PlayReady on the same title
A single manifest can advertise both Widevine and PlayReady. unshackle picks the CDM
per track: if a track needs a system that your loaded CDM does not provide, it
per track: if a track needs a system that your loaded CDM cannot supply, it
transparently switches to the appropriate device for that track using your `cdm`
mapping. If it cannot find a matching device, it fails with a clear message such as:
@@ -467,7 +476,7 @@ Title needs a Widevine CDM but SOMESERVICE is configured with PlayReady.
```
Use the `cdm` mapping's [DRM-based selection](#advanced-selection-by-quality-drm-or-profile)
to configure a device for each system; unshackle selects the right one per track
to configure a device for each system. unshackle selects the right one per track
automatically.
## Keys, vaults, and caching
@@ -477,12 +486,12 @@ unshackle therefore resolves each key ID in this order before ever contacting a
license server:
1. Keys already known for the current DRM object.
2. An in-process license-key cache (for the running session).
2. An in-process cache of content keys, for the current `dl` run.
3. Your configured [key vaults](vaults.md).
4. Only if still missing, a live license request.
After a successful license request, the recovered keys are written back to all
configured vaults so future downloads can skip the license server. Two flags let you
After a successful license request, unshackle writes the recovered keys back to all
configured vaults, so future downloads can skip the license server. Two flags let you
control this trade-off on the `dl` command:
| Flag | Effect |
@@ -508,7 +517,7 @@ debug_requests: true
```
!!! warning "Keys and paths in logs"
When key logging is enabled, content keys (KID:key pairs) are written to the logs.
When you enable key logging, unshackle writes content keys (KID:key pairs) to the logs.
Treat those logs as sensitive. The unrelated `redact_paths` option (default
`true`) masks local directory paths in log output.
+54 -52
View File
@@ -1,10 +1,10 @@
# Output & Naming
This page explains where unshackle writes finished downloads and how it names them. You control naming with **output templates** (the filename) and **folder templates** (the directory the file lands in), using a set of `{variable}` placeholders that unshackle fills in from the media it just downloaded. It also covers muxing options, release-group tagging, Unicode filenames, and how to relocate unshackle's working directories.
This page tells you where unshackle writes finished downloads and how it names them. You control naming with **output templates** (the filename) and **folder templates** (the directory the file lands in). Both take `{variable}` placeholders that unshackle fills in from the downloaded media. This page also covers muxing options, release-group tagging, Unicode filenames, and how to relocate unshackle's working directories.
If you have not configured `unshackle.yaml` yet, read [Configuration File](../getting-started/configuration-file.md) first. Everything below lives in that file.
## How an output path is assembled
## How unshackle assembles an output path
When a download finishes, unshackle builds the final path from three pieces:
@@ -13,9 +13,9 @@ When a download finishes, unshackle builds the final path from three pieces:
```
- **Output directory**: `directories.downloads` from your config, or whatever you pass to `-o/--output` on the command line for a single run.
- **Folder template**: an optional per-title subfolder. TV episodes and music tracks are **always** placed in a folder; movies only get one if you define a `movies` folder template, or a single-string `folder` template that applies to all title kinds (see [Folder templates](#folder-templates)).
- **Folder template**: an optional per-title subfolder. unshackle **always** puts TV episodes and songs in a folder. Movies only get one if you define a `movies` folder template, or a single-string `folder` template that applies to all title kinds (see [Folder templates](#folder-templates)).
- **Filename template**: the `output_template` for the title kind (movie, series, or song).
- **Extension**: chosen by the muxer for a movie or an episode: `.mkv` for video, `.mka` for audio-only, `.mks` for subtitle-only. A music track is not muxed. It keeps the container the service delivered, such as `.flac`, `.m4a`, or `.mp3`. Read [Music output files](#music-output-files).
- **Extension**: chosen by the muxer for a movie or an episode: `.mkv` for video, `.mka` for audio-only, `.mks` for subtitle-only. unshackle does not mux a song. A song keeps the container the service delivered, such as `.flac`, `.m4a`, or `.mp3`. Read [Music output files](#music-output-files).
!!! example "A typical episode path"
```text
@@ -35,7 +35,7 @@ Two command-line flags change this layout for a single run:
## Output templates
Filename templates live under the `output_template` key, keyed by title kind. Each value is a format string built from `{variable}` placeholders.
Filename templates live under the `output_template` config key, keyed by title kind. Each value is a format string built from `{variable}` placeholders.
```yaml title="unshackle.yaml"
output_template:
@@ -58,20 +58,20 @@ The recognized kinds are:
### Required vs. optional variables
- `{variable}`: **required**. If it is missing from the download's context, formatting raises an error. (Note that most variables are always present in context but resolve to an *empty string* when not applicable, so being "present but empty" is different from "missing".)
- `{variable?}`: **optional**. A trailing `?` marks the variable conditional. When it resolves to empty, unshackle removes the placeholder *and* one adjacent separator (`.`, `-`, or space), so the surrounding text collapses cleanly.
- `{variable}`: **required**. If it is missing from the download's context, formatting raises an error. Note that most variables are always present in context. A variable that does not apply becomes an *empty string*, so "present but empty" is different from "missing".
- `{variable?}`: **optional**. A trailing `?` marks the variable conditional. When it is empty, unshackle removes the placeholder *and* one adjacent separator (`.`, `-`, or space), so the surrounding text collapses cleanly.
!!! tip "Use `?` for anything that isn't always there"
Episode names, editions, tags, HDR labels and audio extras aren't present on every title. Mark them optional so you never get doubled dots or dangling separators:
!!! tip "Use `?` for anything that is not always there"
Episode names, editions, tags, HDR labels and audio extras are not present on every title. Mark them optional so you never get doubled dots or dangling separators:
```yaml
series: "{title}.{season_episode}.{episode_name?}.{quality}.{hdr?}.{source}-{tag?}"
```
For an episode with no on-screen name, `{episode_name?}` and its adjacent dot simply disappear.
For an episode with no on-screen name, `{episode_name?}` and its adjacent dot disappear.
### Separator style (dots vs. spaces)
unshackle looks at the characters you place *between* variables to decide the filename's separator style. If spaces outnumber dots in your template, it sanitizes with spaces; otherwise it uses dots (the scene-style default). This is also applied to the auto-generated date separators for daily/dated content, and to every segment of a nested folder template.
unshackle looks at the characters you place *between* variables to decide the filename's separator style. If spaces outnumber dots in your template, it sanitises with spaces. If not, it uses dots (the scene-style default). unshackle also applies this style to the auto-generated date separators of daily content, and to every segment of a nested folder template.
=== "Scene style (dots)"
@@ -95,7 +95,7 @@ unshackle looks at the characters you place *between* variables to decide the fi
## Template variables
Every variable below is valid in both output and folder templates. Values are derived from the downloaded media (via MediaInfo), from the title metadata, or from your config. Variables that don't apply to a given title resolve to an empty string.
Every variable below is valid in both output and folder templates. unshackle takes the values from the downloaded media (through MediaInfo), from the title metadata, or from your config. A variable that does not apply to a title becomes an empty string.
### Common / video
@@ -116,7 +116,7 @@ Every variable below is valid in both output and folder templates. Values are de
| `lang_tag` | Result of your `language_tags` rules, if configured | `MULTi` |
!!! note "How `language_tags` rules match"
Rules are tried in order, but language comparison is **fuzzy**: a rule condition of `en` matches `en-US`, `en-GB`, and any other `en-*` variant. Within a single rule, **all** conditions must match (AND logic) for it to apply. The first rule that fully matches wins; if none match, `{lang_tag?}` is removed from the filename cleanly.
unshackle tries the rules in order, but language comparison is **fuzzy**: a rule condition of `en` matches `en-US`, `en-GB`, and any other `en-*` form. Within a single rule, **all** conditions must match (AND logic) for it to apply. The first rule that fully matches wins. If none match, unshackle removes `{lang_tag?}` from the filename cleanly.
### Episode-specific
@@ -131,17 +131,17 @@ Every variable below is valid in both output and folder templates. Values are de
| `date` | ISO air date for daily/dated content | `2024-06-01` |
!!! note "Daily & sports content"
When an episode has an air date, unshackle switches to date-based naming automatically: `season` and `season_episode` become the formatted air date, `episode` and `year` are cleared, and `{date}` holds the ISO date. The date's internal separator (dots or spaces) follows your `series` template style.
When an episode has an air date, unshackle switches to date-based naming automatically. `season` and `season_episode` become the formatted air date, unshackle clears `episode` and `year`, and `{date}` holds the ISO date. The date's internal separator (dots or spaces) follows your `series` template style.
### Split episodes {#split-episodes}
A few services split one episode into several separately playable videos. Where a service reports that, the part index is folded into `{episode}` and `{season_episode}`, so the stock `series` template names it with no change to your config.
A few services split one episode into several separately playable videos. Where a service reports that, unshackle folds the part index into `{episode}` and `{season_episode}`, so the stock `series` template names it with no change to your config.
```text
Show.Name.S01E01.Part.2.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
```
The part token sits immediately after the episode token, before the quality tags, and the group tag stays last. Its separator follows your template's own style, the same way dated content does, so a spaced template gives `S01E01 Part 2` instead.
The part token sits immediately after the episode token, before the quality tags, and the group tag stays last. Its separator follows your template's own style, the same way daily content does, so a spaced template gives `S01E01 Part 2` instead.
The part is left out of the folder name, so every part of an episode lands in the same season folder:
@@ -152,10 +152,10 @@ The.Show.S01.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG/
└─ The.Show.S01E02.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
```
The same holds for a folder template of your own: `{season}` never picks up the part, and `{season_episode}` drops it inside a folder template. An episode with no part is named like any other episode.
The same holds for a folder template of your own: `{season}` never picks up the part, and `{season_episode}` drops it inside a folder template. unshackle names an episode with no part like any other episode.
!!! note "What media servers do with the part token"
The form above follows scene practice for genuinely split episodes. Kodi stacks the parts back into one playable episode automatically. Jellyfin lists two versions of S01E01, still correct and playable, because its stacking pattern expects the part marker at the end of the name. Plex's scanner is closed-source, so its handling of text after the part token is unverified.
The form above follows scene practice for genuinely split episodes. Kodi stacks the parts back into one playable episode automatically. Jellyfin lists two versions of S01E01, still correct and playable, because its stacking pattern expects the part marker at the end of the name. Plex uses a closed-source scanner, so what it does with text after the part token is not known.
For custom templates there is also a standalone [`{part}`](../reference/configuration/output.md#output_template) variable. You rarely want it, see the caveat on that page.
@@ -186,11 +186,11 @@ For custom templates there is also a standalone [`{part}`](../reference/configur
| `isrc` / `upc` / `label` | ISRC, UPC and record label |
!!! warning "Unknown variables are ignored, not errors"
If you use a variable that isn't in the list above, unshackle emits a warning at startup but continues. Double-check spelling. A typo'd `{quailty}` will simply be left in the filename literally or dropped, not filled in.
If you use a variable that is not in the list above, unshackle gives a warning at startup but continues. Double-check spelling. A typo'd `{quailty}` stays in the filename literally, or unshackle drops it. unshackle does not fill it in.
## Folder templates
Folder templates are nested under a special `folder` key inside `output_template`. This key is handled separately from the filename kinds. You can give it a single string (used for all title kinds) or a per-kind map.
Folder templates sit under a special `folder` config key inside `output_template`. unshackle reads this config key separately from the filename kinds. You can give it a single string (for all title kinds) or a per-kind map.
=== "Single folder template"
@@ -213,19 +213,19 @@ Folder templates are nested under a special `folder` key inside `output_template
albums: "{artist} - {album} ({year})"
```
The per-kind folder keys are `movies`, `series`, `songs`, and `albums`. Any other key produces a startup warning. Path separators (`/` or `\`) are allowed in folder templates. Each segment is formatted independently, so you can build nested directory structures like `Show/Season 01`. unshackle reads the separator style from the whole template, so every segment is spaced alike.
The per-kind folder config keys are `movies`, `series`, `songs`, and `albums`. Any other config key gives a startup warning. Folder templates permit path separators (`/` or `\`). unshackle formats each segment independently, so you can assemble nested directory structures like `Show/Season 01`. unshackle reads the separator style from the whole template, so it spaces every segment alike.
**Fallback behavior:**
**Fallback behaviour:**
- A per-kind folder template wins if present; otherwise the single `folder` string is used; otherwise unshackle falls back to a built-in default.
- A per-kind folder template wins if present. If there is none, unshackle uses the single `folder` string. If there is no `folder` string either, unshackle falls back to a built-in default.
- Built-in defaults when no folder template is set. Movies get no folder at all unless a `movies` folder template (or a single-string `folder` template) exists. Series fall back to a folder *derived* from the `series` output template (stripping `{episode}`, `{episode_name}`, and collapsing `{season_episode}` down to `{season}`). Music albums fall back to `{artist} - {album} ({year})`. If the song has no year, the folder name is only `{artist} - {album}`.
!!! note "Movies are flat by default"
Episodes and songs are always foldered. Movies are written directly into the output directory *unless* you define a `movies` folder template (or a single-string `folder` template, which folders every kind). Set one if you want each movie in its own directory.
Episodes and songs are always foldered. unshackle writes movies directly into the output directory *unless* you define a `movies` folder template (or a single-string `folder` template, which folders every kind). Set one if you want each movie in its own directory.
## Music output files
A music track does not go through the muxer. unshackle moves the downloaded audio file to
A song does not go through the muxer. unshackle moves the downloaded audio file to
its final path. It keeps the container that the service delivered. A lossless download
arrives as a `.flac` file. An AAC download arrives as a `.m4a` file. unshackle writes no
`.mkv` file for music, and the [muxing options](#muxing-options) below do not apply.
@@ -248,10 +248,10 @@ track title, artist, album artist, track and disc numbers, date, genre, ISRC, UP
copyright, and record label. When `tag` and `tag_group_name` are set, unshackle also writes the group
name into a `GROUP` tag.
Cover art is embedded when the service gives an artwork URL. unshackle downloads the image
unshackle embeds cover art when the service gives an artwork URL. It downloads the image
and puts it in the file.
Lyrics are written when the service supplies them: a `LYRICS` Vorbis comment for FLAC, a
unshackle writes lyrics when the service supplies them: a `LYRICS` Vorbis comment for FLAC, a
`USLT` frame for MP3, and a `©lyr` atom for M4A. Ogg uses the same `LYRICS` field as
FLAC. unshackle does not write synchronized lyrics (`SYLT`).
@@ -263,7 +263,7 @@ warning and continues.
## Muxing options
Muxing (combining video, audio, subtitle, chapter and attachment tracks into a single Matroska file with `mkvmerge`) is configured under the `muxing` key. Only movies and episodes are muxed. Read [Music output files](#music-output-files) for what a music download does instead.
You configure muxing (the combination of video, audio, subtitle, chapter and attachment tracks into a single Matroska file with `mkvmerge`) under the `muxing` config key. unshackle muxes only movies and episodes. Read [Music output files](#music-output-files) for what a music download does instead.
```yaml title="unshackle.yaml"
muxing:
@@ -282,29 +282,29 @@ muxing:
| `merge_audio` | bool | `true` | Merge audio tracks of the same kind so multiple languages sit in one file. |
| `default_language` | map | *(unset)* | Preferred language per track type (`video` / `audio` / `subtitle`). A track in the preferred language is flagged as the default track. |
When no preferred language is matched, unshackle applies sensible defaults: the video default falls back to the title language / original-language track / first track; the audio default is the original-language track; and the subtitle default is a forced track matching the first audio's language.
When no track has the preferred language, unshackle applies sensible defaults. The video default falls back to the title language, then the original-language track, then the first track. The audio default is the original-language track. The subtitle default is a forced track in the first audio's language.
!!! note "`default_language` only sets the default-track flag"
`default_language` controls **which track carries the MKV `--default-track` flag**. Nothing else. It does not change track *selection* (that stays with `-l`/`--alang` and friends), and it does not touch which track is marked as original: `--original-flag` still tags the *true* original-audio track. That is the point: you can make your player default to, say, Polish audio on an English-original title without altering the original marker. When the configured language isn't present in the manifest, each track type falls back to its normal default rule described above.
`default_language` controls **which track carries the MKV `--default-track` flag**. Nothing else. It does not change track *selection* (that stays with `-l`/`--alang` and friends). It also does not touch which track carries the original flag: `--original-flag` still tags the *true* original-audio track. That is the point: you can make your player default to, say, Polish audio on an English-original title without altering the original marker. When the configured language is not present in the manifest, each track type falls back to its normal default rule described above.
!!! warning "`merge_video` collapses only the language dimension"
`merge_video` groups video tracks by `(resolution, range, codec)` and merges them so that only **language** differs within a single file, with no re-encode and no concatenation. Different resolutions, ranges (SDR / HDR10 / HDR10+ / DV / HYBRID), and codecs (H.264 / H.265) **always** land in separate files. So `-r HYBRID,DV,HDR10,SDR --merge-video` produces one file *per range* (never a single fused file), whereas English + French video of identical resolution, range, and codec yields one file containing both video tracks. `merge_audio` works the same way for audio languages.
`merge_video` groups video tracks by `(resolution, range, codec)` and merges them so that only **language** differs within a single file, with no re-encode and no concatenation. Different resolutions, ranges (SDR / HDR10 / HDR10+ / DV / HYBRID), and codecs (H.264 / H.265) **always** land in separate files. So `-r HYBRID,DV,HDR10,SDR --merge-video` gives one file *per range*, never a single fused file. English and French video of identical resolution, range, and codec gives one file that holds both video tracks. `merge_audio` works the same way for audio languages.
A few muxing behaviors are automatic and not configurable:
- `--no-date` is always passed for privacy (no timestamps embedded).
- Descriptive audio gets the visually-impaired flag; SDH subtitles get the hearing-impaired flag; forced subtitles get the forced flag.
- Chapters are written from the title's chapter list (see [Chapters](#chapters-naming) below).
- Descriptive audio gets the visually-impaired flag. SDH subtitles get the hearing-impaired flag. Forced subtitles get the forced flag.
- unshackle writes the chapters from the title's chapter list (see [Chapters](#chapters-naming) below).
The `--no-mux` flag skips muxing entirely and writes the individual track files, each with a track-type suffix, into the same output/folder structure.
## Tags & group naming
The release-group tag is the `-TAG` portion at the end of scene-style names, produced by the `{tag}` variable.
It can be swapped per release with [`tag_rules`](../reference/configuration/output.md#tag_rules): ordered rules that
match on the release-attribute variables only (`quality`, `resolution`, `hdr`, `source`, `lang_tag`,
`title_type`, and so on), first match wins. The title's own fields, such as `title` and `season_episode`,
cannot be matched. See the [reference page](../reference/configuration/output.md#tag_rules) for the exact list.
The release-group tag is the `-TAG` portion at the end of scene-style names, which the `{tag}` variable gives.
You can swap it per release with [`tag_rules`](../reference/configuration/output.md#tag_rules): ordered rules whose
conditions read the release-attribute variables only (`quality`, `resolution`, `hdr`, `source`, `lang_tag`,
`title_type`, and so on), and the first rule that matches wins. `tag_rules` cannot read the title's own fields,
such as `title` and `season_episode`. See the [reference page](../reference/configuration/output.md#tag_rules) for the exact list.
```yaml title="unshackle.yaml"
tag: "MYGROUP"
@@ -318,7 +318,7 @@ tag_imdb_tmdb: true
| `tag_group_name` | bool | `true` | Write the group name (`config.tag`) into the MKV `Group` metadata tag. |
| `tag_imdb_tmdb` | bool | `true` | Look up and embed IMDb / TMDB / TVDB external-ID tags in the MKV metadata (uses `tmdb_api_key` / `tvdb_api_key` / `simkl_client_id` when available). |
The IDs are written as the Matroska `IMDB`, `TMDB`, and `TVDB2` tags. `TVDB2` values carry the
unshackle writes the IDs as the Matroska `IMDB`, `TMDB`, and `TVDB2` tags. `TVDB2` values carry the
entity prefix the [Matroska tagging spec](https://www.matroska.org/technical/tagging.html)
requires: `series/73871` for a show, `movies/113` for a film.
@@ -339,7 +339,7 @@ unshackle dl --repack EXAMPLE "..."
## Unicode filenames
By default unshackle transliterates non-ASCII characters (Korean, Japanese, Chinese, accented Latin, etc.) into ASCII equivalents, which is safest for older tools, DDL, and P2P sharing. Set `unicode_filenames` to keep the native characters.
By default unshackle transliterates non-ASCII characters into ASCII equivalents. Korean, Japanese, Chinese, and accented Latin are examples. This is safest for older tools, DDL, and P2P sharing. Set `unicode_filenames` to keep the native characters.
```yaml title="unshackle.yaml"
unicode_filenames: true
@@ -350,11 +350,13 @@ unicode_filenames: true
| `false` (default) | `기생충` → transliterated ASCII; accents stripped. |
| `true` | Native characters preserved in filenames and folders. |
Regardless of this setting, a set of filesystem-unsafe and structural characters is always removed or replaced during sanitization (for example `/` and `;` become ` & `, and characters like `\ * ! ? , ' " < > | $ # ~` are stripped). This is why you should never put path-unsafe characters directly in a template. unshackle warns about templates containing `< > : " / \ | ? *` at startup (folder templates are checked per path segment, so `/` is allowed there as a directory separator).
Regardless of this setting, unshackle always removes or replaces a set of filesystem-unsafe and structural characters during sanitisation. For example, `/` and `;` become ` & `, and unshackle removes characters like `\ * ! ? , ' " < > | $ # ~`. This is why you must never put path-unsafe characters directly in a template.
unshackle warns at startup about a template that contains `< > : " / \ | ? *`. It examines a folder template per path segment, so you can use `/` there as a directory separator.
## Directory & filename configuration
The `directories` key relocates unshackle's working folders. Only the folders listed here can be moved; core package paths are protected and silently ignored if you try to override them.
The `directories` config key relocates unshackle's working folders. You can move only the folders in the table below. unshackle protects the core package paths and silently ignores them if you try to override them.
```yaml title="unshackle.yaml"
directories:
@@ -376,11 +378,11 @@ directories:
| `services`, `vaults`, `fonts`, `commands` | Search paths for services, key-vault backends, bundled fonts, and CLI commands. |
| `wvds`, `prds`, `dcsl` | Widevine devices, PlayReady devices, and DCSL data. |
Paths support `~` expansion. The names `app_dirs`, `core_dir`, `namespace_dir`, `user_configs`, and `data` are protected and cannot be changed.
Paths can hold a `~` for expansion. unshackle protects the names `app_dirs`, `core_dir`, `namespace_dir`, `user_configs`, and `data`, so you cannot change them.
### Filename patterns
The `filenames` key overrides the naming patterns for a handful of internal files. These are **not** your media output names. They are logs, temp files, and per-service configs.
The `filenames` config key overrides the naming patterns for a handful of internal files. These are **not** your media output names. They are logs, temp files, and per-service configs.
```yaml title="unshackle.yaml"
filenames:
@@ -400,13 +402,13 @@ filenames:
### Chapters naming {#chapters-naming}
When a title has chapters, they are written to an OGM chapters file (`filenames.chapters`) and passed to the muxer. Unnamed chapters use the `chapter_fallback_name` template:
When a title has chapters, unshackle writes them to an OGM chapters file (`filenames.chapters`) and passes it to the muxer. Unnamed chapters use the `chapter_fallback_name` template:
```yaml title="unshackle.yaml"
chapter_fallback_name: "Chapter {i:02}"
```
The fallback supports two placeholders: `{i}` (chapter number, starting at 1) and `{j}` (which increments only for unnamed chapters). Standard format directives like `{i:02}` are allowed.
The fallback can hold two placeholders: `{i}` (chapter number, starting at 1) and `{j}` (which increments only for unnamed chapters). It permits standard format directives like `{i:02}`.
## Removed & migrated options
@@ -509,12 +511,12 @@ how the optional (`?`) variables appear and disappear.
The.Movie.2024.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
```
For finer control (e.g. a `SUBBED` tag driven by subtitle languages), use `{lang_tag?}`
For finer control (for example a `SUBBED` tag driven by subtitle languages), use `{lang_tag?}`
with [`language_tags` rules](../reference/configuration/download.md#language_tags) instead.
=== "Daily shows"
Dated content needs no special template. When an episode carries an air date,
Daily content needs no special template. When an episode carries an air date,
`{season_episode}` becomes the date automatically and `{episode}` and `{year}` clear:
```yaml
@@ -532,8 +534,8 @@ how the optional (`?`) variables appear and disappear.
The date separator follows your template style: dots here, spaces if your `series`
template uses spaces.
In folder names, `{season}` becomes the air year (`Season 2024` style grouping), and a
`{year?}` in the folder template is skipped when it would just repeat that year.
In folder names, `{season}` becomes the air year (`Season 2024` style grouping), and
unshackle skips a `{year?}` in the folder template when it would only repeat that year.
=== "Music"
@@ -548,8 +550,8 @@ how the optional (`?`) variables appear and disappear.
└─ 01.Opening.Track.mka
```
In `"{track_number}. {title}"` dots and spaces are tied, and ties go to dot style; add
more spaces between variables (as below) if you want space-separated names.
In `"{track_number}. {title}"` the number of dots and spaces is equal, and a tie goes to
dot style. Add more spaces between variables (as below) if you want space-separated names.
A multi-disc album with explicit tracks, using more of the music variables. `{disc}` is
empty on disc 1, so `{disc?}` and its separator only appear from disc 2 onward:
+126 -123
View File
@@ -1,28 +1,28 @@
# Proxies & VPN
Most streaming services are geo-restricted, region-priced, or simply refuse to hand
Most streaming services are geo-restricted, region-priced, or refuse to hand
over a manifest from the "wrong" country. unshackle routes its traffic through a proxy
so the service sees the location you choose instead of your own. You can point it at a
single proxy URL, or configure one of several **proxy providers** (commercial VPNs and
self-hosted tunnels), then ask for a country by its two-letter code and let
unshackle pick a working server for you.
The `--proxy` flag controls proxying, and a query is resolved to a real proxy URL by the
providers unshackle supports: [basic static proxies](#basic-static-proxies),
The `--proxy` flag controls proxying, and the proxy providers unshackle supports change
a query to a real proxy URL: [basic static proxies](#basic-static-proxies),
[Gluetun](#gluetun) (documented in the most detail, since it is the most flexible),
[ExpressVPN](#expressvpn), [NordVPN](#nordvpn), [Proton VPN](#proton-vpn),
[Hola](#hola), [Surfshark](#surfshark), and [Windscribe](#windscribe).
!!! note "Where proxies are configured"
All provider configuration lives under the top-level `proxy_providers:` key in your
`unshackle.yaml`. Each provider has its own sub-key, and the keys inside it map
directly onto that provider's settings. See the
All proxy provider configuration lives under the top-level `proxy_providers:`
config key in your `unshackle.yaml`. Each proxy provider has its own sub-key, and
the config keys inside it map directly onto that proxy provider's settings. See the
[Configuration file](../getting-started/configuration-file.md) page for where the
file lives and how it is loaded.
file lives and how unshackle loads it.
## The `--proxy` flag
Proxying is controlled at download time by three options on the `dl` command:
Three options on the `dl` command control proxying at download time:
| Option | Effect |
|---|---|
@@ -43,14 +43,14 @@ unshackle dl --proxy nordvpn:us EXAMPLE 81234567
```
!!! tip "Set a default proxy in config"
Like every `dl` flag, `--proxy` can be given a default in your configuration so you
Like every `dl` flag, you can give `--proxy` a default in your configuration, so you
do not have to pass it on every command. See
[Downloading](downloading.md) for how `dl` options and config defaults interact.
### `--no-proxy-download`
Segment downloads are usually the largest share of traffic, and a residential-grade VPN
proxy is often the slowest link in the chain. `--no-proxy-download` lets you keep the
proxy is frequently the slowest part of the chain. `--no-proxy-download` lets you keep the
proxy where it matters (passing the geo-check for the manifest and the licence) while
pulling the files themselves over your normal connection at full speed.
@@ -66,8 +66,8 @@ unshackle dl --proxy gb --no-proxy-download EXAMPLE 10a1234
## How proxy resolution works
The value you pass to `--proxy` is interpreted in one of three ways. unshackle checks
them in order.
unshackle interprets the value you pass to `--proxy` in one of three ways. It tries
them in this order.
```mermaid
flowchart TD
@@ -78,54 +78,56 @@ flowchart TD
D -- no --> F["Ask every provider in order,<br/>first match wins"]
```
1. **An explicit URI.** Anything shaped like `http://...`, `https://...`, or a `socks...`
URI is used exactly as given. unshackle logs `Using explicit Proxy: ...` and does no
lookup.
1. **An explicit URI.** unshackle uses anything shaped like `http://...`,
`https://...`, or a `socks...` URI exactly as you give it. unshackle logs
`Using explicit Proxy: ...` and does no lookup.
2. **A provider-prefixed query**: `provider:query`, for example `nordvpn:us` or
`gluetun:windscribe:us`. unshackle finds the provider whose name matches the prefix
(case-insensitive) and asks *only* that provider to resolve the remainder. If no such
provider is loaded, or it returns no proxy, the download errors.
`gluetun:windscribe:us`. unshackle finds the proxy provider whose name matches
the prefix (case-insensitive). It then asks *only* that proxy provider to find a
proxy for the remainder. If unshackle loads no such proxy provider, or if that
proxy provider returns no proxy, the download errors.
3. **A bare query**: a region like `us`, `gb`, `us:seattle`, or `us1234`. unshackle
asks each loaded provider **in order** and uses the first proxy any of them returns.
asks each loaded proxy provider **in order** and uses the first proxy any of them
returns.
The query is matched against a region grammar (a two-letter country code, optionally
followed by a server number and/or a `:city` or `-city` part), lowercased, and then
handed to the provider. The exact forms each provider accepts are documented in its
section below.
unshackle compares the query against a region grammar: a two-letter country code,
optionally followed by a server number and/or a `:city` or `-city` part. It then
lowercases the query and hands it to the proxy provider. Its section below gives the
exact forms each proxy provider accepts.
### Provider load order
When you run a download without `--no-proxy`, unshackle instantiates every configured
provider once, in this fixed order, and that same order is used for bare-query
When you do a download without `--no-proxy`, unshackle instantiates every configured
proxy provider once, in this fixed order. It uses that same order for bare-query
resolution:
**Basic → ExpressVPN → NordVPN → Proton VPN → Surfshark → Windscribe → Gluetun → Hola**
So if you have both Basic and NordVPN configured and run `--proxy us`, a `us` entry in
your Basic config wins because Basic is tried first. To skip ahead to a specific
provider, prefix the query (`--proxy nordvpn:us`).
So if you have both Basic and NordVPN configured and use `--proxy us`, a `us` entry in
your Basic config wins, because unshackle tries Basic first. To skip ahead to a specific
proxy provider, prefix the query (`--proxy nordvpn:us`).
!!! note "Auto-loading providers"
Most providers only load when you configure them under `proxy_providers:`. Three are
special: **ExpressVPN** and **Proton VPN** also load automatically once their cached
session file exists on disk (even with no YAML), and **Hola** loads automatically
special: **ExpressVPN** and **Proton VPN** also load automatically once their cache
file exists on disk (even with no YAML), and **Hola** loads automatically
whenever the `hola-proxy` binary is found on your `PATH`.
### Connection feedback
After a provider resolves a query, unshackle prints a line so you can confirm
After a proxy provider resolves a query, unshackle prints a line so you can see
where you came out:
- **Gluetun** reports the verified exit IP, country, and city of the container.
- **ExpressVPN** and **Proton VPN** print a location summary such as
`(USA - New York, #3 of 5): .214`.
- Other providers log the resolved proxy URL.
- Other proxy providers log the resolved proxy URL.
## Basic (static proxies)
The `Basic` provider is pure static configuration, with no accounts and no network calls. You
list proxy URLs per country code, and unshackle serves them back when you query that
country. This is the right choice when you already have proxy endpoints (from a proxy
The `Basic` proxy provider is pure static configuration, with no accounts and no network
calls. You record proxy URLs under each country code, and unshackle serves them back
when you query that country. This is the right choice when you already have proxy endpoints (from a proxy
seller, your own servers, or a corporate gateway).
Under each two-letter country code you can put either a single proxy string or a list of
@@ -148,10 +150,10 @@ proxy_providers:
| `us2` | The **2nd** entry in the US list (1-based). |
- If a country has a single proxy string, that proxy is always returned.
- If it has a list and you give a plain country code, one is picked at random; append an
index (`de1`, `de2`) to pin a specific entry.
- Each URL is normalised (a missing scheme defaults to `http`) and validated, so a
malformed URI is rejected up front rather than failing mid-download.
- If it has a list and you give a plain country code, unshackle picks one at random.
Append an index (`de1`, `de2`) to pin a specific entry.
- unshackle normalises each URL (a missing scheme defaults to `http`) and validates it,
so it rejects a malformed URI up front and does not fail mid-download.
```shell
unshackle dl --proxy de2 EXAMPLE 81234567
@@ -173,13 +175,13 @@ in unshackle itself.
!!! warning "Docker is required"
Gluetun needs a working [Docker](https://docs.docker.com/get-docker/) installation on
your `PATH`. If Docker is not found, the provider fails to load with an error linking
your `PATH`. If Docker is not found, the proxy provider fails to load with an error linking
to the install docs. The container also needs the `NET_ADMIN` capability and access
to `/dev/net/tun` to bring up the tunnel. unshackle requests both automatically.
### Query format
Gluetun uses a nested selector: the provider prefix picks Gluetun, and the rest names the
Gluetun uses a nested selector: the proxy provider prefix picks Gluetun, and the rest names the
**VPN provider** and the **region**.
```
@@ -194,10 +196,10 @@ unshackle dl --proxy gluetun:windscribe:us EXAMPLE 81234567
unshackle dl --proxy gluetun:nordvpn:de EXAMPLE 81234567
```
After unshackle strips the leading `gluetun:`, the provider receives exactly
`provider:region` (two colon-separated parts; anything else is an error). The
`<vpn-provider>` must be one you have configured under `gluetun.providers`, and friendly
names are normalised to Gluetun's identifiers (for example `pia` and
After unshackle strips the leading `gluetun:`, the proxy provider receives exactly
`provider:region`. It must be two colon-separated parts, and anything else is an error.
The `<vpn-provider>` must be one you have configured under `gluetun.providers`, and
unshackle normalises friendly names to Gluetun's identifiers (for example `pia` and
`privateinternetaccess` both map to `private internet access`).
### Configuration
@@ -256,7 +258,7 @@ proxy_providers:
- `private_key` is **always** required.
- `surfshark`, `mullvad`, and `ivpn` also require `addresses`.
- `windscribe` additionally requires **both** `addresses` and `preshared_key`
(`preshared_key` may be an empty string, but the key must be present).
(`preshared_key` may be an empty string, but the config key must be present).
- NordVPN and Proton VPN need only `private_key`.
=== "OpenVPN"
@@ -266,10 +268,10 @@ proxy_providers:
!!! tip "Which `vpn_type` should I pick?"
WireGuard is the default because it **can be faster**, but it needs the fiddlier
credential setup above (private keys, and for some providers `addresses` and a
preshared key). OpenVPN is the **simplest** to configure (just a username and
password) at the cost of some speed. If you are getting started or hitting credential
trouble, OpenVPN is the path of least resistance; switch to WireGuard when you want the
extra throughput.
`preshared_key`). OpenVPN is the **simplest** to configure (only a username and
password) at the cost of some speed. If you start out, or if you have credential
trouble, OpenVPN is the path of least resistance. Switch to WireGuard when you want
the extra throughput.
**Global keys:**
@@ -287,37 +289,38 @@ For a given query region, Gluetun resolves the server like this:
1. An explicit entry in `server_hostnames`, `server_cities`, or `server_countries` for
that alias always wins.
2. A `us1239`-style query (country code + number) with no explicit mapping is turned
into a provider-specific server hostname (for example `us1239.nordvpn.com` for
NordVPN, `us-1239.prod.surfshark.com` for Surfshark).
3. A bare two-letter code is expanded to the full country name Gluetun expects.
2. unshackle changes a `us1239`-style query (country code + number) with no explicit
mapping into a provider-specific server hostname (for example `us1239.nordvpn.com`
for NordVPN, `us-1239.prod.surfshark.com` for Surfshark).
3. unshackle expands a bare two-letter code to the full country name Gluetun expects.
A few providers (Windscribe, VyprVPN, VPN Secure) select by **region** rather than
country, and Windscribe region names are translated automatically. For instance `us`
country, and unshackle converts Windscribe region names automatically. For instance `us`
becomes `US East` and `uk`/`gb` become `United Kingdom`.
### Container lifecycle
Understanding what Gluetun does behind the scenes helps when something goes wrong:
1. **Image check.** `qmcgaw/gluetun:latest` is pulled if it is not already present.
2. **Reuse.** If a container for this exact `provider:region` is already running (from a
concurrent unshackle session), it is adopted instead of starting a new one. Stopped or
dead containers with the same name are pruned first.
1. **Image check.** unshackle pulls `qmcgaw/gluetun:latest` if it is not already
present.
2. **Reuse.** If a container for this exact `provider:region` already runs (from a
concurrent unshackle process), unshackle adopts it and does not start a new one. It
removes stopped or dead containers with the same name first.
!!! note "Expect a slow first request, then instant reuse"
Bringing up a fresh tunnel takes roughly **10 to 30 seconds** for the first request to a
given `provider:region`. This is the VPN handshake and readiness wait, not a hang.
Every subsequent request that reuses that same container is effectively **instant**. A
slow first run is normal.
3. **Port allocation.** A free local port is chosen starting from `base_port`.
4. **Start.** The container is launched bound to `127.0.0.1:{port}`. Credentials are
passed through a temporary, `0600`-permission env-file (never as `-e` flags), which is
overwritten and deleted immediately after.
5. **Readiness wait.** unshackle polls the container logs (up to 60 seconds) until both
the HTTP proxy is listening **and** the VPN reports a completed connection. Fatal
errors like `authentication failed` or `invalid credentials` abort early, and recent
logs are attached to the error to help you diagnose it.
3. **Port allocation.** unshackle chooses a free local port, starting from `base_port`.
4. **Start.** unshackle launches the container bound to `127.0.0.1:{port}`. It passes
credentials through a temporary, `0600`-permission env-file (never as `-e` flags),
and it overwrites and deletes that file immediately after.
5. **Readiness wait.** unshackle polls the container logs (up to 60 seconds) until the
HTTP proxy listens **and** the VPN reports a completed connection. Fatal errors like
`authentication failed` or `invalid credentials` abort early, and unshackle attaches
recent logs to the error to help you diagnose it.
6. **IP verification.** When `verify_ip` is on, unshackle queries the container's exit IP
and checks the country matches what you asked for, raising a *region mismatch* error if
it does not.
@@ -325,23 +328,23 @@ Understanding what Gluetun does behind the scenes helps when something goes wron
configured).
!!! note "Cleanup and Ctrl+C"
Containers are cleaned up via an `atexit` handler on normal exit. Gluetun deliberately
does **not** install its own signal handlers, so `Ctrl+C` continues to work as you
expect. If a run is killed hard (e.g. `kill -9`) a container can be left behind; with
`auto_cleanup: true` the next run adopting that name will still work, or you can remove
it manually with `docker rm -f`.
unshackle cleans up containers with an `atexit` handler on normal exit. Gluetun
deliberately does **not** install its own signal handlers, so `Ctrl+C` continues to
work as you expect. If something kills a run hard (for example `kill -9`), a container
can stay behind. With `auto_cleanup: true` the next run that adopts that name still
works, or you can remove it manually with `docker rm -f`.
### Troubleshooting
!!! example "Common Gluetun issues"
- **`RuntimeError` about Docker not found.** Install Docker and confirm `docker` is
on your `PATH`.
- **`RuntimeError` about Docker not found.** Install Docker and make sure that
`docker` is on your `PATH`.
- **`invalid credentials` / `authentication failed` in the readiness wait.** Your
WireGuard/OpenVPN credentials are wrong or in the wrong fields. Double-check
`private_key`, `addresses`, and (for Windscribe) `preshared_key`.
- **Region mismatch error.** The VPN connected but exited in a different country than
requested. Check your `server_countries` mapping, or set `verify_ip: false` if you
are intentionally using a nearby region.
requested. Examine your `server_countries` mapping, or set `verify_ip: false` if
you intentionally use a nearby region.
- **Timeout after 60 seconds.** The tunnel never came up. Inspect the container logs
(`docker logs <container>`) for the underlying VPN error.
@@ -349,10 +352,10 @@ Understanding what Gluetun does behind the scenes helps when something goes wron
ExpressVPN mirrors the ExpressVPN Android TV app: it signs in through Express's OAuth 2.0
device-login flow, obtains proxy-capable locations from Express's API, and returns an
authenticated HTTPS proxy. The device login runs once; after that the cached refresh token
keeps the session alive headlessly.
authenticated HTTPS proxy. The device login runs once. After that, the cached refresh
token keeps you signed in headlessly.
Enable it and run a download interactively once:
Enable it and do a download interactively once:
```yaml title="unshackle.yaml"
proxy_providers:
@@ -361,8 +364,8 @@ proxy_providers:
```
With `enable: true` and an interactive terminal, unshackle prints a code and a URL to
enter it at. Once approved, the tokens are cached and reused automatically, so later runs
need no terminal. ExpressVPN also **auto-loads** once its token cache exists, so you can
enter it at. Once you approve it, unshackle caches the tokens and reuses them
automatically, so later runs need no terminal. ExpressVPN also **auto-loads** once its token cache exists, so you can
drop `enable` after the first login, but you can keep a YAML block to pin regions or
servers:
@@ -398,14 +401,14 @@ The resolved proxy is an HTTPS proxy on port `443`. On success, unshackle prints
location summary such as `(USA - New York, #3 of 5): .214`.
!!! note "Cache safety"
ExpressVPN's token cache is written with `0600` permissions so the refresh token is
never briefly world-readable.
unshackle writes ExpressVPN's token cache with `0600` permissions, so the refresh
token is never briefly world-readable.
## NordVPN
NordVPN uses your **Service Credentials**, not your normal NordVPN login. Generate them
NordVPN uses your **Service Credentials**, not your normal NordVPN login. Make them
from the [NordVPN dashboard](https://my.nordaccount.com/dashboard/nordvpn/) under manual
setup; they are a long random username/password pair, not your email and password.
setup. They are a long random username/password pair, not your email and password.
```yaml title="unshackle.yaml"
proxy_providers:
@@ -423,9 +426,9 @@ the username must not contain an `@`. If validation fails, you have almost certa
your login credentials instead of your service credentials.
`server_map` is optional and maps a region (or `code:city`) to a pinned NordVPN server ID.
Its main use is tuning: for a given streaming service one specific server often performs
noticeably better than NordVPN's recommended pick, so hard-coding that server for a day or
two is a handy workaround until it degrades and you swap in another.
Its main use is to tune performance: for a given streaming service one specific server
often performs noticeably better than NordVPN's recommended pick. So you can hard-code
that server for a day or two as a workaround, until it degrades and you swap in another.
!!! note "Use `gb`, not `uk`, for the United Kingdom"
Query the UK as `gb`. unshackle uses the Alpha-2 country code `gb` deliberately to stay
@@ -451,13 +454,13 @@ unshackle dl --proxy nordvpn:us:seattle EXAMPLE 81234567
## Proton VPN
Proton VPN supports two authentication methods, and unshackle can work with either. Note
that free Proton accounts work but are limited to Proton's free-tier exit countries.
that free Proton accounts work, but Proton limits them to its free-tier exit countries.
=== "TV login (recommended)"
A self-sustaining, refreshable session obtained through Proton's TV device-login flow.
This is the only method that can refresh itself headlessly, so it keeps working without
re-exporting anything. Enable it and run a download interactively once:
Proton's TV device-login flow gives a self-sustaining, refreshable sign-in. This is
the only method that can refresh itself headlessly, so it keeps working without
re-exporting anything. Enable it and do a download interactively once:
```yaml title="unshackle.yaml"
proxy_providers:
@@ -467,26 +470,26 @@ that free Proton accounts work but are limited to Proton's free-tier exit countr
With `enable: true` and an interactive terminal, unshackle prints a code and asks you
to enter it at [account.proton.me/vpn/tv/code](https://account.proton.me/vpn/tv/code).
Once approved, the session is cached and reused automatically.
Once you approve it, unshackle caches the sign-in and reuses it automatically.
=== "Exported cookies"
Export your `account.proton.me` session cookies (the `AUTH-<UID>` cookie) to the Proton
Export your `account.proton.me` cookies (the `AUTH-<UID>` cookie) to the Proton
cookie file. This method is **access-only**: it cannot refresh, so you must re-export
when it expires.
!!! warning "The `AUTH-<UID>` cookie is HttpOnly"
This is the single most common cause of a failed Proton cookie import: the required
`AUTH-<UID>` cookie is **HttpOnly**, and many one-click cookie-export extensions
silently skip HttpOnly cookies by default. If your import "doesn't work," use an
silently skip HttpOnly cookies by default. If your import "does not work," use an
exporter that explicitly includes HttpOnly cookies.
!!! note "Why this session can't refresh, and lapses after ~24h"
Proton rotates browser refresh tokens and ties them to the live browser, so a
headless refresh is impossible. unshackle therefore deliberately uses only the
cookie's short-lived **access token** and never attempts a refresh, which is why the
session lapses at Proton's ~24h TTL and must be re-exported. This is exactly why TV
login is preferred for set-and-forget use.
cookie's short-lived **access token** and never attempts a refresh. That is why
the sign-in lapses at Proton's ~24h TTL and you must export it again. This is
exactly why TV login is the better method for set-and-forget use.
**Default file locations:**
@@ -495,14 +498,14 @@ that free Proton accounts work but are limited to Proton's free-tier exit countr
| Cookies | `{cookies}/vpn/protonvpn.txt` |
| Session cache | `{cache}/vpn/protonvpn.json` |
Proton VPN **auto-loads** if its cookie file exists. Like ExpressVPN, its cached session
is written with `0600` permissions.
Proton VPN **auto-loads** if its cookie file exists. Like ExpressVPN, unshackle writes
its cache file with `0600` permissions.
!!! note "Cache vs cookie precedence"
If both a TV-login cache and a cookie file exist, the **cache is preferred** unless the
cookie file is newer. Cookies are read fresh every run and never written back to the
cache. So to deliberately force use of the cookie session, just re-save (re-export) the
cookie file so its modification time is newer than the cache.
If both a TV-login cache and a cookie file exist, unshackle **prefers the cache**
unless the cookie file is newer. unshackle reads cookies fresh every run and never
writes them back to the cache. So to deliberately force use of the cookie file,
re-save (re-export) it so its modification time is newer than the cache.
**Query forms (after the `protonvpn:` prefix):**
@@ -513,12 +516,12 @@ is written with `0600` permissions.
| `us:ny` | A server in New York. |
The returned proxy is HTTPS on **port 4443** (or **port 443** for Secure Core servers).
Secure Core and Tor servers are excluded from selection because they are too slow for a
plain proxy. On success, unshackle prints a summary such as `(Name - City): server-host`.
unshackle excludes Secure Core and Tor servers from selection because they are too slow
for a plain proxy. On success, unshackle prints a summary such as `(Name - City): server-host`.
!!! tip "Proton server numbers aren't sequential"
When pinning a server with `protonvpn:deNN`, don't guess a low number. A country may
expose non-contiguous IDs like `#203`, `#813`, and so on. Copy the number from a
When pinning a server with `protonvpn:deNN`, do not guess a low number. A country may
have non-contiguous IDs like `#203`, `#813`, and so on. Copy the number from a
previous run's log line (e.g. `DE#203`) instead.
## Hola
@@ -526,7 +529,7 @@ plain proxy. On success, unshackle prints a summary such as `(Name - City): serv
Hola requires no configuration at all. It uses the
[`hola-proxy`](https://github.com/Snawoot/hola-proxy) binary, and **auto-loads** whenever
that binary is found on your `PATH`. If you query Hola but the binary is missing, the
provider raises an error telling you to install it.
proxy provider raises an error that tells you to install it.
```shell title="Install hola-proxy, then just query a country"
unshackle dl --proxy hola:us EXAMPLE 81234567
@@ -542,7 +545,7 @@ for available proxies in that country and picks one at random, returning an HTTP
## Surfshark
Surfshark, like NordVPN, uses **Service Credentials** rather than your login. Generate
Surfshark, like NordVPN, uses **Service Credentials** rather than your login. Make
them from the [Surfshark manual-setup page](https://my.surfshark.com/vpn/manual-setup/main/openvpn).
```yaml title="unshackle.yaml"
@@ -571,8 +574,8 @@ The returned proxy is HTTPS on **port 443**.
## Windscribe
Windscribe uses **Service Credentials** from its
[OpenVPN config page](https://windscribe.com/getconfig/openvpn). Only the presence of a
username and password is checked (there is no 48-character rule).
[OpenVPN config page](https://windscribe.com/getconfig/openvpn). unshackle only makes
sure that a username and a password are present (there is no 48-character rule).
```yaml title="unshackle.yaml"
proxy_providers:
@@ -598,11 +601,11 @@ proxy_providers:
The returned proxy is HTTPS on **port 443**.
!!! warning "Windscribe is not loaded by the REST API resolver"
The standalone Windscribe provider (and Gluetun) are loaded by the `dl` command, but
**not** by the proxy resolver used by the REST API and remote-download client. If you
drive downloads through the API and need Windscribe
or Gluetun exits, resolve the proxy on the CLI side or use a different provider for API
jobs. See the developer note below.
The `dl` command loads the standalone Windscribe proxy provider (and Gluetun), but
the proxy resolver that the REST API and the remote-download client use does **not**.
If you drive downloads through the API and need Windscribe or Gluetun exits, get the
proxy on the CLI side, or use a different proxy provider for API jobs. See the
developer note below.
## Developer notes
@@ -618,22 +621,22 @@ The returned proxy is HTTPS on **port 443**.
Both paths accept the same query grammar (a direct URI, a `provider:query`, or a bare
country query tried against providers in order), but the set of available providers
differs. Keep this in mind when adding a provider: wiring it into `dl.py` does not make
it available over the API, and vice versa.
differs. Keep this in mind when you add a proxy provider: an entry in `dl.py` does not
make it available over the API, and the opposite is also true.
!!! note "Implementing a new provider (developers)"
Every provider subclasses `unshackle.core.proxies.proxy.Proxy` and implements three
Every proxy provider subclasses `unshackle.core.proxies.proxy.Proxy` and has three
methods:
- `__init__(self, **kwargs)` receives its `proxy_providers` sub-dict splatted as
keyword arguments, so YAML keys map 1:1 to constructor parameters. Do authentication
and catalog pre-fetching here.
and catalogue pre-fetching here.
- `__repr__(self)` is a human summary, conventionally `"{n} Countries ({m} Servers)"`.
- `get_proxy(self, query)` returns a Requests-compatible proxy URI, or `None` only
when the query was valid but no proxy exists. Raise on genuine errors or bad queries.
Optionally expose `get_connection_info(query)` (as Gluetun does) or
`last_connection_display()` (as ExpressVPN and Proton VPN do) to have `dl` print a
Optionally add `get_connection_info(query)` (as Gluetun does) or
`last_connection_display()` (as ExpressVPN and Proton VPN do) to make `dl` print a
friendly connection line instead of the raw proxy URL.
## See also
+69 -67
View File
@@ -1,20 +1,21 @@
# Subtitles
Subtitles are first-class tracks in unshackle. When you download a title, subtitle
tracks are parsed from the manifest alongside video and audio, downloaded, cleaned up,
optionally converted to the format you want, and then either muxed into the final
Matroska file or written out as sidecar files next to it. The sections below cover the
supported formats, how tracks are normalized after download, how to select and filter
subtitles, and every subtitle-related configuration option.
Subtitles are first-class tracks in unshackle. When you download a title, unshackle
parses the subtitle tracks from the manifest alongside video and audio. It downloads
them, cleans them up, and optionally converts them to the format you want. It then muxes
them into the final Matroska file, or writes them out as sidecar files next to it. The
sections below cover the supported formats, how unshackle normalizes tracks after
download, how to select and filter subtitles, and every subtitle-related configuration
option.
Most of this is controlled with a handful of `dl` flags and a small `subtitle` block in
your [configuration file](../getting-started/configuration-file.md). See
A handful of `dl` flags and a small `subtitle` block in your
[configuration file](../getting-started/configuration-file.md) control most of this. See
[Downloading](downloading.md) for how subtitle flags fit into a full download command.
## Supported formats
unshackle understands the following subtitle codecs. The **Format** column is the value
shown in track listings; the **Extension** column is the file extension used on disk.
shown in track listings. The **Extension** column is the file extension used on disk.
| Codec (internal name) | Format | Extension | Notes |
| --- | --- | --- | --- |
@@ -36,30 +37,30 @@ shown in track listings; the **Extension** column is the file extension used on
unwrapped into their plain counterparts (`fTTML` becomes TTML and `fVTT` becomes
WebVTT), so you always end up with an editable text subtitle. Note that this is a
one-way, source-only relationship: you can convert *from* these boxed formats, but no
backend can produce them as an output target, so they can never be a conversion
backend can write them as an output target, so they can never be a conversion
destination.
## What happens to a subtitle after download
A subtitle download does more than save a file. Once the bytes are on disk, unshackle runs
format-appropriate clean-up to produce a single, valid file:
format-appropriate clean-up to make a single, valid file:
- **Encoding**: non-boxed subtitles are forced to UTF-8, and stray `&lrm;` / `&rlm;`
bidirectional entities are unescaped.
- **Encoding**: unshackle forces non-boxed subtitles to UTF-8, and unescapes stray
`&lrm;` / `&rlm;` bidirectional entities.
- **Segment merging**: when a WebVTT or boxed subtitle arrives as many DASH/HLS
segments, the segments are stitched back into one continuous document with correct
timing.
segments, unshackle merges the segments back into one continuous document with
correct timing.
- **WebVTT sanitization**: merged WebVTT runs through a repair pipeline that clamps
negative timestamps to `00:00:00.000`, removes stray cue identifiers (like `Q0`, `Q1`)
that confuse downstream parsers, and merges multi-line cues that a service split into
separate overlapping cues. By default the original styling and layout are preserved
separate overlapping cues. By default unshackle keeps the original styling and layout
(see [`preserve_formatting`](#subtitle-configuration)).
- **TTML for muxing**: Matroska cannot carry TTML. If you do not request a specific
output format, any plain TTML track is automatically converted to WebVTT before
output format, unshackle automatically converts any plain TTML track to WebVTT before
muxing (the closest lossless equivalent). Boxed `fTTML` is likewise unwrapped to TTML
first.
You normally do not need to think about any of this. It just produces a clean file.
You normally do not need to think about any of this. It gives a clean file.
## Selecting and filtering subtitles
@@ -86,7 +87,7 @@ To narrow it down, pass a comma-separated list of language tags:
unshackle dl --s-lang en,es EXAMPLE 81234567
```
Language matching is fuzzy by default: `en` will match `en-US`, `en-GB`, and similar
Language matching is fuzzy by default: `en` matches `en-US`, `en-GB`, and similar
variants. If a requested language is missing but others were found, unshackle continues
with whatever remains and tells you which languages it dropped.
@@ -100,9 +101,9 @@ unshackle dl --s-lang es-419 --exact-lang EXAMPLE 'https://www.example.com/...'
### Requiring languages before downloading
`--require-subs` is a gate rather than a filter. It downloads **all** available
subtitles, but only if every language you list is present; if any is missing the title
errors out. This is useful for batch jobs where a title without your must-have language
should fail loudly rather than silently produce a partial result.
subtitles, but only if every language you name is present. If any is missing, the title
errors out. This is useful for batch jobs where you want a title without your must-have
language to fail loudly, and not to silently give a partial result.
```shell title="Fail unless both English and French subs exist, then grab everything"
unshackle dl --require-subs en,fr EXAMPLE B0ABCDEFGH
@@ -115,7 +116,7 @@ unshackle dl --require-subs en,fr EXAMPLE B0ABCDEFGH
### Forced subtitles
Forced tracks carry only the important on-screen signs and foreign-dialogue lines meant
to display even when you are watching in your own language. They are **excluded by
to display even when you watch in your own language. They are **excluded by
default**. Add `-fs` / `--forced-subs` to include them:
```shell title="Include forced tracks alongside full subtitles"
@@ -139,17 +140,17 @@ track, same as plain `-fs`).
Every subtitle track carries flags describing what kind of captions it holds. These
flags drive selection, sort order, output filenames, and the Matroska track flags.
- **Forced**: only signs and foreign dialogue; meant to play when the audio language
matches. Off by default; enable with `--forced-subs`.
- **SDH** (Subtitles for the Deaf and Hard-of-Hearing; also called HOH): a full
- **Forced**: only signs and foreign dialogue, meant to play when the audio language
matches. Off by default. Enable it with `--forced-subs`.
- **SDH** (Subtitles for the Deaf and Hard-of-Hearing, also called HOH): a full
transcript that includes both dialogue and non-speech sound cues.
- **CC** (Closed Captions): captions originating from an EIA-608/708 broadcast stream,
often uppercase with `>>>` speaker markers and sound cues.
!!! note "SDH and CC are treated together for sorting and stripping"
unshackle orders subtitles from fewest to most captions: **Forced → Normal → SDH/CC**.
For the purposes of hearing-impaired handling, SDH and CC tracks are grouped as the
"most captions" variant.
For hearing-impaired handling, unshackle groups SDH and CC tracks as the "most
captions" variant.
By default this type order is the primary sort, so all forced tracks come first, then
all normal tracks, then all SDH/CC tracks, each block sorted by language. To keep each
@@ -158,14 +159,14 @@ flags drive selection, sort order, output filenames, and the Matroska track flag
[`subtitle.type_priority`](../reference/configuration/download.md#subtitle) to change the
Forced → Normal → SDH/CC order itself.
A single track cannot be both CC and SDH, and a forced track cannot also be flagged
CC or SDH. These combinations are rejected as invalid.
A single track cannot be both CC and SDH, and a forced track cannot also carry the CC or
SDH flag. unshackle rejects these combinations as invalid.
### Closed captions embedded in video
Some services do not ship subtitles as separate tracks but instead embed EIA-608/708
captions inside the video stream. After a video track is downloaded and decrypted,
unshackle probes it for these captions and, when found, extracts them into a SubRip
captions inside the video track. After unshackle downloads and decrypts a video track,
it probes the track for these captions and, when it finds them, extracts them into a SubRip
(`SRT`) subtitle flagged as CC. This runs automatically unless you disabled subtitles
(`--no-subs`) or video (`--no-video` / `--video-only` semantics), and it requires the
CCExtractor tool on your `PATH`.
@@ -173,7 +174,7 @@ CCExtractor tool on your `PATH`.
## Automatic SDH stripping
When a language has an SDH track but **no** plain (non-SDH, non-forced) track, unshackle
can generate a clean non-SDH version for you by stripping the hearing-impaired cues:
can make a clean non-SDH version for you by stripping the hearing-impaired cues:
sound effects, speaker labels, music notes, and similar. This is on by default and
controlled by the [`strip_sdh`](#subtitle-configuration) config key.
@@ -182,9 +183,9 @@ example an `en-GB` non-SDH track when the SDH track is `en-US`), no stripped cop
created. When a stripped copy *is* created, the original SDH track is kept as well, so
you end up with both.
The stripping engine itself is chosen by the [`sdh_method`](#subtitle-configuration)
config key. The default (`auto`) prefers the `subby` cleaner for SRT, falls back to
SubtitleEdit when it is installed, and otherwise uses the built-in `filter-subs`
The [`sdh_method`](#subtitle-configuration) config key selects the stripping engine
itself. The default (`auto`) prefers the `subby` cleaner for SRT, falls back to
SubtitleEdit when you install it, and otherwise uses the built-in `filter-subs`
approach.
!!! note "Why `convert_before_strip` exists"
@@ -192,7 +193,7 @@ approach.
silently without stripping anything, not an error but a no-op that leaves the SDH
cues in place. This is the whole reason for [`convert_before_strip`](#subtitle-configuration):
non-SubtitleEdit engines need the subtitle converted to SRT first, or the
hearing-impaired cues survive the strip untouched.
hearing-impaired cues stay in the file.
## Converting subtitle formats
@@ -218,11 +219,11 @@ Conversion only runs when the source format differs from the target, so
!!! warning "Down-converting styled subtitles is lossy"
Converting styled SSA/ASS subtitles to SRT throws away positioning, colours, and
italics. unshackle never does this automatically. It only happens when you
explicitly ask for it via `--sub-format`. If you want to preserve styling, convert
explicitly ask for it with `--sub-format`. If you want to preserve styling, convert
to a format that supports it (or keep `original`).
Note that this automatic protection guards **only the default muxed track**: there the
conversion is skipped and the styled original is kept. It does **not** cover sidecars.
Note that this automatic protection guards **only the default muxed track**: there
unshackle skips the conversion and keeps the styled original. It does **not** cover sidecars.
A `sidecar_format: srt` will still lossily flatten styled subtitles, dropping
positioning, colours, and styling, and a per-download `--sub-format srt` forces the
muxed track to convert as well. To keep raw styled sidecars, set
@@ -244,13 +245,13 @@ formats involved:
With `conversion_method: auto`, unshackle ranks these automatically per conversion.
Setting it to a specific value pins that backend as the first choice, falling back to
others only if the pin cannot handle the pair.
others only if the pin cannot convert the pair.
#### What each backend actually preserves
The table below comes from round-tripping a subtitle that carries italics, bold,
underline, positioning, and colour through every backend. Each cell lists the
styling that survives; `—` means the backend cannot handle that pair.
underline, positioning, and colour through every backend. Each cell shows the
styling that survives. `—` means that the backend cannot convert that pair.
| Conversion | `subtitleedit` | `pysubs2` | `subby` | `pycaption` |
| --- | --- | --- | --- | --- |
@@ -273,13 +274,13 @@ Reading the table:
- **`pysubs2`**: keeps inline italic/underline on every pair, and bold except when
writing SRT or WebVTT. It never carries positioning or colour. SSA/ASS is its native
model, which makes it the best pick for SSA↔ASS.
- **`subby`**: reads only WebVTT/fVTT/SAMI (never ASS) and is tuned for → SRT, where it
uniquely converts WebVTT cue settings into `{\an8}` positioning. Its
- **`subby`**: reads only WebVTT/fVTT/SAMI (never ASS), and it is best for → SRT, where
it uniquely converts WebVTT cue settings into `{\an8}` positioning. Its
`CommonIssuesFixer` may also drop near-duplicate cues.
- **`pycaption`**: strips all styling; last-resort fallback only.
- **`pycaption`**: removes all styling. Last-resort fallback only.
!!! tip "What `auto` picks"
`auto` prefers `subtitleedit` first when it is installed, then falls back to `subby`
`auto` prefers `subtitleedit` first when you install it, then falls back to `subby`
for → SRT and `pysubs2` otherwise. The one exception comes from the table above:
for ASS/SSA → TTML or WebVTT it picks `pysubs2` even when SubtitleEdit is
installed, since SubtitleEdit flattens inline styling on those pairs. Most installs
@@ -292,11 +293,11 @@ Reading the table:
it ships subtitles that a particular backend handles best). An explicit
`conversion_method` in `unshackle.yaml` always overrides that per-track preference, so
your config takes precedence. This only matters when a service ships a non-default
preference; leave `conversion_method` at `auto` to let the service's hint stand.
preference. Leave `conversion_method` at `auto` to let the service's hint stand.
## Muxed vs. sidecar output
By default subtitles are muxed **into** the final `.mkv`. You can instead (or
By default unshackle muxes subtitles **into** the final `.mkv`. You can instead (or
additionally) write them as separate sidecar files next to the video, controlled by the
[`output_mode`](#subtitle-configuration) config key:
@@ -307,7 +308,7 @@ additionally) write them as separate sidecar files next to the video, controlled
output_mode: mux
```
Subtitles are embedded in the Matroska file. Nothing is written separately.
unshackle embeds the subtitles in the Matroska file. It writes nothing separately.
=== "sidecar"
@@ -317,8 +318,8 @@ additionally) write them as separate sidecar files next to the video, controlled
sidecar_format: srt
```
Subtitles are written as standalone files and **not** muxed into the video (when
there is a video or audio track to sit beside).
unshackle writes the subtitles as standalone files and does **not** mux them into
the video (when there is a video or audio track to sit beside).
=== "both"
@@ -328,10 +329,10 @@ additionally) write them as separate sidecar files next to the video, controlled
sidecar_format: srt
```
Subtitles are muxed into the video **and** written as sidecar files.
unshackle muxes the subtitles into the video **and** writes them as sidecar files.
Sidecar files are named after the output file with the language and flags encoded in the
name, so players and media managers can match them automatically:
unshackle names sidecar files after the output file, with the language and flags encoded
in the name, so players and media managers can match them automatically:
```text
Show.S01E01.1080p.WEB.en.srt
@@ -339,9 +340,10 @@ Show.S01E01.1080p.WEB.en.forced.srt
Show.S01E01.1080p.WEB.en.sdh.srt
```
The pattern is `{base}.{language}[.forced][.sdh].{extension}`. The sidecar file format
is set by [`sidecar_format`](#subtitle-configuration); use a format name like `srt` or
`vtt`, or `original` to keep each subtitle in its downloaded format.
The pattern is `{base}.{language}[.forced][.sdh].{extension}`. The
[`sidecar_format`](#subtitle-configuration) config key sets the sidecar file format. Use
a format name like `srt` or `vtt`, or `original` to keep each subtitle in its downloaded
format.
## Fonts for styled subtitles
@@ -353,7 +355,7 @@ relevant font packages.
## Subtitle configuration
All of the behaviour above can be defaulted in the `subtitle` block of your
You can set defaults for all of the behaviour above in the `subtitle` block of your
[configuration file](../getting-started/configuration-file.md):
```yaml title="unshackle.yaml"
@@ -381,7 +383,7 @@ subtitle:
!!! tip "SubtitleEdit is optional but recommended"
Several high-fidelity paths (the best-quality conversions and the SubtitleEdit SDH
stripper) only activate when the SubtitleEdit CLI is installed and on your `PATH`.
stripper) only activate when you install the SubtitleEdit CLI and put it on your `PATH`.
See [Installation](../getting-started/installation.md) for how to add it. Without it,
unshackle falls back to its built-in Python backends.
@@ -397,23 +399,23 @@ The subtitle model lives in `unshackle.core.tracks.Subtitle`. A few internals wo
knowing when working with subtitle tracks in code:
- **Flags and validation**: `Subtitle(codec=..., cc=..., sdh=..., forced=...)`. A track
cannot be both `cc` and `sdh`, and `forced` cannot be combined with either; both raise
cannot be both `cc` and `sdh`, and you cannot combine `forced` with either. Both raise
`ValueError` at construction.
- **`convert(codec, *, forced=False)`**: converts the downloaded file in place through
the backend chain in `subtitle_convert.run_conversion`. `forced=True` (what
`--sub-format` sets) is the only way lossy styled down-converts such as
ASS→SRT are permitted. Raises `NotImplementedError` if no backend supports the pair and
`--sub-format` sets) is the only way to permit lossy styled down-converts such as
ASS→SRT. Raises `NotImplementedError` if no backend supports the pair and
`RuntimeError` if all attempts fail.
- **`strip_hearing_impaired()`**: removes SDH cues using the engine selected by
`config.subtitle["sdh_method"]`.
- **`reverse_rtl()`**: fixes right-to-left sentence-ending positioning via SubtitleEdit
- **`reverse_rtl()`**: fixes right-to-left sentence-ending positioning with SubtitleEdit
(requires the binary).
- **`parse(data, codec)`**: the central parser, returning a `pycaption.CaptionSet`; it
- **`parse(data, codec)`**: the central parser, returning a `pycaption.CaptionSet`. It
handles the boxed DASH formats, SAMI, and the WebVTT repair helpers.
- **`extract_fonts(text)`**: returns the set of font names referenced by an ASS/SSA
subtitle, used to decide which fonts to attach at mux time.
The conversion backends (`SubtitleEditBackend`, `SubbyBackend`, `Pysubs2Backend`,
`PycaptionBackend`) each implement a small protocol (`is_available()`, `can_convert()`,
`rank()`, `convert()`) and are tried in rank order, lowest rank first. See
`PycaptionBackend`) each have a small protocol (`is_available()`, `can_convert()`,
`rank()`, `convert()`), and unshackle tries them in rank order, lowest rank first. See
[the Tracks API reference](../reference/api/tracks.md) for the full model.
+45 -45
View File
@@ -1,12 +1,12 @@
# Troubleshooting & Debugging
When a download fails, a service misbehaves, or a key won't resolve, unshackle gives you three tools to find out why:
When a download fails, a service misbehaves, or unshackle cannot get a content key, unshackle gives you three tools to find out why:
- **Debug logging**: verbose `DEBUG`-level console output plus a structured, machine-readable log file.
- **[`unshackle env`](#checking-your-environment)**: verify that the external tools unshackle depends on are installed, and see exactly which directories and config file are in use.
- **[`unshackle env`](#checking-your-environment)**: make sure that unshackle finds the external tools it depends on, and see exactly which directories and config file are in use.
- **Targeted toggles**: `debug_keys` and `debug_requests` for the two areas (DRM keys and raw HTTP traffic) that are deliberately quiet by default.
The sections below cover how to enable each of them, how to read what they produce, and which diagnostic fits which failure mode.
The sections below cover how to enable each of them, how to read what they give, and which diagnostic fits which failure mode.
---
@@ -38,27 +38,27 @@ There are two independent ways to enable debug logging, and they do slightly dif
debug: true
```
or via the [`cfg`](cli-reference.md#cfg) command:
or with the [`cfg`](cli-reference.md#cfg) command:
```console
unshackle cfg debug true
```
!!! note "`debug: true` is not identical to `-d`"
The `debug` config key enables the **JSON debug log file**, but it does **not** raise the console to `DEBUG` level and does not add source-path annotations. Those only happen when you pass the `-d` flag. Use the flag when you want to *watch* the verbose output live; use the config key when you just want the log file written every time. Passing `-d` is the most complete option, and you can combine both.
The `debug` config key enables the **JSON debug log file**, but it does **not** raise the console to `DEBUG` level and does not add source-path annotations. Those only happen when you pass the `-d` flag. Use the flag when you want to *watch* the verbose output live. Use the config key when you want only the log file written every time. Passing `-d` is the most complete option, and you can combine both.
!!! warning "Debug logging can be noisy and revealing"
Debug output is verbose and includes request metadata, service internals, and stack traces. Secrets, URLs, and local paths are automatically redacted (see [Redaction](#what-gets-redacted)), but you should still glance over a log before sharing it publicly.
Debug output is verbose and includes request metadata, service internals, and stack traces. unshackle automatically redacts secrets, URLs, and local paths (see [Redaction](#what-gets-redacted)), but you should still read a log before you share it publicly.
---
## The structured JSON debug log
Beyond the console output, unshackle writes a structured log in [JSON Lines](https://jsonlines.org/) format: one complete JSON object per line. This file is designed to be filtered, searched, and analysed with standard tooling (`jq`, `grep`, a text editor), and it is safe to attach to a bug report because it is redacted.
Beyond the console output, unshackle writes a structured log in [JSON Lines](https://jsonlines.org/) format: one complete JSON object per line. You can filter, examine, and analyse this file with standard tooling (`jq`, `grep`, a text editor). It is safe to attach to a bug report, because unshackle redacts it.
### Where it is written
The debug log is created during a `dl` run (and by [`import`](cli-reference.md#import), which wraps `dl`). The path is:
unshackle makes the debug log during a `dl` run, and during [`import`](cli-reference.md#import), which wraps `dl`. The path is:
```
<logs directory>/unshackle_debug_<service>_<timestamp>.jsonl
@@ -67,19 +67,19 @@ The debug log is created during a `dl` run (and by [`import`](cli-reference.md#i
For example, `unshackle_debug_EXAMPLE_20260703-142530.jsonl`. The `<service>` tag and the timestamp mean each download gets its own file, so runs never overwrite each other.
!!! tip "Find your logs directory"
The logs directory defaults to a `logs` folder inside the package data directory, but it can be relocated with the `directories.logs` config key. To see the exact resolved path on your machine, run [`unshackle env info`](#env-info) and read the **Directories** table.
The logs directory defaults to a `logs` folder inside the package data directory, but you can move it with the `directories.logs` config key. To see the exact resolved path on your machine, use [`unshackle env info`](#env-info) and read the **Directories** table.
### What a log entry looks like
Every line is a self-contained JSON object. The first line of a session records the environment:
Every line is a self-contained JSON object. The first line of each log file records the environment:
```json
{"timestamp":"2026-07-03T14:25:30.123456+00:00","session_id":"a1b2c3d4","level":"INFO","operation":"session_start","message":"Debug logging session started","context":{"unshackle_version":"5.3.0","unshackle_code_hash":"1d22a1e","python_version":"3.12.4 ...","platform":"Linux-6.18.5-x86_64","platform_system":"Linux","platform_release":"6.18.5"}}
```
Quote `unshackle_code_hash` in a bug report. It fingerprints the framework source you are running, so it tells us the exact code state even when you did not install from git. A hash that matches no release means the source was edited locally.
Quote `unshackle_code_hash` in a bug report. It fingerprints the framework source you operate, so it tells us the exact code state even when you did not install from git. A hash that matches no release means that someone edited the source locally.
Subsequent lines describe operations: service calls, DRM licence requests, vault lookups, downloader progress, and errors. Common fields include:
Subsequent lines record the operations: service calls, DRM licence requests, vault lookups, downloader progress, and errors. Common fields include:
| Field | Meaning |
|---|---|
@@ -113,13 +113,13 @@ jq -r 'select(.error) | .error.traceback[]' unshackle_debug_EXAMPLE_20260703-142
jq 'select(.operation | startswith("drm_"))' unshackle_debug_EXAMPLE_20260703-142530.jsonl
```
The session's binary-tool versions (Shaka Packager, mp4decrypt, mkvmerge, FFmpeg, FFprobe) are logged near the start of a `dl` run, which is often the fastest way to confirm a muxing or decryption failure is really a wrong-version problem.
unshackle logs the binary-tool versions (shaka-packager, mp4decrypt, mkvmerge, FFmpeg, FFprobe) near the start of a `dl` run. Often that is the fastest way to make sure that a muxing or decryption failure is really a wrong-version problem.
---
## Debugging DRM keys with `debug_keys`
By default, content-decryption **keys are redacted** in the JSON debug log. Any field that looks like a key is written as `[REDACTED]`. This keeps logs shareable. When you are specifically debugging a key-retrieval or decryption problem and need to see the actual KID/KEY values that were fetched, enable `debug_keys`:
By default, unshackle redacts the **content keys** in the JSON debug log. It writes any field that looks like a content key as `[REDACTED]`. This keeps logs shareable. When you specifically debug a key-retrieval or decryption problem and need to see the actual KID/KEY values unshackle fetched, enable `debug_keys`:
```yaml
debug_keys: true
@@ -129,10 +129,10 @@ debug_keys: true
unshackle cfg debug_keys true
```
With `debug_keys: true`, the debug log stops masking key fields, so you can confirm which keys came back from a licence server or vault and compare them against what decryption expected.
With `debug_keys: true`, the debug log stops masking content key fields. You can then see which content keys came back from a licence server or vault, and compare them against what decryption expected.
!!! warning "Keys are sensitive, turn this back off"
`debug_keys` writes real decryption keys into the log file in cleartext. Only enable it while actively diagnosing a key issue, never share a log produced with it on, and set it back to `false` when you're done. Note that `debug_keys` only affects the **JSON debug log file**. It does not change what the console prints.
`debug_keys` writes real content keys into the log file in cleartext. Only enable it while you diagnose a content key problem. Never share a log written with it on, and set it back to `false` when you are done. Note that `debug_keys` only affects the **JSON debug log file**. It does not change what the console prints.
Fields that are *always* redacted regardless of `debug_keys` (anything whose name contains `password`, `token`, `secret`, `auth`, or `cookie`) stay masked, so credentials never leak even in a keys-on log.
@@ -142,7 +142,7 @@ Fields that are *always* redacted regardless of `debug_keys` (anything whose nam
When you pass `-d`, unshackle raises its own modules to `DEBUG` but deliberately **silences** the noisy low-level HTTP libraries (`urllib3`, `requests`, `rnet`, `httpx`, `httpcore`, `hpack`, `h2`) back down to `WARNING`. Without this, a single download would bury the useful output under thousands of connection-pool and HTTP/2 frame messages.
If you need that low-level traffic (for example when a request is failing at the TLS or connection layer and you need to see the underlying library's own logs), enable `debug_requests`:
Sometimes a request fails at the TLS or connection layer, and you need the underlying library's own logs. If you need that low-level traffic, enable `debug_requests`:
```yaml
debug_requests: true
@@ -152,34 +152,34 @@ debug_requests: true
unshackle cfg debug_requests true
```
With `debug_requests: true`, those libraries are left at `DEBUG` level when you run with `-d`, so their request/response chatter appears in the console.
With `debug_requests: true`, unshackle leaves those libraries at `DEBUG` level when you operate it with `-d`, so their request/response chatter appears in the console.
!!! note "`debug_requests` needs `-d` to have any effect"
The `debug_requests` toggle only decides whether the HTTP libraries are silenced when the `-d` flag is active. If you are not running with `-d` (verbose console logging), it changes nothing. Combine them: `unshackle -d dl ...` with `debug_requests: true` in config.
The `debug_requests` toggle only decides whether unshackle silences the HTTP libraries when the `-d` flag is active. If you do not operate unshackle with `-d` (verbose console logging), it changes nothing. Combine them: `unshackle -d dl ...` with `debug_requests: true` in config.
---
## What gets redacted
unshackle redacts logged strings so that debug output is safe to share. Three passes are applied to every logged message, error, and structured value:
unshackle redacts logged strings so that debug output is safe to share. It applies three passes to every logged message, error, and structured value:
- **Secrets**: passwords, tokens, API secrets, auth values, and cookies are replaced with `[REDACTED]`.
- **URLs**: every `http(s)` URL is collapsed to `redacted` (keeping only the file extension, so a manifest still shows as `redacted.mpd` and a segment as `redacted.m4s`). This hides content, CDN, manifest, and licence-server locations while preserving the *type* of resource.
- **Local paths**: install-root, virtualenv, and home-directory prefixes are replaced with tokens, so your username and machine layout don't appear in logs.
- **Secrets**: unshackle replaces passwords, tokens, API secrets, auth values, and cookies with `[REDACTED]`.
- **URLs**: unshackle collapses every `http(s)` URL to `redacted`. It keeps only the file extension, so a manifest still shows as `redacted.mpd` and a segment as `redacted.m4s`. This hides the CDN, manifest, segment, and licence-server locations, but keeps the *type* of resource.
- **Local paths**: unshackle replaces install-root, virtualenv, and home-directory prefixes with tokens, so your username and machine layout do not appear in logs.
Path redaction is controlled by the `redact_paths` config key, which is **on by default**:
The `redact_paths` config key controls path redaction, and it is **on by default**:
```yaml
redact_paths: true # default - mask local base directories in logged paths
```
Set `redact_paths: false` if you are debugging a path problem locally and want to see full, unmasked paths in the console and logs. Secret and URL redaction in the JSON debug log are always applied and are not affected by this toggle.
Set `redact_paths: false` if you debug a path problem locally and want to see full, unmasked paths in the console and logs. unshackle always applies secret and URL redaction in the JSON debug log, and this toggle does not change that.
---
## Checking your environment
Many "download failed" problems are really missing external tools or a config file that isn't where you think it is. The [`unshackle env`](cli-reference.md#env) command group answers both questions before you dig into logs.
Many "download failed" problems are really missing external tools or a config file that is not where you think it is. The [`unshackle env`](cli-reference.md#env) command group answers both questions before you dig into logs.
### `env check`
@@ -189,7 +189,7 @@ Many "download failed" problems are really missing external tools or a config fi
unshackle env check
```
It reports each tool's status (`✓` installed / `✗` missing), whether it is **required** or optional, and what it's for. A summary line tells you the installed-vs-total count and, if anything mandatory is absent, exactly which required tools are missing.
It reports each tool's status (`✓` installed / `✗` missing), whether it is **required** or optional, and what it is for. A summary line tells you the installed-vs-total count and, if anything mandatory is absent, exactly which required tools are missing.
The required tools (unshackle will not get far without them) are:
@@ -204,20 +204,20 @@ The required tools (unshackle will not get far without them) are:
Optional tools unlock extra capabilities. For example, `mp4decrypt` (alternative decryptor), `dovi_tool` / `HDR10Plus_tool` (Dolby Vision / HDR10+), `SubtitleEdit` / `CCExtractor` (subtitle conversion and CC extraction), `MPV` / `FFplay` (playback preview), and `git` (service repositories) or `docker` (Gluetun VPN, see [Proxies & VPN](proxies-and-vpn.md)).
!!! tip "First thing to run when something breaks"
If a decryption step errors, a mux fails, or a Dolby Vision title won't process, run `unshackle env check` first. A red `✗` next to the relevant tool is the answer far more often than a bug in unshackle.
If a decryption step errors, a mux fails, or a Dolby Vision title will not process, use `unshackle env check` first. A red `✗` next to the relevant tool is the answer far more often than a bug in unshackle.
### `env info`
`unshackle env info` shows where unshackle is reading configuration from and where every working directory lives:
`unshackle env info` shows where unshackle reads configuration from and where every working directory lives:
```console
unshackle env info
```
- If a config file was found, it prints the path it was **loaded from**. If none was found, it lists every location it *would* accept one (see [Config file discovery](#why-is-my-config-not-being-picked-up) below), so you can create the file in the right place.
- If unshackle found a config file, it prints the path it was **loaded from**. If it found none, it shows every location it *would* accept one (see [Config file discovery](#why-is-my-config-not-being-picked-up) below). You can then make the file in the right place.
- It then prints a **Directories** table for every configured path: `downloads`, `temp`, `cache`, `cookies`, `logs`, `exports`, `wvds`, `prds`, the `services` search paths, and more.
This is the authoritative way to answer "where are my logs / downloads / cookies actually going?" The defaults are relative to the installed package, and any `directories:` overrides in your config are reflected here.
This is the authoritative way to answer "where do my logs, downloads and cookies go?" The defaults are relative to the installed package. The table also shows any `directories:` overrides from your config.
### `env clear`
@@ -241,17 +241,17 @@ Each command reports how many files it removed and how much space it freed, then
!!! example "A required tool is missing"
**Symptom:** decryption, muxing, or analysis fails early, sometimes with a "binary not found" style error.
**Inspect:** run `unshackle env check` and look for a red `✗` on a **required** row. Install the missing tool and re-run.
**Inspect:** use `unshackle env check` and look for a red `✗` on a **required** row. Install the missing tool and re-run.
!!! example "Decryption produces garbage or fails"
**Symptom:** the download completes but the file won't play, or the decrypt step errors.
**Symptom:** the download completes but the file will not play, or the decrypt step errors.
**Inspect:** enable `debug_keys` and run with `-d`, then check the JSON debug log's `drm_*` and `vault_*` operations to confirm which keys were fetched and from where. Also confirm Shaka Packager (or `mp4decrypt`) shows a sane version in the `download_init` binary-versions entry. See [DRM & CDM](drm-and-cdm.md).
**Inspect:** enable `debug_keys` and operate unshackle with `-d`. Then examine the JSON debug log's `drm_*` and `vault_*` operations to see which content keys unshackle fetched, and from where. Also make sure that shaka-packager (or `mp4decrypt`) shows a sane version in the `download_init` binary-versions entry. See [DRM & CDM](drm-and-cdm.md).
!!! example "A network request fails at the connection or TLS layer"
**Symptom:** timeouts, connection resets, or handshake errors that unshackle's own logs don't fully explain.
**Symptom:** timeouts, connection resets, or handshake errors, and unshackle's own logs do not give the full cause.
**Inspect:** set `debug_requests: true` and run with `-d` to surface the underlying `urllib3` / `rnet` / `httpx` logs. In the JSON debug log, filter for `service_call` operations to see which request failed. If a proxy is involved, cross-check [Proxies & VPN](proxies-and-vpn.md).
**Inspect:** set `debug_requests: true` and operate unshackle with `-d` to show the underlying `urllib3` / `rnet` / `httpx` logs. In the JSON debug log, filter for `service_call` operations to see which request failed. If you use a proxy, also read [Proxies & VPN](proxies-and-vpn.md).
!!! example "The wrong titles/metadata keep coming back"
**Symptom:** a service returns outdated titles even after the source changed.
@@ -261,7 +261,7 @@ Each command reports how many files it removed and how much space it freed, then
!!! example "A config value doesn't seem to apply"
**Symptom:** a setting you edited has no effect.
**Inspect:** run `unshackle env info` to confirm **which** config file is actually loaded, and `unshackle cfg --list` to dump the effective values. It's common to edit a config file in a location unshackle isn't reading from. See below.
**Inspect:** use `unshackle env info` to see **which** config file unshackle loaded, and `unshackle cfg --list` to dump the effective values. It is common to edit a config file in a location unshackle does not read from. See below.
---
@@ -273,19 +273,19 @@ unshackle loads the **first** `unshackle.yaml` it finds, in this order:
2. `unshackle.yaml` in the parent of that directory.
3. `unshackle.yaml` in your OS user-config directory: on Linux `~/.config/unshackle/unshackle.yaml`, on Windows `%LOCALAPPDATA%\unshackle\unshackle.yaml`, on macOS `~/Library/Application Support/unshackle/unshackle.yaml`.
If you have config in more than one of these, an earlier one wins and your edits to a later file are silently ignored. `unshackle env info` prints the path that was actually loaded (or, if none exists, lists all three candidate locations so you can create the file correctly). See [Configuration](cli-reference.md#cfg) for editing values with the `cfg` command.
If you have config in more than one of these, an earlier one wins and unshackle silently ignores your edits to a later file. `unshackle env info` prints the path it loaded (or, if none exists, shows all three candidate locations so you can make the file correctly). See [Configuration](cli-reference.md#cfg) for editing values with the `cfg` command.
---
!!! note "Developer note: structured logging in code"
Service and command code emits entries through the shared `log_event(...)` helper (and the `timed_operation(...)` context manager for timing a block) from `unshackle.core.utilities`. These are no-ops when debug logging is disabled, so they are always safe to leave in place. When adding a new feature, call `log_event("my_feature_event", message="...", context={...})` rather than reimplementing the "is the logger enabled?" guard. The redaction and JSON-serialisation are handled for you, and any field whose name looks like a key/secret/token is redacted automatically (keys only appear when the run has `debug_keys` enabled).
Service and command code sends entries through the shared `log_event(...)` helper (and the `timed_operation(...)` context manager for timing a block) from `unshackle.core.utilities`. These are no-ops when you disable debug logging, so they are always safe to leave in place. When you add a new feature, call `log_event("my_feature_event", message="...", context={...})`. Do not write the "is the logger enabled?" guard again. unshackle does the redaction and the JSON-serialisation for you, and redacts any field whose name looks like a content key, a secret, or a token. Content keys only appear when the run has `debug_keys` enabled.
### Logging conventions when adding entries
The debug log is tuned for **developers troubleshooting pipeline flow** (maximum signal, minimum noise), not for end users. A few conventions keep it that way. They are not enforced by the `log_event` signature, so they are easy to miss and worth stating explicitly.
The debug log is tuned for **developers troubleshooting pipeline flow** (maximum signal, minimum noise), not for end users. A few conventions keep it that way. The `log_event` signature does not enforce them, so they are easy to miss and worth stating explicitly.
!!! tip "Log levels are a flow skeleton, not just severity"
Treat `level` as structure, not decoration. Emit exactly **one `INFO` milestone per pipeline stage** so that `jq 'select(.level=="INFO")'` reads back the whole end-to-end flow of a run. Everything internal goes to `DEBUG`; failures go to `ERROR`. Keeping `INFO` sparse is the whole point. It is the line a developer skims first to see where a run got to before it broke.
Treat `level` as structure, not decoration. Send exactly **one `INFO` milestone per pipeline stage** so that `jq 'select(.level=="INFO")'` reads back the whole end-to-end flow of a run. Everything internal goes to `DEBUG`. Failures go to `ERROR`. A sparse `INFO` is the whole point. It is the line a developer skims first to see where a run got to before it broke.
```console
# Read the pipeline's high-level flow, one line per stage
@@ -293,13 +293,13 @@ The debug log is tuned for **developers troubleshooting pipeline flow** (maximum
```
!!! warning "No raw dumps"
Never log a full `Tracks` object, an MPD/manifest body, or a response payload. Log only **counts, ids, sizes, and `safe_display_url(url)`**. This keeps the log low-noise and prevents leaking content or manifest data into the file *before* redaction even runs. Redaction is a backstop, not a licence to dump raw objects.
Never log a full `Tracks` object, an MPD/manifest body, or a response payload. Log only **counts, ids, sizes, and `safe_display_url(url)`**. This keeps the log low-noise and prevents leaking title or manifest data into the file *before* redaction even runs. Redaction is a backstop, not a licence to dump raw objects.
!!! note "Operation names: `<area>_<event>`, no registry"
Operation names are plain lowercase strings following an `<area>_<event>` convention: `manifest_dash_parse`, `drm_decrypt`, `vault_get_key`, `tool_run`. They are written inline at each call site on purpose: there is **no central registry or enum**, and you should not go hunting for one or create one. The absence is a deliberate design choice, not an omission.
Operation names are plain lowercase strings that follow an `<area>_<event>` convention: `manifest_dash_parse`, `drm_decrypt`, `vault_get_key`, `tool_run`. We write them inline at each call site on purpose. There is **no central registry or enum**. Do not look for one, and do not make one. The absence is a deliberate design choice, not an omission.
!!! note "Message reads alone; data lives in fields"
Give every entry a single-sentence `message` that stands on its own, and put all structured data (`context`, counts, ids, `duration_ms`) in **fields** rather than baking it into the prose. This separation is what lets the log be both human-scannable and machine-filterable; a message like `"Parsed DASH manifest"` with `context={"video": 4, "audio": 2}` beats `"Parsed manifest with 4 video and 2 audio tracks"`.
Give every entry a single-sentence `message` that stands on its own. Put all structured data (`context`, counts, ids, `duration_ms`) in **fields**, not in the prose. This separation lets the log be both human-scannable and machine-filterable. A message like `"Parsed DASH manifest"` with `context={"video": 4, "audio": 2}` beats `"Parsed manifest with 4 video and 2 audio tracks"`.
!!! tip "Let tool runs log themselves"
For external-tool calls (FFmpeg, mkvmerge, `dovi_tool`, and friends), route through `run_step` / `ffprobe`, which auto-emit a `tool_run` entry capturing the binary's version, return code, and duration consistently. Only fall back to calling `log_tool_run` manually for a direct `subprocess.run` you genuinely can't route through `run_step`. Auto-logging is the preferred path precisely because it captures those fields the same way every time.
For external-tool calls (FFmpeg, mkvmerge, `dovi_tool`, and friends), route through `run_step` / `ffprobe`, which auto-emit a `tool_run` entry capturing the binary's version, return code, and duration consistently. Only fall back to calling `log_tool_run` manually for a direct `subprocess.run` you genuinely cannot route through `run_step`. Auto-logging is the preferred path precisely because it captures those fields the same way every time.