From 8a7022a09127be5972e74c0955b963126a9e9c97 Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Sun, 9 Aug 2026 11:55:06 -0600 Subject: [PATCH] feat(dl): allow --tvdb as an --enrich metadata source --- docs/dev/rest-api/endpoints.md | 2 +- docs/guide/cli-reference.md | 4 ++-- docs/guide/downloading.md | 8 +++++--- docs/reference/configuration/misc.md | 2 +- tests/core/test_tvdb.py | 21 +++++++++++++++++++++ unshackle/commands/dl.py | 20 ++++++++++++++++---- unshackle/core/api/routes.py | 4 ++-- 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/dev/rest-api/endpoints.md b/docs/dev/rest-api/endpoints.md index 74348993..9e2e3987 100644 --- a/docs/dev/rest-api/endpoints.md +++ b/docs/dev/rest-api/endpoints.md @@ -394,7 +394,7 @@ Create a download job. Requires `service` and `title_id`; every other field is a | `tag` | string | `null` | Release group tag. | | `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, and fill in the original language. | +| `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`. | | `output_dir` | string | `null` | Override output directory. | | `no_cache` / `reset_cache` | boolean | `false` | Title cache controls. | diff --git a/docs/guide/cli-reference.md b/docs/guide/cli-reference.md index a6da11b7..ca6a3813 100644 --- a/docs/guide/cli-reference.md +++ b/docs/guide/cli-reference.md @@ -152,9 +152,9 @@ Keep only certain track types, or skip certain track types. Attachments are alwa |---|---| | `--tmdb` | TMDB ID (integer). | | `--imdb` | IMDb ID, e.g. `tt1375666`. | -| `--tvdb` | TVDB ID (integer). Skips the series lookup that `--tvdb-order` would otherwise do. | +| `--tvdb` | TVDB ID (integer). Skips the series lookup that `--tvdb-order` would otherwise do. `--enrich` reads it too. | | `--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`, or `--animeapi`. | +| `--enrich` | Override show title/year from the external source, and fill in the original language. **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`. | #### Episode ordering diff --git a/docs/guide/downloading.md b/docs/guide/downloading.md index e8228bf6..1b9db683 100644 --- a/docs/guide/downloading.md +++ b/docs/guide/downloading.md @@ -583,7 +583,7 @@ tagging and naming: | `--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, and fill in the original language. **Requires** one of `--tmdb`, `--imdb`, or `--animeapi`. | +| `--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`. | | `--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" @@ -611,8 +611,10 @@ all end up as the same language. - `--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. -- `--animeapi` supplies a title only, so pair it with `--tmdb` or `--imdb` if you also want - the year and language. +- `--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 diff --git a/docs/reference/configuration/misc.md b/docs/reference/configuration/misc.md index 705a3cca..1ab9764a 100644 --- a/docs/reference/configuration/misc.md +++ b/docs/reference/configuration/misc.md @@ -14,7 +14,7 @@ each metadata provider can actually answer with, see | `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_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. | diff --git a/tests/core/test_tvdb.py b/tests/core/test_tvdb.py index c50711a8..32eb2836 100644 --- a/tests/core/test_tvdb.py +++ b/tests/core/test_tvdb.py @@ -157,6 +157,27 @@ def test_cached_tvdb_result_keeps_enriched_ids() -> None: assert result.external_ids.tmdb_id == 615 +@pytest.mark.parametrize(("kind", "path"), [("tv", "/series/73871/extended"), ("movie", "/movies/73871/extended")]) +def test_get_by_id_supplies_what_enrich_needs(monkeypatch: pytest.MonkeyPatch, kind: str, path: str) -> None: + p = TVDBProvider() + seen: list[str] = [] + + def fake_get(request_path: str, params: dict) -> dict: + seen.append(request_path) + return { + "name": "Cowboy Bebop", + "year": "1998", + "originalLanguage": "jpn", + "remoteIds": [{"sourceName": "IMDB", "id": "tt0213338"}], + } + + monkeypatch.setattr(p, "_get", fake_get) + result = p.get_by_id(73871, kind) + assert seen == [path] + assert (result.title, result.year, result.original_language) == ("Cowboy Bebop", 1998, "jpn") + assert result.external_ids.imdb_id == "tt0213338" + + def test_get_episodes_fails_closed_on_a_truncated_listing(monkeypatch: pytest.MonkeyPatch) -> None: p = TVDBProvider() pages: dict[int, object] = {0: {"episodes": [{"id": n} for n in range(500)]}, 1: None} diff --git a/unshackle/commands/dl.py b/unshackle/commands/dl.py index 1f268ec4..10b572d6 100644 --- a/unshackle/commands/dl.py +++ b/unshackle/commands/dl.py @@ -616,7 +616,7 @@ class dl: "tvdb_id", type=int, default=None, - help="Use this TVDB ID for --tvdb-order instead of looking the series up automatically.", + help="Use this TVDB ID for --tvdb-order and --enrich instead of looking the series up automatically.", ) @click.option( "--tvdb-order", @@ -638,7 +638,7 @@ class dl: default=False, help=( "Override show title and year from external source, and fill in the original language " - "if the service did not provide one. Requires --tmdb, --imdb, or --animeapi." + "if the service did not provide one. Requires --tmdb, --imdb, --tvdb, or --animeapi." ), ) @click.option( @@ -871,8 +871,10 @@ class dl: if not self.tvdb_id and anime_ids.tvdb_id: self.tvdb_id = anime_ids.tvdb_id - if self.enrich and not (self.tmdb_id or self.imdb_id or self.animeapi_title): - raise click.UsageError("--enrich requires --tmdb, --imdb, or --animeapi to provide a metadata source.") + if self.enrich and not (self.tmdb_id or self.imdb_id or self.tvdb_id or self.animeapi_title): + raise click.UsageError( + "--enrich requires --tmdb, --imdb, --tvdb, or --animeapi to provide a metadata source." + ) # Initialize debug logger with service name if debug logging is enabled if config.debug or logging.root.level == logging.DEBUG: @@ -1552,9 +1554,19 @@ class dl: enrich_year = imdb_result.year enrich_lang = parse_language(imdb_result.original_language) break + elif self.tvdb_id: + provider = providers.get_provider("tvdb") + tvdb_result = provider.get_by_id(self.tvdb_id, kind) if provider else None + if tvdb_result: + if not enrich_title: + enrich_title = tvdb_result.title + enrich_year = tvdb_result.year + enrich_lang = parse_language(tvdb_result.original_language) if enrich_lang: self.log.info(f"Original language from metadata: {enrich_lang}") + elif not (enrich_title or enrich_year): + self.log.warning("--enrich found no metadata; is the provider's API key configured?") if enrich_title or enrich_year or enrich_lang: for t in titles if isinstance(titles, (Series, Movies)) else [titles]: diff --git a/unshackle/core/api/routes.py b/unshackle/core/api/routes.py index 3af50bf4..acf86452 100644 --- a/unshackle/core/api/routes.py +++ b/unshackle/core/api/routes.py @@ -764,7 +764,7 @@ async def download(request: web.Request) -> web.Response: description: Anime database ID via AnimeAPI, e.g. mal:12345 (default - None) enrich: type: boolean - description: Override show title and year from external source (default - false) + description: Override show title and year from external source, and fill in the original language. Requires one of tmdb_id, imdb_id, tvdb_id or animeapi_id (default - false) no_folder: type: boolean description: Disable folder creation for TV shows (default - false) @@ -794,7 +794,7 @@ async def download(request: web.Request) -> web.Response: description: Use this IMDB ID (e.g. tt1375666) for tagging (default - None) tvdb_id: type: integer - description: Use this TVDB ID for tagging and episode ordering (default - None) + description: Use this TVDB ID for tagging, episode ordering and enrichment (default - None) tvdb_order: type: string enum: [official, dvd, absolute, alternate, regional]