feat: Add multi-language metadata support

- Add primary_languages config setting (ISO 639-1 codes)
- Fetch metadata in original language when in primary_languages
- Display original titles in search results for configured languages
- Download language-specific posters when available
This commit is contained in:
aasmoe
2025-12-09 10:29:05 +00:00
parent 53091e7204
commit 266d81688c
4 changed files with 115 additions and 23 deletions

View File

@@ -86,6 +86,13 @@ enabled = false
api_key = ""
user = ""
[metadata]
# Primary languages for metadata fetching (ISO 639-1 codes)
# When a TV show or movie's original language matches one of these languages,
# MediaManager will display the original title and fetch metadata in that language.
# Examples: ["en", "no", "da"]
primary_languages = ["en"]
[torrents]
# qBittorrent settings
[torrents.qbittorrent]

View File

@@ -12,3 +12,6 @@ class TvdbConfig(BaseSettings):
class MetadataProviderConfig(BaseSettings):
tvdb: TvdbConfig = TvdbConfig()
tmdb: TmdbConfig = TmdbConfig()
# ISO 639-1 language codes (e.g., ["en", "no", "sv"])
# When media's original language matches one of these, original title and metadata will be used
primary_languages: list[str] = ["en"]

View File

@@ -22,12 +22,28 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
name = "tmdb"
def __init__(self):
config = AllEncompassingConfig().metadata.tmdb
self.url = config.tmdb_relay_url
config = AllEncompassingConfig()
self.url = config.metadata.tmdb.tmdb_relay_url
self.primary_languages = config.metadata.primary_languages
def __get_show_metadata(self, id: int) -> dict:
def __get_language_param(self, original_language: str | None) -> str:
"""
Determine the language parameter to use for TMDB API calls.
Returns the original language if it's in primary_languages, otherwise returns English.
:param original_language: The original language code (ISO 639-1) of the media
:return: Language parameter (ISO 639-1 format, e.g., 'en', 'no')
"""
if original_language and original_language in self.primary_languages:
return original_language
return "en"
def __get_show_metadata(self, id: int, language: str = "en") -> dict:
try:
response = requests.get(url=f"{self.url}/tv/shows/{id}")
response = requests.get(
url=f"{self.url}/tv/shows/{id}",
params={"language": language}
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
@@ -39,10 +55,11 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
)
raise
def __get_season_metadata(self, show_id: int, season_number: int) -> dict:
def __get_season_metadata(self, show_id: int, season_number: int, language: str = "en") -> dict:
try:
response = requests.get(
url=f"{self.url}/tv/shows/{show_id}/{season_number}"
url=f"{self.url}/tv/shows/{show_id}/{season_number}",
params={"language": language}
)
response.raise_for_status()
return response.json()
@@ -87,9 +104,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
)
raise
def __get_movie_metadata(self, id: int) -> dict:
def __get_movie_metadata(self, id: int, language: str = "en") -> dict:
try:
response = requests.get(url=f"{self.url}/movies/{id}")
response = requests.get(
url=f"{self.url}/movies/{id}",
params={"language": language}
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
@@ -132,7 +152,17 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
raise
def download_show_poster_image(self, show: Show) -> bool:
# First fetch to get original_language
show_metadata = self.__get_show_metadata(show.external_id)
original_language = show_metadata.get("original_language")
# Determine which language to use
language = self.__get_language_param(original_language)
# Fetch metadata in the appropriate language to get localized poster
if language != "en":
show_metadata = self.__get_show_metadata(show.external_id, language=language)
# downloading the poster
# all pictures from TMDB should already be jpeg, so no need to convert
if show_metadata["poster_path"] is not None:
@@ -159,12 +189,23 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
:return: returns a ShowMetadata object
:rtype: ShowMetadata
"""
show_metadata = self.__get_show_metadata(id)
original_language = show_metadata.get("original_language")
# Determine which language to use for metadata
language = self.__get_language_param(original_language)
# Fetch show metadata in the appropriate language
show_metadata = self.__get_show_metadata(id, language=language)
season_list = []
# inserting all the metadata into the objects
for season in show_metadata["seasons"]:
season_metadata = self.__get_season_metadata(
show_id=show_metadata["id"], season_number=season["season_number"]
show_id=show_metadata["id"],
season_number=season["season_number"],
language=language
)
episode_list = []
@@ -231,11 +272,21 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
)
else:
poster_url = None
# Determine which name to use based on primary_languages
original_language = result.get("original_language")
original_name = result.get("original_name")
display_name = result["name"]
# Use original name if language is in primary_languages
if original_language and original_language in self.primary_languages:
display_name = original_name
formatted_results.append(
MetaDataProviderSearchResult(
poster_path=poster_url,
overview=result["overview"],
name=result["name"],
name=display_name,
external_id=result["id"],
year=media_manager.metadataProvider.utils.get_year_from_date(
result["first_air_date"]
@@ -243,6 +294,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
metadata_provider=self.name,
added=False,
vote_average=result["vote_average"],
original_language=original_language,
)
)
except Exception as e:
@@ -252,12 +304,21 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
def get_movie_metadata(self, id: int = None) -> Movie:
"""
:param id: the external id of the show
:param id: the external id of the movie
:type id: int
:return: returns a ShowMetadata object
:rtype: ShowMetadata
:return: returns a Movie object
:rtype: Movie
"""
movie_metadata = self.__get_movie_metadata(id=id)
original_language = movie_metadata.get("original_language")
# Determine which language to use for metadata
language = self.__get_language_param(original_language)
# Fetch movie metadata in the appropriate language
movie_metadata = self.__get_movie_metadata(id=id, language=language)
year = media_manager.metadataProvider.utils.get_year_from_date(
movie_metadata["release_date"]
)
@@ -300,11 +361,21 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
)
else:
poster_url = None
# Determine which name to use based on primary_languages
original_language = result.get("original_language")
original_title = result.get("original_title")
display_name = result["title"]
# Use original title if language is in primary_languages
if original_language and original_language in self.primary_languages and original_title:
display_name = original_title
formatted_results.append(
MetaDataProviderSearchResult(
poster_path=poster_url,
overview=result["overview"],
name=result["title"],
name=display_name,
external_id=result["id"],
year=media_manager.metadataProvider.utils.get_year_from_date(
result["release_date"]
@@ -312,6 +383,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
metadata_provider=self.name,
added=False,
vote_average=result["vote_average"],
original_language=original_language,
)
)
except Exception as e:
@@ -319,7 +391,17 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
return formatted_results
def download_movie_poster_image(self, movie: Movie) -> bool:
# First fetch to get original_language
movie_metadata = self.__get_movie_metadata(id=movie.external_id)
original_language = movie_metadata.get("original_language")
# Determine which language to use
language = self.__get_language_param(original_language)
# Fetch metadata in the appropriate language to get localized poster
if language != "en":
movie_metadata = self.__get_movie_metadata(id=movie.external_id, language=language)
# downloading the poster
# all pictures from TMDB should already be jpeg, so no need to convert
if movie_metadata["poster_path"] is not None:
@@ -329,11 +411,11 @@ class TmdbMetadataProvider(AbstractMetadataProvider):
if media_manager.metadataProvider.utils.download_poster_image(
storage_path=self.storage_path, poster_url=poster_url, id=movie.id
):
log.info("Successfully downloaded poster image for show " + movie.name)
log.info("Successfully downloaded poster image for movie " + movie.name)
else:
log.warning(f"download for image of show {movie.name} failed")
log.warning(f"download for image of movie {movie.name} failed")
return False
else:
log.warning(f"image for show {movie.name} could not be downloaded")
log.warning(f"image for movie {movie.name} could not be downloaded")
return False
return True

View File

@@ -24,12 +24,12 @@ else:
return Search().tv(page=page, query=query, include_adult=True)
@router.get("/tv/shows/{show_id}")
async def get_tmdb_show(show_id: int):
return TV(show_id).info()
async def get_tmdb_show(show_id: int, language: str = "en"):
return TV(show_id).info(language=language)
@router.get("/tv/shows/{show_id}/{season_number}")
async def get_tmdb_season(season_number: int, show_id: int):
return TV_Seasons(season_number=season_number, tv_id=show_id).info()
async def get_tmdb_season(season_number: int, show_id: int, language: str = "en"):
return TV_Seasons(season_number=season_number, tv_id=show_id).info(language=language)
@router.get("/movies/trending")
async def get_tmdb_trending_movies():
@@ -40,5 +40,5 @@ else:
return Search().movie(page=page, query=query, include_adult=True)
@router.get("/movies/{movie_id}")
async def get_tmdb_movie(movie_id: int):
return Movies(movie_id).info()
async def get_tmdb_movie(movie_id: int, language: str = "en"):
return Movies(movie_id).info(language=language)