feat(metadata)!: authoritative id resolver and keyless imdb provider

Supplied IDs resolve directly via a shared resolve_by_ids and never fall back to title search; metadata_providers gains per-kind ordering and a disable_metadata switch stops all automatic lookups while explicit IDs keep working.
BREAKING: --tmdb/--imdb/--tvdb are now mutually exclusive and require a usable provider; --enrich also overwrites original language; the imdbapi provider and imdb_api_enabled are replaced by a keyless imdb provider.
This commit is contained in:
imSp4rky
2026-08-11 15:07:24 -06:00
parent 0cec6c9686
commit ebb4e74318
22 changed files with 1035 additions and 293 deletions
+2 -2
View File
@@ -392,9 +392,9 @@ 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` / `tvdb_id` / `animeapi_id` | - | `null` | External ID overrides for tagging. |
| `tmdb_id` / `imdb_id` / `tvdb_id` / `animeapi_id` | - | `null` | External ID overrides. Each resolves its metadata directly instead of by a title search, and is used for tagging. Set `enrich` to also take the title, year and original language. Give at most one of `tmdb_id`, `imdb_id` and `tvdb_id`, since unshackle resolves the others from it. Sending two returns `400`. `animeapi_id` still combines with one of them. `tmdb_id` and `tvdb_id` must be positive integers and `imdb_id` must look like `tt1375666`, or the request returns `400`. An ID whose provider is unconfigured, such as `tmdb_id` with no `tmdb_api_key`, fails the job rather than returning `400`. |
| `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, and fill in the original language. Needs one of `tmdb_id`, `imdb_id`, `tvdb_id` or `animeapi_id`; without one the job fails instead of returning `400`. |
| `enrich` | boolean | `false` | Overwrite title, year and original language with the external source's. Needs one of `tmdb_id`, `imdb_id`, `tvdb_id` or `animeapi_id`. Without one the job fails instead of returning `400`. |
| `output_dir` | string | `null` | Override output directory. |
| `no_cache` / `reset_cache` | boolean | `false` | Title cache controls. |
+10 -4
View File
@@ -150,13 +150,19 @@ 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. `--enrich` reads it too. |
| `--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`. |
| `--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, and fill in the original language. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--animeapi`. |
| `--enrich` | Overwrite show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--animeapi`. |
| `--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. `--animeapi` still pairs with one of them.
An ID with no provider that can resolve it, such as `--tmdb` with no `tmdb_api_key`, fails
before the download starts.
#### Episode ordering
A service does not always number a series the way TVDB's aired order does. Some services list
+43 -15
View File
@@ -621,50 +621,78 @@ 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. |
| `--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`. |
| `--animeapi` | `--animeapi mal:12345` | Resolve via AnimeAPI (`mal:`/`anilist:` prefix; defaults to MAL). |
| `--enrich` | - | Override the show title and year from an external source, and fill in the original language. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, or `--animeapi`. |
| `--enrich` | - | Overwrite the show title, year and original language with the external source's. **Requires** one of `--tmdb`, `--imdb`, `--tvdb`, 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"
```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 `--enrich` reads different fields depending on
which ID you gave it. A provider only runs when its key is configured:
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 |
| IMDxAPI | `imdb_api_enabled` | yes | only when looked up by ID | 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 |
Whatever the tag looks like, unshackle normalises it before use, so `ko`, `kor` and `Korean`
all end up as the same language.
`--enrich` consults these in a fixed 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` tries IMDxAPI, then falls back to OMDb. Since `imdb_api_enabled` is `false` by
default, an OMDb key is what makes this path work for most people.
- `--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`.
- `--animeapi` supplies a title, plus whichever IDs AnimeAPI knows for the show. If that
includes a TMDB, IMDb or TVDB ID, the year and language come from that provider. If it
does not, pair it with `--tmdb`, `--imdb` or `--tvdb`.
Language is the one field `--enrich` will not overwrite. A service that already reported an
original language keeps it, because the platform knows its own catalogue better than a
third-party match does. Everything else follows the usual override behaviour.
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. `--animeapi` is the exception
and still pairs with one of the three, which is what to do when AnimeAPI knows no IDs for the
show.
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.
!!! 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
+1 -1
View File
@@ -36,7 +36,7 @@ Where the file lives, how unshackle discovers it, and how to edit it:
| [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` |
| [External API keys](misc.md#external-api-keys) | `omdb_api_key`, `tmdb_api_key`, `tvdb_api_key`, `tvdb_pin`, `tvdb_order`, `simkl_client_id`, `metadata_providers`, `disable_metadata`, `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` |
+57 -7
View File
@@ -10,14 +10,14 @@ each metadata provider can actually answer with, see
| 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. |
| `omdb_api_key` | str | `""` | [OMDb](https://www.omdbapi.com/) API key for IMDb metadata lookups; a fallback for when the keyless `imdb` provider has no answer. 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. Also the source `--enrich --tvdb` reads. 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. |
| `metadata_providers` | list or map | *(see below)* | Metadata providers to use, in the order they are tried. |
| `disable_metadata` | bool | `false` | Set to `true` to stop all automatic metadata lookups. An ID you give yourself is still looked up. |
| `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. |
@@ -33,12 +33,61 @@ each metadata provider can actually answer with, see
ignored with a warning. Leave it unset for the default order:
```yaml
metadata_providers: [imdbapi, omdb, simkl, tmdb, tvdb]
metadata_providers: [imdb, 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`.
Whatever the order, a provider is skipped when its API key is missing. `imdb` needs no
key, so it is always available. Available names are `imdb`, `omdb`, `simkl`, `tmdb`, and
`tvdb`. The old name `imdbapi` still works and is read as `imdb`.
The order applies to a title search and to a direct lookup of an ID you give with
`--tmdb`, `--imdb`, or `--tvdb`. A supplied ID always goes to the providers that read
that kind of ID, and it never falls back to a title search.
!!! note "What `disable_metadata` turns off"
With `disable_metadata: true`, unshackle never contacts a metadata provider on its own.
The title search is dead, and that includes the keyless `imdb` provider. Titles, years and
languages then come only from the service.
```yaml
disable_metadata: true
```
An ID you give with `--tmdb`, `--imdb`, `--tvdb`, or `--animeapi` is your permission to use
that ID, so it still works: the ID is looked up directly, the other IDs are cross
referenced from it, and `--enrich` still reads that source. Only the lookups you did not
ask for are stopped.
!!! note "The `imdb` provider needs no key"
It replaces the earlier `imdbapi` provider, which read a third-party mirror
(api.tiffara.com) and needed the `imdb_api_enabled` switch. That option is gone: delete it
from your config, where it is now ignored.
!!! tip "A different order per title kind"
Give a map instead of one list when the best source differs by kind. TVDB is strongest on
series, TMDB on films. There are two kinds, `tv` and `movie`, and this example sets both
of them and names every provider, so nothing is left to the default:
```yaml
metadata_providers:
tv: [tvdb, tmdb, simkl, imdb, omdb]
movie: [tmdb, imdb, omdb, simkl, tvdb]
```
Each list is independent: it both orders and filters that kind on its own, so a provider
you leave out of `tv` is still used for `movie`. Here TVDB is tried first for a series and
last for a film.
You do not need to give both keys, or a full list in each. A kind you leave out uses the
default order:
```yaml
metadata_providers:
tv: [tvdb, tmdb] # movies keep the default order
```
`tv` covers every episode and `movie` covers every film. unshackle only looks up
metadata for those two kinds, so any other key, such as `anime` or `music`, is ignored.
!!! 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
@@ -109,3 +158,4 @@ calls.
| `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. |
| `imdb_api_enabled` | **Removed** | Ignored. It gated the old `imdbapi` provider, which the keyless [`imdb`](#external-api-keys) provider replaces. Safe to delete from your config. |