feat(dl): add --animeapi and --enrich options for anime metadata and tagging

Add AnimeAPI integration to resolve anime database IDs (MAL, AniList, Kitsu, etc.) to TMDB/IMDB/TVDB for MKV tagging. The --enrich flag overrides show title and fills in year when missing from the service.

- Add animeapi-py dependency for cross-platform anime ID resolution
- Add --animeapi option (e.g. mal:12345, anilist:98765, defaults to MAL)
- Add --enrich flag to override title/year from external sources
- Remove --tmdb-name and --tmdb-year in favor of unified --enrich
- Update REST API params and docs to match
This commit is contained in:
Andy
2026-02-28 12:51:14 -07:00
parent 5bd03c67cf
commit 572a894620
7 changed files with 208 additions and 56 deletions

View File

@@ -164,9 +164,9 @@ def _perform_download(
"repack": params.get("repack", False),
"tag": params.get("tag"),
"tmdb_id": params.get("tmdb_id"),
"tmdb_name": params.get("tmdb_name", False),
"tmdb_year": params.get("tmdb_year", False),
"imdb_id": params.get("imdb_id"),
"animeapi_id": params.get("animeapi_id"),
"enrich": params.get("enrich", False),
"output_dir": Path(params["output_dir"]) if params.get("output_dir") else None,
"no_cache": params.get("no_cache", False),
"reset_cache": params.get("reset_cache", False),
@@ -180,9 +180,9 @@ def _perform_download(
repack=params.get("repack", False),
tag=params.get("tag"),
tmdb_id=params.get("tmdb_id"),
tmdb_name=params.get("tmdb_name", False),
tmdb_year=params.get("tmdb_year", False),
imdb_id=params.get("imdb_id"),
animeapi_id=params.get("animeapi_id"),
enrich=params.get("enrich", False),
output_dir=Path(params["output_dir"]) if params.get("output_dir") else None,
)

View File

@@ -623,12 +623,12 @@ async def download(request: web.Request) -> web.Response:
tmdb_id:
type: integer
description: Use this TMDB ID for tagging (default - None)
tmdb_name:
animeapi_id:
type: string
description: Anime database ID via AnimeAPI, e.g. mal:12345 (default - None)
enrich:
type: boolean
description: Rename titles using TMDB name (default - false)
tmdb_year:
type: boolean
description: Use release year from TMDB (default - false)
description: Override show title and year from external source (default - false)
no_folder:
type: boolean
description: Disable folder creation for TV shows (default - false)

View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import logging
from typing import Optional
from unshackle.core.providers._base import ExternalIds
log = logging.getLogger("ANIMEAPI")
PLATFORM_MAP: dict[str, str] = {
"mal": "myanimelist",
"anilist": "anilist",
"kitsu": "kitsu",
"tmdb": "themoviedb",
"trakt": "trakt",
"tvdb": "thetvdb",
}
def resolve_animeapi(value: str) -> tuple[Optional[str], ExternalIds]:
"""Resolve an anime database ID via AnimeAPI to a title and external IDs.
Accepts formats like 'mal:12345', 'anilist:98765', or just '12345' (defaults to MAL).
Returns (anime_title, ExternalIds) with any TMDB/IMDB/TVDB IDs found.
"""
import animeapi
platform_str, id_str = _parse_animeapi_value(value)
platform_enum = _get_platform(platform_str)
if platform_enum is None:
log.warning("Unknown AnimeAPI platform: %s (supported: %s)", platform_str, ", ".join(PLATFORM_MAP))
return None, ExternalIds()
log.info("Resolving AnimeAPI %s:%s", platform_str, id_str)
try:
with animeapi.AnimeAPI() as api:
relation = api.get_anime_relations(id_str, platform_enum)
except Exception as exc:
log.warning("AnimeAPI lookup failed for %s:%s: %s", platform_str, id_str, exc)
return None, ExternalIds()
title = getattr(relation, "title", None)
tmdb_id = getattr(relation, "themoviedb", None)
tmdb_type = getattr(relation, "themoviedb_type", None)
imdb_id = getattr(relation, "imdb", None)
tvdb_id = getattr(relation, "thetvdb", None)
tmdb_kind: Optional[str] = None
if tmdb_type is not None:
tmdb_kind = tmdb_type.value if hasattr(tmdb_type, "value") else str(tmdb_type).lower()
if tmdb_kind not in ("movie", "tv"):
tmdb_kind = "tv"
external_ids = ExternalIds(
tmdb_id=int(tmdb_id) if tmdb_id is not None else None,
tmdb_kind=tmdb_kind,
imdb_id=str(imdb_id) if imdb_id is not None else None,
tvdb_id=int(tvdb_id) if tvdb_id is not None else None,
)
log.info(
"AnimeAPI resolved: title=%r, tmdb=%s, imdb=%s, tvdb=%s",
title,
external_ids.tmdb_id,
external_ids.imdb_id,
external_ids.tvdb_id,
)
return title, external_ids
def _parse_animeapi_value(value: str) -> tuple[str, str]:
"""Parse 'platform:id' format. Defaults to 'mal' if no prefix."""
if ":" in value:
platform, _, id_str = value.partition(":")
return platform.lower().strip(), id_str.strip()
return "mal", value.strip()
def _get_platform(platform_str: str) -> object | None:
"""Map a platform string to an animeapi.Platform enum value."""
import animeapi
canonical = PLATFORM_MAP.get(platform_str)
if canonical is None:
return None
platform_name = canonical.upper()
return getattr(animeapi.Platform, platform_name, None)