mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-13 12:35:05 +00:00
feat(tags): add OMDb metadata provider with omdb_api_key config
This commit is contained in:
@@ -37,7 +37,7 @@ This page assumes you already know the basics and want precise details.
|
|||||||
| [DRM & CDM](#drm-cdm) | `cdm`, `remote_cdm`, `decryption` |
|
| [DRM & CDM](#drm-cdm) | `cdm`, `remote_cdm`, `decryption` |
|
||||||
| [Network & proxy](#network-proxy) | `network`, `headers`, `proxy_providers` |
|
| [Network & proxy](#network-proxy) | `network`, `headers`, `proxy_providers` |
|
||||||
| [Key vaults](#key-vaults) | `key_vaults`, `vault_timeout` |
|
| [Key vaults](#key-vaults) | `key_vaults`, `vault_timeout` |
|
||||||
| [External API keys](#external-api-keys) | `imdb_api_enabled`, `tmdb_api_key`, `simkl_client_id`, `decrypt_labs_api_key`, `ipinfo_api_key` |
|
| [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` |
|
| [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` |
|
| [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` |
|
| [Deprecated & removed](#deprecated-removed-keys) | `curl_impersonate`, `downloader`, `scene_naming` |
|
||||||
@@ -749,6 +749,7 @@ All default to an empty string and enable optional metadata/geolocation features
|
|||||||
| Key | Type | Default | Description |
|
| Key | Type | Default | Description |
|
||||||
|-----|------|---------|-------------|
|
|-----|------|---------|-------------|
|
||||||
| `imdb_api_enabled` | bool | `false` | Use the free imdbapi.dev metadata provider, which needs no API key. Off by default since the site has been unreliable. |
|
| `imdb_api_enabled` | bool | `false` | Use the free imdbapi.dev 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 imdbapi.dev. Free keys are available on the OMDb site. |
|
||||||
| `tmdb_api_key` | str | `""` | TMDB API key for metadata enrichment and external-ID tags. |
|
| `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. |
|
| `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). |
|
| `decrypt_labs_api_key` | str | `""` | Global Decrypt Labs API key (used by remote CDM / vault). |
|
||||||
@@ -872,6 +873,7 @@ proxy_providers:
|
|||||||
basic:
|
basic:
|
||||||
us: http://user:pass@1.2.3.4:8080
|
us: http://user:pass@1.2.3.4:8080
|
||||||
|
|
||||||
|
omdb_api_key: "your-omdb-key"
|
||||||
tmdb_api_key: "your-tmdb-key"
|
tmdb_api_key: "your-tmdb-key"
|
||||||
title_cache_time: 3600
|
title_cache_time: 3600
|
||||||
redact_paths: true
|
redact_paths: true
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class Config:
|
|||||||
self.tag_group_name: bool = kwargs.get("tag_group_name", True)
|
self.tag_group_name: bool = kwargs.get("tag_group_name", True)
|
||||||
self.tag_imdb_tmdb: bool = kwargs.get("tag_imdb_tmdb", True)
|
self.tag_imdb_tmdb: bool = kwargs.get("tag_imdb_tmdb", True)
|
||||||
self.imdb_api_enabled: bool = kwargs.get("imdb_api_enabled", False)
|
self.imdb_api_enabled: bool = kwargs.get("imdb_api_enabled", False)
|
||||||
|
self.omdb_api_key: str = kwargs.get("omdb_api_key") or ""
|
||||||
self.tmdb_api_key: str = kwargs.get("tmdb_api_key") or ""
|
self.tmdb_api_key: str = kwargs.get("tmdb_api_key") or ""
|
||||||
self.simkl_client_id: str = kwargs.get("simkl_client_id") or ""
|
self.simkl_client_id: str = kwargs.get("simkl_client_id") or ""
|
||||||
self.decrypt_labs_api_key: str = kwargs.get("decrypt_labs_api_key") or ""
|
self.decrypt_labs_api_key: str = kwargs.get("decrypt_labs_api_key") or ""
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import requests
|
|||||||
|
|
||||||
from unshackle.core.providers._base import ExternalIds, MetadataProvider, MetadataResult, fuzzy_match, log
|
from unshackle.core.providers._base import ExternalIds, MetadataProvider, MetadataResult, fuzzy_match, log
|
||||||
from unshackle.core.providers.imdbapi import IMDBApiProvider
|
from unshackle.core.providers.imdbapi import IMDBApiProvider
|
||||||
|
from unshackle.core.providers.omdb import OMDBProvider
|
||||||
from unshackle.core.providers.simkl import SimklProvider
|
from unshackle.core.providers.simkl import SimklProvider
|
||||||
from unshackle.core.providers.tmdb import TMDBProvider
|
from unshackle.core.providers.tmdb import TMDBProvider
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from unshackle.core.title_cacher import TitleCacher
|
from unshackle.core.title_cacher import TitleCacher
|
||||||
|
|
||||||
# Ordered by priority: IMDBApi (free), SIMKL, TMDB
|
# Ordered by priority: IMDBApi (free), OMDb, SIMKL, TMDB
|
||||||
ALL_PROVIDERS: list[type[MetadataProvider]] = [IMDBApiProvider, SimklProvider, TMDBProvider]
|
ALL_PROVIDERS: list[type[MetadataProvider]] = [IMDBApiProvider, OMDBProvider, SimklProvider, TMDBProvider]
|
||||||
|
|
||||||
|
|
||||||
def get_available_providers() -> list[MetadataProvider]:
|
def get_available_providers() -> list[MetadataProvider]:
|
||||||
@@ -390,6 +391,24 @@ def _cached_to_result(cached: dict, provider_name: str, kind: str) -> Optional[M
|
|||||||
source="simkl",
|
source="simkl",
|
||||||
raw=cached,
|
raw=cached,
|
||||||
)
|
)
|
||||||
|
elif provider_name == "omdb":
|
||||||
|
title = cached.get("Title")
|
||||||
|
year_str = cached.get("Year")
|
||||||
|
year = int(year_str[:4]) if year_str and len(year_str) >= 4 and year_str[:4].isdigit() else None
|
||||||
|
enriched = cached.get("_enriched_ids", {})
|
||||||
|
return MetadataResult(
|
||||||
|
title=title,
|
||||||
|
year=year,
|
||||||
|
kind=kind,
|
||||||
|
external_ids=ExternalIds(
|
||||||
|
imdb_id=cached.get("imdbID"),
|
||||||
|
tmdb_id=enriched.get("tmdb_id"),
|
||||||
|
tmdb_kind=enriched.get("tmdb_kind"),
|
||||||
|
tvdb_id=enriched.get("tvdb_id"),
|
||||||
|
),
|
||||||
|
source="omdb",
|
||||||
|
raw=cached,
|
||||||
|
)
|
||||||
elif provider_name == "imdbapi":
|
elif provider_name == "imdbapi":
|
||||||
title = cached.get("primaryTitle") or cached.get("originalTitle")
|
title = cached.get("primaryTitle") or cached.get("originalTitle")
|
||||||
year = cached.get("startYear")
|
year = cached.get("startYear")
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from unshackle.core.config import config
|
||||||
|
from unshackle.core.providers._base import ExternalIds, MetadataProvider, MetadataResult, _clean, fuzzy_match
|
||||||
|
|
||||||
|
# Mapping from our kind ("movie"/"tv") to OMDb title types
|
||||||
|
KIND_TO_TYPE: dict[str, str] = {
|
||||||
|
"movie": "movie",
|
||||||
|
"tv": "series",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_year(value: Optional[str]) -> Optional[int]:
|
||||||
|
# OMDb years look like "2017", "2008–2013" or "2023–"
|
||||||
|
if value and len(value) >= 4 and value[:4].isdigit():
|
||||||
|
return int(value[:4])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class OMDBProvider(MetadataProvider):
|
||||||
|
"""OMDb metadata provider (omdbapi.com). Native IDs are IMDb IDs."""
|
||||||
|
|
||||||
|
NAME = "omdb"
|
||||||
|
REQUIRES_KEY = True
|
||||||
|
BASE_URL = "https://www.omdbapi.com/"
|
||||||
|
|
||||||
|
def is_available(self) -> bool:
|
||||||
|
return bool(config.omdb_api_key)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _api_key(self) -> str:
|
||||||
|
return config.omdb_api_key
|
||||||
|
|
||||||
|
def _get(self, params: dict[str, str]) -> Optional[dict]:
|
||||||
|
try:
|
||||||
|
r = self.session.get(self.BASE_URL, params={"apikey": self._api_key, **params}, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
except (requests.RequestException, ValueError) as exc:
|
||||||
|
self.log.debug("OMDb request failed: %s", exc)
|
||||||
|
return None
|
||||||
|
if data.get("Response") != "True":
|
||||||
|
self.log.debug("OMDb error: %s", data.get("Error"))
|
||||||
|
return None
|
||||||
|
return data
|
||||||
|
|
||||||
|
def search(self, title: str, year: Optional[int], kind: str) -> Optional[MetadataResult]:
|
||||||
|
self.log.debug("Searching OMDb for %r (%s, %s)", title, kind, year)
|
||||||
|
|
||||||
|
params: dict[str, str] = {"s": title}
|
||||||
|
type_filter = KIND_TO_TYPE.get(kind)
|
||||||
|
if type_filter:
|
||||||
|
params["type"] = type_filter
|
||||||
|
if year is not None:
|
||||||
|
params["y"] = str(year)
|
||||||
|
|
||||||
|
data = self._get(params)
|
||||||
|
if not data and year is not None:
|
||||||
|
# OMDb's year filter is exact; retry without it for off-by-one metadata
|
||||||
|
del params["y"]
|
||||||
|
data = self._get(params)
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
results = data.get("Search") or []
|
||||||
|
|
||||||
|
best_match: Optional[dict] = None
|
||||||
|
best_ratio = 0.0
|
||||||
|
for candidate in results:
|
||||||
|
name = candidate.get("Title") or ""
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
ratio = SequenceMatcher(None, _clean(title), _clean(name)).ratio()
|
||||||
|
if ratio > best_ratio:
|
||||||
|
candidate_year = _parse_year(candidate.get("Year"))
|
||||||
|
if year and candidate_year and abs(year - candidate_year) > 1:
|
||||||
|
continue
|
||||||
|
best_ratio = ratio
|
||||||
|
best_match = candidate
|
||||||
|
|
||||||
|
if not best_match:
|
||||||
|
self.log.debug("No matching result found in OMDb for %r", title)
|
||||||
|
return None
|
||||||
|
|
||||||
|
result_title = best_match.get("Title")
|
||||||
|
if not result_title or not fuzzy_match(result_title, title):
|
||||||
|
self.log.debug("OMDb title mismatch: searched %r, got %r", title, result_title)
|
||||||
|
return None
|
||||||
|
|
||||||
|
imdb_id = best_match.get("imdbID")
|
||||||
|
self.log.debug("OMDb -> %s (ID %s)", result_title, imdb_id)
|
||||||
|
|
||||||
|
# Fetch full detail so raw carries ratings, genre, rating cert, etc.
|
||||||
|
detail = self._get({"i": imdb_id}) if imdb_id else None
|
||||||
|
|
||||||
|
return MetadataResult(
|
||||||
|
title=result_title,
|
||||||
|
year=_parse_year(best_match.get("Year")),
|
||||||
|
kind=kind,
|
||||||
|
external_ids=ExternalIds(imdb_id=imdb_id),
|
||||||
|
source="omdb",
|
||||||
|
raw=detail or best_match,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_by_id(self, provider_id: Union[int, str], kind: str) -> Optional[MetadataResult]:
|
||||||
|
"""Fetch metadata by IMDb ID (e.g. 'tt1375666')."""
|
||||||
|
imdb_id = str(provider_id)
|
||||||
|
self.log.debug("Fetching OMDb title %s", imdb_id)
|
||||||
|
|
||||||
|
data = self._get({"i": imdb_id})
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MetadataResult(
|
||||||
|
title=data.get("Title"),
|
||||||
|
year=_parse_year(data.get("Year")),
|
||||||
|
kind=kind,
|
||||||
|
external_ids=ExternalIds(imdb_id=data.get("imdbID")),
|
||||||
|
source="omdb",
|
||||||
|
raw=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_external_ids(self, provider_id: Union[int, str], kind: str) -> ExternalIds:
|
||||||
|
"""Return external IDs. For OMDb, the provider_id IS the IMDb ID."""
|
||||||
|
return ExternalIds(imdb_id=str(provider_id))
|
||||||
Reference in New Issue
Block a user