Merge branch 'dev' into feat/downloader-speed

# Conflicts:
#	unshackle/core/api/download_manager.py
#	unshackle/core/downloaders/requests.py
#	unshackle/core/manifests/dash.py
#	unshackle/core/session.py
#	unshackle/core/utils/webvtt.py
This commit is contained in:
imSp4rky
2026-08-16 12:43:01 -06:00
165 changed files with 6590 additions and 2076 deletions
+29 -12
View File
@@ -90,13 +90,13 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
| Flag | Default | Description |
|---|---|---|
| `-l`, `--lang` | `orig` | Language(s) for **both** video and audio. `orig` = the title's original language; e.g. `orig,en`. |
| `-vl`, `--v-lang` | - | Video-only language (overrides `-l` for video). |
| `-al`, `--a-lang` | - | Audio-only language (overrides `-l` for audio). |
| `-sl`, `--s-lang` | `all` | Subtitle language(s). |
| `-l`, `--lang` | `orig` | Language(s) for **both** video and audio. `orig` = the title's original language; e.g. `orig,en`. A `-` prefix excludes, e.g. `all,-es`. |
| `-vl`, `--v-lang` | - | Video-only language (overrides `-l` for video). A `-` prefix excludes. |
| `-al`, `--a-lang` | - | Audio-only language (overrides `-l` for audio). A `-` prefix excludes. |
| `-sl`, `--s-lang` | `all` | Subtitle language(s). A `-` prefix excludes, e.g. `all,-es`. |
| `--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. |
| `-fsl`, `--forced-s-lang` | none | Language(s) wanted for forced subtitles; implies `-fs`. |
| `-fsl`, `--forced-s-lang` | none | Language(s) wanted for forced subtitles; implies `-fs`. A `-` prefix excludes. |
| `--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. |
@@ -112,7 +112,7 @@ The special language tokens `orig`, `all`, and `best` are honoured everywhere a
| Flag | Description |
|---|---|
| `-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. |
| `-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. For [dated content](downloading.md#daily-and-date-based-content), a token can also be an ISO air date (`2026-08-11`) or a date range with a colon (`2026-08-01:2026-08-31`). |
| `--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. |
@@ -150,13 +150,21 @@ Keep only certain track types, or skip certain track types. Attachments are alwa
| Flag | Description |
|---|---|
| `--tmdb` | TMDB ID (integer). |
| `--imdb` | IMDb ID, e.g. `tt1375666`. |
| `--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`. |
| `--tmdb` | TMDB ID (integer). Used for the tags. Skips the title search. `--enrich` reads it too. Needs `tmdb_api_key`. |
| `--imdb` | IMDb ID, e.g. `tt1375666`. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no key. |
| `--tvdb` | TVDB ID (integer). Used for the tags. Skips the series lookup that `--tvdb-order` would otherwise do. `--enrich` reads it too. Needs `tvdb_api_key`. |
| `--anilist` | AniList ID (integer), e.g. `--anilist 21`. A MyAnimeList ID is accepted as `mal:12345` and resolved to the AniList entry. Used for the tags. Skips the title search. `--enrich` reads it too. Needs no key. |
| `--enrich` | Overwrite show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--anilist`. |
| `--daily` | Treat the title as daily/date-based content and fill missing episode air dates from TVDB. The fill needs `--enrich` and a TVDB ID. See [Daily and date-based content](downloading.md#daily-and-date-based-content). |
| `--tvdb-order` | Renumber episodes to a TVDB season order: `official` (aired), `dvd`, `absolute`, `alternate`, or `regional`. Needs `tvdb_api_key`. |
!!! note "One ID at a time"
`--tmdb`, `--imdb` and `--tvdb` cannot be combined. Give one and unshackle resolves the
others from it, writing all three to the tags. `--anilist` is outside that rule and pairs
with one of them, which is how you attach a western ID to an anime title.
An ID with no provider that can resolve it, such as `--tmdb` with no `tmdb_api_key`, fails
before the download starts. `--anilist` needs no key, so it never fails this check.
#### Episode ordering
A service does not always number a series the way TVDB's aired order does. Some services list
@@ -300,7 +308,7 @@ Prints a dependency table (Category / Tool / Status / Required / Purpose) showin
| Category | Tools |
|---|---|
| Core | FFmpeg*, FFprobe*, MKVToolNix*, mkvpropedit* |
| DRM | shaka-packager*, mp4decrypt, ML-Worker |
| DRM | shaka-packager*, mp4decrypt |
| HDR | dovi_tool, HDR10Plus_tool |
| Subtitle | SubtitleEdit, CCExtractor |
| Player | FFplay, MPV |
@@ -316,6 +324,14 @@ unshackle env info
Shows where the config was loaded from (or lists the candidate config locations if none was found) and prints a table of every configured directory (downloads, temp, cache, cookies, logs, exports, WVDs, PRDs, services, and more).
### `env theme`
```
unshackle env theme
```
Prints a sample of every available CLI theme: colour swatches, help text with option rows, a track listing with video/audio/subtitle entries, log lines, a progress bar, and the gradient pulse bar. The active theme is marked and each theme's aliases are listed. Set your choice with the [`theme`](../reference/configuration/misc.md#appearance) config key.
### `env clear`
Clear an environment directory. The directory is emptied and recreated, and the number of files and bytes freed is reported.
@@ -329,6 +345,7 @@ Clear an environment directory. The directory is emptied and recreated, and the
```shell
unshackle env check
unshackle env info
unshackle env theme
unshackle env clear cache
unshackle env clear cache EXAMPLE # one service only
unshackle env clear temp
+176 -15
View File
@@ -254,6 +254,47 @@ it is used. You can override each stream type independently:
unshackle dl -al en EXAMPLE 81234567
```
### Excluding languages
Put a `-` in front of a language to remove it from the selection. This works on `-l`,
`-vl`, `-al`, `-sl`, and `-fsl`, in the same way as it works on `-w` / `--wanted`.
```shell title="Every subtitle language except Spanish"
unshackle dl -sl all,-es EXAMPLE 81234567
```
The exclusions are subtracted from the languages the flag would otherwise select, and the
order of the values does not matter. If you give only exclusions, the flag keeps its own
default: `-sl -es` is the same as `-sl all,-es`, and `-l -es` is the same as `-l orig,-es`.
```shell title="Only exclusions"
# every subtitle language except Spanish and French
unshackle dl -sl -es,-fr EXAMPLE 81234567
# the original audio language, unless it is Spanish
unshackle dl -l -es EXAMPLE 81234567
```
`-vl` and `-al` have no default of their own: they cascade to `-l`. An override that names
a language replaces `-l` for that stream type, exclusions included. An override that gives
only exclusions adds them to the ones from `-l`.
```shell title="Cascade"
# video keeps the Japanese track; only the audio drops Spanish
unshackle dl -l en,-es -vl ja EXAMPLE 81234567
```
!!! note "Rules for exclusion tokens"
- `orig` is resolved per title, so `-sl -orig` drops the subtitles in the title's
original language.
- `--exact-lang` applies to exclusions with the same matching rules as selection:
`-es` removes exactly the tracks that `es` would select. Without it, `-es` also
removes `es-419` and `es-ES`.
- Tracks with no language tag are never excluded.
- `-all` is rejected. Name the languages you do not want instead.
- The comma form is the documented style. `-sl -es` also works, but a shell can read
a lone `-es` as another option.
### Sort order
`-l` and `-sl` **select** languages: naming some removes the rest. To keep every language
@@ -296,6 +337,10 @@ every available subtitle language is downloaded.
unshackle dl -sl en,es EXAMPLE 81234567
```
To keep the default and drop one language, use the `-` prefix described in
[Excluding languages](#excluding-languages). `-sl all,-es` keeps every subtitle language
except Spanish, forced Spanish subtitles included.
### Requiring subtitles
`--require-subs` takes a list of languages that **must** exist. If they all exist, *all*
@@ -311,7 +356,8 @@ download on the presence of a specific subtitle track.
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.
subtitle but only the English forced track. It accepts exclusions too: `-fsl all,-es`
keeps every forced subtitle except the Spanish one, and `-fsl -es` means the same.
- `--sub-format`: set the output subtitle format, converting only when necessary.
Accepts codec names/values and common aliases (`srt`, `vtt`, `ass`, `ssa`, `ttml`,
etc.), or the literal `original` to keep the source format.
@@ -381,6 +427,43 @@ with `-` to exclude it.
unshackle dl -w S01,-S01E01.2 EXAMPLE 81234567
```
=== "Air dates"
```shell
# One day
unshackle dl -w 2026-08-11 EXAMPLE 81234567
# A range of days, colon separated
unshackle dl -w 2026-08-01:2026-08-31 EXAMPLE 81234567
# August, but not the 15th
unshackle dl -w 2026-08-01:2026-08-31,-2026-08-15 EXAMPLE 81234567
```
### Daily and date-based content
Talk shows, news and sports have no official episode numbering, so unshackle names them
by air date. An episode that carries an air date is written as `Show.YYYY.MM.DD` instead of
`SxxExx`, the `{date}` token holds the ISO date, and the season folder becomes the year.
A service that only carries this kind of content sets `DAILY = True` on its class, and a
service can set `air_date` on each episode itself. Add `--daily` to mark any other title
as date-based:
```shell
unshackle dl --daily --tvdb 73871 --enrich EXAMPLE 81234567
```
With `--enrich` and a TVDB ID, `--daily` fills in the air date of every episode that has
none. An air date the service already set is kept. Dates before 1970 and dates in the
future are skipped, because TVDB carries placeholder schedule dates for episodes that
have not aired. Without `--enrich` unshackle has no source to fill from, and says so.
A dated episode answers to its air date in `-w`, as well as to its `SxxExx` key. A date
token is a plain ISO date (`2026-08-11`). A date range uses a colon (`2026-08-01:2026-08-31`),
because the dashes in a date are part of the date. A range cannot span more than 1000 days.
Date tokens and `SxxExx` tokens can be mixed in one `-w`.
### Split episodes
A few services split one episode into several separately playable videos. Where a service
@@ -577,21 +660,98 @@ unshackle dl --skip-dl --export EXAMPLE 81234567
## Metadata and tagging
unshackle looks up metadata automatically, but you can override the identifiers used for
tagging and naming:
tagging and naming. An ID you give is authoritative: unshackle looks it up directly instead of
searching by title, and the ID stays in the tags. An ID only settles *which* title this is.
To also take that source's title, year and original language, add `--enrich`. Give at most one
of `--tmdb`, `--imdb` and `--tvdb`; unshackle works the rest out from it. To stop the automatic
lookups but keep the IDs you give, set
[`disable_metadata`](../reference/configuration/misc.md#external-api-keys) in your config.
| Flag | Example | Purpose |
| --- | --- | --- |
| `--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`. |
| `--tmdb` | `--tmdb 27205` | Use this TMDB ID instead of an automatic search. Needs `tmdb_api_key`. |
| `--imdb` | `--imdb tt1375666` | Use this IMDb ID instead of an automatic search. Needs no key. |
| `--tvdb` | `--tvdb 73871` | Use this TVDB ID instead of looking the series up. Needs `tvdb_api_key`. |
| `--anilist` | `--anilist 21` | Use this AniList ID instead of an automatic search. `mal:12345` is also accepted. Needs no key. |
| `--enrich` | - | Overwrite the show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--anilist`. |
| `--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"
```shell title="Force the right IMDb match, and keep the service's own naming"
unshackle dl --imdb tt1375666 EXAMPLE 81234567
```
```shell title="Force the match and take IMDb's title, year and language too"
unshackle dl --imdb tt1375666 --enrich EXAMPLE 81234567
```
### What each metadata provider supplies
Providers differ in what they answer with, so the fields `--enrich` can replace depend on
which ID you gave. Every provider except `imdb` only runs when its key is configured:
| Provider | Config key | Title and year | Original language | External IDs it returns |
| --- | --- | --- | --- | --- |
| TMDB | `tmdb_api_key` | yes | yes, alpha-2 such as `ko` | IMDb, TMDB, TVDB |
| TVDB | `tvdb_api_key` | yes | yes, alpha-3 such as `kor` | TMDB, TVDB, sometimes IMDb |
| OMDb | `omdb_api_key` | yes | yes, as an English name such as `Korean` | IMDb |
| IMDb | *(no key needed)* | yes | yes, alpha-2 such as `ko` | IMDb |
| SIMKL | `simkl_client_id` | yes | no, it publishes a country and no language | IMDb, TMDB, TVDB |
| AniList | *(no key needed)* | yes | yes, worked out from the country of origin, such as `ja` | AniList |
Whatever the tag looks like, unshackle normalises it before use, so `ko`, `kor` and `Korean`
all end up as the same language.
!!! note "AniList only answers for anime"
It is last in the default provider order and returns nothing for a title that is not
anime. The only cost is one search that misses. Put `anilist` earlier in
[`metadata_providers`](../reference/configuration/misc.md#external-api-keys) if you mostly
download anime. Which of the three AniList title variants is used is set by
[`anilist_title_language`](../reference/configuration/misc.md#external-api-keys).
A service can also mark its titles as anime. For those titles unshackle tries AniList
first. When AniList has no match, it falls through to the normal order.
Each ID goes to the providers that read that kind of ID, in
[`metadata_providers`](../reference/configuration/misc.md#external-api-keys) order:
- `--tmdb` reads TMDB.
- `--imdb` reads IMDb, then falls back to OMDb. IMDb needs no key, so this path works out of
the box, and an OMDb key only matters when IMDb has no answer.
- `--tvdb` reads TVDB. It needs `tvdb_api_key`.
- `--anilist` reads AniList, which needs no key. It supplies the title, the year and the
original language, and writes an AniList tag. It knows no TMDB, IMDB or TVDB ID, so pair it
with `--tmdb`, `--imdb` or `--tvdb` when you want those tags as well.
Give **one** of `--tmdb`, `--imdb` and `--tvdb`. They cannot be combined, because one ID does
the job on its own: unshackle resolves the other two from it and writes all three to the tags.
Passing two is an error rather than a silent choice between them. `--anilist` is the exception
and still pairs with one of the three, which is how you tag an anime title with a western ID.
The ID you give must have a provider that can actually resolve it, so unshackle checks that
before it downloads anything. `--tmdb` without `tmdb_api_key`, or an ID whose providers your
`metadata_providers` list leaves out, fails immediately with a message naming what to set.
`--imdb` needs no key, since the `imdb` provider is keyless and in the default order.
With `--enrich`, the title, year and original language are all replaced with the external
source's values, whether or not the service already filled them in. A field the source does
not answer with is left alone, and unshackle logs which fields those were, so a provider with
a thin record cannot blank out what the service told you.
`--enrich` also fills in the absolute episode number of each episode from TVDB's absolute
order, for any series that has one and where the service did not supply it. Anime is the
usual beneficiary, but no part of this is limited to anime. This only adds the
[`{absolute}`](../reference/configuration/output.md#output_template) naming variable. The
season and episode numbers are never changed.
With `--daily`, `--enrich` also fills in the air date of each episode from TVDB. See
[Daily and date-based content](#daily-and-date-based-content).
!!! warning "`--enrich` replaces the original language, which affects more than the filename"
Track selection reads the original language, so replacing it changes which audio is
treated as the original. That is the point when a service mislabels it, but it means a
wrong ID can pick the wrong audio track and not only write a wrong name. Without
`--enrich` the service's own value is kept and track selection is untouched.
### Episode ordering
A service does not always number a series the way TVDB's aired order does. Some services list
@@ -637,14 +797,15 @@ authoritative list.
| `--range` | `-r` | Color range(s); default `SDR`. |
| `--channels` | `-c` | Audio channel layout. |
| `--noatmos` | `-naa` | Exclude Atmos audio. |
| `--lang` | `-l` | Video + audio language(s); default `orig`. |
| `--a-lang` | `-al` | Audio-only language override. |
| `--v-lang` | `-vl` | Video-only language override. |
| `--s-lang` | `-sl` | Subtitle language(s); default `all`. |
| `--lang` | `-l` | Video + audio language(s); default `orig`. `-` excludes, e.g. `all,-es`. |
| `--a-lang` | `-al` | Audio-only language override. `-` excludes. |
| `--v-lang` | `-vl` | Video-only language override. `-` excludes. |
| `--s-lang` | `-sl` | Subtitle language(s); default `all`. `-` excludes, e.g. `all,-es`. |
| `--forced-subs` | `-fs` | Include forced subtitles. |
| `--forced-s-lang` | `-fsl` | Forced subtitle language(s); implies `-fs`. |
| `--forced-s-lang` | `-fsl` | Forced subtitle language(s); implies `-fs`. `-` excludes. |
| `--sub-format` | | Output subtitle format. |
| `--wanted` | `-w` | Episode/season range. |
| `--wanted` | `-w` | Episode/season range, or an air date. |
| `--daily` | | Fill missing air dates from TVDB during `--enrich`. |
| `--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. |
@@ -658,5 +819,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` / `--tvdb` / `--animeapi` / `--enrich` | | Metadata overrides. |
| `--tmdb` / `--imdb` / `--tvdb` / `--anilist` / `--enrich` | | Metadata overrides. |
| `--tvdb-order` | | Renumber episodes to a TVDB season order. |
+1
View File
@@ -127,6 +127,7 @@ Every variable below is valid in both output and folder templates. Values are de
| `season_episode` | Combined | `S01E05` |
| `episode_name` | Episode title | `Pilot` |
| `part` | Part index of a [split episode](#split-episodes), empty otherwise | `2` |
| `absolute` | Absolute episode number, zero-padded to 3 digits, empty when unknown | `007` |
| `date` | ISO air date for daily/dated content | `2024-06-01` |
!!! note "Daily & sports content"
+2 -2
View File
@@ -67,10 +67,10 @@ Subtitle selection happens through `dl` flags. The most important is language se
| Flag | Purpose |
| --- | --- |
| `-sl`, `--s-lang` | Language(s) wanted for subtitles. Defaults to `all`. |
| `-sl`, `--s-lang` | Language(s) wanted for subtitles. Defaults to `all`. A `-` prefix excludes, e.g. `all,-es`. |
| `--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`. |
| `-fsl`, `--forced-s-lang` | Language(s) wanted for forced subtitles; implies `-fs`. A `-` prefix excludes. |
| `--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. |