mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-12 20:15:10 +00:00
docs: apply simplified technical english across docs and docstrings.
This commit is contained in:
+41
-41
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 `‎` / `‏`
|
||||
bidirectional entities are unescaped.
|
||||
- **Encoding**: unshackle forces non-boxed subtitles to UTF-8, and unescapes stray
|
||||
`‎` / `‏` 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user