mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-14 13:05:09 +00:00
Merge branch 'dev' into feat/downloader-speed
Catch the branch up with dev (45 commits) so testers can run the perf work against current dev. Conflict resolutions: - downloaders/requests.py: keep the branch's racing-gated read1, adaptive controller speed reporting and byte-range slice guards; take dev's content-encoding checks (raw reads are skipped on an encoded body) and its rolling SpeedWindow for the no-controller speed fallback. The single-URL ranged-parallel gate now requires no speed limit and no caller Range. - service.py / session.py: keep the branch's TimeoutHTTPAdapter and retry config, raise POOL_MAX_SIZE to 64 so dev's pool-starvation fix survives (pool_block=True stalls threads on a pool smaller than the in-flight count). - docs: dev split configuration.md into per-area pages, so the branch's rnet network option rows moved to reference/configuration/network.md.
This commit is contained in:
@@ -8,7 +8,7 @@ downloading, decryption, muxing, naming) is handled by the unshackle core.
|
||||
!!! info "Who this page is for"
|
||||
This is a **developer guide**. If you only want to *use* services, see
|
||||
[downloading](../guide/downloading.md) and
|
||||
[configuration](../reference/configuration.md). Nothing here is required to run
|
||||
[configuration](../reference/configuration/index.md). Nothing here is required to run
|
||||
the CLI. Service code is deliberately kept separate from the core so it can
|
||||
live in its own private repository.
|
||||
|
||||
@@ -304,11 +304,14 @@ Common constructor fields:
|
||||
|
||||
```python
|
||||
Episode(id_, service, title, season, number, name=None, year=None,
|
||||
language=None, data=None, description=None, air_date=None)
|
||||
language=None, data=None, description=None, air_date=None,
|
||||
part=None)
|
||||
```
|
||||
|
||||
Pass `air_date` for daily/sports content to name by date instead of
|
||||
`SxxExx`. Return a `Series([...])` of episodes.
|
||||
`SxxExx`. Return a `Series([...])` of episodes. Pass `part` only when the
|
||||
service splits one episode into several videos, see
|
||||
[Split episodes](#split-episodes).
|
||||
|
||||
=== "Song"
|
||||
|
||||
@@ -328,6 +331,52 @@ Common constructor fields:
|
||||
framework path to reuse cached results and gracefully fall back to stale data
|
||||
when the API errors. It also applies the per-service `title_map` config.
|
||||
|
||||
#### Split episodes {#split-episodes}
|
||||
|
||||
Some services split one logical episode into several separately playable videos, for
|
||||
example a variety show that ships episode 1 as *Episode 1 (Part 1)*, *(Part 2)* and
|
||||
*(Part 3)*. `Episode.number` stays an `int`. The optional `part` carries the index.
|
||||
|
||||
```python
|
||||
Episode(..., season=1, number=1, part=1, name="The Reckoning")
|
||||
Episode(..., season=1, number=1, part=2, name="The Reckoning")
|
||||
```
|
||||
|
||||
1. `part` counts from 1, contiguous. `part=0` is rejected, and `-w` can only address
|
||||
parts up to 99.
|
||||
2. Every part of one episode shares the same `season` and `number`.
|
||||
3. Each part is a separate `Episode` with its own unique `id_`.
|
||||
4. Unsplit episode: leave `part` unset. Never pass `part=1` for a whole episode.
|
||||
5. Do not put the part in `name` or `number`. Pass a clean `name` (or `None`) and set
|
||||
`part`.
|
||||
6. `part` is a split *episode*, not a split *season*. A service listing *Season 1 Part 2*
|
||||
describes a group of episodes. Keep numbering those in sequence, the way the shipped
|
||||
services do.
|
||||
|
||||
`part` accepts an `int` or a digit string. A value that is not an integer raises
|
||||
`TypeError`. A value of `0` or less raises `ValueError`.
|
||||
|
||||
!!! warning "The framework never infers a part from the episode name"
|
||||
Rule 6 is the reason. Shipped services already use the word "Part" to mean a *season*
|
||||
part, so name-sniffing would misread every one of them. The part has to arrive
|
||||
structurally, as the `part` argument.
|
||||
|
||||
!!! tip "Clean the part out of the name"
|
||||
`Episode` discards any `name` that starts with `Episode <number>`, so a raw
|
||||
`"Episode 1 (Part 2): The Reckoning"` leaves you with no episode name at all. Strip
|
||||
the prefix before you pass it, or pass `None`.
|
||||
|
||||
Once `part` is set, the filename gains a part token (`S01E01.Part.2`, see
|
||||
[Output & naming](../guide/output-and-naming.md#split-episodes)), `-w S01E01.2` selects a
|
||||
single part while a bare `-w S01E01` selects them all, and the `--list-titles` tree labels
|
||||
the episode `01.2`. The REST API reports it as an extra `part` key on the serialized
|
||||
title, described in [Endpoints](rest-api/endpoints.md#post-apilist-titles).
|
||||
|
||||
!!! note "Opt-in and additive"
|
||||
Leaving `part` unset costs you nothing: filenames, sort order, `-w` matching and the
|
||||
API JSON are identical to a plain episode, so a service that does not split episodes
|
||||
has nothing to adopt.
|
||||
|
||||
### `get_tracks`: required
|
||||
|
||||
Given one title, return a `Tracks` object holding `Video`, `Audio`, and
|
||||
|
||||
@@ -43,6 +43,7 @@ Health check. **This is the only route exempt from authentication**: you can cal
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "5.3.0",
|
||||
"code_hash": "1d22a1e",
|
||||
"update_check": {
|
||||
"update_available": false,
|
||||
"current_version": "5.3.0",
|
||||
@@ -51,6 +52,8 @@ Health check. **This is the only route exempt from authentication**: you can cal
|
||||
}
|
||||
```
|
||||
|
||||
`code_hash` fingerprints the framework source the server is running, so it identifies the code itself rather than the release. It is the same value for a git checkout and for an installed package built from that checkout, and it changes if anything under `unshackle/core`, `unshackle/commands`, `unshackle/utils`, or `unshackle/vaults` is edited. Service modules are not included. The value is `null` only when the source files cannot be read.
|
||||
|
||||
If the update check fails (for example, no network), `update_available` and `latest_version` are `null` while `current_version` still reports the installed version.
|
||||
|
||||
| Status | Meaning |
|
||||
@@ -220,6 +223,12 @@ Service-specific CLI options may also be passed as additional keys.
|
||||
|
||||
Each serialized title carries `type` (`"episode"`, `"movie"`, or `"other"`), `name`, `id`, `language`, `description`, `date`, and `cover_url`. Episodes and movies add `year`; episodes additionally add `series_title`, `season`, and `number`.
|
||||
|
||||
!!! info "`part` on a split episode"
|
||||
A service that splits one episode into several separately playable videos reports each
|
||||
one as its own episode. Those titles share a `season` and `number` and add a `part`
|
||||
integer counting from 1. No other title carries the key, so a client that ignores it
|
||||
is unaffected. See [Split episodes](../creating-a-service.md#split-episodes).
|
||||
|
||||
| Status | Error code | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `200` | - | Titles returned. |
|
||||
@@ -233,6 +242,9 @@ Each serialized title carries `type` (`"episode"`, `"movie"`, or `"other"`), `na
|
||||
|
||||
List the video, audio, and subtitle tracks for a title. For series, you can scope to specific episodes.
|
||||
|
||||
!!! tip "Spotting the original-language audio"
|
||||
Each audio track carries `is_original`, which marks the track the default `lang` of `orig` selects. Unshackle resolves it with the same language matcher the downloader uses, so it stays correct for regional variants a client cannot separate from tag text alone (`pt` marks `pt-BR` rather than `pt-PT`). Services need not report a title's language, so every track can legitimately come back `false`.
|
||||
|
||||
**Request body**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
@@ -242,9 +254,10 @@ List the video, audio, and subtitle tracks for a title. For series, you can scop
|
||||
| `wanted` | string | no | Episode/season range (e.g. `"S01E01-S01E03"`). |
|
||||
| `season` | int/string | no | Season number (combined with `episode`). |
|
||||
| `episode` | int/string | no | Episode number (combined with `season`). |
|
||||
| `part` | int/string | no | Part index of a split episode (combined with `season` and `episode`). |
|
||||
| `profile`, `proxy`, `no_proxy`, `cdm_type` | - | no | As on `list-titles`. |
|
||||
|
||||
When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector.
|
||||
When both `season` and `episode` are given, they are combined into a `"{season}x{episode}"` selector. Adding `part` narrows that to `"{season}x{episode}.{part}"`, one part of a [split episode](../creating-a-service.md#split-episodes); without it you get every part of the episode. `part` is read only when `season` and `episode` are both given, and `wanted` takes precedence over all three. To scope a part by range, put it in `wanted` instead (`"S01E01.2"`).
|
||||
|
||||
=== "Single title / movie: Response `200`"
|
||||
|
||||
@@ -262,7 +275,7 @@ When both `season` and `episode` are given, they are combined into a `"{season}x
|
||||
"audio": [
|
||||
{
|
||||
"id": "a-1", "codec": "EC3", "codec_display": "DD+",
|
||||
"bitrate": 640, "channels": "5.1", "language": "en",
|
||||
"bitrate": 640, "channels": "5.1", "language": "en", "is_original": true,
|
||||
"atmos": false, "descriptive": false, "drm": [], "descriptor": "DASH"
|
||||
}
|
||||
],
|
||||
@@ -355,11 +368,12 @@ Create a download job. Requires `service` and `title_id`; every other field is a
|
||||
| `range` | string[] | `["SDR"]` | Dynamic range(s). |
|
||||
| `channels` | number | `null` | Audio channel count. |
|
||||
| `no_atmos` | boolean | `false` | Exclude Atmos tracks. |
|
||||
| `wanted` | string[] | `[]` | Episode/season selectors. |
|
||||
| `wanted` | string[] | `[]` | Episode/season selectors. Accepts the part form, `"S01E01.2"`. |
|
||||
| `latest_episode` | boolean | `false` | Only the newest episode. |
|
||||
| `lang` / `v_lang` / `a_lang` / `s_lang` | string[] | `["orig"]` / `[]` / `[]` / `["all"]` | Language filters. |
|
||||
| `require_subs` | string[] | `[]` | Required subtitle languages. |
|
||||
| `forced_subs` | boolean | `false` | Include forced subtitles. |
|
||||
| `forced_s_lang` | string[] | `[]` | Forced subtitle language(s); implies `forced_subs`. |
|
||||
| `exact_lang` | boolean | `false` | Exact language matching. |
|
||||
| `sub_format` | string | `null` | Output subtitle format. |
|
||||
| `video_only` / `audio_only` / `subs_only` / `chapters_only` | boolean | `false` | Track-type restriction (at most one). |
|
||||
@@ -378,7 +392,8 @@ Create a download job. Requires `service` and `title_id`; every other field is a
|
||||
| `best_available` | boolean | `false` | Fall back to best available. |
|
||||
| `repack` | boolean | `false` | Add REPACK tag. |
|
||||
| `tag` | string | `null` | Release group tag. |
|
||||
| `tmdb_id` / `imdb_id` / `animeapi_id` | - | `null` | External ID overrides for tagging. |
|
||||
| `tmdb_id` / `imdb_id` / `tvdb_id` / `animeapi_id` | - | `null` | External ID overrides for tagging. |
|
||||
| `tvdb_order` | `official`, `dvd`, `absolute`, `alternate`, `regional` | `null` | Renumber episodes to a TVDB season order. Falls back to the `tvdb_order` config option. |
|
||||
| `enrich` | boolean | `false` | Override title/year from external source. |
|
||||
| `output_dir` | string | `null` | Override output directory. |
|
||||
| `no_cache` / `reset_cache` | boolean | `false` | Title cache controls. |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Almost everything about how unshackle behaves is controlled by a single YAML file named `unshackle.yaml`: where it saves downloads, which CDM it uses, your credentials, proxies, filename templates, and more. This page covers where that file lives, how unshackle finds it, the shape of its contents, the directory layout unshackle builds around it, and how to edit values from the command line.
|
||||
|
||||
!!! note "Where the exhaustive key list lives"
|
||||
This page is an overview. For the complete, key-by-key breakdown of every setting, its type, and its default, see the [Configuration Reference](../reference/configuration.md).
|
||||
This page is an overview. For the complete, key-by-key breakdown of every setting, its type, and its default, see the [Configuration Reference](../reference/configuration/index.md).
|
||||
|
||||
## The config filename
|
||||
|
||||
@@ -85,11 +85,11 @@ The top-level keys group loosely into these areas:
|
||||
| Key vaults | `key_vaults`, `vault_timeout` |
|
||||
| Proxies & remote | `proxy_providers`, `remote_services`, `serve`, `services` |
|
||||
| Naming & tagging | `tag`, `output_template`, `chapter_fallback_name` |
|
||||
| External API keys | `tmdb_api_key`, `simkl_client_id`, `ipinfo_api_key` |
|
||||
| External API keys | `tmdb_api_key`, `tvdb_api_key`, `simkl_client_id`, `ipinfo_api_key`, `metadata_providers` |
|
||||
| Behavior & logging | `update_checks`, `redact_paths`, `debug`, `unicode_filenames` |
|
||||
| Paths | `directories`, `filenames` |
|
||||
|
||||
For the full list with types and defaults, see the [Configuration Reference](../reference/configuration.md).
|
||||
For the full list with types and defaults, see the [Configuration Reference](../reference/configuration/index.md).
|
||||
|
||||
!!! warning "Unknown keys are silently ignored"
|
||||
unshackle does not validate your config against a schema. If you misspell a key, it is simply skipped and the default is used. You will not get an error. Double-check key names (and their nesting) if a setting does not seem to apply.
|
||||
@@ -105,7 +105,7 @@ turned into underscores (`--best-available` → `best_available`).
|
||||
dl:
|
||||
lang: [en] # -l en
|
||||
quality: [1080] # -q 1080
|
||||
vcodec: [H265] # -v H265
|
||||
vcodec: [H.265] # -v H.265
|
||||
sub_format: srt # convert subtitles to SRT
|
||||
downloads: 2 # two tracks at once
|
||||
```
|
||||
@@ -125,8 +125,8 @@ services:
|
||||
|
||||
!!! tip "A few keys are named after the flag's internal name"
|
||||
Most keys are obvious, but set these exact ones: `range` (`-r`), `list` (`--list`),
|
||||
`tmdb_id` (`--tmdb`), `imdb_id` (`--imdb`), `no_atmos` (`--noatmos`), and `output_dir`
|
||||
(`-o`). The [Configuration Reference](../reference/configuration.md#dl) has the full list
|
||||
`tmdb_id` (`--tmdb`), `imdb_id` (`--imdb`), `tvdb_id` (`--tvdb`), `no_atmos` (`--noatmos`), and `output_dir`
|
||||
(`-o`). The [Configuration Reference](../reference/configuration/download.md#dl) has the full list
|
||||
and every available key.
|
||||
|
||||
## The directory layout
|
||||
@@ -159,7 +159,7 @@ The directories unshackle uses:
|
||||
| `fonts` | Bundled fonts | Yes |
|
||||
|
||||
!!! note "Some directories cannot be moved"
|
||||
A handful of internal locations are protected: the package root, the core directory, the user-config directory, and the data base. If you list them under `directories`, unshackle silently ignores the override. This is intentional; those paths are tied to where the package is installed.
|
||||
unshackle protects five internal entries: `app_dirs`, `core_dir`, `namespace_dir`, `user_configs`, and `data`. If you list any of them under `directories`, unshackle ignores the override. This is intentional; those paths are tied to where the package is installed.
|
||||
|
||||
### The `services` directory is special
|
||||
|
||||
@@ -168,16 +168,16 @@ Unlike the other entries, `services` is a **list**, and each entry can be either
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
services:
|
||||
- unshackle-dl/services # a GitHub owner/repo shorthand
|
||||
- https://github.com/me/my-services.git
|
||||
- you/your-services # a GitHub owner/repo shorthand
|
||||
- https://example.com/private-services.git
|
||||
- ~/code/local-services # a local folder
|
||||
```
|
||||
|
||||
Entries are searched in the order listed, and **the first source to define a given service tag wins**, so put local folders last if you want them to act as fallbacks rather than overrides. Remote repositories are cloned and periodically updated for you. See [Services](../dev/creating-a-service.md) for the full details on how service discovery and repositories work.
|
||||
unshackle searches entries in the order listed, and **the first source to define a given service tag wins**, so put local folders last if you want them to act as fallbacks rather than overrides. unshackle clones remote repositories on first use and refreshes them at most once a day. See [Creating a Service](../dev/creating-a-service.md) for how service discovery and repositories work.
|
||||
|
||||
## The `filenames` key
|
||||
|
||||
Alongside `directories`, the `filenames` key lets you override the templates unshackle uses when naming its own working and log files (for example the log filename or the temporary chapters file). Most users never need to touch this. The available names and their default templates are listed in the [Configuration Reference](../reference/configuration.md#filenames).
|
||||
Alongside `directories`, the `filenames` key lets you override the templates unshackle uses when naming its own working and log files (for example the log filename or the temporary chapters file). Most users never need to touch this. The available names and their default templates are listed in the [Configuration Reference](../reference/configuration/directories.md#filenames).
|
||||
|
||||
## Editing the config with `unshackle cfg`
|
||||
|
||||
@@ -197,10 +197,10 @@ $ unshackle cfg tag MYGRP
|
||||
$ unshackle cfg cdm.default my_device_l3
|
||||
```
|
||||
|
||||
Values are parsed as Python literals where possible, so numbers, booleans, and lists work naturally, while bare text is treated as a string:
|
||||
unshackle parses the value as a Python literal, so write booleans as `True`/`False` and quote the strings inside a list (`"['en']"`). Anything that is not valid Python literal syntax is stored as a plain string:
|
||||
|
||||
```console
|
||||
$ unshackle cfg update_checks false
|
||||
$ unshackle cfg update_checks False
|
||||
$ unshackle cfg vault_timeout 30
|
||||
```
|
||||
|
||||
@@ -216,13 +216,13 @@ $ unshackle cfg cdm.default --unset
|
||||
$ unshackle cfg --list
|
||||
```
|
||||
|
||||
When it writes, `unshackle cfg` targets the config file that was loaded. If none exists yet, it creates `unshackle.yaml` in your OS user-config directory.
|
||||
When it writes, `unshackle cfg` targets the config file that was loaded. If none exists yet, it creates `unshackle.yaml` inside the `unshackle` package folder (search location 1), not your OS user-config directory. To keep the config outside the package, create the file at the user-config path yourself first, then `unshackle cfg` writes to it.
|
||||
|
||||
!!! warning "Editing with `cfg` strips comments"
|
||||
Because `unshackle cfg` rewrites the whole file when it saves, any comments in `unshackle.yaml` are removed by a write. If you keep important notes as comments, edit the file by hand instead, or keep those notes elsewhere.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Browse the [Configuration Reference](../reference/configuration.md) for every key and default.
|
||||
- Browse the [Configuration Reference](../reference/configuration/index.md) for every key and default.
|
||||
- Set up your first download in [Downloading](../guide/downloading.md).
|
||||
- Learn how services are discovered and updated in [Services](../dev/creating-a-service.md).
|
||||
- Learn how services are discovered and updated in [Creating a Service](../dev/creating-a-service.md).
|
||||
|
||||
@@ -88,8 +88,8 @@ uv run unshackle env check
|
||||
## External tools on your PATH
|
||||
|
||||
unshackle shells out to a number of external command-line programs for media
|
||||
processing and DRM work. It looks for each one first in a `binaries/` folder
|
||||
next to the installed package, and then falls back to your system `PATH`, so
|
||||
processing and DRM work. It looks for each one first in the `binaries/` folder
|
||||
inside the installed package, and then falls back to your system `PATH`, so
|
||||
installing these tools system-wide (or dropping them into that folder) both
|
||||
work.
|
||||
|
||||
@@ -108,7 +108,7 @@ These must be present for core downloading, decryption, and muxing to work:
|
||||
!!! warning "shaka-packager is the default decryptor"
|
||||
unshackle decrypts with shaka-packager unless you explicitly configure a
|
||||
different decryptor. If it is missing, protected downloads will fail at the
|
||||
decryption step. See the [configuration reference](configuration-file.md) for the
|
||||
decryption step. See the [configuration reference](../reference/configuration/drm.md#decryption) for the
|
||||
`decryption` key if you want to switch to `mp4decrypt`.
|
||||
|
||||
### Recommended and optional tools
|
||||
@@ -147,7 +147,7 @@ it depends on.
|
||||
unshackle --help
|
||||
```
|
||||
|
||||
This prints the banner and the list of available commands (`dl`, `env`, `kv`,
|
||||
This prints the list of available commands (`dl`, `env`, `kv`,
|
||||
`serve`, `wvd`, `prd`, `search`, `util`, `cfg`, `import`). If your shell reports
|
||||
that `unshackle` is not found, the `uv tool` bin directory is probably not on
|
||||
your `PATH`. Run `uv tool update-shell` and open a new terminal.
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# Quick Start
|
||||
|
||||
This page takes you from a fresh install to your first downloaded file. You will
|
||||
create the one piece of configuration unshackle insists on, add a streaming
|
||||
service and a CDM so it can actually fetch and decrypt media, run a basic
|
||||
`unshackle dl` command, and learn where the finished file lands.
|
||||
create the one piece of configuration unshackle insists on, supply the service
|
||||
module and CDM it needs to fetch and decrypt media, run a basic `unshackle dl`
|
||||
command, and learn where the finished file lands.
|
||||
|
||||
!!! note "Before you start"
|
||||
This guide assumes unshackle is already installed and on your `PATH`. If
|
||||
`unshackle --help` does not print a help screen, work through
|
||||
[Installation](installation.md) first.
|
||||
|
||||
!!! warning "unshackle includes no services"
|
||||
unshackle handles manifests, DRM, downloading, and muxing. It does not include a
|
||||
module for any streaming platform, so you write that part yourself
|
||||
([step 3](#3-write-a-service)). `EXAMPLE` in this guide stands in for the tag you
|
||||
give yours.
|
||||
|
||||
## 1. Check your environment
|
||||
|
||||
unshackle drives several external tools (FFmpeg, MKVToolNix, shaka-packager, and
|
||||
@@ -58,12 +64,12 @@ Everything else has a sensible default. A minimal, working config looks like thi
|
||||
```yaml title="unshackle.yaml"
|
||||
output_template:
|
||||
movies: "{title} ({year}) {quality} {source}"
|
||||
series: "{title} S{season}E{episode} {episode_name} {quality} {source}"
|
||||
series: "{title} {season_episode} {episode_name?} {quality} {source}"
|
||||
```
|
||||
|
||||
Each `{variable}` is filled in from the title and the tracks you downloaded. The
|
||||
full set of valid variables, including `resolution`, `video`, `audio`, `hdr`,
|
||||
`edition`, `tag`, and more, is documented in [Configuration](configuration-file.md).
|
||||
`edition`, `tag`, and more, is documented in [Output and Naming](../guide/output-and-naming.md).
|
||||
|
||||
!!! note "Spaces or dots?"
|
||||
unshackle auto-detects your naming style from the template: if the separators
|
||||
@@ -77,51 +83,15 @@ full set of valid variables, including `resolution`, `video`, `audio`, `hdr`,
|
||||
rewrites the file and **removes any comments** it contained. If you keep notes
|
||||
in your config, edit the file by hand instead.
|
||||
|
||||
## 3. Add a service
|
||||
## 3. Write a service
|
||||
|
||||
The service tag you pass to `dl` (like `EXAMPLE`) maps to a **service module**, a small
|
||||
plugin that knows how to talk to one streaming platform. unshackle ships without
|
||||
any bundled services, so you add the one(s) you want.
|
||||
The service tag you pass to `dl` (like `EXAMPLE`) maps to a **service module**, the
|
||||
plugin that talks to one streaming platform. unshackle includes none, so you write
|
||||
the one you need. [Creating a Service](../dev/creating-a-service.md) covers how.
|
||||
|
||||
Services live under the `directories.services` path (run `unshackle env info` to
|
||||
see where that is). unshackle discovers them by looking for `*/__init__.py`: the
|
||||
folder name becomes the **service tag**, and the folder must contain a class with
|
||||
that exact same name.
|
||||
|
||||
=== "Local folder"
|
||||
|
||||
Drop a service folder into your services directory. A service tagged `EXAMPLE`
|
||||
lives at:
|
||||
|
||||
```
|
||||
<services>/EXAMPLE/__init__.py # defines a class named EXAMPLE
|
||||
```
|
||||
|
||||
Once the folder is in place, the tag `EXAMPLE` is immediately available to `dl`,
|
||||
`search`, and the other service-aware commands.
|
||||
|
||||
=== "Git repository"
|
||||
|
||||
`directories.services` may also list git repositories. unshackle clones them
|
||||
on first use and keeps them up to date:
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
services:
|
||||
- owner/unshackle-services # a GitHub owner/repo shorthand
|
||||
- /path/to/local/services # local folders still work too
|
||||
```
|
||||
|
||||
Force a refresh of every configured service repo at any time with:
|
||||
|
||||
```shell
|
||||
unshackle util refresh-services
|
||||
```
|
||||
|
||||
!!! info "Where to get services"
|
||||
Service modules are kept separate from the core project. Ask in the community
|
||||
[Discord](https://discord.gg/mHYyPaCbFK) for where to find the service you
|
||||
need. The core repository deliberately does not host them.
|
||||
Put the finished module in your `directories.services` folder (`unshackle env info`
|
||||
shows the path). The tag then works with `dl`, `search`, and the other service
|
||||
commands.
|
||||
|
||||
## 4. Add a CDM for DRM
|
||||
|
||||
@@ -144,7 +114,7 @@ cdm:
|
||||
```
|
||||
|
||||
PlayReady works the same way with `.prd` files created and managed by the
|
||||
[`prd`](configuration-file.md) command. If a title is DRM-free, no CDM is needed.
|
||||
[`prd`](../guide/cli-reference.md#prd) command. If a title is DRM-free, no CDM is needed.
|
||||
|
||||
!!! tip
|
||||
Run `unshackle wvd parse my_device` to inspect a device's security level and
|
||||
@@ -199,7 +169,7 @@ unshackle dl <FLAGS> <SERVICE-TAG> <TITLE>
|
||||
- **`<SERVICE-TAG>`** picks which service to talk to (e.g. `EXAMPLE`).
|
||||
- **`<TITLE>`** is the URL, ID, or slug the service understands.
|
||||
|
||||
A good first command asks for 1080p with English audio and subtitles:
|
||||
A good first command asks for 1080p with English audio:
|
||||
|
||||
```shell title="Your first download"
|
||||
unshackle dl -q 1080 -l en EXAMPLE 81234567
|
||||
@@ -213,7 +183,7 @@ keys through your CDM (and any key vaults), then decrypt, mux, and tag the resul
|
||||
| Flag | Meaning |
|
||||
|---|---|
|
||||
| `-q`, `--quality` | Target resolution(s), e.g. `-q 1080` or `-q 1080,720`. Defaults to best available. |
|
||||
| `-l`, `--lang` | Language(s) for video and audio, e.g. `-l en` or `-l orig,en`. `orig` = the title's original language. |
|
||||
| `-l`, `--lang` | Language(s) for video and audio, e.g. `-l en` or `-l orig,en`. `orig` = the title's original language. Defaults to `orig`. |
|
||||
| `-sl`, `--s-lang` | Subtitle language(s); defaults to `all`. |
|
||||
| `-v`, `--vcodec` | Video codec, e.g. `-v H.265`. Defaults to any. |
|
||||
| `-r`, `--range` | Dynamic range, e.g. `-r HDR10` or `-r DV`. Defaults to `SDR`. |
|
||||
@@ -238,18 +208,18 @@ codec, bitrate, channel-layout, and track-type selection.
|
||||
|
||||
## 8. Where the output lands
|
||||
|
||||
By default, finished files are written to the `downloads` directory
|
||||
By default, unshackle writes finished files to the `downloads` directory
|
||||
(`unshackle env info` shows its exact path; the built-in default is a `downloads`
|
||||
folder alongside the project). Override it per run with `-o`:
|
||||
folder one level above the installed `unshackle` package). Override it per run with `-o`:
|
||||
|
||||
```shell
|
||||
unshackle dl -q 1080 -o /mnt/media/incoming EXAMPLE 81234567
|
||||
```
|
||||
|
||||
- **Movies** are written as a single `.mkv` file named from your `movies` template.
|
||||
- unshackle writes **movies** as a single `.mkv` file named from your `movies` template.
|
||||
- **TV episodes** are grouped into a per-show / per-season folder (from your
|
||||
`series` template) unless you pass `--no-folder`.
|
||||
- The filename is built from your `output_template`, and IMDb/TMDB IDs are added to
|
||||
- unshackle builds the filename from your `output_template` and adds IMDb/TMDB/TVDB IDs to
|
||||
the file's metadata tags when available.
|
||||
|
||||
To change the default output location permanently, set it in your config:
|
||||
@@ -270,5 +240,5 @@ directories:
|
||||
|
||||
!!! tip "You are never far from help"
|
||||
`unshackle --help`, `unshackle dl --help`, and `unshackle <command> --help`
|
||||
list every option with its default. When in doubt, add `--list` to a `dl`
|
||||
list every option. When in doubt, add `--list` to a `dl`
|
||||
command to preview what it would do before it downloads anything.
|
||||
|
||||
@@ -96,17 +96,24 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
|
||||
| `-sl`, `--s-lang` | `all` | Subtitle language(s). |
|
||||
| `--require-subs` | - | Required subtitle langs; keeps **all** subs only if these exist. **Cannot combine with `--s-lang`.** |
|
||||
| `-fs`, `--forced-subs` | off | Include forced subtitle tracks. |
|
||||
| `--exact-lang` | off | Exact matching only: `-l es-419` matches `es-419`, not `es-ES`. |
|
||||
| `-fsl`, `--forced-s-lang` | none | Language(s) wanted for forced subtitles; implies `-fs`. |
|
||||
| `--exact-lang` | off | Exact matching only: `-l es-419` matches `es-419`, not `es-ES`. Applies to selection and to sort order. |
|
||||
| `--sub-format` | - | Output subtitle format (`SRT`/`srt`, `VTT`/`webvtt`, `ASS`/`ssa`, `TTML`, `SMI`, ...), or `original` to keep the source format. |
|
||||
|
||||
!!! tip "These flags select languages, they do not order them"
|
||||
Naming languages with `-l` or `-sl` removes the others. To keep every language but put
|
||||
your preferred ones first, use `audio.language_priority` and
|
||||
[`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.
|
||||
|
||||
### Title & episode selection
|
||||
|
||||
| Flag | Description |
|
||||
|---|---|
|
||||
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). |
|
||||
| `--select-titles` | Interactively select titles (series only). **Cannot combine with `-w`.** |
|
||||
| `-w`, `--wanted` | Wanted episodes, e.g. `S01-S05,S07`, `S01E01-S02E03`. Supports exclusions with a leading `-` (e.g. `-S03`). For a [split episode](downloading.md#split-episodes), `.N` picks one part (`S01E01.2`), a range must stay inside the episode (`S01E01.1-S01E01.3`), and `S01E01` on its own takes every part. |
|
||||
| `--select-titles` | Interactively select what to download: episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
||||
| `--latest-episode` | Download only the single most recent episode. |
|
||||
| `--list-titles` | List titles only; do not download. |
|
||||
|
||||
@@ -145,8 +152,22 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
|
||||
|---|---|
|
||||
| `--tmdb` | TMDB ID (integer). |
|
||||
| `--imdb` | IMDb ID, e.g. `tt1375666`. |
|
||||
| `--animeapi` | AnimeAPI ID, e.g. `mal:12345` or `anilist:98765` (defaults to MAL). Back-fills TMDB/IMDb. |
|
||||
| `--tvdb` | TVDB ID (integer). Skips the series lookup that `--tvdb-order` would otherwise do. |
|
||||
| `--animeapi` | AnimeAPI ID, e.g. `mal:12345` or `anilist:98765` (defaults to MAL). Back-fills TMDB/IMDb/TVDB. |
|
||||
| `--enrich` | Override show title/year from the external source. **Requires** one of `--tmdb`, `--imdb`, or `--animeapi`. |
|
||||
| `--tvdb-order` | Renumber episodes to a TVDB season order: `official` (aired), `dvd`, `absolute`, `alternate`, or `regional`. Needs `tvdb_api_key`. |
|
||||
|
||||
#### 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
|
||||
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
|
||||
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.
|
||||
|
||||
### DRM, keys & decryption
|
||||
|
||||
@@ -162,7 +183,7 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
|
||||
|---|---|
|
||||
| `--proxy` | Proxy URI, a 2-letter country code resolved from configured providers, or `provider:region` (e.g. `nordvpn:ca`, `gluetun:us`, `protonvpn:de:berlin`). |
|
||||
| `--no-proxy` | Force-disable all proxy use. |
|
||||
| `--no-proxy-download` | Bypass the proxy for **segment downloads only** (manifest, licence, and auth stay proxied). |
|
||||
| `--no-proxy-download` | Bypass the proxy for **all downloads** (manifest, licence, and auth stay proxied). |
|
||||
| `--remote` | Use a remote unshackle server. |
|
||||
| `--server` | Name a remote server from the `remote_services` config. |
|
||||
|
||||
@@ -174,6 +195,7 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
|
||||
| `--adaptive-workers` | off | Scale per-track segment workers dynamically (up to the `--workers` cap) based on measured CDN throughput and errors. |
|
||||
| `--download-processes` | `1` | Split large segment batches (24+) across this many download processes to exceed the single-process throughput cap on multi-gigabit connections. |
|
||||
| `--downloads` | `1` | Number of tracks downloaded concurrently. |
|
||||
| `--speed-limit` | unlimited | Cap total download speed across all threads and tracks, e.g. `500k`, `5M`, `1.5G` or plain bytes/sec. Values are bytes, not bits (`5M` = 5.0 MB/s). `off` disables a configured limit. |
|
||||
| `--no-cache` | off | Bypass the title cache. |
|
||||
| `--reset-cache` | off | Clear the title cache. |
|
||||
| `--list` | off | List available/would-be-downloaded tracks; do not download. |
|
||||
|
||||
+119
-22
@@ -4,7 +4,7 @@
|
||||
streaming service, selects exactly the tracks you asked for, acquires and applies
|
||||
decryption keys, and muxes everything into a finished Matroska file. The flags below
|
||||
cover quality and codec selection, language and subtitle handling, episode ranges, and
|
||||
the download flow itself, with examples throughout.
|
||||
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
|
||||
@@ -64,8 +64,9 @@ helps explain why, for example, `--list` stops early and why keys are fetched ev
|
||||
9. **Post-process**: extract closed captions, convert subtitles, repack, mux with
|
||||
`mkvmerge`, and move the finished file to your downloads directory.
|
||||
|
||||
`--list` and `--list-titles` stop after selection/filtering and print what *would* be
|
||||
downloaded. `--skip-dl` runs the license step but skips the actual segment download.
|
||||
`--list` prints the tracks a title exposes and stops before selection. `--list-titles`
|
||||
prints every title the service returned and stops before `--wanted`/`--latest-episode`
|
||||
filtering. `--skip-dl` runs the license step but skips the actual segment download.
|
||||
|
||||
## Choosing quality
|
||||
|
||||
@@ -141,9 +142,11 @@ 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**. It requires the
|
||||
`dovi_tool` binary on your PATH and needs both an HDR10(+) *and* a DV track to be
|
||||
available, otherwise the title fails (unless `--best-available` lets it fall back).
|
||||
metadata onto an HDR10/HDR10+ base layer using **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.
|
||||
|
||||
To keep an HDR10+ deliverable *alongside* the hybrid, request both ranges:
|
||||
@@ -171,8 +174,8 @@ Manifest-declared bitrates are sometimes rounded or wrong. Services often advert
|
||||
**peak** or **nominal** bandwidth that is far from the stream'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 remove that risk by probing the actual media
|
||||
size to compute a true bitrate for the top renditions, overriding the manifest value:
|
||||
unshackle pick the wrong track. These flags probe the actual media size to compute a true
|
||||
bitrate for the top renditions, overriding the manifest value:
|
||||
|
||||
- `-rvb` / `--real-video-bitrate`: probe real video bitrates (per codec/range).
|
||||
- `-rab` / `--real-audio-bitrate`: probe real audio bitrates (per codec/channels/
|
||||
@@ -186,12 +189,13 @@ size to compute a true bitrate for the top renditions, overriding the manifest v
|
||||
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 top rendition of each quality tier is
|
||||
probed in parallel (video grouped by codec+range, audio by codec+channels+language),
|
||||
and the pass then reaches downward only as far as it must to keep the ranking correct.
|
||||
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. 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.
|
||||
|
||||
## Audio codec, channels, and Atmos
|
||||
|
||||
@@ -250,6 +254,26 @@ it is used. You can override each stream type independently:
|
||||
unshackle dl -al en EXAMPLE 81234567
|
||||
```
|
||||
|
||||
### Sort order
|
||||
|
||||
`-l` and `-sl` **select** languages: naming some removes the rest. To keep every language
|
||||
but decide which ones come first, set a priority list in your configuration file:
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
audio:
|
||||
language_priority: [orig, en]
|
||||
subtitle:
|
||||
group_by: language
|
||||
language_priority: [en, es, fr]
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
@@ -285,6 +309,9 @@ download on the presence of a specific subtitle track.
|
||||
|
||||
- `-fs` / `--forced-subs`: include forced subtitle tracks (signs/foreign dialogue).
|
||||
Without this flag, forced subs are dropped from 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.
|
||||
- `--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.
|
||||
@@ -341,13 +368,52 @@ with `-` to exclude it.
|
||||
unshackle dl -w S01-S05,-S03 EXAMPLE 81234567
|
||||
```
|
||||
|
||||
=== "Split episodes"
|
||||
|
||||
```shell
|
||||
# Part 2 of episode 1 only
|
||||
unshackle dl -w S01E01.2 EXAMPLE 81234567
|
||||
|
||||
# Parts 1 through 3 of episode 1
|
||||
unshackle dl -w S01E01.1-S01E01.3 EXAMPLE 81234567
|
||||
|
||||
# All of season 1 except episode 1 part 2
|
||||
unshackle dl -w S01,-S01E01.2 EXAMPLE 81234567
|
||||
```
|
||||
|
||||
### Split episodes
|
||||
|
||||
A few services split one episode into several separately playable videos. Where a service
|
||||
reports that, add `.N` after the episode to pick one part.
|
||||
|
||||
| Token | Selects |
|
||||
| --- | --- |
|
||||
| `S01E01` | All parts of episode 1. |
|
||||
| `S01E01.2` | Part 2 of episode 1, and nothing else. |
|
||||
| `S01E01.1-S01E01.3` | Parts 1 to 3 of episode 1. |
|
||||
| `S01,-S01E01.2` | All of season 1 except episode 1 part 2. Parts 1 and 3 are kept. |
|
||||
|
||||
A season token such as `S01` covers every episode and every part, so you only need `.N`
|
||||
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:
|
||||
`-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
|
||||
`--list-titles` to see which episodes carry parts.
|
||||
|
||||
### Other selection flags
|
||||
|
||||
| Flag | Behaviour |
|
||||
| --- | --- |
|
||||
| `--select-titles` | Interactively pick episodes from a list (Series only). **Cannot combine with `-w`.** |
|
||||
| `--select-titles` | Interactively pick episodes of a series, or films when a title has more than one. **Cannot combine with `-w`.** |
|
||||
| `--latest-episode` | Download only the single most recent episode. |
|
||||
| `--list-titles` | List the titles that would be downloaded, then stop. |
|
||||
| `--list-titles` | List every title the service returned, then stop. `-w`/`--latest-episode` are not applied to this listing. |
|
||||
|
||||
```shell title="Grab just the newest episode"
|
||||
unshackle dl --latest-episode EXAMPLE 81234567
|
||||
@@ -398,8 +464,8 @@ Before committing to a long download, inspect what unshackle *would* do:
|
||||
|
||||
| Flag | Effect |
|
||||
| --- | --- |
|
||||
| `--list` | List the available tracks and which would be selected, then stop. No download. |
|
||||
| `--list-titles` | List the titles (episodes) that would be downloaded, then stop. |
|
||||
| `--list` | List the tracks the service exposes for each title, then stop. No selection, no download. |
|
||||
| `--list-titles` | List every title the service returned, then stop. `-w`/`--latest-episode` are not applied to this listing. |
|
||||
| `--skip-dl` | Skip downloading but still acquire the decryption keys. |
|
||||
|
||||
```shell title="See the track selection without downloading"
|
||||
@@ -428,7 +494,7 @@ flags change how the output is assembled:
|
||||
| --- | --- | --- |
|
||||
| `--no-mux` | Do not mux; keep the individual track files. | - |
|
||||
| `--split-audio` | Write a separate output file per audio codec instead of merging all audio. | config `muxing.merge_audio` (on) |
|
||||
| `--merge-video` | Mux all selected video tracks into one file. | config `muxing.merge_video` (off) |
|
||||
| `--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) |
|
||||
|
||||
### Naming tags
|
||||
|
||||
@@ -453,7 +519,7 @@ unshackle dl --proxy 'http://user:pass@host:8080' EXAMPLE 81234567
|
||||
Two related flags:
|
||||
|
||||
- `--no-proxy`: force-disable all proxy use for this run.
|
||||
- `--no-proxy-download`: bypass the proxy for **segment downloads only**. The manifest,
|
||||
- `--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.
|
||||
|
||||
@@ -501,6 +567,13 @@ unshackle dl --skip-dl --export EXAMPLE 81234567
|
||||
When `--proxy` is used, the export records the region so an import can reproduce the
|
||||
correct geofence. Without a proxy, no region is stored.
|
||||
|
||||
!!! 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
|
||||
their own streams, and those import fine. When a manifest labels nothing, the parse falls
|
||||
back to the title's original language, which comes from `Title.language` on the exporting
|
||||
service. If a service never sets it, importing that export fails with a message naming
|
||||
the service. Neither end guesses a language for you, so the fix belongs in the service.
|
||||
|
||||
## Metadata and tagging
|
||||
|
||||
unshackle looks up metadata automatically, but you can override the identifiers used for
|
||||
@@ -510,13 +583,35 @@ tagging and naming:
|
||||
| --- | --- | --- |
|
||||
| `--tmdb` | `--tmdb 27205` | Use this TMDB ID instead of automatic lookup. |
|
||||
| `--imdb` | `--imdb tt1375666` | Use this IMDb ID. |
|
||||
| `--tvdb` | `--tvdb 73871` | Use this TVDB ID instead of looking the series up. |
|
||||
| `--animeapi` | `--animeapi mal:12345` | Resolve via AnimeAPI (`mal:`/`anilist:` prefix; defaults to MAL). |
|
||||
| `--enrich` | - | Override the show title and year from an external source. **Requires** one of `--tmdb`, `--imdb`, or `--animeapi`. |
|
||||
| `--tvdb-order` | `--tvdb-order dvd` | Renumber episodes to a TVDB season order. Needs `tvdb_api_key`. |
|
||||
|
||||
```shell title="Force the right IMDb match and enrich the title"
|
||||
unshackle dl --imdb tt1375666 --enrich EXAMPLE 81234567
|
||||
```
|
||||
|
||||
### 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
|
||||
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:
|
||||
|
||||
```shell title="Download Futurama in DVD order"
|
||||
unshackle dl --tvdb-order dvd EXAMPLE 81234567
|
||||
```
|
||||
|
||||
Available orders are `official` (aired), `dvd`, `absolute`, `alternate`, and `regional`. Set
|
||||
`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
|
||||
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
|
||||
@@ -547,9 +642,10 @@ authoritative list.
|
||||
| `--v-lang` | `-vl` | Video-only language override. |
|
||||
| `--s-lang` | `-sl` | Subtitle language(s); default `all`. |
|
||||
| `--forced-subs` | `-fs` | Include forced subtitles. |
|
||||
| `--forced-s-lang` | `-fsl` | Forced subtitle language(s); implies `-fs`. |
|
||||
| `--sub-format` | | Output subtitle format. |
|
||||
| `--wanted` | `-w` | Episode/season range. |
|
||||
| `--select-titles` | | Interactively pick episodes. |
|
||||
| `--select-titles` | | Interactively pick episodes or films. |
|
||||
| `--latest-episode` | | Only the newest episode. |
|
||||
| `--video-only` / `--audio-only` / `--subs-only` | `-V` / `-A` / `-S` | Restrict track types. |
|
||||
| `--no-video` / `--no-audio` / `--no-subs` / `--no-chapters` | `-nv` / `-na` / `-ns` / `-nc` | Skip track types. |
|
||||
@@ -562,4 +658,5 @@ authoritative list.
|
||||
| `--list` / `--list-titles` / `--skip-dl` | | Dry runs. |
|
||||
| `--cdm-only` / `--vaults-only` | | Key source control. |
|
||||
| `--export` | | Export track info and keys to JSON. |
|
||||
| `--tmdb` / `--imdb` / `--animeapi` / `--enrich` | | Metadata overrides. |
|
||||
| `--tmdb` / `--imdb` / `--tvdb` / `--animeapi` / `--enrich` | | Metadata overrides. |
|
||||
| `--tvdb-order` | | Renumber episodes to a TVDB season order. |
|
||||
|
||||
@@ -516,5 +516,5 @@ debug_requests: true
|
||||
|
||||
- [Downloading](downloading.md): the `dl` command and its DRM-related flags.
|
||||
- [Key vaults](vaults.md): caching content keys across downloads.
|
||||
- [Configuration](../reference/configuration.md): the full `unshackle.yaml` reference.
|
||||
- [Configuration](../reference/configuration/index.md): the full `unshackle.yaml` reference.
|
||||
- [Dependencies](../getting-started/installation.md): installing shaka-packager and mp4decrypt.
|
||||
|
||||
@@ -71,7 +71,7 @@ The recognized kinds are:
|
||||
|
||||
### 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.
|
||||
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.
|
||||
|
||||
=== "Scene style (dots)"
|
||||
|
||||
@@ -102,12 +102,13 @@ Every variable below is valid in both output and folder templates. Values are de
|
||||
| Variable | Meaning | Example |
|
||||
|---|---|---|
|
||||
| `title` | Title name (movie/show/song name; `$` is rendered as `S`) | `The Show` |
|
||||
| `title_type` | Media kind of the title | `movie`, `series`, `music` |
|
||||
| `year` | Release year | `2024` |
|
||||
| `source` | Service tag / class name (empty with `--no-source`) | `EXAMPLE` |
|
||||
| `quality` | Resolution with scan suffix | `1080p`, `2160p`, `576i` |
|
||||
| `resolution` | Resolution number only | `1080` |
|
||||
| `video` | Video codec | `H.264`, `H.265` |
|
||||
| `hdr` | Dynamic-range label | `HDR`, `HDR10P`, `DV`, `DV.HDR10`, `HLG` |
|
||||
| `hdr` | Dynamic-range label | `HDR`, `HDR10P`, `DV`, `DV.HDR`, `DV.HDR10P`, `HLG` |
|
||||
| `hfr` | High frame rate marker (frame rate > 30) | `HFR` |
|
||||
| `edition` | Edition label from the track, if any | `Directors Cut` |
|
||||
| `tag` | Release-group tag (from `config.tag` or `--tag`) | `TAG` |
|
||||
@@ -125,11 +126,38 @@ Every variable below is valid in both output and folder templates. Values are de
|
||||
| `episode` | Zero-padded episode (`E%02d`) | `E05` |
|
||||
| `season_episode` | Combined | `S01E05` |
|
||||
| `episode_name` | Episode title | `Pilot` |
|
||||
| `part` | Part index of a [split episode](#split-episodes), empty otherwise | `2` |
|
||||
| `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.
|
||||
|
||||
### 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.
|
||||
|
||||
```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 is left out of the folder name, so every part of an episode lands in the same season folder:
|
||||
|
||||
```text
|
||||
The.Show.S01.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG/
|
||||
├─ The.Show.S01E01.Part.1.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
├─ The.Show.S01E01.Part.2.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
└─ 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.
|
||||
|
||||
!!! 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.
|
||||
|
||||
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.
|
||||
|
||||
### Audio
|
||||
|
||||
| Variable | Meaning | Example |
|
||||
@@ -138,8 +166,9 @@ Every variable below is valid in both output and folder templates. Values are de
|
||||
| `audio_channels` | Channel layout | `5.1`, `2.0` |
|
||||
| `audio_full` | Codec + channels combined | `DDP5.1` |
|
||||
| `atmos` | `Atmos` when any track carries JOC | `Atmos` |
|
||||
| `dual` | `DUAL` for exactly two distinct audio languages | `DUAL` |
|
||||
| `dual` | `DUAL` for two audio languages (see [`dual_multi_mode`](../reference/configuration/download.md#dual_multi_mode)) | `DUAL` |
|
||||
| `multi` | `MULTi` for more than two audio languages | `MULTi` |
|
||||
| `dubbed` | `DUBBED` for a single audio language that is not the original (strict mode only) | `DUBBED` |
|
||||
|
||||
### Music-specific
|
||||
|
||||
@@ -183,7 +212,7 @@ 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`.
|
||||
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.
|
||||
|
||||
**Fallback behavior:**
|
||||
|
||||
@@ -233,6 +262,10 @@ The `--no-mux` flag skips muxing entirely and writes the individual track files,
|
||||
## 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.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
tag: "MYGROUP"
|
||||
@@ -244,7 +277,11 @@ tag_imdb_tmdb: true
|
||||
|---|---|---|---|
|
||||
| `tag` | str | `""` | The release-group tag used for `{tag}`. |
|
||||
| `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` / `simkl_client_id` when available). |
|
||||
| `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
|
||||
entity prefix the [Matroska tagging spec](https://www.matroska.org/technical/tagging.html)
|
||||
requires: `series/73871` for a show, `movies/113` for a film.
|
||||
|
||||
You can override the tag per run without editing config:
|
||||
|
||||
@@ -339,6 +376,9 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an
|
||||
|
||||
## Worked examples
|
||||
|
||||
Each tab shows a template and what it produces for a few different downloads, so you can see
|
||||
how the optional (`?`) variables appear and disappear.
|
||||
|
||||
=== "Movies"
|
||||
|
||||
```yaml
|
||||
@@ -347,9 +387,27 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an
|
||||
folder:
|
||||
movies: "{title} ({year})"
|
||||
```
|
||||
|
||||
A 4K Atmos HDR download fills every placeholder:
|
||||
|
||||
```text
|
||||
The Movie (2024)/
|
||||
└─ The.Movie.2024.1080p.EXAMPLE.WEB-DL.DDP5.1.Atmos.H.265.HDR-TAG.mkv
|
||||
└─ The.Movie.2024.2160p.EXAMPLE.WEB-DL.DDP5.1.Atmos.H.265.HDR-TAG.mkv
|
||||
```
|
||||
|
||||
The same template on a plain 1080p SDR stereo download, where `{edition?}`, `{atmos?}`
|
||||
and `{hdr?}` collapse along with their dots:
|
||||
|
||||
```text
|
||||
The Movie (2024)/
|
||||
└─ The.Movie.2024.1080p.EXAMPLE.WEB-DL.AAC2.0.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
A director's cut fills `{edition?}`:
|
||||
|
||||
```text
|
||||
The Movie (2024)/
|
||||
└─ The.Movie.2024.Directors.Cut.2160p.EXAMPLE.WEB-DL.DDP5.1.Atmos.H.265.DV.HDR-TAG.mkv
|
||||
```
|
||||
|
||||
=== "TV series"
|
||||
@@ -365,6 +423,76 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an
|
||||
└─ The.Show.S02E04.The.Reckoning.2160p.EXAMPLE.WEB-DL.DDP5.1.H.265-TAG.mkv
|
||||
```
|
||||
|
||||
unshackle decides the separator style once for the whole folder template, and every
|
||||
segment then follows it. Spaces outnumber dots in `{title} ({year})/Season {season}`,
|
||||
so `Season {season}` keeps its space even though it holds only one variable.
|
||||
|
||||
When an episode has no on-screen name, `{episode_name?}` disappears cleanly:
|
||||
|
||||
```text
|
||||
The Show (2023)/Season S02/
|
||||
└─ The.Show.S02E05.2160p.EXAMPLE.WEB-DL.DDP5.1.H.265-TAG.mkv
|
||||
```
|
||||
|
||||
=== "Multi-language"
|
||||
|
||||
Place `{dual?}`, `{multi?}` and `{dubbed?}` next to each other. At most one of them is
|
||||
ever set (see [`dual_multi_mode`](../reference/configuration/download.md#dual_multi_mode)), and the
|
||||
empty ones collapse:
|
||||
|
||||
```yaml
|
||||
output_template:
|
||||
movies: "{title}.{year}.{quality}.{source}.WEB-DL.{dual?}.{multi?}.{dubbed?}.{audio_full}.{video}-{tag}"
|
||||
```
|
||||
|
||||
Original English audio plus a French dub (`-l en,fr`):
|
||||
|
||||
```text
|
||||
The.Movie.2024.1080p.EXAMPLE.WEB-DL.DUAL.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
Original plus several dubs (`-l en,fr,de,es`):
|
||||
|
||||
```text
|
||||
The.Movie.2024.1080p.EXAMPLE.WEB-DL.MULTi.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
Only a dub, original audio left out (`-l de` on an English-original title):
|
||||
|
||||
```text
|
||||
The.Movie.2024.1080p.EXAMPLE.WEB-DL.DUBBED.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
Two dialects of one language (`en-US` + `en-GB`) count as a single language, so none of the
|
||||
three variables is set:
|
||||
|
||||
```text
|
||||
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?}`
|
||||
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,
|
||||
`{season_episode}` becomes the date automatically and `{episode}` and `{year}` clear:
|
||||
|
||||
```yaml
|
||||
output_template:
|
||||
series: "{title}.{season_episode}.{episode_name?}.{quality}.{source}.WEB-DL.{audio_full}.{video}-{tag}"
|
||||
```
|
||||
|
||||
A normal episode and a dated one, same template:
|
||||
|
||||
```text
|
||||
The.Daily.Show.S28E101.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
The.Daily.Show.2024.06.01.1080p.EXAMPLE.WEB-DL.DDP5.1.H.264-TAG.mkv
|
||||
```
|
||||
|
||||
The date separator follows your template style: dots here, spaces if your `series`
|
||||
template uses spaces.
|
||||
|
||||
=== "Music"
|
||||
|
||||
```yaml
|
||||
@@ -375,7 +503,25 @@ The fallback supports two placeholders: `{i}` (chapter number, starting at 1) an
|
||||
```
|
||||
```text
|
||||
The Artist - The Album (2024)/
|
||||
└─ 01. Opening Track.mka
|
||||
└─ 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.
|
||||
|
||||
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:
|
||||
|
||||
```yaml
|
||||
output_template:
|
||||
songs: "{disc?}-{track_number}. {title} {explicit?}"
|
||||
folder:
|
||||
albums: "{album_artist} - {album} ({year}) [{label?}]"
|
||||
```
|
||||
```text
|
||||
The Artist - The Album (2024) [The Label]/
|
||||
├─ 01. Opening Track.mka
|
||||
└─ 02-03. Closing Track Explicit.mka
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
@@ -28,7 +28,7 @@ Proxying is controlled at download time by three options on the `dl` command:
|
||||
|---|---|
|
||||
| `--proxy` | The proxy to use. Either an explicit URI, or a query that unshackle resolves against your configured providers. |
|
||||
| `--no-proxy` | Force **all** proxy use off. No providers are initialised and no proxy query is resolved. |
|
||||
| `--no-proxy-download` | Use the proxy for the manifest, licence, and authentication, but bypass it for the **segment downloads** themselves. |
|
||||
| `--no-proxy-download` | Use the proxy for the manifest, licence, and authentication, but bypass it for the **downloads** themselves. |
|
||||
|
||||
```shell title="Explicit proxy URI"
|
||||
unshackle dl --proxy http://user:pass@1.2.3.4:8080 EXAMPLE 81234567
|
||||
@@ -52,7 +52,7 @@ unshackle dl --proxy nordvpn:us EXAMPLE 81234567
|
||||
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 where it matters (passing the geo-check for the manifest and the licence) while
|
||||
pulling the actual audio and video segments over your normal connection for full speed.
|
||||
pulling the files themselves over your normal connection at full speed.
|
||||
|
||||
```shell title="Proxy the manifest and licence, download segments direct"
|
||||
unshackle dl --proxy gb --no-proxy-download EXAMPLE 10a1234
|
||||
|
||||
+25
-3
@@ -70,6 +70,7 @@ Subtitle selection happens through `dl` flags. The most important is language se
|
||||
| `-sl`, `--s-lang` | Language(s) wanted for subtitles. Defaults to `all`. |
|
||||
| `--require-subs` | Require these languages to exist; if present, download **all** subtitles. Cannot be combined with `--s-lang`. |
|
||||
| `-fs`, `--forced-subs` | Include forced subtitle tracks (excluded by default). |
|
||||
| `-fsl`, `--forced-s-lang` | Language(s) wanted for forced subtitles; implies `-fs`. |
|
||||
| `--exact-lang` | Exact language matching, with no regional variants. |
|
||||
| `-S`, `--subs-only` | Download only subtitle tracks. |
|
||||
| `-ns`, `--no-subs` | Do not download subtitle tracks at all. |
|
||||
@@ -121,6 +122,18 @@ default**. Add `-fs` / `--forced-subs` to include them:
|
||||
unshackle dl --s-lang en --forced-subs EXAMPLE 81234567
|
||||
```
|
||||
|
||||
`-fs` keeps forced tracks in every selected language. If you only want forced tracks
|
||||
in certain languages, use `-fsl` / `--forced-s-lang` instead. It implies `-fs`, so
|
||||
you do not need both:
|
||||
|
||||
```shell title="All full subtitles, but only the English forced track"
|
||||
unshackle dl --s-lang all --forced-s-lang en EXAMPLE 81234567
|
||||
```
|
||||
|
||||
If you pass both flags, `-fsl` wins and keeps only forced tracks in its languages.
|
||||
It also understands `orig` (the title's original language) and `all` (every forced
|
||||
track, same as plain `-fs`).
|
||||
|
||||
## Track types: forced, SDH, and CC
|
||||
|
||||
Every subtitle track carries flags describing what kind of captions it holds. These
|
||||
@@ -134,9 +147,16 @@ flags drive selection, sort order, output filenames, and the Matroska track flag
|
||||
often uppercase with `>>>` speaker markers and sound cues.
|
||||
|
||||
!!! note "SDH and CC are treated together for sorting and stripping"
|
||||
Within a language, 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.
|
||||
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.
|
||||
|
||||
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
|
||||
language next to its own variants instead, set
|
||||
[`subtitle.group_by`](../reference/configuration/download.md#subtitle) to `language`. Use
|
||||
[`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.
|
||||
@@ -356,6 +376,8 @@ subtitle:
|
||||
| `convert_before_strip` | `true` | `true` / `false` | Convert a subtitle to SRT before stripping SDH (for non-SubtitleEdit engines). |
|
||||
| `output_mode` | `mux` | `mux`, `sidecar`, `both` | Whether subtitles are embedded, written as sidecars, or both. |
|
||||
| `sidecar_format` | `srt` | a format name (e.g. `srt`, `vtt`) or `original` | Format used for sidecar files. |
|
||||
| `group_by` | `type` | `type`, `language` | Whether all tracks of one type stay together, or each language stays next to its own variants. |
|
||||
| `language_priority` | *(unset)* | a list of languages | Languages to sort to the top. The rest keep the default order. Nothing is removed. |
|
||||
|
||||
!!! tip "SubtitleEdit is optional but recommended"
|
||||
Several high-fidelity paths (the best-quality conversions and the SubtitleEdit SDH
|
||||
|
||||
@@ -74,9 +74,11 @@ For example, `unshackle_debug_EXAMPLE_20260703-142530.jsonl`. The `<service>` ta
|
||||
Every line is a self-contained JSON object. The first line of a session 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","python_version":"3.12.4 ...","platform":"Linux-6.18.5-x86_64","platform_system":"Linux","platform_release":"6.18.5"}}
|
||||
{"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.
|
||||
|
||||
Subsequent lines describe operations: service calls, DRM licence requests, vault lookups, downloader progress, and errors. Common fields include:
|
||||
|
||||
| Field | Meaning |
|
||||
|
||||
@@ -208,6 +208,9 @@ key_vaults:
|
||||
name: myapi
|
||||
uri: https://vault.example.com/api
|
||||
token: "your-bearer-token"
|
||||
headers:
|
||||
CF-Access-Client-Id: "CLIENT_ID_HERE"
|
||||
CF-Access-Client-Secret: "CLIENT_SECRET_HERE"
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
@@ -218,6 +221,7 @@ key_vaults:
|
||||
| `token` | yes | Bearer token, sent as `Authorization: Bearer <token>`. |
|
||||
| `no_push` | no | If `true`, keys are read but never written. |
|
||||
| `timeout` | no | Request timeout in seconds; defaults to `vault_timeout`. |
|
||||
| `headers` | no | Extra HTTP `headers` sent with every request, for example the Cloudflare Access service-token headers `CF-Access-Client-Id` and `CF-Access-Client-Secret`. |
|
||||
|
||||
The backend talks to endpoints under `uri` (for example `GET {uri}/{service}/{kid}` to look up a single key) and interprets a numeric `code` field in each JSON response to detect errors such as an invalid token, rate limiting, or an invalid service tag.
|
||||
|
||||
|
||||
+10
-8
@@ -5,18 +5,20 @@ media from streaming manifests, handles the DRM, and muxes everything into clean
|
||||
well-tagged files, all driven from a single command-line tool, or served over a
|
||||
REST API for automation.
|
||||
|
||||
It is a fork of [Devine](https://github.com/devine-dl/devine) with first-class
|
||||
DASH, HLS, and Smooth Streaming (ISM) parsing, both **Widevine** and **PlayReady**
|
||||
DRM support, and a built-in HTTP server.
|
||||
It is a fork of [Devine](https://github.com/devine-dl/devine) with DASH, HLS, and
|
||||
Smooth Streaming (ISM) parsing, both **Widevine** and **PlayReady** DRM support,
|
||||
and a built-in HTTP server.
|
||||
|
||||
!!! warning "Use responsibly"
|
||||
unshackle is licensed under [GPL-3.0](https://github.com/unshackle-dl/unshackle/blob/master/LICENSE).
|
||||
unshackle is licensed under [GPL-3.0](https://github.com/unshackle-dl/unshackle/blob/main/LICENSE).
|
||||
Do not use it for content you do not have the rights to. Keep the core free and
|
||||
open, keep service code private, and be kind.
|
||||
|
||||
## What it does
|
||||
|
||||
Point unshackle at a title on a streaming service and it will:
|
||||
unshackle does everything except talk to the streaming platform. You write that part
|
||||
as a service module ([Creating a Service](dev/creating-a-service.md)). Point unshackle
|
||||
at a title and it will:
|
||||
|
||||
- Fetch the title metadata (movies, full series, seasons, episodes, albums, tracks).
|
||||
- Parse the streaming manifest: **DASH**, **HLS**, or **Smooth Streaming (ISM)**.
|
||||
@@ -40,7 +42,7 @@ Point unshackle at a title on a streaming service and it will:
|
||||
| Subtitles | SRT, WebVTT, ASS/SSA, TTML and more, with optional SDH stripping and format conversion |
|
||||
| Key vaults | SQLite, MySQL, and HTTP/API vaults to store and share content keys |
|
||||
| Proxies | Basic proxies plus NordVPN, ProtonVPN, Surfshark, Windscribe, ExpressVPN, Gluetun, and Hola providers |
|
||||
| Services | Pluggable service modules, loadable from local folders or git repositories |
|
||||
| Services | A plugin API for the service modules you write. unshackle includes none |
|
||||
| Automation | A REST API with a job queue, live progress, history, and remote-download sessions |
|
||||
|
||||
## Installation at a glance
|
||||
@@ -70,7 +72,7 @@ Point unshackle at a title on a streaming service and it will:
|
||||
Every download is three parts: the `dl` command, a **service tag**, and the
|
||||
service's title argument (a URL, ID, or slug).
|
||||
|
||||
```shell title="Download a 1080p title with English audio and subtitles"
|
||||
```shell title="Download a 1080p title with English audio"
|
||||
unshackle dl -q 1080 -l en EXAMPLE 81234567
|
||||
```
|
||||
|
||||
@@ -84,7 +86,7 @@ install to your first file.
|
||||
- **[Installation](getting-started/installation.md)**. Install unshackle and the external tools it depends on.
|
||||
- **[Quickstart](getting-started/quickstart.md)**. Configure `unshackle.yaml` and run your first download.
|
||||
- **[Downloading](guide/downloading.md)**. The full `dl` command guide: quality, codecs, languages, track selection, and output.
|
||||
- **[Configuration](getting-started/configuration-file.md)**. Every `unshackle.yaml` key, directories, vaults, proxies, and naming templates.
|
||||
- **[Configuration](getting-started/configuration-file.md)**. Where `unshackle.yaml` lives, the directory layout, and how to edit values. Full key list in the [Configuration Reference](reference/configuration/index.md).
|
||||
- **[REST API](dev/rest-api/index.md)**. Run the `serve` HTTP server and drive downloads programmatically.
|
||||
|
||||
!!! tip "Getting help"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Auto-generated reference for the [`Config`][unshackle.core.config.Config] object that models the
|
||||
`unshackle` configuration file. For a human-friendly walkthrough of every key, see the
|
||||
[Configuration Reference](../configuration.md).
|
||||
[Configuration Reference](../configuration/index.md).
|
||||
|
||||
::: unshackle.core.config
|
||||
options:
|
||||
|
||||
+15
-888
@@ -1,891 +1,18 @@
|
||||
# Configuration Reference
|
||||
# Configuration Reference (moved)
|
||||
|
||||
This page is the exhaustive, authoritative reference for **every** key you can set in
|
||||
`unshackle.yaml`. For each key you will find its name, expected type, default value, and
|
||||
what it does. Keys are grouped by area so you can jump straight to the part of unshackle
|
||||
you are configuring.
|
||||
This page has moved. The configuration reference is now split by area under
|
||||
[`docs/reference/configuration/`](configuration/index.md), or on the docs site at
|
||||
[docs.unshackle.dev/reference/configuration](https://docs.unshackle.dev/reference/configuration/).
|
||||
|
||||
If you are new to the config file (where it lives, how unshackle discovers it, and how to
|
||||
edit it), start with [The Configuration File](../getting-started/configuration-file.md).
|
||||
This page assumes you already know the basics and want precise details.
|
||||
|
||||
!!! info "How defaults work"
|
||||
Every key is optional. If you omit a key, unshackle uses the built-in default listed
|
||||
here. There is **no schema validation**: unknown keys are silently ignored (they never
|
||||
become settings), and the file is parsed with `yaml.safe_load`. An empty or missing
|
||||
file means *all* defaults apply.
|
||||
|
||||
!!! warning "Two keys behave specially on load"
|
||||
- `curl_impersonate` is a **deprecated** alias of [`network`](#network-proxy) and emits a
|
||||
`DeprecationWarning`.
|
||||
- `scene_naming` has been **removed**. If it is present at all, unshackle exits
|
||||
immediately with an error telling you to use [`output_template`](#output-naming)
|
||||
instead.
|
||||
|
||||
See [Deprecated & removed keys](#deprecated-removed-keys) at the end of this page.
|
||||
|
||||
---
|
||||
|
||||
## Quick index
|
||||
|
||||
| Area | Keys |
|
||||
| Area | Page |
|
||||
|------|------|
|
||||
| [Directories](#directories) | `directories`, `filenames` |
|
||||
| [Services & authentication](#services-auth) | `services`, `credentials`, `firefox_cookies`, `remote_services`, `serve` |
|
||||
| [Download & processing](#download-processing) | `dl`, `subtitle`, `audio`, `muxing`, `language_tags` |
|
||||
| [Output & naming](#output-naming) | `output_template`, `tag`, `tag_group_name`, `tag_imdb_tmdb`, `chapter_fallback_name`, `unicode_filenames` |
|
||||
| [DRM & CDM](#drm-cdm) | `cdm`, `remote_cdm`, `decryption` |
|
||||
| [Network & proxy](#network-proxy) | `network`, `headers`, `proxy_providers` |
|
||||
| [Key vaults](#key-vaults) | `key_vaults`, `vault_timeout` |
|
||||
| [External API keys](#external-api-keys) | `imdb_api_enabled`, `omdb_api_key`, `tmdb_api_key`, `simkl_client_id`, `decrypt_labs_api_key`, `ipinfo_api_key` |
|
||||
| [Caching & updates](#caching-updates) | `title_cache_enabled`, `title_cache_time`, `title_cache_max_retention`, `update_checks`, `update_check_interval` |
|
||||
| [Logging, privacy & debug](#logging-privacy-debug) | `redact_paths`, `debug`, `debug_keys`, `debug_requests`, `set_terminal_bg` |
|
||||
| [Deprecated & removed](#deprecated-removed-keys) | `curl_impersonate`, `downloader`, `scene_naming` |
|
||||
|
||||
---
|
||||
|
||||
## Directories { #directories }
|
||||
|
||||
The `directories` key is a **dict** mapping directory names to filesystem paths. Only the
|
||||
names listed below are honoured; a handful are **protected** and cannot be moved (attempts
|
||||
to override them are silently skipped). All defaults are computed relative to the installed
|
||||
package, and every user-settable path is passed through `Path(...).expanduser()`, so `~`
|
||||
works.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
downloads: ~/Media/unshackle
|
||||
temp: /mnt/fast-scratch/unshackle-temp
|
||||
wvds: ~/.unshackle/WVDs
|
||||
prds: ~/.unshackle/PRDs
|
||||
```
|
||||
|
||||
| Key | Type | Default | Overridable | Purpose |
|
||||
|-----|------|---------|:-----------:|---------|
|
||||
| `downloads` | path | `<repo>/downloads` | ✅ | Default output directory for finished files. |
|
||||
| `temp` | path | `<repo>/temp` | ✅ | Temporary working files during download/decrypt/mux. |
|
||||
| `cache` | path | `<data>/cache` | ✅ | Generic cache, title cache, and the update-check store. |
|
||||
| `cookies` | path | `<data>/cookies` | ✅ | Per-service cookie files (and VPN cookie/token files). |
|
||||
| `logs` | path | `<data>/logs` | ✅ | Log files. |
|
||||
| `exports` | path | `<data>/exports` | ✅ | Export JSON files. |
|
||||
| `wvds` | path | `<data>/WVDs` | ✅ | Widevine device files (`.wvd`). |
|
||||
| `prds` | path | `<data>/PRDs` | ✅ | PlayReady device files (`.prd`). |
|
||||
| `dcsl` | path | `<data>/DCSL` | ✅ | DCSL data. |
|
||||
| `commands` | path | `unshackle/commands` | ✅ | CLI command modules. |
|
||||
| `services` | list \| path | `[unshackle/services]` | ✅ | Service search paths and/or remote repo specs (see below). |
|
||||
| `vaults` | path | `unshackle/vaults` | ✅ | Vault backend modules. |
|
||||
| `fonts` | path | `unshackle/fonts` | ✅ | Bundled fonts. |
|
||||
| `user_configs` | path | `unshackle/` | ❌ protected | Where `unshackle.yaml` lives. |
|
||||
| `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. |
|
||||
| `core_dir` | path | `unshackle/core` | ❌ protected | Package core. |
|
||||
| `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. |
|
||||
| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. |
|
||||
|
||||
!!! note "The `services` directory is special"
|
||||
`services` may be a **list**, and each entry can be either a local directory or a
|
||||
**repository spec**: a git URL (`https://...`, `ssh://...`, `git@...`, or anything ending in
|
||||
`.git`) or `owner/repo` shorthand. Repo specs are cloned and updated automatically; plain
|
||||
paths are used as-is. **List order is priority**: the first source to define a service
|
||||
tag wins, so put your own local service directories **last** to make them fallbacks.
|
||||
|
||||
```yaml
|
||||
directories:
|
||||
services:
|
||||
- unshackle-dl/community-services # cloned from GitHub
|
||||
- https://example.com/private.git@stable
|
||||
- ~/my-services # local, lowest priority
|
||||
```
|
||||
|
||||
!!! info "How git-backed service repos are handled"
|
||||
unshackle's use of git is **read-only on the remote**: it only ever performs a shallow
|
||||
`clone`, `fetch`, `pull`, and a local `reset`; **nothing is ever pushed**. Private repos
|
||||
are authenticated through your existing git credential helper, and unshackle stores no
|
||||
tokens of its own. Clones live under `<first-local-services-dir>/_repos/<repo-name>/` (or
|
||||
the bundled `unshackle/services`), and **nothing is written to the cache directory**. After
|
||||
the first clone unshackle re-pulls **at most once every 24 h**, so it does not touch the
|
||||
network on every run.
|
||||
|
||||
!!! warning "The two refresh paths diverge on purpose"
|
||||
The **automatic** 24 h-TTL refresh that happens during a normal `dl`/`search` run will
|
||||
deliberately **refuse to refresh and exit** (naming the offending clone) if that clone
|
||||
has uncommitted changes to tracked files or unpushed local commits, so a background pull can
|
||||
never clobber work in progress. Untracked files (new service folders, `__pycache__`) never
|
||||
block it. The **manual** `unshackle util refresh-services` command does the opposite: it
|
||||
**hard-resets** the clone to upstream, discarding any local edits. Reach for the manual
|
||||
command only when you actually want to throw local changes away.
|
||||
|
||||
!!! warning "Read-only installs and reinstalls"
|
||||
If the services directory lives inside the installed package, a reinstall can delete the
|
||||
`_repos` clones. They are simply re-cloned on next use. On **read-only installs** you must
|
||||
point `services` at a writable path, or cloning will fail.
|
||||
|
||||
### Filenames { #filenames }
|
||||
|
||||
The `filenames` key is a **dict** of templated file/name patterns. Each value is used
|
||||
verbatim (no path processing). Braced fields like `{time}` and `{service}` are filled in at
|
||||
runtime.
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `log` | str | `"unshackle_{name}_{time}.log"` | Written under `directories.logs`. |
|
||||
| `debug_log` | str | `"unshackle_debug_{service}_{time}.jsonl"` | Structured debug log, under `directories.logs`. |
|
||||
| `config` | str | `"config.yaml"` | Per-service config file, under that service's directory. |
|
||||
| `root_config` | str | `"unshackle.yaml"` | The main config filename itself. |
|
||||
| `chapters` | str | `"Chapters_{title}_{random}.txt"` | Under `directories.temp`. |
|
||||
| `subtitle` | str | `"Subtitle_{id}_{language}.srt"` | Under `directories.temp`. |
|
||||
|
||||
---
|
||||
|
||||
## Services & authentication { #services-auth }
|
||||
|
||||
### `services`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **Default:** `{}`
|
||||
|
||||
Per-service configuration, keyed by the canonical service tag (for example `EXAMPLE1`, `EXAMPLE2`,
|
||||
`EXAMPLE3`). unshackle reads several well-known sub-keys out of each service's block:
|
||||
|
||||
- **`proxy_map`**: a dict mapping `"provider:query"` or `"query"` to a proxy value, used to
|
||||
auto-select a proxy for that service.
|
||||
- **`title_map`**: an exact-match rename map applied to fetched titles (source name →
|
||||
desired name), so a service that names a title differently from your library still matches.
|
||||
|
||||
Individual services may read any additional keys they define. The whole block is handed to
|
||||
the service as `self.config`.
|
||||
|
||||
!!! tip "Why put secrets here and not in the service's own `config.yaml`?"
|
||||
A service ships a per-service `config.yaml` inside its own folder, and that file is
|
||||
typically shared/committed and **not** meant to be edited by you. So anything user- or
|
||||
device-specific (API keys, account IDs, device attributes) belongs in this `services`
|
||||
block of `unshackle.yaml` instead, where it is **merged over** the service's `config.yaml`
|
||||
at load time. That is why the two config locations exist: one for shared service defaults,
|
||||
one for your private overrides.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
EXAMPLE1:
|
||||
proxy_map:
|
||||
default: us
|
||||
title_map:
|
||||
"My Show: The Movie": "My Show Presents The Movie"
|
||||
```
|
||||
|
||||
### `credentials`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **Default:** `{}`
|
||||
|
||||
Per-service login credentials. Each value is a `username:password[:extra]` string (a list or
|
||||
dict form is also accepted where a service supports it). These are parsed into `Credential`
|
||||
objects; the credential's SHA-1 is also used as an account hash for cache keys.
|
||||
|
||||
```yaml
|
||||
credentials:
|
||||
EXAMPLE2: user@example.com:hunter2
|
||||
EXAMPLE1:
|
||||
- primary@example.com:pw1
|
||||
- secondary@example.com:pw2
|
||||
```
|
||||
|
||||
!!! tip "Cookies vs credentials"
|
||||
Cookies are stored as files under [`directories.cookies`](#directories), not in this key.
|
||||
A service's `authenticate()` accepts cookies, credentials, or both.
|
||||
|
||||
### `firefox_cookies`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Settings for extracting cookies directly from a local Firefox profile. A service block is
|
||||
expected to provide `hosts` (a list of cookie hostnames; entries shorter than 3 characters
|
||||
are ignored to prevent dumping the whole cookie store) and an optional `local_storage`
|
||||
boolean to also pull matching local-storage entries. Extraction is read-only.
|
||||
|
||||
!!! note "Firefox does not need to be closed"
|
||||
The extractor copies **both** `cookies.sqlite` **and** its WAL file into a `0700` temp
|
||||
directory; the WAL copy exists to capture writes Firefox has not yet flushed to the main
|
||||
DB, so the copy is current even against a running browser. The only real constraint is that
|
||||
Firefox must not hold an exclusive write lock at the exact instant of the copy. The live
|
||||
profile is otherwise left untouched.
|
||||
|
||||
!!! tip "When you actually need `local_storage: true`"
|
||||
Enable it only for services that keep their auth tokens in the browser's **localStorage**
|
||||
(`webappsstore.sqlite`) rather than in HTTP cookies. If a service authenticates with plain
|
||||
cookies, leaving it off is correct.
|
||||
|
||||
!!! warning "Extraction fails silently to file cookies"
|
||||
If extraction yields no cookies or fails for **any** reason, unshackle silently falls back
|
||||
to the normal file-based cookie path (`cookies/<SERVICE>.txt` or
|
||||
`cookies/<SERVICE>/<profile>.txt`). This is a safety net, but it also means that if you
|
||||
expected Firefox cookies and file cookies are being used instead, extraction quietly failed.
|
||||
Check that first when troubleshooting.
|
||||
|
||||
### `remote_services`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Definitions of remote unshackle service servers, used by the `--remote` mode. Each entry
|
||||
carries a `services` sub-dict of remote service tags, which unshackle turns into synthetic
|
||||
CLI commands that run against the remote server.
|
||||
|
||||
### `serve`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Configuration for the `serve` command (the built-in REST API server). The full server guide
|
||||
is the [REST API](../dev/rest-api/index.md) section; these are the config keys.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `api_secret` | str | *(unset)* | Master secret accepted in the `X-Secret-Key` header. |
|
||||
| `users` | dict | `{}` | Per-user API keys and their allowlists (see below). |
|
||||
| `services` | list | *(unset)* | Global service allowlist. Omit to allow all. |
|
||||
| `remote_only` | bool | `false` | Run without a local `output_template` (CORS/Cloudflare-friendly). |
|
||||
| `session_ttl` | int (s) | `300` | Lifetime of an interactive auth session. |
|
||||
| `max_sessions` | int | `100` | Maximum concurrent sessions. |
|
||||
| `history_limit` | int | `100` | How many finished jobs to retain in history. |
|
||||
| `compression_level` | int | `1` | gzip level for responses. |
|
||||
| `cdm_overrides` | dict | *(unset)* | Allowed per-request CDM overrides. |
|
||||
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
|
||||
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
|
||||
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
|
||||
| `country` / `locations` | str / dict | *(unset)* | Advertised region metadata. |
|
||||
|
||||
Each entry under `users` is keyed by that user's API key and may set its own `services`,
|
||||
`devices`, and `playready_devices` allowlists, narrowing the global ones.
|
||||
|
||||
```yaml
|
||||
serve:
|
||||
api_secret: change-me
|
||||
remote_only: true
|
||||
services: [EXAMPLE1, EXAMPLE2]
|
||||
# server-wide download defaults (same keys as the `dl:` block)
|
||||
downloads: 3
|
||||
best_available: true
|
||||
users:
|
||||
a1b2c3d4: # this user's API key
|
||||
services: [EXAMPLE1] # may only use EXAMPLE1
|
||||
```
|
||||
|
||||
!!! tip "Declaring `dl` defaults under `serve`"
|
||||
You can set any [`dl`](#dl) flag key (`downloads`, `workers`, `best_available`, and so on)
|
||||
directly inside `serve`. The point is to raise concurrency and behaviour defaults
|
||||
**server-wide** (every request the server handles picks them up) without having to change
|
||||
each client call individually.
|
||||
|
||||
---
|
||||
|
||||
## Download & processing { #download-processing }
|
||||
|
||||
### `dl`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Persistent defaults for the `dl` command. This block is used as Click's `default_map`, so
|
||||
**any** `dl` flag can be given a default here and it applies to every download without you
|
||||
typing it. See [Downloading](../guide/downloading.md) and the
|
||||
[CLI Reference](../guide/cli-reference.md) for what each flag does.
|
||||
|
||||
**Key naming.** A key is the flag's long name with dashes replaced by underscores
|
||||
(`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few keys are named
|
||||
after the flag's internal destination rather than its visible name; set these exact keys:
|
||||
|
||||
| Flag | Config key |
|
||||
|------|-----------|
|
||||
| `-r` / `--range` | `range` |
|
||||
| `--list` | `list` |
|
||||
| `--tmdb` | `tmdb_id` |
|
||||
| `--imdb` | `imdb_id` |
|
||||
| `--animeapi` | `animeapi_id` |
|
||||
| `-naa` / `--noatmos` | `no_atmos` |
|
||||
| `-o` / `--output` | `output_dir` |
|
||||
| `--cdm-only` / `--vaults-only` | `cdm_only` |
|
||||
|
||||
**Precedence** (highest first): an explicit CLI flag or environment variable → a per-service
|
||||
[`services.<TAG>.dl`](#services-auth) override → this global `dl:` block → the built-in
|
||||
default. In other words, config only fills in options you did not set on the command line.
|
||||
|
||||
Common keys (a useful subset; every `dl` flag works):
|
||||
|
||||
| Key | Type | Default | Sets |
|
||||
|-----|------|---------|------|
|
||||
| `lang` | list | `["orig"]` | Video/audio language(s); `orig` = original language. |
|
||||
| `a_lang` / `v_lang` | list | `[]` | Audio- / video-only language override. |
|
||||
| `s_lang` | list | `["all"]` | Subtitle language(s). |
|
||||
| `quality` | list | `[]` (best) | Resolution(s), e.g. `[1080]`. |
|
||||
| `vcodec` | list | `[]` (any) | Video codec(s), e.g. `[H265]`. |
|
||||
| `acodec` | list | `[]` (any) | Audio codec(s), e.g. `[EC3]`. |
|
||||
| `range` | list | `["SDR"]` | Colour range(s): `SDR`, `HDR10`, `HDR10+`, `DV`. |
|
||||
| `channels` | float | *(unset)* | Audio channels, e.g. `6` for 5.1. |
|
||||
| `sub_format` | str | *(unset)* | Convert subtitles to this format (`srt`, `vtt`, `original`, ...). |
|
||||
| `forced_subs` | bool | `false` | Include forced subtitle tracks. |
|
||||
| `no_subs` / `no_audio` / `no_chapters` | bool | `false` | Skip that track type. |
|
||||
| `downloads` | int | `1` | Tracks downloaded concurrently. |
|
||||
| `workers` | int | *(downloader default)* | Threads per track. |
|
||||
| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. |
|
||||
| `best_available` | bool | `false` | Fall back to best quality if the request is unavailable. |
|
||||
| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. |
|
||||
| `no_folder` | bool | `false` | Do not create a per-title folder. |
|
||||
|
||||
```yaml title="Global download defaults"
|
||||
dl:
|
||||
lang: [en]
|
||||
quality: [1080]
|
||||
range: [HDR10]
|
||||
vcodec: [H265]
|
||||
sub_format: srt
|
||||
downloads: 2
|
||||
```
|
||||
|
||||
!!! tip "Per-service overrides"
|
||||
Nest a `dl` block under a service in [`services`](#services-auth) to override these defaults
|
||||
for just that service. It uses the same keys and wins over the global `dl:` block (but still
|
||||
loses to an explicit CLI flag).
|
||||
|
||||
```yaml
|
||||
dl:
|
||||
lang: [en] # global default
|
||||
services:
|
||||
EXAMPLE1:
|
||||
dl:
|
||||
lang: [en, ja] # EXAMPLE1 downloads both; everything else stays English
|
||||
range: [DV]
|
||||
```
|
||||
|
||||
!!! note "`serve` uses these same keys"
|
||||
The [`serve`](#serve) block accepts the same `dl` option keys at its top level to set
|
||||
server-wide download defaults for every request it handles.
|
||||
|
||||
### `subtitle`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Subtitle handling: SDH stripping, format conversion, and whether subtitles are muxed in or
|
||||
written as sidecar files. See [Subtitles](../guide/subtitles.md) for the full guide.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `strip_sdh` | bool | `true` | Strip SDH/CC cues (hearing-impaired annotations) into a clean track. |
|
||||
| `sdh_method` | str | `"auto"` | SDH-stripping backend to use. |
|
||||
| `preserve_formatting` | bool | `true` | Keep styling/positioning when converting or stripping. |
|
||||
| `convert_before_strip` | bool | `true` | Convert to the working format before stripping SDH. |
|
||||
| `conversion_method` | str | `"auto"` | Subtitle conversion backend. |
|
||||
| `preferred_conversion_method` | str | *(unset)* | Preferred converter when several are available. |
|
||||
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files. |
|
||||
| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode: sidecar`. |
|
||||
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH) that sorts the variants within each language. Types you leave out fall to the end. |
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
strip_sdh: true
|
||||
output_mode: sidecar
|
||||
sidecar_format: srt
|
||||
```
|
||||
|
||||
!!! note "`type_priority` also picks the default subtitle"
|
||||
`type_priority` sorts the tracks within each language, and at mux time the first track
|
||||
in the default subtitle language gets the `default` flag. The built-in order is `forced`,
|
||||
`normal`, `sdh`, which means a forced track becomes default whenever one exists. If you
|
||||
want the full dialogue track as default instead, set:
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
type_priority: [normal, sdh, forced]
|
||||
```
|
||||
|
||||
### `audio`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Audio handling options. The most notable sub-key is **`codec_priority`**, an ordered list of
|
||||
audio codecs used to break ties in track selection.
|
||||
|
||||
```yaml
|
||||
audio:
|
||||
codec_priority:
|
||||
- EAC3
|
||||
- AC3
|
||||
```
|
||||
|
||||
!!! note "`codec_priority` is a soft priority: nothing is dropped"
|
||||
`codec_priority` only breaks ties **among tracks that share the same bitrate and language**.
|
||||
Codecs not in the list keep their normal bitrate-based ordering and are simply placed after
|
||||
all listed codecs, and none are ever removed. Two higher-level rules still override it: **Atmos
|
||||
tracks always take precedence over `codec_priority`**, and **audio-description tracks are
|
||||
always forced to the end** regardless of it. When `codec_priority` is unset, tracks sort by
|
||||
bitrate alone, with those same Atmos/descriptive rules still applied.
|
||||
|
||||
### `muxing`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Matroska (MKV) muxing options.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `set_title` | bool | `true` | Write a human-readable title into the MKV container. |
|
||||
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. |
|
||||
| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. |
|
||||
| `merge_video` | bool | `false` | Merge all video tracks into one file. `--merge-video` on the CLI flips this on. |
|
||||
|
||||
```yaml
|
||||
muxing:
|
||||
set_title: true
|
||||
default_language:
|
||||
audio: en
|
||||
subtitle: en
|
||||
```
|
||||
|
||||
### `language_tags`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Language-tag remapping and the rule engine behind the `{lang_tag}` filename variable. When a
|
||||
`rules` list is present, each rule is evaluated in order and the first match's `tag` is used.
|
||||
A rule may test:
|
||||
|
||||
| Condition | Meaning |
|
||||
|-----------|---------|
|
||||
| `audio` | Matches against the audio track languages. |
|
||||
| `subs_contain` | At least one subtitle language matches. |
|
||||
| `subs_contain_all` | Every listed language must be present (scalar or list). |
|
||||
|
||||
```yaml
|
||||
language_tags:
|
||||
rules:
|
||||
- audio: [en]
|
||||
subs_contain_all: [en]
|
||||
tag: "" # native English release, no tag
|
||||
- audio: [ja]
|
||||
subs_contain: [en]
|
||||
tag: "SUBBED"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output & naming { #output-naming }
|
||||
|
||||
For the full picture of how templates render, see
|
||||
[Output & naming](../guide/output-and-naming.md). The keys are summarised here.
|
||||
|
||||
### `output_template`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Filename templates keyed by media kind, using `{variable}` placeholders. Recognised kinds are
|
||||
`movies`, `series`, and `songs` (and `albums` for folder templates). A trailing `?` on a
|
||||
variable (e.g. `{quality?}`) marks it optional: if empty, it and one adjacent separator are
|
||||
dropped.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
output_template:
|
||||
movies: "{title}.{year}.{quality?}.{source}-{tag}"
|
||||
series: "{title}.{season_episode}.{episode_name?}.{quality?}.{source}-{tag}"
|
||||
songs: "{track_number}. {title}"
|
||||
folder: # special nested key, see below
|
||||
movies: "{title} ({year})"
|
||||
series: "{title} ({year})"
|
||||
```
|
||||
|
||||
**The nested `folder` sub-key** controls output subfolders and is popped out of
|
||||
`output_template`:
|
||||
|
||||
- A **dict** sets per-kind folder templates (kinds: `movies`, `series`, `songs`, `albums`).
|
||||
- A **str** sets a single folder template for everything.
|
||||
|
||||
unshackle validates templates on load and emits warnings for unknown variables, non-string
|
||||
values, filesystem-unsafe characters (`< > : " / \ | ? *`), empty templates, and unknown
|
||||
folder-kind keys.
|
||||
|
||||
??? example "All valid template variables"
|
||||
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `date`,
|
||||
`quality`, `resolution`, `source`, `tag`, `track_number`, `artist`, `album_artist`,
|
||||
`album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`, `explicit`,
|
||||
`isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`, `dual`,
|
||||
`multi`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`.
|
||||
|
||||
### Tagging & naming keys
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `tag` | str | `""` | Release-group tag appended to filenames and written into MKV tags. |
|
||||
| `tag_group_name` | bool | `true` | Include the group name (`tag`) in the MKV `Group` tag. |
|
||||
| `tag_imdb_tmdb` | bool | `true` | Write IMDb/TMDB/TVDB external-ID tags into the MKV (needs metadata providers). |
|
||||
| `chapter_fallback_name` | str | `""` | Fallback chapter-name template, e.g. `"Chapter {i:02}"`. |
|
||||
| `unicode_filenames` | bool | `false` | Allow Unicode in filenames; when `false`, names are ASCII-sanitised. |
|
||||
|
||||
---
|
||||
|
||||
## DRM & CDM { #drm-cdm }
|
||||
|
||||
The full conceptual guide lives at [DRM & CDM](../guide/drm-and-cdm.md). These are the config
|
||||
keys. Device files themselves live in [`directories.wvds`](#directories) (`.wvd`) and
|
||||
`directories.prds` (`.prd`).
|
||||
|
||||
### `cdm`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a `default` fallback.
|
||||
Resolution is case-insensitive: an explicit CLI override wins, then the per-service entry,
|
||||
then `default`.
|
||||
|
||||
```yaml
|
||||
cdm:
|
||||
default: chromecdm_l3
|
||||
EXAMPLE2: android_l1
|
||||
```
|
||||
|
||||
A value may also be a **nested dict** for advanced selection by quality, DRM system, or
|
||||
profile:
|
||||
|
||||
```yaml
|
||||
cdm:
|
||||
EXAMPLE1:
|
||||
">=1080": android_l1 # by track height
|
||||
"<1080": chromecdm_l3
|
||||
widevine: android_l1 # by DRM system
|
||||
playready: sl3000
|
||||
default: chromecdm_l3
|
||||
```
|
||||
|
||||
Quality keys support `1080`, `>=1080`, `>720`, `<=576`, `<480` style comparisons; DRM keys
|
||||
are `widevine` / `playready`.
|
||||
|
||||
### `remote_cdm`
|
||||
|
||||
- **Type:** `list[dict]` · **Default:** `[]`
|
||||
|
||||
A list of remote CDM server definitions. Each entry is matched by its `name` (referenced from
|
||||
[`cdm`](#cdm)), and its `type` selects the backend:
|
||||
|
||||
| `type` | Backend | Key fields |
|
||||
|--------|---------|-----------|
|
||||
| `decrypt_labs` | Decrypt Labs KeyXtractor | `host` (default `https://keyxtractor.decryptlabs.com`), `device_name`, `secret` (falls back to `decrypt_labs_api_key`) |
|
||||
| `custom_api` | Fully YAML-configurable remote API | `device`, `auth`, `endpoints`, `request_mapping`, `response_mapping`, `caching`, `timeout` |
|
||||
| *(none)*, `Device Type: PLAYREADY` | pyplayready `RemoteCdm` | `host`, `secret`, `device_name`, `security_level` (default 3000) |
|
||||
| *(none)*, otherwise | pywidevine `RemoteCdm` | `host`, `secret`, `device_name`, `device_type`, `system_id`, `security_level` (default 3000) |
|
||||
|
||||
```yaml
|
||||
remote_cdm:
|
||||
- name: keyxtractor
|
||||
type: decrypt_labs
|
||||
device_name: L1
|
||||
- name: my_wv_server
|
||||
host: https://cdm.example.com
|
||||
secret: s3cr3t
|
||||
device_name: android_l1
|
||||
device_type: ANDROID
|
||||
system_id: 26830
|
||||
security_level: 1
|
||||
```
|
||||
|
||||
### `decryption`
|
||||
|
||||
- **Type:** `str` or `dict` · **Default:** `"shaka"`
|
||||
|
||||
Selects the tool used to physically decrypt CENC content. The value is compared
|
||||
case-insensitively; `"mp4decrypt"` selects Bento4's mp4decrypt, and **anything else**
|
||||
(including `"shaka"`) selects shaka-packager.
|
||||
|
||||
=== "Global"
|
||||
|
||||
```yaml
|
||||
decryption: shaka
|
||||
```
|
||||
|
||||
=== "Per-service"
|
||||
|
||||
```yaml
|
||||
decryption:
|
||||
default: shaka
|
||||
SOMESERVICE: mp4decrypt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network & proxy { #network-proxy }
|
||||
|
||||
### `network`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
TLS-fingerprinting and HTTP client settings for the rnet-based session.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `browser` | str | `"Chrome131"` | Impersonation preset. Must be an exact preset name (e.g. `Chrome131`, `Firefox135`, `Edge101`, `Safari18`, `OkHttp4_12`, `OkHttp5`, `Opera118`); unknown names raise an error. |
|
||||
| `http1_only` | bool | *(unset)* | Force HTTP/1.1. |
|
||||
| `http2_only` | bool | *(unset)* | Force HTTP/2. |
|
||||
| `connect_timeout` | int | `10` | Connect timeout in seconds. |
|
||||
| `read_timeout` | int | `30` | Read timeout in seconds (max gap between response chunks). |
|
||||
| `timeout` | int | *(unset)* | Total request timeout in seconds. |
|
||||
| `allow_redirects` | bool | *(unset)* | Follow redirects automatically (rnet client default when unset). |
|
||||
| `pool_idle_timeout` | int | `55` | Seconds an idle pooled connection is kept (default stays under typical ~60s CDN idle kills). |
|
||||
| `pool_max_idle_per_host` | int | `16` | Max idle connections kept per host. |
|
||||
| `pool_max_size` | int | *(unset)* | Max total connections in the pool. |
|
||||
| `tcp_keepalive` | int | `30` | TCP keepalive time in seconds. |
|
||||
| `tcp_keepalive_interval` | int | *(unset)* | TCP keepalive probe interval in seconds. |
|
||||
| `tcp_keepalive_retries` | int | *(unset)* | TCP keepalive probe retry count. |
|
||||
| `tcp_nodelay` | bool | *(unset)* | Disable Nagle's algorithm. |
|
||||
|
||||
```yaml
|
||||
network:
|
||||
browser: Firefox135
|
||||
http2_only: true
|
||||
```
|
||||
|
||||
!!! note "When to force `http1_only`"
|
||||
HTTP/2 is the right default on large CDNs, so leave it on. Force HTTP/1.1 only for a host that
|
||||
throttles per-connection, or a slow origin stuck behind HTTP/2 flow control. In benchmarks
|
||||
this won 30 to 50% on such hosts but **cost up to 27% on fast CDNs**, so enable it only when
|
||||
your own measurements justify it.
|
||||
|
||||
!!! warning "Renamed from `curl_impersonate`"
|
||||
The old `curl_impersonate` section is a deprecated alias. If you still use it, unshackle
|
||||
honours it (only when `network` is absent) but emits a `DeprecationWarning`. Rename it to
|
||||
`network`.
|
||||
|
||||
### `headers`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Default HTTP headers merged into every session unshackle creates.
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
```
|
||||
|
||||
!!! warning "Don't set `Accept-Encoding` (and similar) here"
|
||||
Keep this block to sane cross-service defaults such as `Accept-Language` and `User-Agent`.
|
||||
Compatibility headers like `Accept-Encoding` are set for you by the rnet HTTP backend as
|
||||
part of its browser-impersonation profile, and overriding them can break the impersonation
|
||||
fingerprint. Anything useful to only some services belongs in that service's config, not
|
||||
here.
|
||||
|
||||
### `proxy_providers`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Proxy/VPN provider configuration. Each sub-key names a provider, and its block is passed
|
||||
straight to that provider's constructor. See [Proxies & VPN](../guide/proxies-and-vpn.md) for
|
||||
the full provider guide. Recognised providers and their exit ports:
|
||||
|
||||
| Provider | Config key | Credentials | Proxy scheme/port |
|
||||
|----------|-----------|-------------|-------------------|
|
||||
| Basic (static) | `basic` | country → URI(s) | as specified |
|
||||
| NordVPN | `nordvpn` | 48-char **service** credentials | `https://...:89` |
|
||||
| Surfshark | `surfsharkvpn` | 48-char **service** credentials | `https://...:443` |
|
||||
| Windscribe | `windscribevpn` | service credentials | `https://...:443` |
|
||||
| ExpressVPN | `expressvpn` | device login (`enable: true`) / token cache | `https://cat:...@...:443` |
|
||||
| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://...:4443` (or `:443` Secure Core) |
|
||||
| Gluetun | `gluetun` | per-VPN keys/creds | `http://localhost:{port}` (local Docker) |
|
||||
| Hola | *(none, auto)* | none | `http://...:{peer}` |
|
||||
|
||||
```yaml
|
||||
proxy_providers:
|
||||
basic:
|
||||
us: http://user:pass@1.2.3.4:8080
|
||||
de:
|
||||
- http://a.example:8080
|
||||
- socks5://b.example:1080
|
||||
nordvpn:
|
||||
username: <48-char service username>
|
||||
password: <48-char service password>
|
||||
```
|
||||
|
||||
!!! note "Provider loading differs between CLI and REST server"
|
||||
The `dl` CLI loads all providers, including `windscribevpn` and `gluetun`. The REST API /
|
||||
remote-client path uses a separate resolver that does **not** load `windscribevpn` or
|
||||
`gluetun`. ExpressVPN and ProtonVPN also auto-load when their cached session exists, and Hola
|
||||
auto-loads whenever the `hola-proxy` binary is present.
|
||||
|
||||
---
|
||||
|
||||
## Key vaults { #key-vaults }
|
||||
|
||||
The full guide is at [Vaults](../guide/vaults.md). Two keys configure them.
|
||||
|
||||
### `key_vaults`
|
||||
|
||||
- **Type:** `list[dict]` · **Default:** `[]`
|
||||
|
||||
An ordered list of key-vault backends. unshackle queries them in order and reuses content
|
||||
keys instead of re-licensing. Each entry needs a `type` (the backend module name) and a
|
||||
`name`, plus backend-specific keys.
|
||||
|
||||
| `type` | Purpose | Required keys | Notes |
|
||||
|--------|---------|--------------|-------|
|
||||
| `SQLite` | Local SQLite database | `name`, `path` | Loaded **critically**; a failure aborts the run. |
|
||||
| `MySQL` | Remote MySQL database | `name`, `host`, `database`, `username` | Extra keys (e.g. `password`, `port`) forwarded to pymysql. |
|
||||
| `API` | RESTful JSON API | `name`, `uri`, `token` | Honours `vault_timeout`. |
|
||||
| `HTTP` | HTTP API with modes | `name`, `host`, one of `password`/`api_key` | `api_mode`: `query` (default), `json`, `decrypt_labs`. |
|
||||
|
||||
```yaml
|
||||
key_vaults:
|
||||
- type: SQLite
|
||||
name: Local
|
||||
path: ~/.unshackle/keys.db
|
||||
- type: MySQL
|
||||
name: Team
|
||||
host: db.example.com
|
||||
database: keys
|
||||
username: unshackle
|
||||
password: hunter2
|
||||
no_push: false
|
||||
```
|
||||
|
||||
!!! tip "Common per-entry options"
|
||||
- `no_push: true` makes a vault read-only (keys are fetched but never written to it).
|
||||
- A vault of `type: api` whose `name` contains `decrypt_labs` auto-fills its `token` from
|
||||
[`decrypt_labs_api_key`](#external-api-keys) when not set inline.
|
||||
- An all-zero content key (32 zeros) is treated as "no key" everywhere and is never
|
||||
stored.
|
||||
|
||||
### `vault_timeout`
|
||||
|
||||
- **Type:** `float` · **Default:** `10.0`
|
||||
|
||||
Timeout in seconds for vault operations. Injected automatically into any backend whose
|
||||
constructor accepts a `timeout` parameter (a per-vault `timeout` still wins).
|
||||
|
||||
---
|
||||
|
||||
## External API keys { #external-api-keys }
|
||||
|
||||
All default to an empty string and enable optional metadata/geolocation features.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `imdb_api_enabled` | bool | `false` | Use the free IMDxAPI (api.tiffara.com) metadata provider, which needs no API key. Off by default since the site has been unreliable. |
|
||||
| `omdb_api_key` | str | `""` | [OMDb](https://www.omdbapi.com/) API key for IMDb metadata lookups; a more reliable alternative to IMDxAPI. Free keys are available on the OMDb site. |
|
||||
| `tmdb_api_key` | str | `""` | TMDB API key for metadata enrichment and external-ID tags. |
|
||||
| `simkl_client_id` | str | `""` | SIMKL client ID for metadata lookups; an alternative/fallback source to TMDB. |
|
||||
| `decrypt_labs_api_key` | str | `""` | Global Decrypt Labs API key (used by remote CDM / vault). |
|
||||
| `ipinfo_api_key` | str | `""` | ipinfo.io API key for IP/region lookups. |
|
||||
|
||||
!!! tip "SIMKL is a fallback for TMDB"
|
||||
SIMKL is positioned as an alternative metadata source, and it is most valuable precisely
|
||||
**when no `tmdb_api_key` is configured**. It improves title-matching and tagging
|
||||
reliability in that case rather than replacing TMDB where TMDB is available.
|
||||
|
||||
!!! note "`ipinfo_api_key` never touches your service sessions"
|
||||
The token is only ever sent to `api.ipinfo.io` as a per-request `Authorization` header; it
|
||||
is **never** attached to your service session, so it cannot leak to a streaming provider.
|
||||
Lookups degrade gracefully through a fallback chain: the authenticated Lite endpoint (higher
|
||||
rate limits and richer fields) → anonymous ipinfo → `ip-api.in` as a last resort.
|
||||
|
||||
---
|
||||
|
||||
## Caching & updates { #caching-updates }
|
||||
|
||||
### Title cache
|
||||
|
||||
unshackle caches fetched title metadata (region- and account-aware) to avoid repeat API
|
||||
calls.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `title_cache_enabled` | bool | `true` | Master switch for the title cache. |
|
||||
| `title_cache_time` | int (seconds) | `1800` (30 min) | Lifetime of fresh cached titles. |
|
||||
| `title_cache_max_retention` | int (seconds) | `86400` (24 h) | How long stale cached data may still be served as a fallback when a live fetch fails. |
|
||||
|
||||
### Update checks
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `update_checks` | bool | `true` | Whether to check for new unshackle releases. |
|
||||
| `update_check_interval` | int (hours) | `24` | Minimum hours between update checks. |
|
||||
|
||||
!!! note
|
||||
Update checks query the GitHub releases API with a fixed 5-second timeout and cache the
|
||||
result in `directories.cache/update_check.json`.
|
||||
|
||||
---
|
||||
|
||||
## Logging, privacy & debug { #logging-privacy-debug }
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `redact_paths` | bool | `true` | Mask install-root / venv / home prefixes in logged paths (`<unshackle>`, `<venv>`, `~`). Set `false` to show full paths. |
|
||||
| `debug` | bool | `false` | Global debug mode. |
|
||||
| `debug_keys` | bool | `false` | Log decryption keys. |
|
||||
| `debug_requests` | bool | `false` | Log HTTP requests. |
|
||||
| `set_terminal_bg` | bool | `false` | Append the theme's background colour to output styling. |
|
||||
|
||||
!!! tip "When to enable `set_terminal_bg`"
|
||||
When on, the theme's background colour is appended to the foreground styles so the full
|
||||
colour palette renders correctly on terminals whose *default* background differs from
|
||||
unshackle's theme. Turn it on if the ASCII-art banner or coloured output **looks off** on
|
||||
your terminal. Otherwise leave it off.
|
||||
|
||||
!!! danger "Key exposure"
|
||||
Decryption content keys are written to the structured debug log at INFO level during the
|
||||
DRM handshake. Treat `debug`, `debug_keys`, and the `unshackle_debug_*.jsonl` files as
|
||||
sensitive, and keep `redact_paths` enabled when sharing logs.
|
||||
|
||||
`debug_keys` affects **only** content-encryption keys (the `content_key`/`key` fields).
|
||||
Passwords, tokens, cookies, and session tokens are **always** redacted regardless of this
|
||||
setting, and KIDs, key counts, and other metadata are always logged either way.
|
||||
|
||||
---
|
||||
|
||||
## Deprecated & removed keys { #deprecated-removed-keys }
|
||||
|
||||
| Key | Status | Behaviour |
|
||||
|-----|--------|-----------|
|
||||
| `curl_impersonate` | **Deprecated** → use [`network`](#network-proxy) | Emits a `DeprecationWarning`; still honoured only if `network` is absent. |
|
||||
| `downloader` | **Deprecated** | Any value other than `"requests"` emits a `DeprecationWarning`; the value is otherwise ignored (the unified requests downloader is always used). |
|
||||
| `scene_naming` | **Removed** | If present at all, unshackle exits with an error directing you to configure [`output_template`](#output-naming) instead. |
|
||||
|
||||
---
|
||||
|
||||
## A complete annotated example
|
||||
|
||||
The following config sets a handful of common keys. Every key not shown keeps its default.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
downloads: ~/Media/unshackle
|
||||
temp: /mnt/scratch/unshackle
|
||||
|
||||
network:
|
||||
browser: Chrome131
|
||||
|
||||
headers:
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
|
||||
cdm:
|
||||
default: chromecdm_l3
|
||||
EXAMPLE2: android_l1
|
||||
|
||||
decryption: shaka
|
||||
|
||||
credentials:
|
||||
EXAMPLE2: user@example.com:hunter2
|
||||
|
||||
output_template:
|
||||
movies: "{title}.{year}.{quality?}.{source}-{tag}"
|
||||
series: "{title}.{season_episode}.{quality?}.{source}-{tag}"
|
||||
folder:
|
||||
movies: "{title} ({year})"
|
||||
series: "{title} ({year})"
|
||||
|
||||
tag: MYGRP
|
||||
|
||||
key_vaults:
|
||||
- type: SQLite
|
||||
name: Local
|
||||
path: ~/.unshackle/keys.db
|
||||
|
||||
proxy_providers:
|
||||
basic:
|
||||
us: http://user:pass@1.2.3.4:8080
|
||||
|
||||
omdb_api_key: "your-omdb-key"
|
||||
tmdb_api_key: "your-tmdb-key"
|
||||
title_cache_time: 3600
|
||||
redact_paths: true
|
||||
```
|
||||
|
||||
For how these interact at runtime, continue to [Downloading](../guide/downloading.md),
|
||||
[DRM & CDM](../guide/drm-and-cdm.md), and [Output & naming](../guide/output-and-naming.md).
|
||||
| Overview & quick index | [configuration/index.md](configuration/index.md) |
|
||||
| Directories & filenames | [configuration/directories.md](configuration/directories.md) |
|
||||
| Services & authentication | [configuration/services.md](configuration/services.md) |
|
||||
| Download & processing | [configuration/download.md](configuration/download.md) |
|
||||
| Output & naming | [configuration/output.md](configuration/output.md) |
|
||||
| DRM & CDM | [configuration/drm.md](configuration/drm.md) |
|
||||
| Network & proxy | [configuration/network.md](configuration/network.md) |
|
||||
| Key vaults | [configuration/vaults.md](configuration/vaults.md) |
|
||||
| Other settings | [configuration/misc.md](configuration/misc.md) |
|
||||
| Complete annotated example | [configuration/example.md](configuration/example.md) |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Directories { #directories }
|
||||
|
||||
The `directories` key is a **dict** mapping directory names to filesystem paths. Only the
|
||||
names listed below are honoured; a handful are **protected** and cannot be moved (attempts
|
||||
to override them are silently skipped). All defaults are computed relative to the installed
|
||||
package, and every user-settable path is passed through `Path(...).expanduser()`, so `~`
|
||||
works.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
downloads: ~/Media/unshackle
|
||||
temp: /mnt/fast-scratch/unshackle-temp
|
||||
wvds: ~/.unshackle/WVDs
|
||||
prds: ~/.unshackle/PRDs
|
||||
```
|
||||
|
||||
| Key | Type | Default | Overridable | Purpose |
|
||||
|-----|------|---------|:-----------:|---------|
|
||||
| `downloads` | path | `<repo>/downloads` | ✅ | Default output directory for finished files. |
|
||||
| `temp` | path | `<repo>/temp` | ✅ | Temporary working files during download/decrypt/mux. |
|
||||
| `cache` | path | `<data>/cache` | ✅ | Generic cache, title cache, and the update-check store. |
|
||||
| `cookies` | path | `<data>/cookies` | ✅ | Per-service cookie files (and VPN cookie files). |
|
||||
| `logs` | path | `<data>/logs` | ✅ | Log files. |
|
||||
| `exports` | path | `<data>/exports` | ✅ | Export JSON files. |
|
||||
| `wvds` | path | `<data>/WVDs` | ✅ | Widevine device files (`.wvd`). |
|
||||
| `prds` | path | `<data>/PRDs` | ✅ | PlayReady device files (`.prd`). |
|
||||
| `dcsl` | path | `<data>/DCSL` | ✅ | DCSL data. |
|
||||
| `commands` | path | `unshackle/commands` | ✅ | CLI command modules. |
|
||||
| `services` | list \| path | `[unshackle/services]` | ✅ | Service search paths and/or remote repo specs (see below). |
|
||||
| `vaults` | path | `unshackle/vaults` | ✅ | Vault backend modules. |
|
||||
| `fonts` | path | `unshackle/fonts` | ✅ | Bundled fonts. |
|
||||
| `user_configs` | path | `unshackle/` | ❌ protected | Where `unshackle.yaml` lives. |
|
||||
| `data` | path | `unshackle/` | ❌ protected | Base for the data subdirectories above. |
|
||||
| `core_dir` | path | `unshackle/core` | ❌ protected | Package core. |
|
||||
| `namespace_dir` | path | `unshackle/` | ❌ protected | Package root. |
|
||||
| `app_dirs` | - | `AppDirs("unshackle", False)` | ❌ protected | Internal AppDirs instance. |
|
||||
|
||||
!!! note "The `services` directory is special"
|
||||
`services` may be a **list**, and each entry can be either a local directory or a
|
||||
**repository spec**: a git URL (`https://...`, `ssh://...`, `git@...`, or anything ending in
|
||||
`.git`) or `owner/repo` shorthand. Repo specs are cloned and updated automatically; plain
|
||||
paths are used as-is. **List order is priority**: the first source to define a service
|
||||
tag wins.
|
||||
|
||||
```yaml
|
||||
directories:
|
||||
services:
|
||||
- you/your-services # cloned from GitHub
|
||||
- https://example.com/private.git@stable
|
||||
- ~/my-services # local, lowest priority
|
||||
```
|
||||
|
||||
!!! info "How git-backed service repos are handled"
|
||||
unshackle's use of git is **read-only on the remote**: it only ever performs a shallow
|
||||
`clone`, `fetch`, `pull`, and a local `reset`; **nothing is ever pushed**. Private repos
|
||||
are authenticated through your existing git credential helper, and unshackle stores no
|
||||
tokens of its own. Clones live under `<first-local-services-dir>/_repos/<host>__<owner>__<repo>/`
|
||||
(or the bundled `unshackle/services`), and **nothing is written to the cache directory**. After
|
||||
the first clone unshackle re-pulls **at most once every 24 h**, so it does not touch the
|
||||
network on every run.
|
||||
|
||||
!!! warning "Automatic and manual refresh behave differently"
|
||||
The **automatic** 24 h-TTL refresh that happens during a normal `dl`/`search` run
|
||||
**refuses to refresh and exits** (naming the offending clone) if that clone has
|
||||
uncommitted changes to tracked files or unpushed local commits. Untracked files (new
|
||||
service folders, `__pycache__`) never block it. The **manual**
|
||||
`unshackle util refresh-services` command instead **hard-resets** the clone to upstream,
|
||||
discarding any local edits.
|
||||
|
||||
!!! warning "Read-only installs and reinstalls"
|
||||
If the services directory lives inside the installed package, a reinstall can delete the
|
||||
`_repos` clones. They are re-cloned on next use. On **read-only installs** you must
|
||||
point `services` at a writable path, or cloning will fail.
|
||||
|
||||
## Filenames { #filenames }
|
||||
|
||||
The `filenames` key is a **dict** of templated file/name patterns. Each value is used
|
||||
verbatim (no path processing). Braced fields like `{time}` and `{service}` are filled in at
|
||||
runtime.
|
||||
|
||||
| Key | Type | Default | Notes |
|
||||
|-----|------|---------|-------|
|
||||
| `log` | str | `"unshackle_{name}_{time}.log"` | Written under `directories.logs`. |
|
||||
| `debug_log` | str | `"unshackle_debug_{service}_{time}.jsonl"` | Structured debug log, under `directories.logs`. |
|
||||
| `config` | str | `"config.yaml"` | Per-service config file, under that service's directory. |
|
||||
| `root_config` | str | `"unshackle.yaml"` | The main config filename itself. |
|
||||
| `chapters` | str | `"Chapters_{title}_{random}.txt"` | Under `directories.temp`. |
|
||||
| `subtitle` | str | `"Subtitle_{id}_{language}.srt"` | Under `directories.temp`. |
|
||||
@@ -0,0 +1,253 @@
|
||||
# Download & processing { #download-processing }
|
||||
|
||||
## `dl`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Persistent defaults for the `dl` command. The block is used as Click's `default_map`, so any
|
||||
`dl` flag can be given a default here. See [Downloading](../../guide/downloading.md) and the
|
||||
[CLI Reference](../../guide/cli-reference.md) for what each flag does.
|
||||
|
||||
**Key naming.** A key is the flag's long name with dashes replaced by underscores
|
||||
(`--best-available` → `best_available`, `--sub-format` → `sub_format`). A few keys are named
|
||||
after the flag's internal destination rather than its visible name; set these exact keys:
|
||||
|
||||
| Flag | Config key |
|
||||
|------|-----------|
|
||||
| `-r` / `--range` | `range` |
|
||||
| `--list` | `list` |
|
||||
| `--tmdb` | `tmdb_id` |
|
||||
| `--imdb` | `imdb_id` |
|
||||
| `--animeapi` | `animeapi_id` |
|
||||
| `-naa` / `--noatmos` | `no_atmos` |
|
||||
| `-o` / `--output` | `output_dir` |
|
||||
| `--cdm-only` / `--vaults-only` | `cdm_only` |
|
||||
|
||||
**Precedence** (highest first): an explicit CLI flag → a per-service
|
||||
[`services.<TAG>.dl`](services.md) override → this global `dl:` block → the built-in
|
||||
default. In other words, config only fills in options you did not set on the command line.
|
||||
|
||||
Common keys (a useful subset; every `dl` flag works):
|
||||
|
||||
| Key | Type | Default | Sets |
|
||||
|-----|------|---------|------|
|
||||
| `lang` | list | `["orig"]` | Video/audio language(s); `orig` = original language. |
|
||||
| `a_lang` / `v_lang` | list | `[]` | Audio- / video-only language override. |
|
||||
| `s_lang` | list | `["all"]` | Subtitle language(s). |
|
||||
| `quality` | list | `[]` (best) | Resolution(s), e.g. `[1080]`. |
|
||||
| `vcodec` | list | `[]` (any) | Video codec(s), e.g. `[H265]`. |
|
||||
| `acodec` | list | `[]` (any) | Audio codec(s), e.g. `[EC3]`. |
|
||||
| `range` | list | `["SDR"]` | Colour range(s): `SDR`, `HLG`, `HDR10`, `HDR10P`, `DV`, `HYBRID`. |
|
||||
| `channels` | float | *(unset)* | Audio channels, e.g. `6` for 5.1. |
|
||||
| `sub_format` | str | *(unset)* | Convert subtitles to this format (`srt`, `vtt`, `original`, ...). |
|
||||
| `forced_subs` | bool | `false` | Include forced subtitle tracks. |
|
||||
| `forced_s_lang` | list | `[]` | Forced subtitle language(s); implies `forced_subs`. |
|
||||
| `no_subs` / `no_audio` / `no_chapters` | bool | `false` | Skip that track type. |
|
||||
| `downloads` | int | `1` | Tracks downloaded concurrently. |
|
||||
| `workers` | int | *(downloader default)* | Threads per track. |
|
||||
| `speed_limit` | str | *(unlimited)* | Total download speed cap across all downloads combined, e.g. `500k`, `5M`, `1.5G` or plain bytes/sec. Values are bytes, not bits (`5M` = 5.0 MB/s). |
|
||||
| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. |
|
||||
| `best_available` | bool | `false` | Fall back to best quality if the request is unavailable. |
|
||||
| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. |
|
||||
| `no_folder` | bool | `false` | Do not create a per-title folder. |
|
||||
|
||||
```yaml title="Global download defaults"
|
||||
dl:
|
||||
lang: [en]
|
||||
quality: [1080]
|
||||
range: [HDR10]
|
||||
vcodec: [H265]
|
||||
sub_format: srt
|
||||
downloads: 2
|
||||
```
|
||||
|
||||
!!! note "Per-service overrides"
|
||||
A `dl` block nested under a service in [`services`](services.md) takes the same keys and
|
||||
applies to that service only.
|
||||
|
||||
```yaml
|
||||
dl:
|
||||
lang: [en]
|
||||
services:
|
||||
EXAMPLE1:
|
||||
dl:
|
||||
lang: [en, ja]
|
||||
range: [DV]
|
||||
```
|
||||
|
||||
!!! note "`serve` uses these same keys"
|
||||
The [`serve`](services.md#serve) block accepts most of the same `dl` option keys at its top level
|
||||
to set server-wide download defaults for every request it handles. Keys the API does not know
|
||||
are ignored there (`speed_limit`, for example; `serve` has its own `global_speed_limit`).
|
||||
|
||||
## `subtitle`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Subtitle handling: SDH stripping, format conversion, and whether subtitles are muxed in or
|
||||
written as sidecar files. See [Subtitles](../../guide/subtitles.md) for the full guide.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `strip_sdh` | bool | `true` | Strip SDH/CC cues (hearing-impaired annotations) into a clean track. |
|
||||
| `sdh_method` | str | `"auto"` | SDH-stripping backend to use. |
|
||||
| `preserve_formatting` | bool | `true` | Keep styling/positioning when converting or stripping. |
|
||||
| `convert_before_strip` | bool | `true` | Convert to the working format before stripping SDH. |
|
||||
| `conversion_method` | str | `"auto"` | Subtitle conversion backend. |
|
||||
| `output_mode` | str | `"mux"` | `mux` embeds subtitles in the MKV; `sidecar` writes separate files; `both` writes sidecars and still muxes. |
|
||||
| `sidecar_format` | str | `"srt"` | Format for sidecar files when `output_mode` is `sidecar` or `both`. |
|
||||
| `type_priority` | list | *(unset)* | Ordered ranking of subtitle types (`forced`, `normal`, `sdh`; CC counts as SDH). Types you leave out fall to the end. |
|
||||
| `group_by` | str | `"type"` | Which key groups the subtitle tracks. `type` puts all forced tracks together, then all normal, then all SDH. `language` keeps each language next to its own variants. |
|
||||
| `language_priority` | list | *(unset)* | Languages to sort to the top, in the order you give. The rest keep the default order: original language first, then alphabetical. Nothing is removed. |
|
||||
|
||||
```yaml
|
||||
subtitle:
|
||||
strip_sdh: true
|
||||
output_mode: sidecar
|
||||
sidecar_format: srt
|
||||
```
|
||||
|
||||
!!! note "`group_by` track order"
|
||||
| `group_by` | Result |
|
||||
|---|---|
|
||||
| `type` | Danish, Finnish, French, … Danish SDH, Finnish SDH, French SDH |
|
||||
| `language` | Danish, Danish SDH, Finnish, Finnish SDH, French, French SDH |
|
||||
|
||||
`type_priority` still sets the order of the variants, so with `group_by: language` it
|
||||
controls the order inside each language.
|
||||
|
||||
!!! note "`language_priority` sorts languages, it does not select them"
|
||||
```yaml
|
||||
subtitle:
|
||||
group_by: language
|
||||
language_priority: [en, es, fr]
|
||||
```
|
||||
|
||||
| | Result for an English title |
|
||||
|---|---|
|
||||
| Default | English, Arabic, Bulgarian, Spanish, French, Japanese |
|
||||
| With the list above | English, Spanish, French, Arabic, Bulgarian, Japanese |
|
||||
|
||||
Nothing is removed, unlike `-sl`, which drops every subtitle language it does not name.
|
||||
|
||||
Use `orig` in the list for the title's original language. A base tag also covers its
|
||||
regional forms, so `en` sorts `en-US` and `en-GB`, but `zh` does **not** cover
|
||||
`zh-Hant`, which you must name. The sort ignores entries that match no track, and it
|
||||
ignores entries that are not valid language tags.
|
||||
|
||||
`--exact-lang` applies here too: with it, `en` sorts only `en`, and `en-US` falls to
|
||||
the alphabetical tail with the rest. Name each tag you want when you use that flag.
|
||||
|
||||
!!! note "`type_priority` picks the default subtitle only with `muxing.default_language`"
|
||||
At mux time the `default` flag goes to every forced subtitle whose language matches the
|
||||
first audio track, so a full dialogue track never becomes default on its own.
|
||||
`type_priority` alone does not change that. Name a subtitle language in
|
||||
[`muxing.default_language`](#muxing) and the first track in that language gets the flag
|
||||
instead, which is where `type_priority` decides which track that is:
|
||||
|
||||
```yaml
|
||||
muxing:
|
||||
default_language:
|
||||
subtitle: en
|
||||
subtitle:
|
||||
type_priority: [normal, sdh, forced]
|
||||
```
|
||||
|
||||
## `audio`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Audio handling options. The most notable sub-key is **`codec_priority`**, an ordered list of
|
||||
audio codecs used to break ties in track selection. **`language_priority`** is an ordered list
|
||||
of languages that sort to the top of the audio tracks.
|
||||
|
||||
```yaml
|
||||
audio:
|
||||
codec_priority:
|
||||
- EAC3
|
||||
- AC3
|
||||
language_priority:
|
||||
- orig
|
||||
- en
|
||||
```
|
||||
|
||||
!!! note "`codec_priority` is a soft priority: nothing is dropped"
|
||||
`codec_priority` outranks bitrate: any listed codec sorts above every unlisted one, and among
|
||||
listed codecs the list order decides. Bitrate order survives only within a single codec rank.
|
||||
Nothing is ever removed. Two higher-level rules still override it: **Atmos
|
||||
tracks always take precedence over `codec_priority`**, and **audio-description tracks are
|
||||
always forced to the end** regardless of it. When `codec_priority` is unset, tracks sort by
|
||||
bitrate alone, with those same Atmos/descriptive rules still applied.
|
||||
|
||||
## `muxing`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Matroska (MKV) muxing options.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `set_title` | bool | `true` | Write a human-readable title into the MKV container. |
|
||||
| `default_language` | dict | `{}` | Force which language is flagged *default* per track type, e.g. `{audio: en, subtitle: en}`. |
|
||||
| `merge_audio` | bool | `true` | Merge all audio into one file. `--split-audio` on the CLI flips this off. |
|
||||
| `merge_video` | bool | `false` | Merge video tracks that share height, range, and codec into one file, so only language varies inside a file. `--merge-video` on the CLI flips this on. |
|
||||
|
||||
```yaml
|
||||
muxing:
|
||||
set_title: true
|
||||
default_language:
|
||||
audio: en
|
||||
subtitle: en
|
||||
```
|
||||
|
||||
## `language_tags`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Language-tag remapping and the rule engine behind the `{lang_tag}` filename variable. When a
|
||||
`rules` list is present, each rule is evaluated in order and the first match's `tag` is used.
|
||||
A rule may test:
|
||||
|
||||
| Condition | Meaning |
|
||||
|-----------|---------|
|
||||
| `audio` | Matches against the audio track languages. |
|
||||
| `subs_contain` | At least one subtitle language matches. |
|
||||
| `subs_contain_all` | Every listed language must be present (scalar or list). |
|
||||
|
||||
```yaml
|
||||
language_tags:
|
||||
rules:
|
||||
- audio: [ja]
|
||||
subs_contain: [en]
|
||||
tag: "SUBBED"
|
||||
```
|
||||
|
||||
## `dual_multi_mode`
|
||||
|
||||
- **Type:** `str` · **Default:** `strict`
|
||||
|
||||
Rule set for the `{dual}`, `{multi}`, and `{dubbed}` filename variables.
|
||||
|
||||
=== "`strict` (default)"
|
||||
|
||||
| Variable | Set when |
|
||||
|----------|----------|
|
||||
| `{dual}` | Exactly two audio languages **and one is the title's original language**. |
|
||||
| `{multi}` | Three or more audio languages, even without the original. |
|
||||
| `{dubbed}` | A single audio language that is not the title's original language (requires a known original). |
|
||||
|
||||
=== "`count` (legacy)"
|
||||
|
||||
| Variable | Set when |
|
||||
|----------|----------|
|
||||
| `{dual}` | Exactly two audio languages (original language ignored). |
|
||||
| `{multi}` | Three or more audio languages. |
|
||||
| `{dubbed}` | Never set. |
|
||||
|
||||
```yaml
|
||||
dual_multi_mode: strict
|
||||
```
|
||||
|
||||
!!! note "Dialects count as one language"
|
||||
Regional variants of the same base language (e.g. `en-US` and `en-GB`) collapse to a single
|
||||
language in both modes, so they never trigger `{dual}` on their own.
|
||||
@@ -0,0 +1,157 @@
|
||||
# DRM & CDM { #drm-cdm }
|
||||
|
||||
These are the config keys. For concepts, device provisioning, and the `wvd`/`prd` commands see
|
||||
the [DRM & CDM guide](../../guide/drm-and-cdm.md). Device files themselves live in
|
||||
[`directories.wvds`](directories.md) (`.wvd`) and `directories.prds` (`.prd`).
|
||||
|
||||
## `cdm`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a `default` fallback.
|
||||
Resolution is case-insensitive: a per-request API override wins, then the per-service entry,
|
||||
then `default`.
|
||||
|
||||
```yaml
|
||||
cdm:
|
||||
default: chromecdm_l3
|
||||
EXAMPLE2: android_l1
|
||||
```
|
||||
|
||||
A value may also be a **nested dict** for advanced selection by quality, DRM system, or
|
||||
profile:
|
||||
|
||||
```yaml
|
||||
cdm:
|
||||
EXAMPLE1:
|
||||
">=1080": android_l1 # by track height
|
||||
"<1080": chromecdm_l3
|
||||
widevine: android_l1 # by DRM system
|
||||
playready: sl3000
|
||||
default: chromecdm_l3
|
||||
```
|
||||
|
||||
Quality keys support `1080`, `>=1080`, `>720`, `<=576`, `<480` style comparisons; DRM keys
|
||||
are `widevine` / `playready`. Quality keys are matched first, but only when a video height is
|
||||
known; if the value is still a dict after that, `widevine`/`playready` are used when present,
|
||||
otherwise the credential profile name, then `default`.
|
||||
|
||||
The resolved name is looked up in [`remote_cdm`](#remote_cdm) by `name` first. If no entry
|
||||
matches, it is loaded as a local device file, in this order: `<name>.prd` in
|
||||
`directories.prds`, `<name>.prd` in `directories.wvds`, then `<name>.wvd` in
|
||||
`directories.wvds`.
|
||||
|
||||
## `remote_cdm`
|
||||
|
||||
- **Type:** `list[dict]` · **Default:** `[]`
|
||||
|
||||
A list of remote CDM server definitions. Each entry is matched by its `name` (referenced from
|
||||
[`cdm`](#cdm)), and its `type` selects the backend:
|
||||
|
||||
| `type` | Backend | Key fields |
|
||||
|--------|---------|-----------|
|
||||
| `decrypt_labs` | Decrypt Labs KeyXtractor | `host`, `device_name`, `secret` ([fields](#remote-cdm-decrypt-labs)) |
|
||||
| `custom_api` | Fully YAML-configurable remote API | `host` (required), `device`, `auth`, `endpoints`, `request_mapping`, `response_mapping`, `caching`, `timeout` ([fields](#remote-cdm-custom-api)) |
|
||||
| *(none)*, `Device Type: PLAYREADY` | pyplayready `RemoteCdm` | `host`, `secret`, `device_name`, `security_level` (default 3000) |
|
||||
| *(none)*, otherwise | pywidevine `RemoteCdm` | `host`, `secret`, `device_name`, `device_type`, `system_id`, `security_level` (default 3000) |
|
||||
|
||||
```yaml
|
||||
remote_cdm:
|
||||
- name: keyxtractor
|
||||
type: decrypt_labs
|
||||
device_name: L1
|
||||
- name: my_wv_server
|
||||
host: https://cdm.example.com
|
||||
secret: s3cr3t
|
||||
device_name: android_l1
|
||||
device_type: ANDROID
|
||||
system_id: 26830
|
||||
security_level: 1
|
||||
```
|
||||
|
||||
For the two pywidevine/pyplayready backends, field names are read case-insensitively in both
|
||||
styles: `Device Type`/`device_type`, `System ID`/`system_id`, `Security Level`/`security_level`,
|
||||
`Host`/`host`, `Secret`/`secret`, `Device Name`/`device_name`.
|
||||
|
||||
!!! warning "PlayReady `host` needs a `/playready` suffix"
|
||||
pyplayready's `RemoteCdm` treats `host` as a base URL and appends its own endpoint paths,
|
||||
so a PlayReady entry's `host` must include the trailing `/playready` segment (for example
|
||||
`https://cdm.example.com/playready`). Without it the server returns `404` rather than a
|
||||
configuration error.
|
||||
|
||||
### `decrypt_labs` fields { #remote-cdm-decrypt-labs }
|
||||
|
||||
| Field | Default | Notes |
|
||||
|-------|---------|-------|
|
||||
| `host` | `https://keyxtractor.decryptlabs.com` | |
|
||||
| `device_name` | `ChromeCDM` | `ChromeCDM`, `L1`, `L2` (Widevine) or `SL2`, `SL3` (PlayReady) |
|
||||
| `secret` | from [`decrypt_labs_api_key`](misc.md#external-api-keys) | Sent as the `decrypt-labs-api-key` header. An error is raised if neither is set. |
|
||||
| `system_id` | `26830` (Widevine), `0` (PlayReady) | |
|
||||
| `security_level` | Widevine `3`; PlayReady `2000` for `SL2`, else `3000` | |
|
||||
|
||||
PlayReady mode is used when `device_type` is `PLAYREADY` or `device_name` starts with `SL`.
|
||||
|
||||
### `custom_api` fields { #remote-cdm-custom-api }
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `host` | str | Required. Base URL of the API. |
|
||||
| `device` | dict | `name`, `type` (`CHROME`, `ANDROID`, `PLAYREADY`), `system_id`, `security_level` |
|
||||
| `auth` | dict | `type` (default `header`): `header` uses `header_name` (default `Authorization`) and `key`; `bearer` uses `bearer_token` or `key`; `basic` uses `username` and `password`. `custom_headers` is merged in for any type. |
|
||||
| `endpoints` | dict | `get_request` and `decrypt_response`, each `{path, method, timeout}` |
|
||||
| `request_mapping` | dict | Per endpoint: `param_names`, `static_params`, `conditional_params`, `transforms`, `nested_params`, `exclude_params` |
|
||||
| `response_mapping` | dict | Per endpoint: `fields`, `transforms`, `response_types`, `success_conditions`, `error_fields`, `key_fields` |
|
||||
| `caching` | dict | `enabled`, `use_vaults`, `check_cached_first` |
|
||||
| `legacy` | dict | Legacy mode options. |
|
||||
| `timeout` | int | Default `30`. Request timeout in seconds. |
|
||||
|
||||
Transforms cover base64/hex/JSON encoding and `kid:key` parsing. PlayReady mode is used when
|
||||
`device.type` is `PLAYREADY` or the device name is `SL2`/`SL3`.
|
||||
|
||||
```yaml
|
||||
remote_cdm:
|
||||
- name: my_custom
|
||||
type: custom_api
|
||||
host: https://cdm.example.com
|
||||
device:
|
||||
name: ChromeCDM
|
||||
type: CHROME
|
||||
system_id: 26830
|
||||
security_level: 3
|
||||
auth:
|
||||
type: bearer
|
||||
key: your-token
|
||||
endpoints:
|
||||
get_request:
|
||||
path: /get-challenge
|
||||
method: POST
|
||||
decrypt_response:
|
||||
path: /get-keys
|
||||
method: POST
|
||||
timeout: 30
|
||||
```
|
||||
|
||||
## `decryption`
|
||||
|
||||
- **Type:** `str` or `dict` · **Default:** `"shaka"`
|
||||
|
||||
Selects the tool used to physically decrypt CENC content. The value is compared
|
||||
case-insensitively; `"mp4decrypt"` selects Bento4's mp4decrypt, and **anything else**
|
||||
(including `"shaka"`) selects shaka-packager. When the value is a dict, the service key is
|
||||
matched case-insensitively and the `default` entry is the fallback.
|
||||
|
||||
HLS AES-128 ClearKey is decrypted in-process and ignores this setting.
|
||||
|
||||
=== "Global"
|
||||
|
||||
```yaml
|
||||
decryption: shaka
|
||||
```
|
||||
|
||||
=== "Per-service"
|
||||
|
||||
```yaml
|
||||
decryption:
|
||||
default: shaka
|
||||
EXAMPLE1: mp4decrypt
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# A complete annotated example
|
||||
|
||||
The following config sets a handful of common keys. Every key not shown keeps its default.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
directories:
|
||||
downloads: ~/Media/unshackle
|
||||
temp: /mnt/scratch/unshackle
|
||||
|
||||
network:
|
||||
browser: Chrome131
|
||||
|
||||
headers:
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
|
||||
cdm:
|
||||
default: chromecdm_l3
|
||||
EXAMPLE2: android_l1
|
||||
|
||||
decryption: shaka
|
||||
|
||||
credentials:
|
||||
EXAMPLE2: user@example.com:hunter2
|
||||
|
||||
output_template:
|
||||
movies: "{title}.{year}.{quality?}.{source}-{tag}"
|
||||
series: "{title}.{season_episode}.{quality?}.{source}-{tag}"
|
||||
folder:
|
||||
movies: "{title} ({year})"
|
||||
series: "{title} ({year})"
|
||||
|
||||
tag: MYGRP
|
||||
|
||||
key_vaults:
|
||||
- type: SQLite
|
||||
name: Local
|
||||
path: ~/.unshackle/keys.db
|
||||
|
||||
proxy_providers:
|
||||
basic:
|
||||
us: http://user:pass@1.2.3.4:8080
|
||||
|
||||
omdb_api_key: "your-omdb-key"
|
||||
tmdb_api_key: "your-tmdb-key"
|
||||
tvdb_api_key: "your-tvdb-key"
|
||||
title_cache_time: 3600
|
||||
redact_paths: true
|
||||
```
|
||||
|
||||
For how these interact at runtime, continue to [Downloading](../../guide/downloading.md),
|
||||
[DRM & CDM](../../guide/drm-and-cdm.md), and [Output & naming](../../guide/output-and-naming.md).
|
||||
@@ -0,0 +1,42 @@
|
||||
# Configuration Reference
|
||||
|
||||
Every key you can set in `unshackle.yaml`, with its name, type, default value, and behaviour,
|
||||
split into one page per area. A worked config is on the [annotated example](example.md) page.
|
||||
|
||||
Where the file lives, how unshackle discovers it, and how to edit it:
|
||||
[The Configuration File](../../getting-started/configuration-file.md).
|
||||
|
||||
!!! info "How defaults work"
|
||||
Every key is optional except [`output_template`](output.md), which `dl` requires.
|
||||
Without it, `dl` aborts with an error. If you omit any other key, unshackle uses
|
||||
the built-in default listed here. There is **no schema validation**: unknown keys are
|
||||
silently ignored (they never become settings), and the file is parsed with
|
||||
`yaml.safe_load`. An empty or missing file means every other key falls back to its
|
||||
default.
|
||||
|
||||
!!! warning "Two keys behave specially on load"
|
||||
- `curl_impersonate` is a **deprecated** alias of [`network`](network.md) and emits a
|
||||
`DeprecationWarning`.
|
||||
- `scene_naming` has been **removed**. If it is set to any value, unshackle exits
|
||||
immediately with an error telling you to use [`output_template`](output.md)
|
||||
instead.
|
||||
|
||||
See [Deprecated & removed keys](misc.md#deprecated-removed-keys).
|
||||
|
||||
---
|
||||
|
||||
## Quick index
|
||||
|
||||
| Area | Keys |
|
||||
|------|------|
|
||||
| [Directories](directories.md) | `directories`, `filenames` |
|
||||
| [Services & authentication](services.md) | `services`, `credentials`, `firefox_cookies`, `remote_services`, `serve` |
|
||||
| [Download & processing](download.md) | `dl`, `subtitle`, `audio`, `muxing`, `language_tags`, `dual_multi_mode` |
|
||||
| [Output & naming](output.md) | `output_template`, `tag`, `tag_rules`, `tag_group_name`, `tag_imdb_tmdb`, `chapter_fallback_name`, `unicode_filenames` |
|
||||
| [DRM & CDM](drm.md) | `cdm`, `remote_cdm`, `decryption` |
|
||||
| [Network & proxy](network.md) | `network`, `headers`, `proxy_providers` |
|
||||
| [Key vaults](vaults.md) | `key_vaults`, `vault_timeout` |
|
||||
| [External API keys](misc.md#external-api-keys) | `imdb_api_enabled`, `omdb_api_key`, `tmdb_api_key`, `tvdb_api_key`, `tvdb_pin`, `tvdb_order`, `simkl_client_id`, `metadata_providers`, `decrypt_labs_api_key`, `ipinfo_api_key` |
|
||||
| [Caching & updates](misc.md#caching-updates) | `title_cache_enabled`, `title_cache_time`, `title_cache_max_retention`, `update_checks`, `update_check_interval` |
|
||||
| [Logging, privacy & debug](misc.md#logging-privacy-debug) | `redact_paths`, `debug`, `debug_keys`, `debug_requests`, `set_terminal_bg` |
|
||||
| [Deprecated & removed](misc.md#deprecated-removed-keys) | `curl_impersonate`, `downloader`, `scene_naming` |
|
||||
@@ -0,0 +1,109 @@
|
||||
# Other Settings
|
||||
|
||||
Smaller configuration areas: external metadata API keys, caching and update checks, logging and debug flags, and deprecated keys.
|
||||
|
||||
## External API keys { #external-api-keys }
|
||||
|
||||
All are unset by default and enable optional metadata/geolocation features.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `imdb_api_enabled` | bool | `false` | Use the free IMDxAPI (api.tiffara.com) metadata provider, which needs no API key. Off by default since the site has been unreliable. |
|
||||
| `omdb_api_key` | str | `""` | [OMDb](https://www.omdbapi.com/) API key for IMDb metadata lookups; a more reliable alternative to IMDxAPI. Free keys are available on the OMDb site. |
|
||||
| `tmdb_api_key` | str | `""` | TMDB API key for metadata enrichment and external-ID tags. |
|
||||
| `simkl_client_id` | str | `""` | SIMKL client ID for metadata lookups; an alternative/fallback source to TMDB. |
|
||||
| `tvdb_api_key` | str | `""` | [TheTVDB v4](https://thetvdb.com/api-information) API key; a fallback source to TMDB, strongest on TV series. Free keys are available on the TVDB site. |
|
||||
| `tvdb_pin` | str | `""` | Subscriber PIN, only needed for a user-supported TVDB key. Leave empty for a normal project key. |
|
||||
| `tvdb_order` | str | `""` | Default for `--tvdb-order`: renumber episodes to a TVDB season order (`official`, `dvd`, `absolute`, `alternate`, `regional`). Empty keeps the numbering the service gives. |
|
||||
| `metadata_providers` | list | *(see below)* | Metadata providers to use, in the order they are tried. |
|
||||
| `decrypt_labs_api_key` | str | `""` | Global Decrypt Labs API key (used by remote CDM / vault). |
|
||||
| `ipinfo_api_key` | str | `""` | ipinfo.io API key for IP/region lookups. |
|
||||
|
||||
!!! note "How `metadata_providers` is applied"
|
||||
A lookup goes to each provider in turn and stops at the first good match, so the order
|
||||
decides which source wins. `metadata_providers` sets that order:
|
||||
|
||||
```yaml
|
||||
metadata_providers: [tvdb, tmdb, simkl]
|
||||
```
|
||||
|
||||
Names not in the list are never used, so this both orders and filters. Unknown names are
|
||||
ignored with a warning. Leave it unset for the default order:
|
||||
|
||||
```yaml
|
||||
metadata_providers: [imdbapi, omdb, simkl, tmdb, tvdb]
|
||||
```
|
||||
|
||||
Whatever the order, a provider is skipped when its API key is missing (or, for
|
||||
`imdbapi`, when `imdb_api_enabled` is `false`). Available names are
|
||||
`imdbapi`, `omdb`, `simkl`, `tmdb`, and `tvdb`.
|
||||
|
||||
!!! note "`ipinfo_api_key` never touches your service sessions"
|
||||
The token is only ever sent to `api.ipinfo.io` as a per-request `Authorization` header; it
|
||||
is **never** attached to your service session, so it cannot leak to a streaming provider.
|
||||
Lookups degrade gracefully through a fallback chain: the authenticated Lite endpoint (higher
|
||||
rate limits, and ASN/continent data, but no city or region) → anonymous ipinfo →
|
||||
`ip-api.in` as a last resort.
|
||||
|
||||
---
|
||||
|
||||
## Caching & updates { #caching-updates }
|
||||
|
||||
### Title cache
|
||||
|
||||
unshackle caches fetched title metadata (region- and account-aware) to avoid repeat API
|
||||
calls.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `title_cache_enabled` | bool | `true` | Master switch for the title cache. |
|
||||
| `title_cache_time` | int (seconds) | `1800` (30 min) | Lifetime of fresh cached titles. |
|
||||
| `title_cache_max_retention` | int (seconds) | `86400` (24 h) | Total retention window for a cached title, counted from when it was cached. Past `title_cache_time` the stale copy is only served as a fallback when a live fetch fails. |
|
||||
|
||||
### Update checks
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `update_checks` | bool | `true` | Whether to check for new unshackle releases. |
|
||||
| `update_check_interval` | int (hours) | `24` | Minimum hours between update checks. |
|
||||
|
||||
!!! note
|
||||
Update checks query the GitHub releases API with a fixed 5-second timeout and cache the
|
||||
result in `directories.cache/update_check.json`.
|
||||
|
||||
---
|
||||
|
||||
## Logging, privacy & debug { #logging-privacy-debug }
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `redact_paths` | bool | `true` | Mask install-root / venv / home prefixes in logged paths (`<unshackle>`, `<venv>`, `~`). Set `false` to show full paths. |
|
||||
| `debug` | bool | `false` | Global debug mode. |
|
||||
| `debug_keys` | bool | `false` | Log decryption keys. |
|
||||
| `debug_requests` | bool | `false` | Log HTTP requests. |
|
||||
| `set_terminal_bg` | bool | `false` | Append the theme's background colour to output styling. |
|
||||
|
||||
!!! note "Effect of `set_terminal_bg`"
|
||||
When on, the theme's background colour is appended to the foreground styles, which makes
|
||||
the full colour palette render correctly on terminals whose *default* background differs
|
||||
from unshackle's theme. When off, such terminals can render the ASCII-art banner and
|
||||
coloured output incorrectly.
|
||||
|
||||
!!! danger "Key exposure"
|
||||
With `debug_keys` on, decryption content keys are written to the structured debug log at
|
||||
INFO level during the DRM handshake. Treat `debug`, `debug_keys`, and the `unshackle_debug_*.jsonl` files as
|
||||
sensitive, and keep `redact_paths` enabled when sharing logs.
|
||||
|
||||
`debug_keys` affects **only** content-encryption keys (the `content_key`/`key` fields).
|
||||
Passwords, tokens, cookies, and session tokens are **always** redacted regardless of this
|
||||
setting, and KIDs, key counts, and other metadata are always logged either way.
|
||||
|
||||
---
|
||||
|
||||
## Deprecated & removed keys { #deprecated-removed-keys }
|
||||
|
||||
| Key | Status | Behaviour |
|
||||
|-----|--------|-----------|
|
||||
| `curl_impersonate` | **Deprecated** → use [`network`](network.md) | Emits a `DeprecationWarning`; still honoured only if `network` is absent. |
|
||||
| `downloader` | **Deprecated** | Any value other than `"requests"` emits a `DeprecationWarning`; the value is otherwise ignored (the unified requests downloader is always used). |
|
||||
| `scene_naming` | **Removed** | If set to any non-null value, unshackle exits with an error directing you to configure [`output_template`](output.md) instead. |
|
||||
@@ -0,0 +1,94 @@
|
||||
# Network & proxy { #network-proxy }
|
||||
|
||||
## `network`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
TLS-fingerprinting and HTTP client settings for the rnet-based session.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `browser` | str | `"Chrome131"` | Impersonation preset. Must be an exact preset name (e.g. `Chrome131`, `Firefox135`, `Edge101`, `Safari18`, `OkHttp4_12`, `OkHttp5`, `Opera118`); unknown names raise an error. |
|
||||
| `http1_only` | bool | *(unset)* | Force HTTP/1.1. |
|
||||
| `http2_only` | bool | *(unset)* | Force HTTP/2. |
|
||||
| `connect_timeout` | int | `10` | Connect timeout in seconds. |
|
||||
| `read_timeout` | int | `30` | Read timeout in seconds (max gap between response chunks). |
|
||||
| `timeout` | int | *(unset)* | Total request timeout in seconds. |
|
||||
| `allow_redirects` | bool | *(unset)* | Follow redirects automatically (rnet client default when unset). |
|
||||
| `pool_idle_timeout` | int | `55` | Seconds an idle pooled connection is kept (default stays under typical ~60s CDN idle kills). |
|
||||
| `pool_max_idle_per_host` | int | `64` | Max idle connections kept per host. |
|
||||
| `pool_max_size` | int | *(unset)* | Max total connections in the pool. |
|
||||
| `tcp_keepalive` | int | `30` | TCP keepalive time in seconds. |
|
||||
| `tcp_keepalive_interval` | int | *(unset)* | TCP keepalive probe interval in seconds. |
|
||||
| `tcp_keepalive_retries` | int | *(unset)* | TCP keepalive probe retry count. |
|
||||
| `tcp_nodelay` | bool | *(unset)* | Disable Nagle's algorithm. |
|
||||
|
||||
```yaml
|
||||
network:
|
||||
browser: Firefox135
|
||||
http2_only: true
|
||||
```
|
||||
|
||||
!!! note "Measured effect of `http1_only`"
|
||||
Both `http1_only` and `http2_only` are unset by default. In benchmarks, forcing HTTP/1.1
|
||||
gained 30 to 50% on hosts that throttle per-connection or stall behind HTTP/2 flow control,
|
||||
and **cost up to 27% on fast CDNs**.
|
||||
|
||||
!!! warning "Renamed from `curl_impersonate`"
|
||||
The old `curl_impersonate` section is a deprecated alias. If you still use it, unshackle
|
||||
honours it (only when `network` is absent) but emits a `DeprecationWarning`. Rename it to
|
||||
`network`.
|
||||
|
||||
## `headers`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Default HTTP headers merged into every session unshackle creates.
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
Accept-Language: en-US,en;q=0.9
|
||||
```
|
||||
|
||||
!!! warning "Don't set `Accept-Encoding` (and similar) here"
|
||||
Compatibility headers such as `Accept-Encoding` are set by the rnet HTTP backend as part of
|
||||
its browser-impersonation profile; overriding them breaks the impersonation fingerprint.
|
||||
This block is for cross-service defaults only (for example `Accept-Language`,
|
||||
`User-Agent`); per-service headers belong in that service's own config.
|
||||
|
||||
## `proxy_providers`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Proxy/VPN provider configuration. Each sub-key names a provider, and its block is passed
|
||||
straight to that provider's constructor. See [Proxies & VPN](../../guide/proxies-and-vpn.md) for
|
||||
the full provider guide. Recognised providers and their exit ports:
|
||||
|
||||
| Provider | Config key | Credentials | Proxy scheme/port |
|
||||
|----------|-----------|-------------|-------------------|
|
||||
| Basic (static) | `basic` | country → URI(s) | as specified |
|
||||
| NordVPN | `nordvpn` | **service** credentials (48 chars combined) | `https://...:89` |
|
||||
| Surfshark | `surfsharkvpn` | **service** credentials (48 chars combined) | `https://...:443` |
|
||||
| Windscribe | `windscribevpn` | service credentials | `https://...:443` |
|
||||
| ExpressVPN | `expressvpn` | device login (`enable: true`) / token cache | `https://cat:...@...:443` |
|
||||
| ProtonVPN | `protonvpn` | TV login or exported cookies | `https://...:4443` (or `:443` Secure Core) |
|
||||
| Gluetun | `gluetun` | per-VPN keys/creds | `http://localhost:{port}` (local Docker) |
|
||||
| Hola | *(none, auto)* | none | `http://...:{peer}` |
|
||||
|
||||
```yaml
|
||||
proxy_providers:
|
||||
basic:
|
||||
us: http://user:pass@1.2.3.4:8080
|
||||
de:
|
||||
- http://a.example:8080
|
||||
- socks5://b.example:1080
|
||||
nordvpn:
|
||||
username: <service username>
|
||||
password: <service password>
|
||||
```
|
||||
|
||||
!!! note "Provider loading differs between CLI and REST server"
|
||||
The `dl` CLI loads all providers, including `windscribevpn` and `gluetun`. The REST API /
|
||||
remote-client path uses a separate resolver that does **not** load `windscribevpn` or
|
||||
`gluetun`. ExpressVPN and ProtonVPN also auto-load when their cached session exists, and Hola
|
||||
auto-loads whenever the `hola-proxy` binary is present.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Output & naming { #output-naming }
|
||||
|
||||
Keys that control the output filename, the per-title subfolder, release-group tagging, and
|
||||
filename character handling. For template cookbook material and worked naming examples, see
|
||||
the [Output & naming guide](../../guide/output-and-naming.md).
|
||||
|
||||
## `output_template`
|
||||
|
||||
- **Type:** `dict` · **Required** (no usable default; `dl` exits with an error if this is missing)
|
||||
|
||||
Filename templates keyed by media kind, using `{variable}` placeholders. Recognised kinds are
|
||||
`movies`, `series`, and `songs` (and `albums` for folder templates). A trailing `?` on a
|
||||
variable (e.g. `{quality?}`) marks it optional: if empty, it and one adjacent separator are
|
||||
dropped.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
output_template:
|
||||
movies: "{title}.{year}.{quality?}.{source}-{tag}"
|
||||
series: "{title}.{season_episode}.{episode_name?}.{quality?}.{source}-{tag}"
|
||||
songs: "{track_number}. {title}"
|
||||
folder: # special nested key, see below
|
||||
movies: "{title} ({year})"
|
||||
series: "{title} ({year})"
|
||||
```
|
||||
|
||||
**The nested `folder` sub-key** controls output subfolders and is popped out of
|
||||
`output_template`:
|
||||
|
||||
- A **dict** sets per-kind folder templates (kinds: `movies`, `series`, `songs`, `albums`).
|
||||
- A **str** sets a single folder template for everything.
|
||||
|
||||
unshackle validates templates on load and emits warnings for unknown variables, non-string
|
||||
values, filesystem-unsafe characters (`< > : " / \ | ? *`), empty templates, and unknown
|
||||
folder-kind keys.
|
||||
|
||||
??? example "All valid template variables"
|
||||
`title`, `year`, `season`, `episode`, `season_episode`, `episode_name`, `part`,
|
||||
`date`, `quality`, `resolution`, `source`, `tag`, `track_number`, `artist`,
|
||||
`album_artist`, `album`, `disc`, `track_total`, `disc_total`, `release_type`, `genre`,
|
||||
`explicit`, `isrc`, `upc`, `label`, `audio`, `audio_channels`, `audio_full`, `atmos`,
|
||||
`dual`, `multi`, `dubbed`, `video`, `hdr`, `hfr`, `edition`, `repack`, `lang_tag`,
|
||||
`title_type`.
|
||||
|
||||
!!! warning "`{part?}` is already inside `{season_episode}`"
|
||||
`part` holds the bare part index of a
|
||||
[split episode](../../guide/output-and-naming.md#split-episodes) (`2`) and is empty on
|
||||
every other title. unshackle folds it into `{episode}` and `{season_episode}`, so the
|
||||
shipped `series` template names split episodes correctly with no edit. Putting
|
||||
`{part?}` next to either of them renders the part twice (`S01E01.Part.2.2`). Use the
|
||||
standalone variable only in a template that names neither `{episode}` nor
|
||||
`{season_episode}`, and note that it is always empty in a folder template, since
|
||||
folders do not carry the part.
|
||||
|
||||
## Tagging & naming keys
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `tag` | str | `""` | Release-group tag. Fills the `{tag}` template variable and is written into MKV/audio tags. |
|
||||
| `tag_group_name` | bool | `true` | Include the group name (`tag`) in the MKV `Group` tag. |
|
||||
| `tag_imdb_tmdb` | bool | `true` | Write IMDb/TMDB/TVDB external-ID tags into the MKV (needs metadata providers). |
|
||||
| `chapter_fallback_name` | str | `""` | Fallback chapter-name template, e.g. `"Chapter {i:02}"`. |
|
||||
| `unicode_filenames` | bool | `false` | Allow Unicode in filenames; when `false`, names are ASCII-sanitised. |
|
||||
|
||||
## `tag_rules`
|
||||
|
||||
- **Type:** `list` · **Default:** `[]`
|
||||
|
||||
Conditionally replaces [`tag`](#tagging-naming-keys) for a given download. Each rule pairs a `when`
|
||||
dict of conditions with the `tag` to use when they hold. Rules are evaluated in order and the
|
||||
first match wins; if no rule matches, the global `tag` is kept.
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
tag_rules:
|
||||
- when: { title_type: movie, lang_tag: SUBBED }
|
||||
tag: SUBMOVIES
|
||||
- when: { quality: 2160p, hdr: [DV, DV.HDR, DV.HDR10P, HDR, HDR10P] }
|
||||
tag: UHDGROUP
|
||||
```
|
||||
|
||||
Rule semantics:
|
||||
|
||||
- Every condition in `when` must hold (they are AND-ed).
|
||||
- A value may be a scalar or a list; a list matches if **any** entry matches.
|
||||
- Comparison is case-insensitive string equality.
|
||||
- The keys of `when` are the filename template variables that describe the release itself:
|
||||
`title_type`, `quality`, `resolution`, `source`, `video`, `hdr`, `hfr`, `audio`,
|
||||
`audio_channels`, `audio_full`, `atmos`, `dual`, `multi`, `dubbed`, `edition`, `repack`,
|
||||
and `lang_tag`. Only these are available. The title's own fields (`title`, `year`, `season`,
|
||||
`episode`, and so on) are added after the rules run and cannot be matched.
|
||||
- An unknown key in `when` is a mistake: unshackle logs a warning that names the key and skips
|
||||
that rule. A rule without conditions or without a `tag` is also warned about and skipped.
|
||||
|
||||
Put the more specific rules first, because the first match ends the search.
|
||||
|
||||
!!! warning "Match `hdr` on the exact value"
|
||||
Matching is exact, and `hdr` is a composite label for Dolby Vision titles: a DV title with
|
||||
an HDR10 base layer gives `DV.HDR`, and with an HDR10+ base layer `DV.HDR10P`. Plain `DV`
|
||||
does **not** match those. List every variant you want, as the example above does. Note
|
||||
also that HDR10 gives `HDR`, not `HDR10`. The full set of values is `HDR`, `HDR10P`, `DV`,
|
||||
`DV.HDR`, `DV.HDR10P`, and `HLG`, as listed in the
|
||||
[variable table](../../guide/output-and-naming.md#template-variables).
|
||||
|
||||
### Operators
|
||||
|
||||
A condition value may start with a comparison operator, which is applied to the rest of the
|
||||
value:
|
||||
|
||||
```yaml title="unshackle.yaml"
|
||||
tag_rules:
|
||||
- when: { resolution: "<1080" }
|
||||
tag: SDGROUP
|
||||
- when: { quality: ">=2160" }
|
||||
tag: UHDGROUP
|
||||
- when: { video: "!=H.265" }
|
||||
tag: AVCGROUP
|
||||
```
|
||||
|
||||
- `>`, `<`, `>=`, `<=` compare numbers. unshackle reads the first number out of the actual
|
||||
value, so `">=2160"` matches a quality of `2160p`. If the actual value has no number, the
|
||||
condition fails.
|
||||
- `=`, `==`, and `!=` compare text, case-insensitively, when the operand is not a number.
|
||||
With a numeric operand they compare numbers like the operators above. A value with no number
|
||||
in it is never equal to a number, so `"!=2160"` holds for it and `"==2160"` does not.
|
||||
- An ordering operator with a text operand (for example `">DV"`) is a mistake: unshackle logs
|
||||
a warning and the condition fails. So does an operator with nothing after it (`">="`).
|
||||
- A value with no operator keeps the plain equality behaviour described above.
|
||||
|
||||
!!! warning "Quote operator values in YAML"
|
||||
Two of these operators break an unquoted YAML value, and both stop the config from
|
||||
loading:
|
||||
|
||||
- `>` opens a folded block scalar, so `quality: >=2160` fails with a "while scanning a
|
||||
block scalar" error.
|
||||
- `!` starts a tag, so `hdr: !=DV` fails with a "could not determine a constructor"
|
||||
error.
|
||||
|
||||
Quote every operator value: `quality: ">=2160"`, `resolution: "<1080"`, `video: "!=H.265"`.
|
||||
|
||||
!!! note "Keep a negation out of lists"
|
||||
A list matches if any entry matches, so two or more `!=` entries in one list are always
|
||||
true. Give a negation as a single value, not as a list entry.
|
||||
|
||||
`title_type` is the media kind of the title: `movie`, `series`, or `music`. It is useful to
|
||||
give each kind its own group:
|
||||
|
||||
```yaml
|
||||
tag_rules:
|
||||
- when: { title_type: movie }
|
||||
tag: MOVIEGRP
|
||||
- when: { title_type: series }
|
||||
tag: TVGRP
|
||||
```
|
||||
|
||||
The resolved tag applies to the `{tag}` filename variable only. The `Group` metadata tag
|
||||
written into MKV and music files always uses [`tag`](#tagging-naming-keys).
|
||||
|
||||
!!! note "`--tag` wins"
|
||||
An explicit `dl --tag` on the command line disables the rules completely and uses the
|
||||
given tag.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Services & authentication { #services-auth }
|
||||
|
||||
## `services`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **Default:** `{}`
|
||||
|
||||
Per-service configuration, keyed by the canonical service tag (for example `EXAMPLE1`, `EXAMPLE2`,
|
||||
`EXAMPLE3`). unshackle reads several well-known sub-keys out of each service's block:
|
||||
|
||||
- **`proxy_map`**: remaps the region query you passed to `-p/--proxy` for this service. A key is
|
||||
that query, or `"provider:query"` when the flag also named a provider; the value is the query
|
||||
asked of the proxy provider instead. It has no effect when `-p` was given a full proxy URL.
|
||||
- **`title_map`**: an exact-match rename map applied to fetched titles (source name →
|
||||
desired name), so a service that names a title differently from your library still matches.
|
||||
- **`dl`**: per-service download defaults, using the same keys as the global
|
||||
[`dl`](download.md#dl) block.
|
||||
|
||||
Individual services may read any additional keys they define. The merged result (the service's
|
||||
own `config.yaml` with this block layered on top) is handed to the service as `self.config`.
|
||||
|
||||
User- or device-specific values (API keys, account IDs, device attributes) belong in this block
|
||||
rather than in the service's own `config.yaml`, which holds shared defaults; see
|
||||
[creating a service](../../dev/creating-a-service.md).
|
||||
|
||||
```yaml
|
||||
services:
|
||||
EXAMPLE1:
|
||||
proxy_map:
|
||||
us: us-nyc
|
||||
"nordvpn:ca": ca-toronto
|
||||
title_map:
|
||||
"My Show: The Movie": "My Show Presents The Movie"
|
||||
```
|
||||
|
||||
## `credentials`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **Default:** `{}`
|
||||
|
||||
Per-service login credentials. Each value is a `username:password[:extra]` string, the same
|
||||
data as a `[username, password]` (or `[username, password, extra]`) list, or a dict of profile
|
||||
name to either of those. With the dict form, `-p/--profile` selects the entry, and the
|
||||
`default` key is used when no profile was given or the named profile is missing. These are
|
||||
parsed into `Credential` objects; the credential's SHA-1 is also used as an account hash for
|
||||
cache keys.
|
||||
|
||||
```yaml
|
||||
credentials:
|
||||
EXAMPLE2: user@example.com:hunter2
|
||||
EXAMPLE1:
|
||||
default: primary@example.com:pw1
|
||||
second: secondary@example.com:pw2
|
||||
```
|
||||
|
||||
!!! tip "Cookies vs credentials"
|
||||
Cookies are stored as files under [`directories.cookies`](directories.md), not in this key.
|
||||
A service's `authenticate()` accepts cookies, credentials, or both.
|
||||
|
||||
## `firefox_cookies`
|
||||
|
||||
- **Type:** `dict` keyed by service tag · **Default:** `{}`
|
||||
|
||||
Settings for extracting cookies directly from a local Firefox profile. A service block is
|
||||
expected to provide `hosts` (a list of cookie hostnames; entries shorter than 3 characters
|
||||
are ignored to prevent dumping the whole cookie store) and an optional `local_storage`
|
||||
boolean to also pull matching entries from `webappsstore.sqlite`, which only services that
|
||||
keep auth tokens in localStorage rather than in HTTP cookies need. Extraction is read-only.
|
||||
|
||||
!!! note "Firefox does not need to be closed"
|
||||
The extractor copies **both** `cookies.sqlite` **and** its WAL file into a `0700` temp
|
||||
directory, so writes Firefox has not yet flushed to the main DB are included. Extraction
|
||||
fails only if Firefox holds an exclusive write lock at the instant of the copy. The live
|
||||
profile is not modified.
|
||||
|
||||
!!! warning "Extraction falls back silently to file cookies"
|
||||
If extraction yields no cookies or fails for **any** reason, unshackle silently falls back
|
||||
to the normal file-based cookie path (`cookies/<SERVICE>.txt` or
|
||||
`cookies/<SERVICE>/<profile>.txt`), with no error reported.
|
||||
|
||||
## `remote_services`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Definitions of remote unshackle service servers, used by the `--remote` mode. Each entry is
|
||||
named by you (pick it with `--server`, or omit that flag when only one is configured) and gives
|
||||
the server's `url` (required), an optional `api_key` sent as `X-Secret-Key`, an optional
|
||||
`server_cdm` boolean, and an optional `services` sub-dict of per-service local overrides such
|
||||
as `title_map`. In `--remote` mode unshackle turns the server's service list into synthetic CLI
|
||||
commands that run against it, falling back to the tags in that `services` sub-dict when the
|
||||
list cannot be fetched. See [remote sessions](../../dev/rest-api/remote-sessions.md) for the
|
||||
full setup.
|
||||
|
||||
## `serve`
|
||||
|
||||
- **Type:** `dict` · **Default:** `{}`
|
||||
|
||||
Configuration for the `serve` command (the built-in REST API server). The full server guide
|
||||
is the [REST API](../../dev/rest-api/index.md) section; these are the config keys.
|
||||
|
||||
| Sub-key | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `api_secret` | str | *(unset)* | Master secret accepted in the `X-Secret-Key` header. Required unless the server is started with `--no-key`. |
|
||||
| `users` | dict | `{}` | Per-user API keys and their allowlists (see below). |
|
||||
| `services` | list | *(unset)* | Global service allowlist. Omit to allow all. |
|
||||
| `remote_only` | bool | `false` | Expose only the remote service session endpoints (health, services, search, session) and disable the rest of the REST API. |
|
||||
| `session_ttl` | int (s) | `300` | Lifetime of an interactive auth session. |
|
||||
| `max_sessions` | int | `100` | Maximum concurrent sessions. |
|
||||
| `history_limit` | int | `100` | How many finished jobs to retain in history. |
|
||||
| `compression_level` | int | `1` | gzip level for responses. |
|
||||
| `global_speed_limit` | str | *(unlimited)* | Server-wide download speed cap, e.g. `10M`, `1.5G` or plain bytes/sec (same format as `speed_limit`). One shared budget across all concurrent jobs; per-job speed limits are ignored while it is set. |
|
||||
| `cdm_overrides` | list or bool | *(unset)* | Allowed per-request CDM overrides: a list of permitted device names, or `true` for any. Unset rejects every override. |
|
||||
| `allow_job_credentials` | bool | `false` | Permit clients to supply credentials per job. |
|
||||
| `devices` | list | *(auto)* | Widevine devices offered; auto-filled from `directories.wvds`. |
|
||||
| `playready_devices` | list | *(auto)* | PlayReady devices; auto-filled from `directories.prds`. |
|
||||
|
||||
Each entry under `users` is keyed by that user's API key and may set its own `services`,
|
||||
`devices`, and `playready_devices` allowlists, narrowing the global ones, plus an optional
|
||||
`username` used as the log label for that key (defaults to a truncated form of the key). A
|
||||
user with no `playready_devices` key gets no PlayReady access at all, not the global list.
|
||||
|
||||
```yaml
|
||||
serve:
|
||||
api_secret: change-me
|
||||
remote_only: true
|
||||
services: [EXAMPLE1, EXAMPLE2]
|
||||
# server-wide download defaults (same keys as the `dl:` block)
|
||||
downloads: 3
|
||||
best_available: true
|
||||
users:
|
||||
a1b2c3d4: # this user's API key
|
||||
services: [EXAMPLE1] # may only use EXAMPLE1
|
||||
```
|
||||
|
||||
!!! note "`dl` keys inside `serve`"
|
||||
Most [`dl`](download.md#dl) flag keys (`downloads`, `workers`, `best_available`, and so on)
|
||||
can be set directly inside `serve`, where they apply to every request the server handles.
|
||||
The server recognises a fixed subset of download parameters, so a few CLI-only flags are
|
||||
ignored here.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Key vaults { #key-vaults }
|
||||
|
||||
The full guide is at [Vaults](../../guide/vaults.md). Two keys configure them.
|
||||
|
||||
## `key_vaults`
|
||||
|
||||
- **Type:** `list[dict]` · **Default:** `[]`
|
||||
|
||||
An ordered list of key-vault backends. unshackle queries them in order and reuses content
|
||||
keys instead of re-licensing. Each entry needs a `type` (the backend module name) and a
|
||||
`name`, plus backend-specific keys.
|
||||
|
||||
| `type` | Purpose | Required keys | Notes |
|
||||
|--------|---------|--------------|-------|
|
||||
| `SQLite` | Local SQLite database | `name`, `path` | Loaded **critically**; a failure aborts the run. |
|
||||
| `MySQL` | Remote MySQL database | `name`, `host`, `database`, `username` | Extra keys (e.g. `password`, `port`) forwarded to pymysql. |
|
||||
| `API` | RESTful JSON API | `name`, `uri`, `token` | Optional `headers` map is sent with every request. Honours `vault_timeout`. |
|
||||
| `HTTP` | HTTP API with modes | `name`, `host`, one of `password`/`api_key`, and `username` in query mode | `api_mode`: `query` (default), `json`, `decrypt_labs`. Honours `vault_timeout`. |
|
||||
|
||||
```yaml
|
||||
key_vaults:
|
||||
- type: SQLite
|
||||
name: Local
|
||||
path: ~/.unshackle/keys.db
|
||||
- type: MySQL
|
||||
name: Team
|
||||
host: db.example.com
|
||||
database: keys
|
||||
username: unshackle
|
||||
password: hunter2
|
||||
no_push: false
|
||||
```
|
||||
|
||||
!!! note "Per-entry options"
|
||||
- `no_push: true` makes a vault read-only (keys are fetched but never written to it).
|
||||
- A vault of `type: API` whose `name` contains `decrypt_labs` auto-fills its `token` from
|
||||
[`decrypt_labs_api_key`](misc.md#external-api-keys) when not set inline. Vault `type` values are
|
||||
case-sensitive module names.
|
||||
- An all-zero content key (32 zeros) is treated as "no key" everywhere and is never
|
||||
stored.
|
||||
|
||||
## `vault_timeout`
|
||||
|
||||
- **Type:** `float` · **Default:** `10.0`
|
||||
|
||||
Timeout in seconds for vault operations. Injected automatically into any backend whose
|
||||
constructor accepts a `timeout` parameter (a per-vault `timeout` still wins).
|
||||
Reference in New Issue
Block a user