diff --git a/media_manager/metadataProvider/abstractMetaDataProvider.py b/media_manager/metadataProvider/abstractMetaDataProvider.py index 8bf16cd..c5d023a 100644 --- a/media_manager/metadataProvider/abstractMetaDataProvider.py +++ b/media_manager/metadataProvider/abstractMetaDataProvider.py @@ -2,8 +2,9 @@ import logging from abc import ABC, abstractmethod import media_manager.config -from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult +from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult from media_manager.tv.schemas import Show +from media_manager.movies.schemas import Movie log = logging.getLogger(__name__) @@ -20,10 +21,21 @@ class AbstractMetadataProvider(ABC): def get_show_metadata(self, id: int = None) -> Show: raise NotImplementedError() + @abstractmethod + def get_movie_metadata(self, id: int = None) -> Movie: + raise NotImplementedError() + + @abstractmethod def search_show( self, query: str | None = None - ) -> list[MetaDataProviderShowSearchResult]: + ) -> list[MetaDataProviderSearchResult]: + raise NotImplementedError() + + @abstractmethod + def search_movie( + self, query: str | None = None + ) -> list[MetaDataProviderSearchResult]: raise NotImplementedError() @abstractmethod @@ -35,6 +47,14 @@ class AbstractMetadataProvider(ABC): """ raise NotImplementedError() + @abstractmethod + def download_movie_poster_image(self, show: Show) -> bool: + """ + Downloads the poster image for a show. + :param show: The show to download the poster image for. + :return: True if the image was downloaded successfully, False otherwise. + """ + raise NotImplementedError() metadata_providers = {} diff --git a/media_manager/metadataProvider/tmdb.py b/media_manager/metadataProvider/tmdb.py index 9caac26..bb0f071 100644 --- a/media_manager/metadataProvider/tmdb.py +++ b/media_manager/metadataProvider/tmdb.py @@ -9,8 +9,9 @@ from media_manager.exceptions import InvalidConfigError from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) -from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult +from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber, EpisodeNumber +from media_manager.movies.schemas import Movie class TmdbConfig(BaseSettings): @@ -40,7 +41,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider): "https://image.tmdb.org/t/p/original" + show_metadata["poster_path"] ) if media_manager.metadataProvider.utils.download_poster_image( - storage_path=self.storage_path, poster_url=poster_url, show=show + storage_path=self.storage_path, poster_url=poster_url, id=show.id ): log.info("Successfully downloaded poster image for show " + show.name) else: @@ -87,7 +88,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider): ) ) - year = media_manager.metadataProvider.utils.get_year_from_first_air_date( + year = media_manager.metadataProvider.utils.get_year_from_date( show_metadata["first_air_date"] ) @@ -105,7 +106,7 @@ class TmdbMetadataProvider(AbstractMetadataProvider): def search_show( self, query: str | None = None, max_pages: int = 5 - ) -> list[MetaDataProviderShowSearchResult]: + ) -> list[MetaDataProviderSearchResult]: """ Search for shows using TMDB API. If no query is provided, it will return the most popular shows. @@ -136,12 +137,12 @@ class TmdbMetadataProvider(AbstractMetadataProvider): else: poster_url = None formatted_results.append( - MetaDataProviderShowSearchResult( + MetaDataProviderSearchResult( poster_path=poster_url, overview=result["overview"], name=result["name"], external_id=result["id"], - year=media_manager.metadataProvider.utils.get_year_from_first_air_date( + year=media_manager.metadataProvider.utils.get_year_from_date( result["first_air_date"] ), metadata_provider=self.name, @@ -152,3 +153,117 @@ class TmdbMetadataProvider(AbstractMetadataProvider): except Exception as e: log.warning(f"Error processing search result {result}: {e}") return formatted_results + + def get_movie_metadata(self, id: int = None) -> Movie: + """ + + :param id: the external id of the show + :type id: int + :return: returns a ShowMetadata object + :rtype: ShowMetadata + """ + movie_metadata = tmdbsimple.Movies(id).info() + year = media_manager.metadataProvider.utils.get_year_from_date( + movie_metadata["release_date"] + ) + + movie = Movie( + external_id=id, + name=movie_metadata["title"], + overview=movie_metadata["overview"], + year=year, + metadata_provider=self.name, + ) + + return movie + + def search_movie( + self, query: str | None = None, max_pages: int = 5 + ) -> list[MetaDataProviderSearchResult]: + """ + Search for movies using TMDB API. + If no query is provided, it will return the most popular movies. + """ + if query is None: + result_factory = lambda page: tmdbsimple.Trending(media_type="movie").info() # noqa: E731 + else: + result_factory = lambda page: tmdbsimple.Search().movie( # noqa: E731 + page=page, query=query, include_adult=True + ) + + results = [] + for i in range(1, max_pages + 1): + result_page = result_factory(i) + + if not result_page["results"]: + break + else: + results.extend(result_page["results"]) + + formatted_results = [] + for result in results: + try: + if result["poster_path"] is not None: + poster_url = ( + "https://image.tmdb.org/t/p/original" + result["poster_path"] + ) + else: + poster_url = None + formatted_results.append( + MetaDataProviderSearchResult( + poster_path=poster_url, + overview=result["overview"], + name=result["title"], + external_id=result["id"], + year=media_manager.metadataProvider.utils.get_year_from_date( + result["release_date"] + ), + metadata_provider=self.name, + added=False, + vote_average=result["vote_average"], + ) + ) + except Exception as e: + log.warning(f"Error processing search result {result}: {e}") + return formatted_results + + def download_movie_poster_image(self, movie: Movie) -> bool: + movie_metadata = tmdbsimple.Movies(movie.external_id).info() + # downloading the poster + # all pictures from TMDB should already be jpeg, so no need to convert + if movie_metadata["poster_path"] is not None: + poster_url = ( + "https://image.tmdb.org/t/p/original" + movie_metadata["poster_path"] + ) + 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) + else: + log.warning(f"download for image of show {movie.name} failed") + return False + else: + log.warning(f"image for show {movie.name} could not be downloaded") + return False + return True + + + + + + + + + + + + + + + + + + + + + diff --git a/media_manager/metadataProvider/tvdb.py b/media_manager/metadataProvider/tvdb.py index 6a26696..75a1345 100644 --- a/media_manager/metadataProvider/tvdb.py +++ b/media_manager/metadataProvider/tvdb.py @@ -8,8 +8,9 @@ from media_manager.exceptions import InvalidConfigError from media_manager.metadataProvider.abstractMetaDataProvider import ( AbstractMetadataProvider, ) -from media_manager.metadataProvider.schemas import MetaDataProviderShowSearchResult +from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult from media_manager.tv.schemas import Episode, Season, Show, SeasonNumber +from media_manager.movies.schemas import Movie class TvdbConfig(BaseSettings): @@ -24,7 +25,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider): tvdb_client: tvdb_v4_official.TVDB - def __init__(self, api_key: str = None): + def __init__(self): config = TvdbConfig() if config.TVDB_API_KEY is None: raise InvalidConfigError("TVDB_API_KEY is not set") @@ -37,7 +38,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider): media_manager.metadataProvider.utils.download_poster_image( storage_path=self.storage_path, poster_url=show_metadata["image"], - show=show, + id=show.id, ) log.info("Successfully downloaded poster image for show " + show.name) return True @@ -105,7 +106,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider): def search_show( self, query: str | None = None - ) -> list[MetaDataProviderShowSearchResult]: + ) -> list[MetaDataProviderSearchResult]: if query is None: results = self.tvdb_client.get_all_series() else: @@ -120,7 +121,7 @@ class TvdbMetadataProvider(AbstractMetadataProvider): year = None formatted_results.append( - MetaDataProviderShowSearchResult( + MetaDataProviderSearchResult( poster_path=result["image_url"], overview=result["overview"], name=result["name"], @@ -134,3 +135,72 @@ class TvdbMetadataProvider(AbstractMetadataProvider): except Exception as e: log.warning(f"Error processing search result {result}: {e}") return formatted_results + + def search_movie(self, query: str | None = None) -> list[MetaDataProviderSearchResult]: + if query is None: + results = self.tvdb_client.get_all_movies() + else: + results = self.tvdb_client.search(query) + formatted_results = [] + for result in results: + try: + if result["type"] == "movie": + try: + year = result["year"] + except KeyError: + year = None + + formatted_results.append( + MetaDataProviderSearchResult( + poster_path=result["image_url"], + overview="TVDB doesn't provide Movie Overviews", + name=result["name"], + external_id=result["tvdb_id"], + year=year, + metadata_provider=self.name, + added=False, + vote_average=None, + ) + ) + except Exception as e: + log.warning(f"Error processing search result {result}: {e}") + return formatted_results + + def download_movie_poster_image(self, movie: Movie) -> bool: + movie_metadata = self.tvdb_client.get_movie_extended(movie.external_id) + + if movie_metadata["image"] is not None: + media_manager.metadataProvider.utils.download_poster_image( + storage_path=self.storage_path, + poster_url=movie_metadata["image"], + id=movie.id, + ) + log.info("Successfully downloaded poster image for show " + movie.name) + return True + else: + log.warning(f"image for show {movie.name} could not be downloaded") + return False + + def get_movie_metadata(self, id: int = None) -> Movie: + """ + + :param id: the external id of the movie + :type id: int + :return: returns a Movie object + :rtype: Movie + """ + movie = self.tvdb_client.get_movie_extended(id) + try: + year = movie["year"] + except KeyError: + year = None + + movie = Movie( + name=movie["name"], + overview="TVDB doesn't provide Movie Overviews", + year=year, + external_id=movie["id"], + metadata_provider=self.name, + ) + + return movie \ No newline at end of file diff --git a/media_manager/metadataProvider/utils.py b/media_manager/metadataProvider/utils.py index d6df112..a116a1b 100644 --- a/media_manager/metadataProvider/utils.py +++ b/media_manager/metadataProvider/utils.py @@ -1,18 +1,22 @@ +from uuid import UUID + from PIL import Image import requests +import pillow_avif +pillow_avif -def get_year_from_first_air_date(first_air_date: str | None) -> int | None: +def get_year_from_date(first_air_date: str | None) -> int | None: if first_air_date: return int(first_air_date.split("-")[0]) else: return None -def download_poster_image(storage_path=None, poster_url=None, show=None) -> bool: +def download_poster_image(storage_path=None, poster_url=None, id: UUID=None) -> bool: res = requests.get(poster_url, stream=True) if res.status_code == 200: - image_file_path = storage_path.joinpath(str(show.id)) + image_file_path = storage_path.joinpath(str(id)) with open(str(image_file_path) + ".jpg", "wb") as f: f.write(res.content)