From 7de11919ad9004a882700f789961776e39fa266d Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 16:58:27 -0300 Subject: [PATCH 01/27] chg: feat: add localized image priorization --- .../implementations/themoviedatabase.py | 64 +++++++++++-------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index de08b062..0e06293e 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -138,6 +138,38 @@ class TheMovieDatabase(Provider): }, ) + def get_best_image(self, item, lng, key): + """ + Retrieves the best available images for a item based on localization. + + Args: + item (dict): A dictionary containing item information, including images and language details. + lng (str): The preferred language code for the images in ISO 639-1 format. + + Returns: + list: A list of images, prioritized by localization, original language, and any available image. + """ + # Step 1: Try to get localized images + localized_images = [ + image + for image in item["images"][key] + if image.get("iso_639_1") == lng + ] + + # Step 2: If no localized images, try images in the original language + if not localized_images: + localized_images = [ + image + for image in item["images"][key] + if image.get("iso_639_1") == item["original_language"] + ] + + # Step 3: If still no images, use any available images + if not localized_images: + localized_images = item["images"][key] + + return localized_images + async def search_movie(self, name: str, year: Optional[int]) -> Movie: search_results = ( await self.get("search/movie", params={"query": name, "year": year}) @@ -211,21 +243,11 @@ class TheMovieDatabase(Provider): tags=list(map(lambda x: x["name"], movie["keywords"]["keywords"])), overview=movie["overview"], posters=self.get_image( - movie["images"]["posters"] - + ( - [{"file_path": movie["poster_path"]}] - if lng == movie["original_language"] - else [] - ) + self.get_best_image(movie, lng, "posters") ), - logos=self.get_image(movie["images"]["logos"]), + logos=self.get_image(self.get_best_image(movie, lng, "logos")), thumbnails=self.get_image( - movie["images"]["backdrops"] - + ( - [{"file_path": movie["backdrop_path"]}] - if lng == movie["original_language"] - else [] - ) + self.get_best_image(movie, lng, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" @@ -312,21 +334,11 @@ class TheMovieDatabase(Provider): tags=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], posters=self.get_image( - show["images"]["posters"] - + ( - [{"file_path": show["poster_path"]}] - if lng == show["original_language"] - else [] - ) + self.get_best_image(show, lng, "posters") ), - logos=self.get_image(show["images"]["logos"]), + logos=self.get_image(self.get_best_image(show, lng, "logos")), thumbnails=self.get_image( - show["images"]["backdrops"] - + ( - [{"file_path": show["backdrop_path"]}] - if lng == show["original_language"] - else [] - ) + self.get_best_image(show, lng, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" From a1cd3ba7524db3f12fa3589bcc27b83b7c166599 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 17:03:07 -0300 Subject: [PATCH 02/27] chg: docs: add missing parameter docs --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 0e06293e..204b3df9 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -145,7 +145,7 @@ class TheMovieDatabase(Provider): Args: item (dict): A dictionary containing item information, including images and language details. lng (str): The preferred language code for the images in ISO 639-1 format. - + key (str): The key to access the images in the item dictionary. (e.g. "posters", "backdrops", "logos") Returns: list: A list of images, prioritized by localization, original language, and any available image. """ From beb09468021b9cf36f5a3d08d51f7642e2a50768 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 17:40:53 -0300 Subject: [PATCH 03/27] debug --- scanner/providers/implementations/themoviedatabase.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 204b3df9..fdfd73c2 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -149,6 +149,15 @@ class TheMovieDatabase(Provider): Returns: list: A list of images, prioritized by localization, original language, and any available image. """ + print(f"Trying to get best image for {item['title']} in {lng}") + print(f"All available languageCodes: {map(lambda x: x['iso_639_1'], item['images'][key])}") + # Order images by size and vote average + item["images"][key] = sorted( + item["images"][key], + key=lambda x: (x.get("width", 0), x.get("vote_average", 0)), + reverse=True, + ) + # Step 1: Try to get localized images localized_images = [ image From db15a5649afa825841c78d510732e82a0281cb31 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 17:52:49 -0300 Subject: [PATCH 04/27] chg: fix: always use iso639-1 when calling function --- .../implementations/themoviedatabase.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index fdfd73c2..f39d3b33 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -149,8 +149,6 @@ class TheMovieDatabase(Provider): Returns: list: A list of images, prioritized by localization, original language, and any available image. """ - print(f"Trying to get best image for {item['title']} in {lng}") - print(f"All available languageCodes: {map(lambda x: x['iso_639_1'], item['images'][key])}") # Order images by size and vote average item["images"][key] = sorted( item["images"][key], @@ -246,17 +244,18 @@ class TheMovieDatabase(Provider): else [], # TODO: Add cast information ) + lng_iso639_1 = lng.split("-").pop(0) translation = MovieTranslation( name=movie["title"], tagline=movie["tagline"] if movie["tagline"] else None, tags=list(map(lambda x: x["name"], movie["keywords"]["keywords"])), overview=movie["overview"], posters=self.get_image( - self.get_best_image(movie, lng, "posters") + self.get_best_image(movie, lng_iso639_1, "posters") ), - logos=self.get_image(self.get_best_image(movie, lng, "logos")), + logos=self.get_image(self.get_best_image(movie, lng_iso639_1, "logos")), thumbnails=self.get_image( - self.get_best_image(movie, lng, "backdrops") + self.get_best_image(movie, lng_iso639_1, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" @@ -337,17 +336,18 @@ class TheMovieDatabase(Provider): ], # TODO: Add cast information ) + lng_iso639_1 = lng.split("-").pop(0) translation = ShowTranslation( name=show["name"], tagline=show["tagline"] if show["tagline"] else None, tags=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], posters=self.get_image( - self.get_best_image(show, lng, "posters") + self.get_best_image(show, lng_iso639_1, "posters") ), - logos=self.get_image(self.get_best_image(show, lng, "logos")), + logos=self.get_image(self.get_best_image(show, lng_iso639_1, "logos")), thumbnails=self.get_image( - self.get_best_image(show, lng, "backdrops") + self.get_best_image(show, lng_iso639_1, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" From 196f5678dffecace9a85c2981c88481cfe1aa231 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 18:04:39 -0300 Subject: [PATCH 05/27] chg: add debug log --- scanner/providers/implementations/themoviedatabase.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index f39d3b33..dc09394c 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -149,6 +149,8 @@ class TheMovieDatabase(Provider): Returns: list: A list of images, prioritized by localization, original language, and any available image. """ + print(f"Trying to get best image for {item['title']} in {lng}") + print(f"All available languageCodes: {list(map(lambda x: x['iso_639_1'], item['images'][key]))}") # Order images by size and vote average item["images"][key] = sorted( item["images"][key], @@ -163,8 +165,11 @@ class TheMovieDatabase(Provider): if image.get("iso_639_1") == lng ] + print(f"Localized images: {localized_images}") + # Step 2: If no localized images, try images in the original language if not localized_images: + print(f"Could not find localized images for {lng}, trying original language") localized_images = [ image for image in item["images"][key] @@ -173,6 +178,7 @@ class TheMovieDatabase(Provider): # Step 3: If still no images, use any available images if not localized_images: + print(f"Could not find images for {lng} or original language, using any available") localized_images = item["images"][key] return localized_images From 8f2b487fb4a197247a47f327c5e2a4cb5d0f940c Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 18:39:44 -0300 Subject: [PATCH 06/27] chg: fix: modify tmdb request --- scanner/providers/implementations/themoviedatabase.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index dc09394c..52bd2cfa 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -196,11 +196,13 @@ class TheMovieDatabase(Provider): languages = self.get_languages() async def for_language(lng: str) -> Movie: + lng_iso639_1 = lng.split("-").pop(0) movie = await self.get( f"movie/{movie_id}", params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images", + "include_image_language": f"{lng},{lng_iso639_1},null", }, ) logger.debug("TMDb responded: %s", movie) @@ -250,7 +252,6 @@ class TheMovieDatabase(Provider): else [], # TODO: Add cast information ) - lng_iso639_1 = lng.split("-").pop(0) translation = MovieTranslation( name=movie["title"], tagline=movie["tagline"] if movie["tagline"] else None, @@ -290,11 +291,15 @@ class TheMovieDatabase(Provider): languages = self.get_languages() async def for_language(lng: str) -> Show: + # ISO 639-1 (eg. "en") is required as TMDB does not support fetching images + # by ISO 639-2 + ISO 3166-1 alpha-2 (eg. "en-US") + lng_iso639_1 = lng.split("-").pop(0) show = await self.get( f"tv/{show_id}", params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", + "include_image_language": f"{lng},{lng_iso639_1},null", }, ) logger.debug("TMDb responded: %s", show) @@ -342,7 +347,6 @@ class TheMovieDatabase(Provider): ], # TODO: Add cast information ) - lng_iso639_1 = lng.split("-").pop(0) translation = ShowTranslation( name=show["name"], tagline=show["tagline"] if show["tagline"] else None, From 88fff8ff2436cbbe803c534915d815c454ff523d Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 19:01:16 -0300 Subject: [PATCH 07/27] chg: fix: do not prioritize original language --- scanner/providers/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index c2141a16..28304e0a 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -28,6 +28,7 @@ def normalize_lang(lang: str) -> str: # For now, the API of kyoo only support one language so we remove the others. default_languages = os.environ.get("LIBRARY_LANGUAGES", "").split(",") +image_prefer_original_language = os.environ.get("IMAGE_PREFER_ORIGINAL_LANGUAGE", "false").lower() == "true" def sort_translations( @@ -64,7 +65,7 @@ def select_image( chain( *( getattr(trans, kind) - for trans in sort_translations(value, prefer_orginal=True) + for trans in sort_translations(value, prefer_orginal=image_prefer_original_language) ) ), None, From 81cc6d752be5bba21b7eda0f1d6a2df5c7679930 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 20:11:33 -0300 Subject: [PATCH 08/27] chg: fix: add all images in response --- .../providers/implementations/themoviedatabase.py | 12 +++--------- scanner/providers/utils.py | 6 ++++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 52bd2cfa..bb2f7767 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -5,7 +5,7 @@ from logging import getLogger from typing import Awaitable, Callable, Dict, List, Optional, Any, TypeVar from itertools import accumulate, zip_longest -from providers.utils import ProviderError +from providers.utils import ProviderError, get_iso_639_1_codes from matcher.cache import cache from ..provider import Provider @@ -149,8 +149,6 @@ class TheMovieDatabase(Provider): Returns: list: A list of images, prioritized by localization, original language, and any available image. """ - print(f"Trying to get best image for {item['title']} in {lng}") - print(f"All available languageCodes: {list(map(lambda x: x['iso_639_1'], item['images'][key]))}") # Order images by size and vote average item["images"][key] = sorted( item["images"][key], @@ -165,11 +163,8 @@ class TheMovieDatabase(Provider): if image.get("iso_639_1") == lng ] - print(f"Localized images: {localized_images}") - # Step 2: If no localized images, try images in the original language if not localized_images: - print(f"Could not find localized images for {lng}, trying original language") localized_images = [ image for image in item["images"][key] @@ -178,7 +173,6 @@ class TheMovieDatabase(Provider): # Step 3: If still no images, use any available images if not localized_images: - print(f"Could not find images for {lng} or original language, using any available") localized_images = item["images"][key] return localized_images @@ -202,7 +196,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng},{lng_iso639_1},null", + "include_image_language": f"{get_iso_639_1_codes().join(',')},null", }, ) logger.debug("TMDb responded: %s", movie) @@ -299,7 +293,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", - "include_image_language": f"{lng},{lng_iso639_1},null", + "include_image_language": f"{get_iso_639_1_codes().join(',')},null", }, ) logger.debug("TMDb responded: %s", show) diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index 28304e0a..63149a6d 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -13,6 +13,8 @@ if TYPE_CHECKING: from providers.types.episode import Episode from providers.types.collection import Collection +def get_iso_639_1_codes() -> list[str]: + return [code for code in Language.all_codes() if len(code) == 2] def format_date(date: date | int | None) -> str | None: if date is None: @@ -28,7 +30,7 @@ def normalize_lang(lang: str) -> str: # For now, the API of kyoo only support one language so we remove the others. default_languages = os.environ.get("LIBRARY_LANGUAGES", "").split(",") -image_prefer_original_language = os.environ.get("IMAGE_PREFER_ORIGINAL_LANGUAGE", "false").lower() == "true" +media_prefer_original_language = os.environ.get("MEDIA_PREFER_ORIGINAL_LANGUAGE", "false").lower() == "true" def sort_translations( @@ -65,7 +67,7 @@ def select_image( chain( *( getattr(trans, kind) - for trans in sort_translations(value, prefer_orginal=image_prefer_original_language) + for trans in sort_translations(value, prefer_orginal=media_prefer_original_language) ) ), None, From 6fa4bd889f8234d8b9f1e25634faff4012ccbbcf Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 20:13:56 -0300 Subject: [PATCH 09/27] chg: fix: prioritize language --- scanner/providers/implementations/themoviedatabase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index bb2f7767..460fa833 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -196,7 +196,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{get_iso_639_1_codes().join(',')},null", + "include_image_language": f"{lng_iso639_1},{get_iso_639_1_codes().join(',')},null", }, ) logger.debug("TMDb responded: %s", movie) @@ -293,7 +293,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", - "include_image_language": f"{get_iso_639_1_codes().join(',')},null", + "include_image_language": f"{lng_iso639_1},{get_iso_639_1_codes().join(',')},null", }, ) logger.debug("TMDb responded: %s", show) From a23293c0b77e7a399082b21b93252104251fc59b Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 20:26:52 -0300 Subject: [PATCH 10/27] chg: fix: scanner errors --- scanner/providers/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index 63149a6d..79bbaeba 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from datetime import date from itertools import chain -from langcodes import Language +from langcodes import Language, LANGUAGE_ALPHA3 from typing import TYPE_CHECKING, Literal, Any, Optional if TYPE_CHECKING: @@ -14,7 +14,7 @@ if TYPE_CHECKING: from providers.types.collection import Collection def get_iso_639_1_codes() -> list[str]: - return [code for code in Language.all_codes() if len(code) == 2] + return [code for code in LANGUAGE_ALPHA3.keys() if len(code) == 2] def format_date(date: date | int | None) -> str | None: if date is None: From e403c35741efcd520baa0f4fac317e6c9fcad21e Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 20:30:01 -0300 Subject: [PATCH 11/27] chg: fix: join bug --- scanner/providers/implementations/themoviedatabase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 460fa833..db7ad060 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -196,7 +196,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng_iso639_1},{get_iso_639_1_codes().join(',')},null", + "include_image_language": f"{lng_iso639_1},{','.join(get_iso_639_1_codes())},null", }, ) logger.debug("TMDb responded: %s", movie) @@ -293,7 +293,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", - "include_image_language": f"{lng_iso639_1},{get_iso_639_1_codes().join(',')},null", + "include_image_language": f"{lng_iso639_1},{','.join(get_iso_639_1_codes())},null", }, ) logger.debug("TMDb responded: %s", show) From e73aa83c83e767cc45248ea670c2fae5c979305a Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 21:06:55 -0300 Subject: [PATCH 12/27] chg: fix: only use 3 languages --- scanner/providers/implementations/themoviedatabase.py | 10 +++++----- scanner/providers/utils.py | 5 +---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index db7ad060..93bdc8f5 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -5,7 +5,7 @@ from logging import getLogger from typing import Awaitable, Callable, Dict, List, Optional, Any, TypeVar from itertools import accumulate, zip_longest -from providers.utils import ProviderError, get_iso_639_1_codes +from providers.utils import ProviderError from matcher.cache import cache from ..provider import Provider @@ -184,9 +184,9 @@ class TheMovieDatabase(Provider): if len(search_results) == 0: raise ProviderError(f"No result for a movie named: {name}") search = self.get_best_result(search_results, name, year) - return await self.identify_movie(search["id"]) + return await self.identify_movie(search["id"], original_language=search["original_language"]) - async def identify_movie(self, movie_id: str) -> Movie: + async def identify_movie(self, movie_id: str, original_language: str) -> Movie: languages = self.get_languages() async def for_language(lng: str) -> Movie: @@ -196,7 +196,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng_iso639_1},{','.join(get_iso_639_1_codes())},null", + "include_image_language": f"{lng_iso639_1},null,{original_language}", }, ) logger.debug("TMDb responded: %s", movie) @@ -293,7 +293,7 @@ class TheMovieDatabase(Provider): params={ "language": lng, "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", - "include_image_language": f"{lng_iso639_1},{','.join(get_iso_639_1_codes())},null", + "include_image_language": f"{lng_iso639_1},null,en", }, ) logger.debug("TMDb responded: %s", show) diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index 79bbaeba..c8fae98d 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from datetime import date from itertools import chain -from langcodes import Language, LANGUAGE_ALPHA3 +from langcodes import Language from typing import TYPE_CHECKING, Literal, Any, Optional if TYPE_CHECKING: @@ -13,9 +13,6 @@ if TYPE_CHECKING: from providers.types.episode import Episode from providers.types.collection import Collection -def get_iso_639_1_codes() -> list[str]: - return [code for code in LANGUAGE_ALPHA3.keys() if len(code) == 2] - def format_date(date: date | int | None) -> str | None: if date is None: return None From b96ce5d46fc0cc20388524857b7c6119a6fcd0cc Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 21:20:46 -0300 Subject: [PATCH 13/27] chg: docs: add setting to the envfile --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 889216c1..e6c9b772 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ LIBRARY_ROOT=./video # It will automatically be cleaned up on kyoo's startup/shutdown/runtime. CACHE_ROOT=/tmp/kyoo_cache LIBRARY_LANGUAGES=en +# If this is true, kyoo will prefer to download the media in the original language of the item. +MEDIA_PREFER_ORIGINAL_LANGUAGE=false # A pattern (regex) to ignore video files. LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*" From 7c0424fcda5afe397025b83ab45698a8134b1fb8 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Wed, 13 Nov 2024 21:24:38 -0300 Subject: [PATCH 14/27] chg: feat: better ignore regex --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index e6c9b772..57267d11 100644 --- a/.env.example +++ b/.env.example @@ -12,8 +12,8 @@ CACHE_ROOT=/tmp/kyoo_cache LIBRARY_LANGUAGES=en # If this is true, kyoo will prefer to download the media in the original language of the item. MEDIA_PREFER_ORIGINAL_LANGUAGE=false -# A pattern (regex) to ignore video files. -LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*" +# A pattern (regex) to ignore files. +LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*|.*\.(mp3|srt|jpg|jpeg|png|gif|bmp|tiff|svg)$|.*[Tt][Rr][Aa][Ii][Ll][Ee][Rr].*" # If this is true, new accounts wont have any permissions before you approve them in your admin dashboard. REQUIRE_ACCOUNT_VERIFICATION=true From 28fe8bd9bc1ddbbec6e953ca0b3cdda8240c1e72 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 15:12:56 -0300 Subject: [PATCH 15/27] chg: refactor: use langcodes instead of string for language representation --- .../implementations/themoviedatabase.py | 98 +++++++++++-------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 93bdc8f5..b5b8b86d 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from logging import getLogger from typing import Awaitable, Callable, Dict, List, Optional, Any, TypeVar from itertools import accumulate, zip_longest +from langcodes import Language from providers.utils import ProviderError from matcher.cache import cache @@ -31,7 +32,7 @@ class TheMovieDatabase(Provider): api_key: str, ) -> None: super().__init__() - self._languages = languages + self._languages = [Language.get(l) for l in languages] self._client = client self.base = "https://api.themoviedb.org/3" self.api_key = api_key @@ -78,7 +79,7 @@ class TheMovieDatabase(Provider): [self.genre_map[x["id"]] for x in genres if x["id"] in self.genre_map] ) - def get_languages(self, *args): + def get_languages(self, *args) -> list[Language]: return self._languages + list(args) async def get( @@ -99,16 +100,16 @@ class TheMovieDatabase(Provider): T = TypeVar("T") - def merge_translations(self, host, translations, *, languages: list[str]): + def merge_translations(self, host, translations, *, languages: list[Language]): host.translations = { - k: v.translations[k] for k, v in zip(languages, translations) + k.to_tag(): v.translations[k.to_tag()] for k, v in zip(languages, translations) } return host async def process_translations( self, for_language: Callable[[str], Awaitable[T]], - languages: list[str], + languages: list[Language], post_merge: Callable[[T, list[T]], T] | None = None, ) -> T: tasks = map(lambda lng: for_language(lng), languages) @@ -138,13 +139,13 @@ class TheMovieDatabase(Provider): }, ) - def get_best_image(self, item, lng, key): + async def get_best_image(self, item, lng, key): """ Retrieves the best available images for a item based on localization. Args: item (dict): A dictionary containing item information, including images and language details. - lng (str): The preferred language code for the images in ISO 639-1 format. + lng (Language): The preferred language for the images. key (str): The key to access the images in the item dictionary. (e.g. "posters", "backdrops", "logos") Returns: list: A list of images, prioritized by localization, original language, and any available image. @@ -160,7 +161,7 @@ class TheMovieDatabase(Provider): localized_images = [ image for image in item["images"][key] - if image.get("iso_639_1") == lng + if image.get("iso_639_1") == lng.language ] # Step 2: If no localized images, try images in the original language @@ -175,6 +176,23 @@ class TheMovieDatabase(Provider): if not localized_images: localized_images = item["images"][key] + # Corner case: If there are no images at all, call TMDB images API. + # Although doing another API call is not ideal, this would only be called very rarely. + if not localized_images: + logger.debug( + "No images found for %s %s. Calling TMDB images API.", + item["media_type"], + item["id"], + ) + images = await self.get( + f"{item['media_type']}/{item['id']}/images", + ) + localized_images = sorted( + images[key], + key=lambda x: (x.get("width", 0), x.get("vote_average", 0)), + reverse=True, + ) + return localized_images async def search_movie(self, name: str, year: Optional[int]) -> Movie: @@ -189,14 +207,13 @@ class TheMovieDatabase(Provider): async def identify_movie(self, movie_id: str, original_language: str) -> Movie: languages = self.get_languages() - async def for_language(lng: str) -> Movie: - lng_iso639_1 = lng.split("-").pop(0) + async def for_language(lng: Language) -> Movie: movie = await self.get( f"movie/{movie_id}", params={ - "language": lng, + "language": lng.to_tag(), "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng_iso639_1},null,{original_language}", + "include_image_language": f"{lng.language},null,{original_language}", }, ) logger.debug("TMDb responded: %s", movie) @@ -252,11 +269,11 @@ class TheMovieDatabase(Provider): tags=list(map(lambda x: x["name"], movie["keywords"]["keywords"])), overview=movie["overview"], posters=self.get_image( - self.get_best_image(movie, lng_iso639_1, "posters") + await self.get_best_image(movie, lng, "posters") ), - logos=self.get_image(self.get_best_image(movie, lng_iso639_1, "logos")), + logos=self.get_image(await self.get_best_image(movie, lng, "logos")), thumbnails=self.get_image( - self.get_best_image(movie, lng_iso639_1, "backdrops") + await self.get_best_image(movie, lng, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" @@ -264,7 +281,7 @@ class TheMovieDatabase(Provider): if x["type"] == "Trailer" and x["site"] == "YouTube" ], ) - ret.translations = {lng: translation} + ret.translations = {lng.to_tag(): translation} return ret ret = await self.process_translations(for_language, languages) @@ -272,9 +289,10 @@ class TheMovieDatabase(Provider): ret.original_language is not None and ret.original_language not in ret.translations ): - ret.translations[ret.original_language] = ( - await for_language(ret.original_language) - ).translations[ret.original_language] + orig_language = Language.get(ret.original_language) + ret.translations[orig_language.to_tag()] = ( + await for_language(orig_language) + ).translations[orig_language.to_tag()] return ret @cache(ttl=timedelta(days=1)) @@ -284,16 +302,13 @@ class TheMovieDatabase(Provider): ) -> Show: languages = self.get_languages() - async def for_language(lng: str) -> Show: - # ISO 639-1 (eg. "en") is required as TMDB does not support fetching images - # by ISO 639-2 + ISO 3166-1 alpha-2 (eg. "en-US") - lng_iso639_1 = lng.split("-").pop(0) + async def for_language(lng: Language) -> Show: show = await self.get( f"tv/{show_id}", params={ - "language": lng, + "language": lng.to_tag(), "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", - "include_image_language": f"{lng_iso639_1},null,en", + "include_image_language": f"{lng.language},null,en", }, ) logger.debug("TMDb responded: %s", show) @@ -347,11 +362,11 @@ class TheMovieDatabase(Provider): tags=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], posters=self.get_image( - self.get_best_image(show, lng_iso639_1, "posters") + await self.get_best_image(show, lng, "posters") ), - logos=self.get_image(self.get_best_image(show, lng_iso639_1, "logos")), + logos=self.get_image(await self.get_best_image(show, lng, "logos")), thumbnails=self.get_image( - self.get_best_image(show, lng_iso639_1, "backdrops") + await self.get_best_image(show, lng, "backdrops") ), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" @@ -359,7 +374,7 @@ class TheMovieDatabase(Provider): if x["type"] == "Trailer" and x["site"] == "YouTube" ], ) - ret.translations = {lng: translation} + ret.translations = {lng.to_tag(): translation} return ret def merge_seasons_translations(item: Show, items: list[Show]) -> Show: @@ -387,13 +402,14 @@ class TheMovieDatabase(Provider): ret.original_language is not None and ret.original_language not in ret.translations ): - ret.translations[ret.original_language] = ( - await for_language(ret.original_language) - ).translations[ret.original_language] + orig_language = Language.get(ret.original_language) + ret.translations[orig_language.to_tag()] = ( + await for_language(orig_language) + ).translations[orig_language.to_tag()] return ret def to_season( - self, season: dict[str, Any], *, language: str, show_id: str + self, season: dict[str, Any], *, language: Language, show_id: str ) -> Season: return Season( season_number=season["season_number"], @@ -409,7 +425,7 @@ class TheMovieDatabase(Provider): ) }, translations={ - language: SeasonTranslation( + language.to_tag(): SeasonTranslation( name=season["name"], overview=season["overview"], posters=[ @@ -481,19 +497,19 @@ class TheMovieDatabase(Provider): async def identify_episode( self, show_id: str, season: Optional[int], episode_nbr: int, absolute: int ) -> Episode: - async def for_language(lng: str) -> Episode: + async def for_language(lng: Language) -> Episode: try: episode = await self.get( f"tv/{show_id}/season/{season}/episode/{episode_nbr}", params={ - "language": lng, + "language": lng.to_tag(), }, ) except: episode = await self.get( f"tv/{show_id}/season/{season}/episode/{absolute}", params={ - "language": lng, + "language": lng.to_tag(), }, not_found_fail=f"Could not find episode {episode_nbr} of season {season} of serie {show_id} (absolute: {absolute})", ) @@ -534,7 +550,7 @@ class TheMovieDatabase(Provider): name=episode["name"], overview=episode["overview"], ) - ret.translations = {lng: translation} + ret.translations = {lng.to_tag(): translation} return ret return await self.process_translations(for_language, self.get_languages()) @@ -723,11 +739,11 @@ class TheMovieDatabase(Provider): async def identify_collection(self, provider_id: str) -> Collection: languages = self.get_languages() - async def for_language(lng: str) -> Collection: + async def for_language(lng: Language) -> Collection: collection = await self.get( f"collection/{provider_id}", params={ - "language": lng, + "language": lng.to_tag(), }, ) logger.debug("TMDb responded: %s", collection) @@ -751,7 +767,7 @@ class TheMovieDatabase(Provider): f"https://image.tmdb.org/t/p/original{collection['backdrop_path']}" ], ) - ret.translations = {lng: translation} + ret.translations = {lng.to_tag(): translation} return ret return await self.process_translations(for_language, languages) From 6b751e11f3f494492922dae05768643587e8c6d5 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 15:14:39 -0300 Subject: [PATCH 16/27] chg: fix: add fallback --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index b5b8b86d..41d56523 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -180,7 +180,7 @@ class TheMovieDatabase(Provider): # Although doing another API call is not ideal, this would only be called very rarely. if not localized_images: logger.debug( - "No images found for %s %s. Calling TMDB images API.", + "[Fallback] No images found for %s %s. Calling TMDB images API.", item["media_type"], item["id"], ) From 50e18ee99d8ce51d1bc3c446f61a64e9b811327f Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 15:20:45 -0300 Subject: [PATCH 17/27] chg: refactor: remove repetitive code --- .../implementations/themoviedatabase.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 41d56523..023e8df0 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -193,7 +193,7 @@ class TheMovieDatabase(Provider): reverse=True, ) - return localized_images + return self.get_image(localized_images) async def search_movie(self, name: str, year: Optional[int]) -> Movie: search_results = ( @@ -268,13 +268,9 @@ class TheMovieDatabase(Provider): tagline=movie["tagline"] if movie["tagline"] else None, tags=list(map(lambda x: x["name"], movie["keywords"]["keywords"])), overview=movie["overview"], - posters=self.get_image( - await self.get_best_image(movie, lng, "posters") - ), - logos=self.get_image(await self.get_best_image(movie, lng, "logos")), - thumbnails=self.get_image( - await self.get_best_image(movie, lng, "backdrops") - ), + posters=(await self.get_best_image(movie, lng, "posters")), + logos=(await self.get_best_image(movie, lng, "logos")), + thumbnails=(await self.get_best_image(movie, lng, "backdrops")), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" for x in movie["videos"]["results"] @@ -361,13 +357,9 @@ class TheMovieDatabase(Provider): tagline=show["tagline"] if show["tagline"] else None, tags=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], - posters=self.get_image( - await self.get_best_image(show, lng, "posters") - ), - logos=self.get_image(await self.get_best_image(show, lng, "logos")), - thumbnails=self.get_image( - await self.get_best_image(show, lng, "backdrops") - ), + posters=(await self.get_best_image(show, lng, "posters")), + logos=(await self.get_best_image(show, lng, "logos")), + thumbnails=(await self.get_best_image(show, lng, "backdrops")), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" for x in show["videos"]["results"] From bc7920fad3da39cd5e4ca3efd5aa847f3416e423 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 17:07:20 -0300 Subject: [PATCH 18/27] chg: fix: add media type --- scanner/matcher/matcher.py | 2 +- scanner/providers/implementations/themoviedatabase.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/scanner/matcher/matcher.py b/scanner/matcher/matcher.py index a61a7393..ecc06363 100644 --- a/scanner/matcher/matcher.py +++ b/scanner/matcher/matcher.py @@ -52,7 +52,7 @@ class Matcher: if "mimetype" not in raw or not raw["mimetype"].startswith("video"): return - logger.info("Identied %s: %s", path, raw) + logger.info("Identified %s: %s", path, raw) title = raw.get("title") if not isinstance(title, str): diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 023e8df0..71f179ed 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -153,7 +153,7 @@ class TheMovieDatabase(Provider): # Order images by size and vote average item["images"][key] = sorted( item["images"][key], - key=lambda x: (x.get("width", 0), x.get("vote_average", 0)), + key=lambda x: (x.get("vote_average", 0), x.get("width", 0)), reverse=True, ) @@ -189,7 +189,7 @@ class TheMovieDatabase(Provider): ) localized_images = sorted( images[key], - key=lambda x: (x.get("width", 0), x.get("vote_average", 0)), + key=lambda x: x.get("vote_average", 0), reverse=True, ) @@ -216,6 +216,7 @@ class TheMovieDatabase(Provider): "include_image_language": f"{lng.language},null,{original_language}", }, ) + movie["media_type"] = "movie" logger.debug("TMDb responded: %s", movie) ret = Movie( @@ -307,6 +308,7 @@ class TheMovieDatabase(Provider): "include_image_language": f"{lng.language},null,en", }, ) + show["media_type"] = "tv" logger.debug("TMDb responded: %s", show) ret = Show( From 26b839fc4a8481019e0be9d2c297a313e7732140 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 17:22:15 -0300 Subject: [PATCH 19/27] chg: fix: make the extra parameter optional --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 71f179ed..ec8f984e 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -204,7 +204,7 @@ class TheMovieDatabase(Provider): search = self.get_best_result(search_results, name, year) return await self.identify_movie(search["id"], original_language=search["original_language"]) - async def identify_movie(self, movie_id: str, original_language: str) -> Movie: + async def identify_movie(self, movie_id: str, original_language: Optional[str]) -> Movie: languages = self.get_languages() async def for_language(lng: Language) -> Movie: From 4be28127804771611fab99b3a113247b8cee2247 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 17:32:16 -0300 Subject: [PATCH 20/27] chg: fix: add null as default original language --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index ec8f984e..65024ae0 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -204,7 +204,7 @@ class TheMovieDatabase(Provider): search = self.get_best_result(search_results, name, year) return await self.identify_movie(search["id"], original_language=search["original_language"]) - async def identify_movie(self, movie_id: str, original_language: Optional[str]) -> Movie: + async def identify_movie(self, movie_id: str, original_language: Optional[str] = "null") -> Movie: languages = self.get_languages() async def for_language(lng: Language) -> Movie: From 60410481c13af243b4445680d338847d83c8b9d5 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Thu, 14 Nov 2024 17:40:39 -0300 Subject: [PATCH 21/27] chg: lint: format code --- scanner/providers/implementations/themoviedatabase.py | 11 ++++++++--- scanner/providers/utils.py | 9 +++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 65024ae0..0c56be7d 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -102,7 +102,8 @@ class TheMovieDatabase(Provider): def merge_translations(self, host, translations, *, languages: list[Language]): host.translations = { - k.to_tag(): v.translations[k.to_tag()] for k, v in zip(languages, translations) + k.to_tag(): v.translations[k.to_tag()] + for k, v in zip(languages, translations) } return host @@ -202,9 +203,13 @@ class TheMovieDatabase(Provider): if len(search_results) == 0: raise ProviderError(f"No result for a movie named: {name}") search = self.get_best_result(search_results, name, year) - return await self.identify_movie(search["id"], original_language=search["original_language"]) + return await self.identify_movie( + search["id"], original_language=search["original_language"] + ) - async def identify_movie(self, movie_id: str, original_language: Optional[str] = "null") -> Movie: + async def identify_movie( + self, movie_id: str, original_language: Optional[str] = "null" + ) -> Movie: languages = self.get_languages() async def for_language(lng: Language) -> Movie: diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index c8fae98d..ddcc4ffb 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from providers.types.episode import Episode from providers.types.collection import Collection + def format_date(date: date | int | None) -> str | None: if date is None: return None @@ -27,7 +28,9 @@ def normalize_lang(lang: str) -> str: # For now, the API of kyoo only support one language so we remove the others. default_languages = os.environ.get("LIBRARY_LANGUAGES", "").split(",") -media_prefer_original_language = os.environ.get("MEDIA_PREFER_ORIGINAL_LANGUAGE", "false").lower() == "true" +media_prefer_original_language = ( + os.environ.get("MEDIA_PREFER_ORIGINAL_LANGUAGE", "false").lower() == "true" +) def sort_translations( @@ -64,7 +67,9 @@ def select_image( chain( *( getattr(trans, kind) - for trans in sort_translations(value, prefer_orginal=media_prefer_original_language) + for trans in sort_translations( + value, prefer_orginal=media_prefer_original_language + ) ) ), None, From 9416c06a0d2d2d5ac7610717df7d9e4abd6f7d2d Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 15 Nov 2024 10:51:52 -0300 Subject: [PATCH 22/27] chg: fix: apply MR suggestions --- .env.example | 2 +- .../implementations/themoviedatabase.py | 48 +++++++++---------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 57267d11..8735891b 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ LIBRARY_LANGUAGES=en # If this is true, kyoo will prefer to download the media in the original language of the item. MEDIA_PREFER_ORIGINAL_LANGUAGE=false # A pattern (regex) to ignore files. -LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*|.*\.(mp3|srt|jpg|jpeg|png|gif|bmp|tiff|svg)$|.*[Tt][Rr][Aa][Ii][Ll][Ee][Rr].*" +LIBRARY_IGNORE_PATTERN=".*/[dD]ownloads?/.*|.*[Tt][Rr][Aa][Ii][Ll][Ee][Rr].*" # If this is true, new accounts wont have any permissions before you approve them in your admin dashboard. REQUIRE_ACCOUNT_VERIFICATION=true diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 0c56be7d..5edcef01 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -140,7 +140,7 @@ class TheMovieDatabase(Provider): }, ) - async def get_best_image(self, item, lng, key): + def get_best_image(self, item: dict[str, Any], lng: Language, key: str) -> list[dict]: """ Retrieves the best available images for a item based on localization. @@ -177,25 +177,23 @@ class TheMovieDatabase(Provider): if not localized_images: localized_images = item["images"][key] - # Corner case: If there are no images at all, call TMDB images API. - # Although doing another API call is not ideal, this would only be called very rarely. + # Step 4: If there are no images at all, fallback to _path attribute. if not localized_images: - logger.debug( - "[Fallback] No images found for %s %s. Calling TMDB images API.", - item["media_type"], - item["id"], - ) - images = await self.get( - f"{item['media_type']}/{item['id']}/images", - ) - localized_images = sorted( - images[key], - key=lambda x: x.get("vote_average", 0), - reverse=True, - ) + localized_images = self._get_image_fallback(item, key) return self.get_image(localized_images) + def _get_image_fallback(self, item: dict[str, Any], key: str) -> list[dict]: + """ + Fallback to _path attribute if there are no images available in the images list. + """ + if key == "posters": + return [{"file_path": item.get("poster_path")}] + elif key == "backdrops": + return [{"file_path": item.get("backdrop_path")}] + + return [] + async def search_movie(self, name: str, year: Optional[int]) -> Movie: search_results = ( await self.get("search/movie", params={"query": name, "year": year}) @@ -208,7 +206,7 @@ class TheMovieDatabase(Provider): ) async def identify_movie( - self, movie_id: str, original_language: Optional[str] = "null" + self, movie_id: str, original_language: Optional[Language] = None ) -> Movie: languages = self.get_languages() @@ -218,10 +216,9 @@ class TheMovieDatabase(Provider): params={ "language": lng.to_tag(), "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng.language},null,{original_language}", + "include_image_language": f"{lng.language},null,{original_language.language if original_language else ""}", }, ) - movie["media_type"] = "movie" logger.debug("TMDb responded: %s", movie) ret = Movie( @@ -274,9 +271,9 @@ class TheMovieDatabase(Provider): tagline=movie["tagline"] if movie["tagline"] else None, tags=list(map(lambda x: x["name"], movie["keywords"]["keywords"])), overview=movie["overview"], - posters=(await self.get_best_image(movie, lng, "posters")), - logos=(await self.get_best_image(movie, lng, "logos")), - thumbnails=(await self.get_best_image(movie, lng, "backdrops")), + posters=self.get_best_image(movie, lng, "posters"), + logos=self.get_best_image(movie, lng, "logos"), + thumbnails=self.get_best_image(movie, lng, "backdrops"), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" for x in movie["videos"]["results"] @@ -313,7 +310,6 @@ class TheMovieDatabase(Provider): "include_image_language": f"{lng.language},null,en", }, ) - show["media_type"] = "tv" logger.debug("TMDb responded: %s", show) ret = Show( @@ -364,9 +360,9 @@ class TheMovieDatabase(Provider): tagline=show["tagline"] if show["tagline"] else None, tags=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], - posters=(await self.get_best_image(show, lng, "posters")), - logos=(await self.get_best_image(show, lng, "logos")), - thumbnails=(await self.get_best_image(show, lng, "backdrops")), + posters=self.get_best_image(show, lng, "posters"), + logos=self.get_best_image(show, lng, "logos"), + thumbnails=self.get_best_image(show, lng, "backdrops"), trailers=[ f"https://www.youtube.com/watch?v={x['key']}" for x in show["videos"]["results"] From 31a38d08616d64e53eb47273a7dae69f034a3980 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 15 Nov 2024 10:53:44 -0300 Subject: [PATCH 23/27] chg: fix: original_language param --- scanner/providers/implementations/themoviedatabase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 5edcef01..227298d8 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -201,8 +201,9 @@ class TheMovieDatabase(Provider): if len(search_results) == 0: raise ProviderError(f"No result for a movie named: {name}") search = self.get_best_result(search_results, name, year) + original_language = Language.get(search["original_language"]) return await self.identify_movie( - search["id"], original_language=search["original_language"] + search["id"], original_language=original_language ) async def identify_movie( From eaa09aba3b5874ddf03e8fa3a9ebff937eab5061 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 15 Nov 2024 10:59:36 -0300 Subject: [PATCH 24/27] chg: fix: lint issues --- scanner/providers/implementations/themoviedatabase.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 227298d8..4fe86346 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -140,7 +140,9 @@ class TheMovieDatabase(Provider): }, ) - def get_best_image(self, item: dict[str, Any], lng: Language, key: str) -> list[dict]: + def get_best_image( + self, item: dict[str, Any], lng: Language, key: str + ) -> list[dict]: """ Retrieves the best available images for a item based on localization. From f121b718a94649ec4f7990813bb4df09cadf5581 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 15 Nov 2024 11:13:06 -0300 Subject: [PATCH 25/27] chg: feat: add localization to collections as well --- scanner/providers/implementations/themoviedatabase.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 4fe86346..0d0ee8ae 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -742,6 +742,8 @@ class TheMovieDatabase(Provider): f"collection/{provider_id}", params={ "language": lng.to_tag(), + "append_to_response": "images", + "include_image_language": f"{lng.language},null,en", }, ) logger.debug("TMDb responded: %s", collection) @@ -757,13 +759,9 @@ class TheMovieDatabase(Provider): translation = CollectionTranslation( name=collection["name"], overview=collection["overview"], - posters=[ - f"https://image.tmdb.org/t/p/original{collection['poster_path']}" - ], + posters=self.get_best_image(collection, lng, "posters"), logos=[], - thumbnails=[ - f"https://image.tmdb.org/t/p/original{collection['backdrop_path']}" - ], + thumbnails=self.get_best_image(collection, lng, "backdrops"), ) ret.translations = {lng.to_tag(): translation} return ret From 5941fc3ee77ef910b0df3aa1daf42b6266afe674 Mon Sep 17 00:00:00 2001 From: Felipe Marinho Date: Fri, 15 Nov 2024 12:01:16 -0300 Subject: [PATCH 26/27] chg: fix: key_error when original_language is unavailable --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 0d0ee8ae..277e9be4 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -172,7 +172,7 @@ class TheMovieDatabase(Provider): localized_images = [ image for image in item["images"][key] - if image.get("iso_639_1") == item["original_language"] + if image.get("iso_639_1") == item.get("original_language") ] # Step 3: If still no images, use any available images From c0596eb3ab422794f259de8484d9a5609aa6059a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 9 Apr 2025 00:18:31 +0200 Subject: [PATCH 27/27] Format stuff --- scanner/providers/implementations/themoviedatabase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 277e9be4..85a8fb4b 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -219,7 +219,7 @@ class TheMovieDatabase(Provider): params={ "language": lng.to_tag(), "append_to_response": "alternative_titles,videos,credits,keywords,images", - "include_image_language": f"{lng.language},null,{original_language.language if original_language else ""}", + "include_image_language": f"{lng.language},null,{original_language.language if original_language else ''}", }, ) logger.debug("TMDb responded: %s", movie)