diff --git a/.gitignore b/.gitignore index bde577dc..66015f42 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ __pycache__/ # Distribution / packaging .idea .Python -/test.py +/test* logs/ config/* !config/overlays/ diff --git a/README.md b/README.md index c185528e..56f9d454 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Before posting on GitHub about an enhancement, error, or configuration question ## Wiki Table of Contents - [Home](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Home) - [Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Installation) +- [Run Commands & Environmental Variables](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Run-Commands-&-Environmental-Variables) - [Local Walkthrough](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Walkthrough) - [Docker Walkthrough](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker-Walkthrough) - [unRAID Walkthrough](https://github.com/meisnate12/Plex-Meta-Manager/wiki/unRAID-Walkthrough) @@ -79,9 +80,9 @@ Before posting on GitHub about an enhancement, error, or configuration question - [MyAnimeList Attributes](https://github.com/meisnate12/Plex-Meta-Manager/wiki/MyAnimeList-Attributes) - [Metadata and Playlist Files](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Metadata-and-Playlist-Files) - Metadata - - [Movies Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Movies-Metadata) - - [Shows Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Shows-Metadata) - - [Artists Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Artists-Metadata) + - [Movie Library Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Movie-Library-Metadata) + - [TV Show Library Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/TV-Show-Library-Metadata) + - [Music Library Metadata](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Music-Library-Metadata) - [Templates](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Templates) - [Filters](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Filters) - Builders @@ -92,6 +93,7 @@ Before posting on GitHub about an enhancement, error, or configuration question - [IMDb Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/IMDb-Builders) - [Trakt Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Trakt-Builders) - [Tautulli Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Tautulli-Builders) + - [MdbList Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/MdbList-Builders) - [Letterboxd Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Letterboxd-Builders) - [ICheckMovies Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/ICheckMovies-Builders) - [FlixPatrol Builders](https://github.com/meisnate12/Plex-Meta-Manager/wiki/FlixPatrol-Builders) diff --git a/VERSION b/VERSION index d19d0890..795d8700 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.0 \ No newline at end of file +1.15.1 \ No newline at end of file diff --git a/config/config.yml.template b/config/config.yml.template index 81ca2ede..e4a58450 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -26,6 +26,8 @@ settings: # Can be individually specified dimensional_asset_rename: false download_url_assets: false show_missing_season_assets: false + show_missing_episode_assets: false + show_asset_not_needed: true sync_mode: append minimum_items: 1 default_collection_order: @@ -43,6 +45,7 @@ settings: # Can be individually specified tvdb_language: eng ignore_ids: ignore_imdb_ids: + item_refresh_delay: 0 playlist_sync_to_user: all verify_ssl: true webhooks: # Can be individually specified per library as well diff --git a/modules/builder.py b/modules/builder.py index 6e4df0d8..0acca81c 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -65,7 +65,9 @@ filter_translation = { "last_played": "lastViewedAt", "plays": "viewCount", "user_rating": "userRating", - "writer": "writers" + "writer": "writers", + "mood": "moods", + "style": "styles" } modifier_alias = {".greater": ".gt", ".less": ".lt"} all_builders = anidb.builders + anilist.builders + flixpatrol.builders + icheckmovies.builders + imdb.builders + \ @@ -93,21 +95,21 @@ string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "delete_not_scheduled", "tmdb_person", "build_collection", "collection_order", "collection_level", - "validate_builders", "sort_by", "libraries", "sync_to_users", "collection_name", "playlist_name", "name" + "validate_builders", "libraries", "sync_to_users", "collection_name", "playlist_name", "name" ] details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "changes_webhooks", "collection_mode", "minimum_items", "label", "album_sorting"] + boolean_details + scheduled_boolean + string_details collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ poster_details + background_details + summary_details + string_details item_bool_details = ["item_tmdb_season_titles", "item_assets", "revert_overlay", "item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh"] -item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay"] + item_bool_details + list(plex.item_advance_keys.keys()) +item_details = ["non_item_remove_label", "item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_refresh_delay"] + item_bool_details + list(plex.item_advance_keys.keys()) none_details = ["label.sync", "item_label.sync"] radarr_details = ["radarr_add_missing", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"] sonarr_details = [ "sonarr_add_missing", "sonarr_add_existing", "sonarr_folder", "sonarr_monitor", "sonarr_language", "sonarr_series", "sonarr_quality", "sonarr_season", "sonarr_search", "sonarr_cutoff_search", "sonarr_tag" ] -album_details = ["item_label", "item_album_sorting"] +album_details = ["non_item_remove_label", "item_label", "item_album_sorting"] filters_by_type = { "movie_show_season_episode_artist_album_track": ["title", "summary", "collection", "has_collection", "added", "last_played", "user_rating", "plays"], "movie_show_season_episode_album_track": ["year"], @@ -118,7 +120,7 @@ filters_by_type = { "movie_show_episode": ["actor", "content_rating", "audience_rating"], "movie_show_album": ["label"], "movie_episode_track": ["audio_track_title"], - "movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year"], + "movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year", "tmdb_genre"], "movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language"], "movie_artist": ["country"], "show": ["network", "first_episode_aired", "last_episode_aired"], @@ -133,12 +135,12 @@ filters = { "album": [item for check, sub in filters_by_type.items() for item in sub if "album" in check], "track": [item for check, sub in filters_by_type.items() for item in sub if "track" in check] } -tmdb_filters = ["original_language", "tmdb_vote_count", "tmdb_year", "first_episode_aired", "last_episode_aired"] +tmdb_filters = ["original_language", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "first_episode_aired", "last_episode_aired"] string_filters = ["title", "summary", "studio", "record_label", "filepath", "audio_track_title"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", - "writer", "original_language", "resolution", "audio_language", "subtitle_language" + "writer", "original_language", "resolution", "audio_language", "subtitle_language", "tmdb_genre" ] tag_modifiers = ["", ".not"] boolean_filters = ["has_collection", "has_overlay"] @@ -163,24 +165,25 @@ custom_sort_builders = [ "flixpatrol_url", "flixpatrol_demographics", "flixpatrol_popular", "flixpatrol_top", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", - "tautulli_popular", "tautulli_watched", "letterboxd_list", "icheckmovies_list", + "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list", "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" ] +episode_parts_only = ["plex_pilots"] parts_collection_valid = [ - "plex_all", "plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", "changes_webhooks" - "visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll", - "item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "imdb_list" -] + summary_details + poster_details + background_details + string_details + "plex_all", "plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", + "visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll", "changes_webhooks", + "item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "item_refresh_delay", "imdb_list" +] + episode_parts_only + summary_details + poster_details + background_details + string_details playlist_attributes = [ "filters", "name_mapping", "show_filtered", "show_missing", "save_missing", "missing_only_released", "only_filter_missing", "delete_below_minimum", "ignore_ids", "ignore_imdb_ids", "server_preroll", "changes_webhooks", "minimum_items", ] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details music_attributes = [ - "item_label", "item_assets", "item_lock_background", "item_lock_poster", "item_lock_title", - "item_refresh", "plex_search", "plex_all", "filters" + "non_item_remove_label", "item_label", "item_assets", "item_lock_background", "item_lock_poster", "item_lock_title", + "item_refresh", "item_refresh_delay", "plex_search", "plex_all", "filters" ] + details + summary_details + poster_details + background_details class CollectionBuilder: @@ -213,6 +216,8 @@ class CollectionBuilder: self.missing_movies = [] self.missing_shows = [] self.missing_parts = [] + self.added_to_radarr = [] + self.added_to_sonarr = [] self.builders = [] self.filters = [] self.tmdb_filters = [] @@ -374,10 +379,10 @@ class CollectionBuilder: for tmdb_id in util.get_int_list(self.data[methods["tmdb_person"]], "TMDb Person ID"): person = self.config.TMDb.get_person(tmdb_id) valid_names.append(person.name) - if hasattr(person, "biography") and person.biography: + if person.biography: self.summaries["tmdb_person"] = person.biography - if hasattr(person, "profile_path") and person.profile_path: - self.posters["tmdb_person"] = f"{self.config.TMDb.image_url}{person.profile_path}" + if person.profile_url: + self.posters["tmdb_person"] = person.profile_url if len(valid_names) > 0: self.details["tmdb_person"] = valid_names else: @@ -503,6 +508,8 @@ class CollectionBuilder: raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for album collections") elif not self.library.is_music and method_name in music_only_builders: raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for music libraries") + elif self.collection_level != "episode" and method_name in episode_parts_only: + raise Failed(f"{self.Type} Error: {method_final} attribute only allowed with Collection Level: episode") elif self.parts_collection and method_name not in parts_collection_valid: raise Failed(f"{self.Type} Error: {method_final} attribute not allowed with Collection Level: {self.collection_level.capitalize()}") elif self.smart and method_name in smart_invalid: @@ -563,13 +570,13 @@ class CollectionBuilder: else: logger.error(e) + if not self.server_preroll and not self.smart_url and len(self.builders) == 0: + raise Failed(f"{self.Type} Error: No builders were found") + if self.custom_sort is True and (len(self.builders) > 1 or self.builders[0][0] not in custom_sort_builders): raise Failed(f"{self.Type} Error: " + ('Playlists' if playlist else 'collection_order: custom') + (f" can only be used with a single builder per {self.type}" if len(self.builders) > 1 else f" cannot be used with {self.builders[0][0]}")) - if not self.server_preroll and not self.smart_url and len(self.builders) == 0: - raise Failed(f"{self.Type} Error: No builders were found") - if "add_missing" not in self.radarr_details: self.radarr_details["add_missing"] = self.library.Radarr.add_missing if self.library.Radarr else False if "add_existing" not in self.radarr_details: @@ -715,11 +722,9 @@ class CollectionBuilder: if method_name == "url_poster": self.posters[method_name] = method_data elif method_name == "tmdb_poster": - url_slug = self.config.TMDb.get_movie_show_or_collection(util.regex_first_int(method_data, 'TMDb ID'), self.library.is_movie).poster_path - self.posters[method_name] = f"{self.config.TMDb.image_url}{url_slug}" + self.posters[method_name] = self.config.TMDb.get_movie_show_or_collection(util.regex_first_int(method_data, 'TMDb ID'), self.library.is_movie).poster_url elif method_name == "tmdb_profile": - url_slug = self.config.TMDb.get_person(util.regex_first_int(method_data, 'TMDb Person ID')).profile_path - self.posters[method_name] = f"{self.config.TMDb.image_url}{url_slug}" + self.posters[method_name] = self.config.TMDb.get_person(util.regex_first_int(method_data, 'TMDb Person ID')).profile_url elif method_name == "tvdb_poster": self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.library.is_movie).poster_path}" elif method_name == "file_poster": @@ -732,8 +737,7 @@ class CollectionBuilder: if method_name == "url_background": self.backgrounds[method_name] = method_data elif method_name == "tmdb_background": - url_slug = self.config.TMDb.get_movie_show_or_collection(util.regex_first_int(method_data, 'TMDb ID'), self.library.is_movie).poster_path - self.backgrounds[method_name] = f"{self.config.TMDb.image_url}{url_slug}" + self.backgrounds[method_name] = self.config.TMDb.get_movie_show_or_collection(util.regex_first_int(method_data, 'TMDb ID'), self.library.is_movie).backdrop_url elif method_name == "tvdb_background": self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.library.is_movie).background_path}" elif method_name == "file_background": @@ -792,6 +796,10 @@ class CollectionBuilder: if "item_label.remove" in methods and "item_label.sync" in methods: raise Failed(f"{self.Type} Error: Cannot use item_label.remove and item_label.sync together") self.item_details[method_final] = util.get_list(method_data) if method_data else [] + elif method_name == "non_item_remove_label": + if not method_data: + raise Failed(f"{self.Type} Error: non_item_remove_label is blank") + self.item_details[method_final] = util.get_list(method_data) elif method_name in ["item_radarr_tag", "item_sonarr_tag"]: if method_name in methods and f"{method_name}.sync" in methods: raise Failed(f"{self.Type} Error: Cannot use {method_name} and {method_name}.sync together") @@ -833,6 +841,8 @@ class CollectionBuilder: raise Failed("Each Overlay can only be used once per Library") self.library.overlays.append(name) self.item_details[method_name] = name + elif method_name == "item_refresh_delay": + self.item_details[method_name] = self._parse(method_name, method_data, datatype="int", default=0, minimum=0) elif method_name in item_bool_details: if self._parse(method_name, method_data, datatype="bool", default=False): self.item_details[method_name] = True @@ -913,9 +923,9 @@ class CollectionBuilder: for dict_data, dict_methods in self._parse(method_name, method_data, datatype="dictlist"): new_dictionary = {} for search_method, search_data in dict_data.items(): - search_attr, modifier, search_final = self._split(search_method) - if search_final not in anilist.searches: - raise Failed(f"{self.Type} Error: {method_name} {search_final} attribute not supported") + search_attr, modifier = os.path.splitext(str(search_method).lower()) + if search_method not in anilist.searches: + raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported") elif search_attr == "season": new_dictionary[search_attr] = self._parse(search_attr, search_data, parent=method_name, default=current_season, options=util.seasons) if "year" not in dict_methods: @@ -924,7 +934,7 @@ class CollectionBuilder: elif search_attr == "year": new_dictionary[search_attr] = self._parse(search_attr, search_data, datatype="int", parent=method_name, default=default_year, minimum=1917, maximum=default_year + 1) elif search_data is None: - raise Failed(f"{self.Type} Error: {method_name} {search_final} attribute is blank") + raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute is blank") elif search_attr == "adult": new_dictionary[search_attr] = self._parse(search_attr, search_data, datatype="bool", parent=method_name) elif search_attr == "country": @@ -932,17 +942,17 @@ class CollectionBuilder: elif search_attr == "source": new_dictionary[search_attr] = self._parse(search_attr, search_data, options=anilist.media_source, parent=method_name) elif search_attr in ["episodes", "duration", "score", "popularity"]: - new_dictionary[search_final] = self._parse(search_final, search_data, datatype="int", parent=method_name) + new_dictionary[search_method] = self._parse(search_method, search_data, datatype="int", parent=method_name) elif search_attr in ["format", "status", "genre", "tag", "tag_category"]: - new_dictionary[search_final] = self.config.AniList.validate(search_attr.replace("_", " ").title(), self._parse(search_final, search_data)) + new_dictionary[search_method] = self.config.AniList.validate(search_attr.replace("_", " ").title(), self._parse(search_method, search_data)) elif search_attr in ["start", "end"]: - new_dictionary[search_final] = util.validate_date(search_data, f"{method_name} {search_final} attribute", return_as="%m/%d/%Y") + new_dictionary[search_method] = util.validate_date(search_data, f"{method_name} {search_method} attribute", return_as="%m/%d/%Y") elif search_attr == "min_tag_percent": new_dictionary[search_attr] = self._parse(search_attr, search_data, datatype="int", parent=method_name, minimum=0, maximum=100) elif search_attr == "search": new_dictionary[search_attr] = str(search_data) - elif search_final not in ["sort_by", "limit"]: - raise Failed(f"{self.Type} Error: {method_name} {search_final} attribute not supported") + elif search_method not in ["sort_by", "limit"]: + raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported") if len(new_dictionary) == 0: raise Failed(f"{self.Type} Error: {method_name} must have at least one valid search option") new_dictionary["sort_by"] = self._parse("sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=anilist.sort_options) @@ -1056,7 +1066,7 @@ class CollectionBuilder: })) def _plex(self, method_name, method_data): - if method_name == "plex_all": + if method_name in ["plex_all", "plex_pilots"]: self.builders.append((method_name, self.collection_level)) elif method_name in ["plex_search", "plex_collectionless"]: for dict_data, dict_methods in self._parse(method_name, method_data, datatype="dictlist"): @@ -1080,7 +1090,7 @@ class CollectionBuilder: self.builders.append((method_name, self._parse(method_name, method_data, "bool"))) def _mdblist(self, method_name, method_data): - for mdb_dict in self.config.Mdblist.validate_mdb_lists(method_data, self.language): + for mdb_dict in self.config.Mdblist.validate_mdblist_lists(method_data): self.builders.append((method_name, mdb_dict)) def _tautulli(self, method_name, method_data): @@ -1098,59 +1108,59 @@ class CollectionBuilder: for dict_data, dict_methods in self._parse(method_name, method_data, datatype="dictlist"): new_dictionary = {"limit": self._parse("limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name)} for discover_method, discover_data in dict_data.items(): - discover_attr, modifier, discover_final = self._split(discover_method) + discover_attr, modifier = os.path.splitext(str(discover_method).lower()) if discover_data is None: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute is blank") - elif discover_final not in tmdb.discover_all: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute not supported") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute is blank") + elif discover_method not in tmdb.discover_all: + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute not supported") elif self.library.is_movie and discover_attr in tmdb.discover_tv_only: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute only works for show libraries") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute only works for show libraries") elif self.library.is_show and discover_attr in tmdb.discover_movie_only: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute only works for movie libraries") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute only works for movie libraries") elif discover_attr in ["language", "region"]: regex = ("([a-z]{2})-([A-Z]{2})", "en-US") if discover_attr == "language" else ("^[A-Z]{2}$", "US") new_dictionary[discover_attr] = self._parse(discover_attr, discover_data, parent=method_name, regex=regex) - elif discover_attr == "sort_by" and self.library.is_movie: + elif discover_attr == "sort_by": options = tmdb.discover_movie_sort if self.library.is_movie else tmdb.discover_tv_sort - new_dictionary[discover_final] = self._parse(discover_attr, discover_data, parent=method_name, options=options) + new_dictionary[discover_method] = self._parse(discover_attr, discover_data, parent=method_name, options=options) elif discover_attr == "certification_country": if "certification" in dict_data or "certification.lte" in dict_data or "certification.gte" in dict_data: - new_dictionary[discover_final] = discover_data + new_dictionary[discover_method] = discover_data else: raise Failed(f"{self.Type} Error: {method_name} {discover_attr} attribute: must be used with either certification, certification.lte, or certification.gte") elif discover_attr == "certification": if "certification_country" in dict_data: - new_dictionary[discover_final] = discover_data + new_dictionary[discover_method] = discover_data else: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute: must be used with certification_country") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with certification_country") elif discover_attr == "watch_region": if "with_watch_providers" in dict_data or "without_watch_providers" in dict_data or "with_watch_monetization_types" in dict_data: - new_dictionary[discover_final] = discover_data + new_dictionary[discover_method] = discover_data else: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute: must be used with either with_watch_providers, without_watch_providers, or with_watch_monetization_types") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with either with_watch_providers, without_watch_providers, or with_watch_monetization_types") elif discover_attr == "with_watch_monetization_types": if "watch_region" in dict_data: - new_dictionary[discover_final] = self._parse(discover_attr, discover_data, parent=method_name, options=tmdb.discover_monetization_types) + new_dictionary[discover_method] = self._parse(discover_attr, discover_data, parent=method_name, options=tmdb.discover_monetization_types) else: - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute: must be used with watch_region") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with watch_region") elif discover_attr in tmdb.discover_booleans: new_dictionary[discover_attr] = self._parse(discover_attr, discover_data, datatype="bool", parent=method_name) elif discover_attr == "vote_average": - new_dictionary[discover_final] = self._parse(discover_final, discover_data, datatype="float", parent=method_name) + new_dictionary[discover_method] = self._parse(discover_method, discover_data, datatype="float", parent=method_name) elif discover_attr == "with_status": new_dictionary[discover_attr] = self._parse(discover_attr, discover_data, datatype="int", parent=method_name, minimum=0, maximum=5) elif discover_attr == "with_type": new_dictionary[discover_attr] = self._parse(discover_attr, discover_data, datatype="int", parent=method_name, minimum=0, maximum=6) - elif discover_final in tmdb.discover_dates: - new_dictionary[discover_final] = util.validate_date(discover_data, f"{method_name} {discover_final} attribute", return_as="%m/%d/%Y") + elif discover_method in tmdb.discover_dates: + new_dictionary[discover_method] = util.validate_date(discover_data, f"{method_name} {discover_method} attribute", return_as="%m/%d/%Y") elif discover_attr in tmdb.discover_years: new_dictionary[discover_attr] = self._parse(discover_attr, discover_data, datatype="int", parent=method_name, minimum=1800, maximum=self.current_year + 1) elif discover_attr in tmdb.discover_ints: - new_dictionary[discover_final] = self._parse(discover_final, discover_data, datatype="int", parent=method_name) - elif discover_final in tmdb.discover_strings: - new_dictionary[discover_final] = discover_data + new_dictionary[discover_method] = self._parse(discover_method, discover_data, datatype="int", parent=method_name) + elif discover_method in tmdb.discover_strings: + new_dictionary[discover_method] = discover_data elif discover_attr != "limit": - raise Failed(f"{self.Type} Error: {method_name} {discover_final} attribute not supported") + raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute not supported") if len(new_dictionary) > 1: self.builders.append((method_name, new_dictionary)) else: @@ -1162,21 +1172,21 @@ class CollectionBuilder: if method_name.endswith("_details"): if method_name.startswith(("tmdb_collection", "tmdb_movie", "tmdb_show")): item = self.config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie) - if hasattr(item, "overview") and item.overview: + if item.overview: self.summaries[method_name] = item.overview - if hasattr(item, "backdrop_path") and item.backdrop_path: - self.backgrounds[method_name] = f"{self.config.TMDb.image_url}{item.backdrop_path}" - if hasattr(item, "poster_path") and item.poster_path: - self.posters[method_name] = f"{self.config.TMDb.image_url}{item.poster_path}" + if item.backdrop_url: + self.backgrounds[method_name] = item.backdrop_url + if item.poster_path: + self.posters[method_name] = item.poster_url elif method_name.startswith(("tmdb_actor", "tmdb_crew", "tmdb_director", "tmdb_producer", "tmdb_writer")): item = self.config.TMDb.get_person(values[0]) - if hasattr(item, "biography") and item.biography: + if item.biography: self.summaries[method_name] = item.biography - if hasattr(item, "profile_path") and item.profile_path: - self.posters[method_name] = f"{self.config.TMDb.image_url}{item.profile_path}" + if item.profile_path: + self.posters[method_name] = item.profile_url elif method_name.startswith("tmdb_list"): item = self.config.TMDb.get_list(values[0]) - if hasattr(item, "description") and item.description: + if item.description: self.summaries[method_name] = item.description for value in values: self.builders.append((method_name[:-8] if method_name.endswith("_details") else method_name, value)) @@ -1204,12 +1214,10 @@ class CollectionBuilder: if method_name.endswith("_details"): if method_name.startswith(("tvdb_movie", "tvdb_show")): item = self.config.TVDb.get_item(values[0], method_name.startswith("tvdb_movie")) - if hasattr(item, "description") and item.description: - self.summaries[method_name] = item.description - if hasattr(item, "background_path") and item.background_path: - self.backgrounds[method_name] = f"{self.config.TMDb.image_url}{item.background_path}" - if hasattr(item, "poster_path") and item.poster_path: - self.posters[method_name] = f"{self.config.TMDb.image_url}{item.poster_path}" + if item.background_path: + self.backgrounds[method_name] = item.background_path + if item.poster_path: + self.posters[method_name] = item.poster_path elif method_name.startswith("tvdb_list"): self.summaries[method_name] = self.config.TVDb.get_list_description(values[0]) for value in values: @@ -1348,7 +1356,7 @@ class CollectionBuilder: if tvdb_id not in self.missing_shows: self.missing_shows.append(tvdb_id) except Failed as e: - logger.error(e) + logger.warning(e) elif show_id not in self.missing_shows: self.missing_shows.append(show_id) else: @@ -1366,7 +1374,7 @@ class CollectionBuilder: try: input_id = self.config.Convert.tmdb_to_tvdb(input_id, fail=True) except Failed as e: - logger.error(e) + logger.warning(e) continue if input_id not in self.ignore_ids: if input_id in self.library.show_map: @@ -1388,7 +1396,7 @@ class CollectionBuilder: if tvdb_id not in self.missing_shows: self.missing_shows.append(tvdb_id) except Failed as e: - logger.error(e) + logger.warning(e) continue if not isinstance(rating_keys, list): rating_keys = [rating_keys] @@ -1513,54 +1521,63 @@ class CollectionBuilder: display_line = f"{indent}{param_s} {mod_s} {arg_s}" return f"{arg_key}{mod}={arg}&", display_line + error = None if final_attr not in plex.searches and not final_attr.startswith(("any", "all")): - raise Failed(f"{self.Type} Error: {final_attr} is not a valid {method} attribute") + error = f"{self.Type} Error: {final_attr} is not a valid {method} attribute" elif self.library.is_show and final_attr in plex.movie_only_searches: - raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries") + error = f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries" elif self.library.is_movie and final_attr in plex.show_only_searches: - raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries") + error = f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries" elif self.library.is_music and final_attr not in plex.music_searches: - raise Failed(f"{self.Type} Error: {final_attr} {method} attribute does not work for music libraries") + error = f"{self.Type} Error: {final_attr} {method} attribute does not work for music libraries" elif not self.library.is_music and final_attr in plex.music_searches: - raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries") - elif _data is None: - raise Failed(f"{self.Type} Error: {final_attr} {method} attribute is blank") - elif final_attr.startswith(("any", "all")): - dicts = util.get_list(_data) - results = "" - display_add = "" - for dict_data in dicts: - if not isinstance(dict_data, dict): - raise Failed(f"{self.Type} Error: {attr} must be either a dictionary or list of dictionaries") - inside_filter, inside_display = _filter(dict_data, is_all=attr == "all", level=level) - if len(inside_filter) > 0: - display_add += inside_display - results += f"{conjunction if len(results) > 0 else ''}push=1&{inside_filter}pop=1&" + error = f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries" + elif _data is not False and not _data: + error = f"{self.Type} Error: {final_attr} {method} attribute is blank" else: - validation = self.validate_attribute(attr, modifier, final_attr, _data, validate, pairs=True) - if validation is None: - continue - elif attr in plex.date_attributes and modifier in ["", ".not"]: - last_text = "is not in the last" if modifier == ".not" else "is in the last" - last_mod = "%3E%3E" if modifier == "" else "%3C%3C" - results, display_add = build_url_arg(f"-{validation}d", mod=last_mod, arg_s=f"{validation} Days", mod_s=last_text) - elif attr == "duration" and modifier in [".gt", ".gte", ".lt", ".lte"]: - results, display_add = build_url_arg(validation * 60000) - elif attr in plex.boolean_attributes: - bool_mod = "" if validation else "!" - bool_arg = "true" if validation else "false" - results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") - elif (attr in plex.tag_attributes + plex.string_attributes + plex.year_attributes) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]: + if final_attr.startswith(("any", "all")): + dicts = util.get_list(_data) results = "" display_add = "" - for og_value, result in validation: - built_arg = build_url_arg(quote(str(result)) if attr in plex.string_attributes else result, arg_s=og_value) - display_add += built_arg[1] - results += f"{conjunction if len(results) > 0 else ''}{built_arg[0]}" + for dict_data in dicts: + if not isinstance(dict_data, dict): + raise Failed( + f"{self.Type} Error: {attr} must be either a dictionary or list of dictionaries") + inside_filter, inside_display = _filter(dict_data, is_all=attr == "all", level=level) + if len(inside_filter) > 0: + display_add += inside_display + results += f"{conjunction if len(results) > 0 else ''}push=1&{inside_filter}pop=1&" else: - results, display_add = build_url_arg(validation) - display += display_add - output += f"{conjunction if len(output) > 0 else ''}{results}" + validation = self.validate_attribute(attr, modifier, final_attr, _data, validate, pairs=True) + if validation is None: + continue + elif attr in plex.date_attributes and modifier in ["", ".not"]: + last_text = "is not in the last" if modifier == ".not" else "is in the last" + last_mod = "%3E%3E" if modifier == "" else "%3C%3C" + results, display_add = build_url_arg(f"-{validation}d", mod=last_mod, arg_s=f"{validation} Days", mod_s=last_text) + elif attr == "duration" and modifier in [".gt", ".gte", ".lt", ".lte"]: + results, display_add = build_url_arg(validation * 60000) + elif attr in plex.boolean_attributes: + bool_mod = "" if validation else "!" + bool_arg = "true" if validation else "false" + results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") + elif (attr in plex.tag_attributes + plex.string_attributes + plex.year_attributes) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]: + results = "" + display_add = "" + for og_value, result in validation: + built_arg = build_url_arg(quote(str(result)) if attr in plex.string_attributes else result, arg_s=og_value) + display_add += built_arg[1] + results += f"{conjunction if len(results) > 0 else ''}{built_arg[0]}" + else: + results, display_add = build_url_arg(validation) + display += display_add + output += f"{conjunction if len(output) > 0 else ''}{results}" + if error: + if validate: + raise Failed(error) + else: + logger.error(error) + continue return output, display if "any" not in filter_alias and "all" not in filter_alias: @@ -1619,7 +1636,7 @@ class CollectionBuilder: return smart_pair(util.get_list(data, split=False)) elif attribute == "original_language": return util.get_list(data, lower=True) - elif attribute == "filepath": + elif attribute in ["filepath", "tmdb_genre"]: return util.get_list(data) elif attribute == "history": try: @@ -1783,20 +1800,21 @@ class CollectionBuilder: if item is None: item = self.config.TMDb.get_movie(item_id) if is_movie else self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id)) if check_released: - if util.validate_date(item.release_date if is_movie else item.first_air_date, "") > self.current_time: + date_to_check = item.release_date if is_movie else item.first_air_date + if not date_to_check or date_to_check > self.current_time: return False for filter_method, filter_data in self.tmdb_filters: filter_attr, modifier, filter_final = self._split(filter_method) if filter_attr == "original_language": - if (modifier == ".not" and item.original_language in filter_data) \ - or (modifier == "" and item.original_language not in filter_data): + if (modifier == ".not" and item.original_language.iso_639_1 in filter_data) \ + or (modifier == "" and item.original_language.iso_639_1 not in filter_data): return False elif filter_attr in ["first_episode_aired", "last_episode_aired"]: tmdb_date = None if filter_attr == "first_episode_aired": - tmdb_date = util.validate_date(item.first_air_date, "TMDB First Air Date") + tmdb_date = item.first_air_date elif filter_attr == "last_episode_aired": - tmdb_date = util.validate_date(item.last_air_date, "TMDB Last Air Date") + tmdb_date = item.last_air_date if util.is_date_filter(tmdb_date, modifier, filter_data, filter_final, self.current_time): return False elif modifier in [".gt", ".gte", ".lt", ".lte"]: @@ -1805,12 +1823,15 @@ class CollectionBuilder: attr = item.vote_count elif filter_attr == "tmdb_year" and is_movie: attr = item.year - elif filter_attr == "tmdb_year" and not is_movie: - air_date = item.first_air_date - if air_date: - attr = util.validate_date(air_date, "TMDb Year Filter").year + elif filter_attr == "tmdb_year" and not is_movie and item.first_air_date: + attr = item.first_air_date.year if util.is_number_filter(attr, modifier, filter_data): return False + elif filter_attr == "tmdb_genre": + attrs = [g.name for g in item.genres] + if (not list(set(filter_data) & set(attrs)) and modifier == "") \ + or (list(set(filter_data) & set(attrs)) and modifier == ".not"): + return False except Failed: return False return True @@ -1944,7 +1965,7 @@ class CollectionBuilder: except Failed as e: logger.error(e) continue - current_title = f"{movie.title} ({util.validate_date(movie.release_date, 'test').year})" if movie.release_date else movie.title + current_title = f"{movie.title} ({movie.release_date.year})" if movie.release_date else movie.title if self.check_tmdb_filter(missing_id, True, item=movie, check_released=self.details["missing_only_released"]): missing_movies_with_names.append((current_title, missing_id)) if self.details["show_missing"] is True: @@ -1962,7 +1983,9 @@ class CollectionBuilder: if self.library.Radarr: if self.radarr_details["add_missing"]: try: - added_to_radarr += self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_details) + added = self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_details) + self.added_to_radarr.extend([movie.tmdbId for movie in added]) + added_to_radarr += len(added) except Failed as e: logger.error(e) if "item_radarr_tag" in self.item_details: @@ -2001,7 +2024,9 @@ class CollectionBuilder: if self.library.Sonarr: if self.sonarr_details["add_missing"]: try: - added_to_sonarr += self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_details) + added = self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_details) + self.added_to_sonarr.extend([show.tvdbId for show in added]) + added_to_sonarr += len(added) except Failed as e: logger.error(e) if "item_sonarr_tag" in self.item_details: @@ -2063,6 +2088,13 @@ class CollectionBuilder: remove_tags = self.item_details["item_label.remove"] if "item_label.remove" in self.item_details else None sync_tags = self.item_details["item_label.sync"] if "item_label.sync" in self.item_details else None + if "non_item_remove_label" in self.item_details: + rk_compare = [item.rakingKey for item in self.items] + for remove_label in self.item_details["non_item_remove_label"]: + for non_item in self.library.get_labeled_items(remove_label): + if non_item.ratingKey not in rk_compare: + self.library.edit_tags("label", non_item, remove_tags=[remove_label]) + tmdb_paths = [] tvdb_paths = [] for item in self.items: @@ -2084,20 +2116,22 @@ class CollectionBuilder: path = path[:-1] if path.endswith(('/', '\\')) else path tvdb_paths.append((self.library.show_rating_key_map[item.ratingKey], path)) advance_edits = {} - for method_name, method_data in self.item_details.items(): - if method_name in plex.item_advance_keys: - key, options = plex.item_advance_keys[method_name] - if getattr(item, key) != options[method_data]: - advance_edits[key] = options[method_data] + if hasattr(item, "preferences"): + prefs = [p.id for p in item.preferences()] + for method_name, method_data in self.item_details.items(): + if method_name in plex.item_advance_keys: + key, options = plex.item_advance_keys[method_name] + if key in prefs and getattr(item, key) != options[method_data]: + advance_edits[key] = options[method_data] self.library.edit_item(item, item.title, self.collection_level.capitalize(), advance_edits, advanced=True) if "item_tmdb_season_titles" in self.item_details and item.ratingKey in self.library.show_rating_key_map: try: tmdb_id = self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[item.ratingKey]) - names = {str(s.season_number): s.name for s in self.config.TMDb.get_show(tmdb_id).seasons} + names = {s.season_number: s.name for s in self.config.TMDb.get_show(tmdb_id).seasons} for season in self.library.query(item.seasons): - if str(season.index) in names: - self.library.edit_query(season, {"title.locked": 1, "title.value": names[str(season.index)]}) + if season.index in names: + self.library.edit_query(season, {"title.locked": 1, "title.value": names[season.index]}) except Failed as e: logger.error(e) @@ -2110,19 +2144,24 @@ class CollectionBuilder: if "item_lock_title" in self.item_details: self.library.edit_query(item, {"title.locked": 1}) if "item_refresh" in self.item_details: + delay = self.item_details["item_refresh_delay"] if "item_refresh_delay" in self.item_details else self.library.item_refresh_delay + if delay > 0: + time.sleep(delay) self.library.query(item.refresh) if self.library.Radarr and tmdb_paths: if "item_radarr_tag" in self.item_details: self.library.Radarr.edit_tags([t[0] if isinstance(t, tuple) else t for t in tmdb_paths], self.item_details["item_radarr_tag"], self.item_details["apply_tags"]) if self.radarr_details["add_existing"]: - self.library.Radarr.add_tmdb(tmdb_paths, **self.radarr_details) + added = self.library.Radarr.add_tmdb(tmdb_paths, **self.radarr_details) + self.added_to_radarr.extend([movie.tmdbId for movie in added]) if self.library.Sonarr and tvdb_paths: if "item_sonarr_tag" in self.item_details: self.library.Sonarr.edit_tags([t[0] if isinstance(t, tuple) else t for t in tvdb_paths], self.item_details["item_sonarr_tag"], self.item_details["apply_tags"]) if self.sonarr_details["add_existing"]: - self.library.Sonarr.add_tvdb(tvdb_paths, **self.sonarr_details) + added = self.library.Sonarr.add_tvdb(tvdb_paths, **self.sonarr_details) + self.added_to_sonarr.extend([show.tvdbId for show in added]) for rating_key in rating_keys: try: @@ -2400,6 +2439,8 @@ class CollectionBuilder: deleted=self.deleted, additions=self.notification_additions, removals=self.notification_removals, + radarr=self.added_to_radarr, + sonarr=self.added_to_sonarr, playlist=playlist ) except Failed as e: @@ -2413,6 +2454,8 @@ class CollectionBuilder: rating_keys = [] amount_added = 0 self.notification_additions = [] + self.added_to_radarr = [] + self.added_to_sonarr = [] for mm in self.run_again_movies: if mm in self.library.movie_map: rating_keys.extend(self.library.movie_map[mm]) @@ -2453,7 +2496,7 @@ class CollectionBuilder: logger.error(e) continue if self.details["show_missing"] is True: - current_title = f"{movie.title} ({util.validate_date(movie.release_date, 'test').year})" if movie.release_date else movie.title + current_title = f"{movie.title} ({movie.release_date.year})" if movie.release_date else movie.title logger.info(f"{name} {self.Type} | ? | {current_title} (TMDb: {missing_id})") logger.info("") logger.info(f"{len(self.run_again_movies)} Movie{'s' if len(self.run_again_movies) > 1 else ''} Missing") @@ -2471,4 +2514,4 @@ class CollectionBuilder: logger.info(f"{name} {self.Type} | ? | {title} (TVDb: {missing_id})") logger.info(f"{len(self.run_again_shows)} Show{'s' if len(self.run_again_shows) > 1 else ''} Missing") - return amount_added \ No newline at end of file + return amount_added diff --git a/modules/config.py b/modules/config.py index 26229e85..7b39024e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -54,6 +54,7 @@ class ConfigFile: self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour self.requested_collections = util.get_list(attrs["collections"]) if "collections" in attrs else None self.requested_libraries = util.get_list(attrs["libraries"]) if "libraries" in attrs else None + self.requested_metadata_files = util.get_list(attrs["metadata_files"]) if "metadata_files" in attrs else None self.resume_from = attrs["resume"] if "resume" in attrs else None yaml.YAML().allow_duplicate_keys = True @@ -126,6 +127,8 @@ class ConfigFile: temp = new_config.pop("settings") if "collection_minimum" in temp: temp["minimum_items"] = temp.pop("collection_minimum") + if "playlist_sync_to_user" in temp: + temp["playlist_sync_to_users"] = temp.pop("playlist_sync_to_user") new_config["settings"] = temp if "webhooks" in new_config: temp = new_config.pop("webhooks") @@ -266,9 +269,12 @@ class ConfigFile: "dimensional_asset_rename": check_for_attribute(self.data, "dimensional_asset_rename", parent="settings", var_type="bool", default=False), "download_url_assets": check_for_attribute(self.data, "download_url_assets", parent="settings", var_type="bool", default=False), "show_missing_season_assets": check_for_attribute(self.data, "show_missing_season_assets", parent="settings", var_type="bool", default=False), + "show_missing_episode_assets": check_for_attribute(self.data, "show_missing_episode_assets", parent="settings", var_type="bool", default=False), + "show_asset_not_needed": check_for_attribute(self.data, "show_asset_not_needed", parent="settings", var_type="bool", default=True), "sync_mode": check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=sync_modes), "default_collection_order": check_for_attribute(self.data, "default_collection_order", parent="settings", default_is_none=True), "minimum_items": check_for_attribute(self.data, "minimum_items", parent="settings", var_type="int", default=1), + "item_refresh_delay": check_for_attribute(self.data, "item_refresh_delay", parent="settings", var_type="int", default=0), "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), "delete_not_scheduled": check_for_attribute(self.data, "delete_not_scheduled", parent="settings", var_type="bool", default=False), "run_again_delay": check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0), @@ -283,10 +289,12 @@ class ConfigFile: "tvdb_language": check_for_attribute(self.data, "tvdb_language", parent="settings", default="default"), "ignore_ids": check_for_attribute(self.data, "ignore_ids", parent="settings", var_type="int_list", default_is_none=True), "ignore_imdb_ids": check_for_attribute(self.data, "ignore_imdb_ids", parent="settings", var_type="list", default_is_none=True), - "playlist_sync_to_user": check_for_attribute(self.data, "playlist_sync_to_user", parent="settings", default="all", default_is_none=True), + "playlist_sync_to_users": check_for_attribute(self.data, "playlist_sync_to_users", parent="settings", default="all", default_is_none=True), "verify_ssl": check_for_attribute(self.data, "verify_ssl", parent="settings", var_type="bool", default=True), + "custom_repo": check_for_attribute(self.data, "custom_repo", parent="settings", default_is_none=True), "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False, save=False, do_print=False) } + self.custom_repo = self.general["custom_repo"] self.session = requests.Session() if not self.general["verify_ssl"]: @@ -412,7 +420,7 @@ class ConfigFile: except Failed as e: self.errors.append(e) logger.error(e) - logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") + logger.info(f"AniDB Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") if self.AniDB is None: self.AniDB = AniDB(self, None) @@ -443,6 +451,9 @@ class ConfigFile: git = check_dict("git") if git: playlists_pairs.append(("Git", git)) + repo = check_dict("repo") + if repo: + playlists_pairs.append(("Repo", repo)) file = check_dict("file") if file: playlists_pairs.append(("File", file)) @@ -575,7 +586,10 @@ class ConfigFile: params["dimensional_asset_rename"] = check_for_attribute(lib, "dimensional_asset_rename", parent="settings", var_type="bool", default=self.general["dimensional_asset_rename"], do_print=False, save=False) params["download_url_assets"] = check_for_attribute(lib, "download_url_assets", parent="settings", var_type="bool", default=self.general["download_url_assets"], do_print=False, save=False) params["show_missing_season_assets"] = check_for_attribute(lib, "show_missing_season_assets", parent="settings", var_type="bool", default=self.general["show_missing_season_assets"], do_print=False, save=False) + params["show_missing_episode_assets"] = check_for_attribute(lib, "show_missing_episode_assets", parent="settings", var_type="bool", default=self.general["show_missing_episode_assets"], do_print=False, save=False) + params["show_asset_not_needed"] = check_for_attribute(lib, "show_asset_not_needed", parent="settings", var_type="bool", default=self.general["show_asset_not_needed"], do_print=False, save=False) params["minimum_items"] = check_for_attribute(lib, "minimum_items", parent="settings", var_type="int", default=self.general["minimum_items"], do_print=False, save=False) + params["item_refresh_delay"] = check_for_attribute(lib, "item_refresh_delay", parent="settings", var_type="int", default=self.general["item_refresh_delay"], do_print=False, save=False) params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) params["delete_not_scheduled"] = check_for_attribute(lib, "delete_not_scheduled", parent="settings", var_type="bool", default=self.general["delete_not_scheduled"], do_print=False, save=False) params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", parent="settings", var_type="bool", default=False, do_print=False, save=False) @@ -653,8 +667,14 @@ class ConfigFile: if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict): params["genre_mapper"] = {} for new_genre, old_genres in lib["operations"]["genre_mapper"].items(): - for old_genre in util.get_list(old_genres, split=False): - params["genre_mapper"][old_genre] = new_genre + if old_genres is None: + params["genre_mapper"][new_genre] = old_genres + else: + for old_genre in util.get_list(old_genres): + if old_genre == new_genre: + logger.error("Config Error: genres cannot be mapped to themselves") + else: + params["genre_mapper"][old_genre] = new_genre else: logger.error("Config Error: genre_mapper is blank") if "genre_collections" in lib["operations"]: @@ -719,6 +739,7 @@ class ConfigFile: params["metadata_path"].append((name, path[attr])) check_dict("url", "URL") check_dict("git", "Git") + check_dict("repo", "Repo") check_dict("file", "File") check_dict("folder", "Folder") else: diff --git a/modules/convert.py b/modules/convert.py index 5fd95f2f..109ec85d 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -75,9 +75,9 @@ class Convert: elif anidb_id in self.anidb_to_tvdb: ids.append((self.anidb_to_tvdb[anidb_id], "tvdb")) elif anidb_id in self.anidb_ids: - logger.error(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}") + logger.warning(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}") else: - logger.error(f"Convert Error: AniDB ID: {anidb_id} not found") + logger.warning(f"Convert Error: AniDB ID: {anidb_id} not found") return ids def anilist_to_ids(self, anilist_ids, library): @@ -86,7 +86,7 @@ class Convert: if anilist_id in self.anilist_to_anidb: anidb_ids.append(self.anilist_to_anidb[anilist_id]) else: - logger.error(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}") + logger.warning(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}") return self.anidb_to_ids(anidb_ids, library) def myanimelist_to_ids(self, mal_ids, library): @@ -97,7 +97,7 @@ class Convert: elif int(mal_id) in self.mal_to_anidb: ids.extend(self.anidb_to_ids(self.mal_to_anidb[int(mal_id)], library)) else: - logger.error(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}") + logger.warning(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}") return ids def tmdb_to_imdb(self, tmdb_id, is_movie=True, fail=False): diff --git a/modules/library.py b/modules/library.py index b63114a6..a7277140 100644 --- a/modules/library.py +++ b/modules/library.py @@ -1,4 +1,4 @@ -import logging, os, requests, shutil, time +import logging, os, shutil, time from abc import ABC, abstractmethod from modules import util from modules.meta import MetadataFile @@ -46,9 +46,12 @@ class Library(ABC): self.dimensional_asset_rename = params["dimensional_asset_rename"] self.download_url_assets = params["download_url_assets"] self.show_missing_season_assets = params["show_missing_season_assets"] + self.show_missing_episode_assets = params["show_missing_episode_assets"] + self.show_asset_not_needed = params["show_asset_not_needed"] self.sync_mode = params["sync_mode"] self.default_collection_order = params["default_collection_order"] self.minimum_items = params["minimum_items"] + self.item_refresh_delay = params["item_refresh_delay"] self.delete_below_minimum = params["delete_below_minimum"] self.delete_not_scheduled = params["delete_not_scheduled"] self.missing_only_released = params["missing_only_released"] @@ -85,12 +88,12 @@ class Library(ABC): self.stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} self.status = {} - self.tmdb_library_operation = self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ - or self.mass_critic_rating_update or self.mass_trakt_rating_update \ + self.items_library_operation = self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ + or self.mass_critic_rating_update or self.mass_trakt_rating_update or self.genre_mapper \ or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing - self.library_operation = self.tmdb_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ + self.library_operation = self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ - or self.genre_collections or self.genre_mapper or self.show_unmanaged + or self.genre_collections or self.show_unmanaged metadata = [] for file_type, metadata_file in self.metadata_path: if file_type == "Folder": @@ -143,7 +146,7 @@ class Library(ABC): self._upload_image(item, poster) poster_uploaded = True logger.info(f"Detail: {poster.attribute} updated {poster.message}") - else: + elif self.show_asset_not_needed: logger.info(f"Detail: {poster.prefix}poster update not needed") except Failed: util.print_stacktrace() @@ -193,7 +196,7 @@ class Library(ABC): self._upload_image(item, background) background_uploaded = True logger.info(f"Detail: {background.attribute} updated {background.message}") - else: + elif self.show_asset_not_needed: logger.info(f"Detail: {background.prefix}background update not needed") except Failed: util.print_stacktrace() diff --git a/modules/mdblist.py b/modules/mdblist.py index 2645bb75..934e1fd4 100644 --- a/modules/mdblist.py +++ b/modules/mdblist.py @@ -1,19 +1,20 @@ import logging from modules import util from modules.util import Failed +from urllib.parse import urlparse logger = logging.getLogger("Plex Meta Manager") builders = ["mdblist_list"] base_url = "https://mdblist.com/lists" -headers = { 'User-Agent': 'Plex-Meta-Manager' } +headers = {"User-Agent": "Plex-Meta-Manager"} class Mdblist: def __init__(self, config): self.config = config - def validate_mdb_lists(self, mdb_lists, language): + def validate_mdblist_lists(self, mdb_lists): valid_lists = [] for mdb_dict in util.get_list(mdb_lists, split=False): if not isinstance(mdb_dict, dict): @@ -49,7 +50,9 @@ class Mdblist: if method == "mdblist_list": limit_status = f" Limit at: {data['limit']} items" if data['limit'] > 0 else '' logger.info(f"Processing Mdblist.com List: {data['url']}{limit_status}") - url = f"{data['url']}?limit={data['limit']}" - return [(i["imdb_id"], "imdb") for i in self.config.get_json(url,headers=headers)] + parsed_url = urlparse(data["url"]) + url_base = parsed_url._replace(query=None).geturl() + params = {"limit": data["limit"]} if data["limit"] > 0 else None + return [(i["imdb_id"], "imdb") for i in self.config.get_json(url_base, headers=headers, params=params)] else: raise Failed(f"Mdblist Error: Method {method} not supported") diff --git a/modules/meta.py b/modules/meta.py index 99953b08..59461d7a 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -31,18 +31,18 @@ def get_dict(attribute, attr_data, check_list=None): new_dict = {} for _name, _data in attr_data[attribute].items(): if _name in check_list: - logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}") + logger.warning(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}") elif _data is None: - logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data") + logger.error(f"Config Error: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data") elif not isinstance(_data, dict): - logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary") + logger.error(f"Config Error: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary") else: new_dict[str(_name)] = _data return new_dict else: - logger.warning(f"Config Warning: {attribute} must be a dictionary") + logger.error(f"Config Error: {attribute} must be a dictionary") else: - logger.warning(f"Config Warning: {attribute} attribute is blank") + logger.error(f"Config Error: {attribute} attribute is blank") return None @@ -54,10 +54,21 @@ class DataFile: self.data_type = "" self.templates = {} + def get_file_name(self): + data = f"{github_base}{self.path}.yml" if self.type == "GIT" else self.path + if "/" in data: + return data[data.rfind("/") + 1:-4] + elif "\\" in data: + return data[data.rfind("\\") + 1:-4] + else: + return data + def load_file(self): try: - if self.type in ["URL", "Git"]: - content_path = self.path if self.type == "URL" else f"{github_base}{self.path}.yml" + if self.type in ["URL", "Git", "Repo"]: + if self.type == "Repo" and not self.config.custom_repo: + raise Failed("Config Error: No custom_repo defined") + content_path = self.path if self.type == "URL" else f"{self.config.custom_repo if self.type == 'Repo' else github_base}{self.path}.yml" response = self.config.get(content_path) if response.status_code >= 400: raise Failed(f"URL Error: No file found at {content_path}") @@ -227,11 +238,11 @@ class MetadataFile(DataFile): logger.info("") logger.info(f"Loading Metadata {file_type}: {path}") data = self.load_file() - self.metadata = get_dict("metadata", data, library.metadatas) + self.metadata = get_dict("metadata", data, library.metadata_files) self.templates = get_dict("templates", data) self.collections = get_dict("collections", data, library.collections) - if self.metadata is None and self.collections is None: + if not self.metadata and not self.collections: raise Failed("YAML Error: metadata or collections attribute is required") logger.info(f"Metadata File Loaded Successfully") @@ -294,7 +305,6 @@ class MetadataFile(DataFile): updated = False edits = {} - advance_edits = {} def add_edit(name, current_item, group, alias, key=None, value=None, var_type="str"): if value or name in alias: @@ -334,21 +344,6 @@ class MetadataFile(DataFile): else: logger.error(f"Metadata Error: {name} attribute is blank") - def add_advanced_edit(attr, obj, group, alias, new_agent=False): - key, options = plex.item_advance_keys[f"item_{attr}"] - if attr in alias: - if new_agent and self.library.agent not in plex.new_plex_agents: - logger.error(f"Metadata Error: {attr} attribute only works for with the New Plex Movie Agent and New Plex TV Agent") - elif group[alias[attr]]: - method_data = str(group[alias[attr]]).lower() - if method_data not in options: - logger.error(f"Metadata Error: {group[alias[attr]]} {attr} attribute invalid") - elif getattr(obj, key) != options[method_data]: - advance_edits[key] = options[method_data] - logger.info(f"Detail: {attr} updated to {method_data}") - else: - logger.error(f"Metadata Error: {attr} attribute is blank") - logger.info("") util.separator() logger.info("") @@ -423,15 +418,15 @@ class MetadataFile(DataFile): summary = None genres = [] if tmdb_item: - originally_available = tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date - if tmdb_item and tmdb_is_movie is True and tmdb_item.original_title != tmdb_item.title: + originally_available = datetime.strftime(tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date, "%Y-%m-%d") + if tmdb_is_movie and tmdb_item.original_title != tmdb_item.title: original_title = tmdb_item.original_title - elif tmdb_item and tmdb_is_movie is False and tmdb_item.original_name != tmdb_item.name: + elif not tmdb_is_movie and tmdb_item.original_name != tmdb_item.name: original_title = tmdb_item.original_name rating = tmdb_item.vote_average - if tmdb_is_movie is True and tmdb_item.production_companies: + if tmdb_is_movie and tmdb_item.production_companies: studio = tmdb_item.production_companies[0].name - elif tmdb_is_movie is False and tmdb_item.networks: + elif not tmdb_is_movie and tmdb_item.networks: studio = tmdb_item.networks[0].name tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None summary = tmdb_item.overview @@ -454,9 +449,21 @@ class MetadataFile(DataFile): updated = True advance_edits = {} + prefs = [p.id for p in item.preferences()] for advance_edit in advance_tags_to_edit[self.library.type]: - is_new_agent = advance_edit in ["metadata_language", "use_original_title"] - add_advanced_edit(advance_edit, item, meta, methods, new_agent=is_new_agent) + key, options = plex.item_advance_keys[f"item_{advance_edit}"] + if advance_edit in methods: + if advance_edit in ["metadata_language", "use_original_title"] and self.library.agent not in plex.new_plex_agents: + logger.error(f"Metadata Error: {advance_edit} attribute only works for with the New Plex Movie Agent and New Plex TV Agent") + elif meta[methods[advance_edit]]: + method_data = str(meta[methods[advance_edit]]).lower() + if method_data not in options: + logger.error(f"Metadata Error: {meta[methods[advance_edit]]} {advance_edit} attribute invalid") + elif key in prefs and getattr(item, key) != options[method_data]: + advance_edits[key] = options[method_data] + logger.info(f"Detail: {advance_edit} updated to {method_data}") + else: + logger.error(f"Metadata Error: {advance_edit} attribute is blank") if self.library.edit_item(item, mapping_name, self.library.type, advance_edits, advanced=True): updated = True @@ -474,16 +481,17 @@ class MetadataFile(DataFile): elif not isinstance(meta[methods["seasons"]], dict): logger.error("Metadata Error: seasons attribute must be a dictionary") else: + seasons = {} + for season in item.seasons(): + seasons[season.title] = season + seasons[int(season.index)] = season for season_id, season_dict in meta[methods["seasons"]].items(): updated = False logger.info("") logger.info(f"Updating season {season_id} of {mapping_name}...") - try: - if isinstance(season_id, int): - season = item.season(season=season_id) - else: - season = item.season(title=season_id) - except NotFound: + if season_id in seasons: + season = seasons[season_id] + else: logger.error(f"Metadata Error: Season: {season_id} not found") continue season_methods = {sm.lower(): sm for sm in season_dict} @@ -516,16 +524,17 @@ class MetadataFile(DataFile): elif not isinstance(season_dict[season_methods["episodes"]], dict): logger.error("Metadata Error: episodes attribute must be a dictionary") else: + episodes = {} + for episode in season.episodes(): + episodes[episode.title] = episode + episodes[int(episode.index)] = episode for episode_str, episode_dict in season_dict[season_methods["episodes"]].items(): updated = False logger.info("") logger.info(f"Updating episode {episode_str} in {season_id} of {mapping_name}...") - try: - if isinstance(episode_str, int): - episode = season.episode(episode=episode_str) - else: - episode = season.episode(title=episode_str) - except NotFound: + if episode_str in episodes: + episode = episodes[episode_str] + else: logger.error(f"Metadata Error: Episode {episode_str} in Season {season_id} not found") continue episode_methods = {em.lower(): em for em in episode_dict} @@ -614,24 +623,21 @@ class MetadataFile(DataFile): elif not isinstance(meta[methods["albums"]], dict): logger.error("Metadata Error: albums attribute must be a dictionary") else: + albums = {album.title: album for album in item.albums()} for album_name, album_dict in meta[methods["albums"]].items(): updated = False title = None album_methods = {am.lower(): am for am in album_dict} logger.info("") logger.info(f"Updating album {album_name} of {mapping_name}...") - try: - album = item.album(album_name) - except NotFound: - try: - if "alt_title" not in album_methods or not album_dict[album_methods["alt_title"]]: - raise NotFound - album = item.album(album_dict[album_methods["alt_title"]]) - title = album_name - except NotFound: - logger.error(f"Metadata Error: Album: {album_name} not found") - continue - + if album_name in albums: + album = albums[album_name] + elif "alt_title" in album_methods and album_dict[album_methods["alt_title"]] and album_dict[album_methods["alt_title"]] in albums: + album = albums[album_dict[album_methods["alt_title"]]] + title = album_name + else: + logger.error(f"Metadata Error: Album: {album_name} not found") + continue if not title: title = album.title edits = {} @@ -655,26 +661,24 @@ class MetadataFile(DataFile): elif not isinstance(album_dict[album_methods["tracks"]], dict): logger.error("Metadata Error: tracks attribute must be a dictionary") else: + tracks = {} + for track in album.tracks(): + tracks[track.title] = track + tracks[int(track.index)] = track for track_num, track_dict in album_dict[album_methods["tracks"]].items(): updated = False title = None track_methods = {tm.lower(): tm for tm in track_dict} logger.info("") logger.info(f"Updating track {track_num} on {album_name} of {mapping_name}...") - try: - if isinstance(track_num, int): - track = album.track(track=track_num) - else: - track = album.track(title=track_num) - except NotFound: - try: - if "alt_title" not in track_methods or not track_dict[track_methods["alt_title"]]: - raise NotFound - track = album.track(title=track_dict[track_methods["alt_title"]]) - title = track_num - except NotFound: - logger.error(f"Metadata Error: Track: {track_num} not found") - continue + if track_num in tracks: + track = tracks[track_num] + elif "alt_title" in track_methods and track_dict[track_methods["alt_title"]] and track_dict[track_methods["alt_title"]] in tracks: + track = tracks[track_dict[track_methods["alt_title"]]] + title = track_num + else: + logger.error(f"Metadata Error: Track: {track_num} not found") + continue if not title: title = track.title @@ -684,7 +688,7 @@ class MetadataFile(DataFile): add_edit("track", track, track_dict, track_methods, key="index", var_type="int") add_edit("disc", track, track_dict, track_methods, key="parentIndex", var_type="int") add_edit("original_artist", track, track_dict, track_methods, key="originalTitle") - if self.library.edit_item(album, title, "Track", edits): + if self.library.edit_item(track, title, "Track", edits): updated = True if self.edit_tags("mood", track, track_dict, track_methods): updated = True diff --git a/modules/plex.py b/modules/plex.py index 64d3abdd..cdd90499 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -4,6 +4,7 @@ from modules.library import Library from modules.util import Failed, ImageData from PIL import Image from plexapi import utils +from plexapi.audio import Artist from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.collection import Collection from plexapi.playlist import Playlist @@ -15,7 +16,7 @@ from xml.etree.ElementTree import ParseError logger = logging.getLogger("Plex Meta Manager") -builders = ["plex_all", "plex_collectionless", "plex_search"] +builders = ["plex_all", "plex_pilots", "plex_collectionless", "plex_search"] search_translation = { "episode_title": "episode.title", "network": "show.network", @@ -245,7 +246,7 @@ show_only_searches = [ ] string_attributes = ["title", "studio", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"] float_attributes = [ - "user_rating", "episode_user_rating", "critic_rating", "audience_rating", + "user_rating", "episode_user_rating", "critic_rating", "audience_rating", "duration", "artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating" ] boolean_attributes = [ @@ -259,7 +260,7 @@ date_attributes = [ "album_added", "album_released", "track_last_played", "track_last_skipped", "track_last_rated", "track_added" ] year_attributes = ["decade", "year", "episode_year", "album_year", "album_decade"] -number_attributes = ["plays", "episode_plays", "duration", "tmdb_vote_count", "album_plays", "track_plays", "track_skips"] + year_attributes +number_attributes = ["plays", "episode_plays", "tmdb_vote_count", "album_plays", "track_plays", "track_skips"] + year_attributes search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"} tag_attributes = [ "actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", @@ -552,15 +553,16 @@ class Plex(Library): try: names = [] choices = {} + use_title = title and final_search not in ["contentRating", "audioLanguage", "subtitleLanguage", "resolution"] for choice in self.Plex.listFilterChoices(final_search): if choice.title not in names: names.append(choice.title) if choice.key not in names: names.append(choice.key) - choices[choice.title] = choice.title if title else choice.key - choices[choice.key] = choice.title if title else choice.key - choices[choice.title.lower()] = choice.title if title else choice.key - choices[choice.key.lower()] = choice.title if title else choice.key + choices[choice.title] = choice.title if use_title else choice.key + choices[choice.key] = choice.title if use_title else choice.key + choices[choice.title.lower()] = choice.title if use_title else choice.key + choices[choice.key.lower()] = choice.title if use_title else choice.key return choices, names except NotFound: logger.debug(f"Search Attribute: {final_search}") @@ -699,6 +701,14 @@ class Plex(Library): if method == "plex_all": logger.info(f"Processing Plex All {data.capitalize()}s") items = self.get_all(collection_level=data) + elif method == "plex_pilots": + logger.info(f"Processing Plex Pilot {data.capitalize()}s") + items = [] + for item in self.get_all(): + try: + items.append(item.episode(season=1, episode=1)) + except NotFound: + logger.warning(f"Plex Warning: {item.title} has no Season 1 Episode 1 ") elif method == "plex_search": util.print_multiline(data[1], info=True) items = self.get_filter_items(data[2]) @@ -826,9 +836,9 @@ class Plex(Library): def find_assets(self, item, name=None, upload=True, overlay=None, folders=None, create=None): if isinstance(item, Movie): name = os.path.basename(os.path.dirname(str(item.locations[0]))) - elif isinstance(item, Show): + elif isinstance(item, (Artist, Show)): name = os.path.basename(str(item.locations[0])) - elif isinstance(item, Collection): + elif isinstance(item, (Collection, Playlist)): name = name if name else item.title else: return None, None, None @@ -894,7 +904,7 @@ class Plex(Library): return poster, background, item_dir if isinstance(item, Show): missing_assets = "" - found_season = False + found_image = False for season in self.query(item.seasons): season_name = f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}" if item_dir: @@ -908,8 +918,8 @@ class Plex(Library): matches = util.glob_filter(season_poster_filter) if len(matches) > 0: season_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Season {season.seasonNumber}'s ", is_url=False) - found_season = True - elif season.seasonNumber > 0: + found_image = True + elif self.show_missing_season_assets and season.seasonNumber > 0: missing_assets += f"\nMissing Season {season.seasonNumber} Poster" matches = util.glob_filter(season_background_filter) if len(matches) > 0: @@ -924,9 +934,38 @@ class Plex(Library): matches = util.glob_filter(episode_filter) if len(matches) > 0: episode_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} {episode.seasonEpisode.upper()}'s ", is_url=False) + found_image = True self.upload_images(episode, poster=episode_poster) - if self.show_missing_season_assets and found_season and missing_assets: - util.print_multiline(f"Missing Season Posters for {item.title}{missing_assets}", info=True) + elif self.show_missing_episode_assets: + missing_assets += f"\nMissing {episode.seasonEpisode.upper()} Title Card" + + if found_image and missing_assets: + util.print_multiline(f"Missing Posters for {item.title}{missing_assets}", info=True) + if isinstance(item, Artist): + missing_assets = "" + found_album = False + for album in self.query(item.albums): + if item_dir: + album_poster_filter = os.path.join(item_dir, f"{album.title}.*") + album_background_filter = os.path.join(item_dir, f"{album.title}_background.*") + else: + album_poster_filter = os.path.join(ad, f"{name}_{album.title}.*") + album_background_filter = os.path.join(ad, f"{name}_{album.title}_background.*") + album_poster = None + album_background = None + matches = util.glob_filter(album_poster_filter) + if len(matches) > 0: + album_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Album {album.title}'s ", is_url=False) + found_album = True + else: + missing_assets += f"\nMissing Album {album.title} Poster" + matches = util.glob_filter(album_background_filter) + if len(matches) > 0: + album_background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Album {album.title}'s ", is_poster=False, is_url=False) + if album_poster or album_background: + self.upload_images(album, poster=album_poster, background=album_background) + if self.show_missing_season_assets and found_album and missing_assets: + util.print_multiline(f"Missing Album Posters for {item.title}{missing_assets}", info=True) if isinstance(item, (Movie, Show)) and not poster and overlay: self.upload_images(item, overlay=overlay) diff --git a/modules/radarr.py b/modules/radarr.py index b026f424..940fc37e 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -170,7 +170,7 @@ class Radarr: logger.info(f"Invalid Root Folder for TMDb ID | {tmdb_id:<7} | {path}") logger.info(f"{len(invalid_root)} Movie{'s' if len(invalid_root) > 1 else ''} with Invalid Paths") - return len(added) + return added def edit_tags(self, tmdb_ids, tags, apply_tags): logger.info("") diff --git a/modules/sonarr.py b/modules/sonarr.py index 2e8e1c75..a959cdb2 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -196,7 +196,7 @@ class Sonarr: logger.info(f"Invalid Root Folder for TVDb ID | {tvdb_id:<7} | {path}") logger.info(f"{len(invalid_root)} Series with Invalid Paths") - return len(added) + return added def edit_tags(self, tvdb_ids, tags, apply_tags): logger.info("") diff --git a/modules/tmdb.py b/modules/tmdb.py index 8c0ec53c..61062d49 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -1,8 +1,7 @@ -import logging, tmdbv3api +import logging from modules import util from modules.util import Failed -from retrying import retry -from tmdbv3api.exceptions import TMDbException +from tmdbapis import TMDbAPIs, TMDbException, NotFound logger = logging.getLogger("Plex Meta Manager") @@ -62,61 +61,42 @@ discover_monetization_types = ["flatrate", "free", "ads", "rent", "buy"] class TMDb: def __init__(self, config, params): self.config = config - self.TMDb = tmdbv3api.TMDb(session=self.config.session) - self.TMDb.api_key = params["apikey"] - self.TMDb.language = params["language"] - try: - response = tmdbv3api.Configuration().info() - if hasattr(response, "status_message"): - raise Failed(f"TMDb Error: {response.status_message}") - except TMDbException as e: - raise Failed(f"TMDb Error: {e}") self.apikey = params["apikey"] self.language = params["language"] - self.Movie = tmdbv3api.Movie() - self.TV = tmdbv3api.TV() - self.Discover = tmdbv3api.Discover() - self.Trending = tmdbv3api.Trending() - self.Keyword = tmdbv3api.Keyword() - self.List = tmdbv3api.List() - self.Company = tmdbv3api.Company() - self.Network = tmdbv3api.Network() - self.Collection = tmdbv3api.Collection() - self.Person = tmdbv3api.Person() - self.image_url = "https://image.tmdb.org/t/p/original" - - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def convert_from(self, tmdb_id, convert_to, is_movie): try: - id_to_return = self.Movie.external_ids(tmdb_id)[convert_to] if is_movie else self.TV.external_ids(tmdb_id)[convert_to] - if not id_to_return or (convert_to == "tvdb_id" and id_to_return == 0): - raise Failed(f"TMDb Error: No {convert_to.upper().replace('B_', 'b ')} found for TMDb ID {tmdb_id}") - return id_to_return if convert_to == "imdb_id" else int(id_to_return) - except TMDbException: - raise Failed(f"TMDb Error: TMDb {'Movie' if is_movie else 'Show'} ID: {tmdb_id} not found") + self.TMDb = TMDbAPIs(self.apikey, language=self.language, session=self.config.session) + except TMDbException as e: + raise Failed(f"TMDb Error: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def convert_to(self, external_id, external_source): - return self.Movie.external(external_id=external_id, external_source=external_source) + def convert_from(self, tmdb_id, convert_to, is_movie): + item = self.get_movie(tmdb_id) if is_movie else self.get_show(tmdb_id) + check_id = item.tvdb_id if convert_to == "tvdb_id" and not is_movie else item.imdb_id + if not check_id: + raise Failed(f"TMDb Error: No {convert_to.upper().replace('B_', 'b ')} found for TMDb ID {tmdb_id}") + return check_id def convert_tvdb_to(self, tvdb_id): - search = self.convert_to(tvdb_id, "tvdb_id") - if len(search["tv_results"]) == 1: - return int(search["tv_results"][0]["id"]) - else: - raise Failed(f"TMDb Error: No TMDb ID found for TVDb ID {tvdb_id}") + try: + results = self.TMDb.find_by_id(tvdb_id=tvdb_id) + if results.tv_results: + return results.tv_results[0].id + except NotFound: + pass + raise Failed(f"TMDb Error: No TMDb ID found for TVDb ID {tvdb_id}") def convert_imdb_to(self, imdb_id): - search = self.convert_to(imdb_id, "imdb_id") - if len(search["movie_results"]) > 0: - return int(search["movie_results"][0]["id"]), "movie" - elif len(search["tv_results"]) > 0: - return int(search["tv_results"][0]["id"]), "show" - elif len(search["tv_episode_results"]) > 0: - item = search['tv_episode_results'][0] - return f"{item['show_id']}_{item['season_number']}_{item['episode_number']}", "episode" - else: - raise Failed(f"TMDb Error: No TMDb ID found for IMDb ID {imdb_id}") + try: + results = self.TMDb.find_by_id(imdb_id=imdb_id) + if results.movie_results: + return results.movie_results[0].id, "movie" + elif results.tv_results: + return results.tv_results[0].id, "show" + elif results.tv_episode_results: + item = results.tv_episode_results[0] + return f"{item.tv_id}_{item.season_number}_{item.episode_number}", "episode" + except NotFound: + pass + raise Failed(f"TMDb Error: No TMDb ID found for IMDb ID {imdb_id}") def get_movie_show_or_collection(self, tmdb_id, is_movie): if is_movie: @@ -126,112 +106,38 @@ class TMDb: except Failed: raise Failed(f"TMDb Error: No Movie or Collection found for TMDb ID {tmdb_id}") else: return self.get_show(tmdb_id) - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_movie(self, tmdb_id): - try: return self.Movie.details(tmdb_id) + try: return self.TMDb.movie(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Movie found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_show(self, tmdb_id): - try: return self.TV.details(tmdb_id) + try: return self.TMDb.tv_show(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_collection(self, tmdb_id): - try: return self.Collection.details(tmdb_id) + try: return self.TMDb.collection(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Collection found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_person(self, tmdb_id): - try: return self.Person.details(tmdb_id) + try: return self.TMDb.person(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Person found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def _person_credits(self, tmdb_id): - try: return self.Person.combined_credits(tmdb_id) - except TMDbException as e: raise Failed(f"TMDb Error: No Person found for TMDb ID {tmdb_id}: {e}") - - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def _company(self, tmdb_id): - try: return self.Company.details(tmdb_id) + try: return self.TMDb.company(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Company found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def _network(self, tmdb_id): - try: return self.Network.details(tmdb_id) + try: return self.TMDb.network(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Network found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def _keyword(self, tmdb_id): - try: return self.Keyword.details(tmdb_id) + try: return self.TMDb.keyword(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Keyword found for TMDb ID {tmdb_id}: {e}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_list(self, tmdb_id): - try: return self.List.details(tmdb_id, all_details=True) + try: return self.TMDb.list(tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No List found for TMDb ID {tmdb_id}: {e}") - def _credits(self, tmdb_id, actor=False, crew=False, director=False, producer=False, writer=False): - ids = [] - actor_credits = self._person_credits(tmdb_id) - if actor: - for credit in actor_credits.cast: - if credit.media_type == "movie": - ids.append((credit.id, "tmdb")) - elif credit.media_type == "tv": - ids.append((credit.id, "tmdb_show")) - for credit in actor_credits.crew: - if crew or \ - (director and credit.department == "Directing") or \ - (producer and credit.department == "Production") or \ - (writer and credit.department == "Writing"): - if credit.media_type == "movie": - ids.append((credit.id, "tmdb")) - elif credit.media_type == "tv": - ids.append((credit.id, "tmdb_show")) - return ids - - def _pagenation(self, method, amount, is_movie): - ids = [] - for x in range(int(amount / 20) + 1): - if method == "tmdb_popular": tmdb_items = self.Movie.popular(x + 1) if is_movie else self.TV.popular(x + 1) - elif method == "tmdb_top_rated": tmdb_items = self.Movie.top_rated(x + 1) if is_movie else self.TV.top_rated(x + 1) - elif method == "tmdb_now_playing" and is_movie: tmdb_items = self.Movie.now_playing(x + 1) - elif method == "tmdb_trending_daily": tmdb_items = self.Trending.movie_day(x + 1) if is_movie else self.Trending.tv_day(x + 1) - elif method == "tmdb_trending_weekly": tmdb_items = self.Trending.movie_week(x + 1) if is_movie else self.Trending.tv_week(x + 1) - else: raise Failed(f"TMDb Error: {method} method not supported") - for tmdb_item in tmdb_items: - try: - ids.append((tmdb_item.id, "tmdb" if is_movie else "tmdb_show")) - except Failed as e: - logger.error(e) - if len(ids) == amount: break - if len(ids) == amount: break - return ids - - def _discover(self, attrs, amount, is_movie): - ids = [] - for date_attr in discover_dates: - if date_attr in attrs: - attrs[date_attr] = util.validate_date(attrs[date_attr], f"tmdb_discover attribute {date_attr}", return_as="%Y-%m-%d") - if self.config.trace_mode: - logger.debug(f"Params: {attrs}") - self.Discover.discover_movies(attrs) if is_movie else self.Discover.discover_tv_shows(attrs) - total_pages = int(self.TMDb.total_pages) - total_results = int(self.TMDb.total_results) - amount = total_results if amount == 0 or total_results < amount else amount - for x in range(total_pages): - attrs["page"] = x + 1 - tmdb_items = self.Discover.discover_movies(attrs) if is_movie else self.Discover.discover_tv_shows(attrs) - for tmdb_item in tmdb_items: - try: - ids.append((tmdb_item.id, "tmdb" if is_movie else "tmdb_show")) - except Failed as e: - logger.error(e) - if len(ids) == amount: break - if len(ids) == amount: break - return ids, amount - def validate_tmdb_ids(self, tmdb_ids, tmdb_method): tmdb_list = util.get_int_list(tmdb_ids, f"TMDb {type_map[tmdb_method]} ID") tmdb_values = [] @@ -249,74 +155,86 @@ class TMDb: elif tmdb_type == "Person": self.get_person(tmdb_id) elif tmdb_type == "Company": self._company(tmdb_id) elif tmdb_type == "Network": self._network(tmdb_id) + elif tmdb_type == "Keyword": self._keyword(tmdb_id) elif tmdb_type == "List": self.get_list(tmdb_id) return tmdb_id def get_tmdb_ids(self, method, data, is_movie): pretty = method.replace("_", " ").title().replace("Tmdb", "TMDb") media_type = "Movie" if is_movie else "Show" + result_type = "tmdb" if is_movie else "tmdb_show" ids = [] - if method in ["tmdb_discover", "tmdb_company", "tmdb_keyword"] or (method == "tmdb_network" and not is_movie): - attrs = None - tmdb_id = "" - tmdb_name = "" - if method in ["tmdb_company", "tmdb_network", "tmdb_keyword"]: - tmdb_id = int(data) - if method == "tmdb_company": - tmdb_name = str(self._company(tmdb_id).name) - attrs = {"with_companies": tmdb_id} - elif method == "tmdb_network": - tmdb_name = str(self._network(tmdb_id).name) - attrs = {"with_networks": tmdb_id} - elif method == "tmdb_keyword": - tmdb_name = str(self._keyword(tmdb_id).name) - attrs = {"with_keywords": tmdb_id} - limit = 0 + if method in ["tmdb_network", "tmdb_company", "tmdb_keyword"]: + if method == "tmdb_company": + item = self._company(int(data)) + elif method == "tmdb_network": + item = self._network(int(data)) else: - attrs = data.copy() - limit = int(attrs.pop("limit")) - ids, amount = self._discover(attrs, limit, is_movie) - if method in ["tmdb_company", "tmdb_network", "tmdb_keyword"]: - logger.info(f"Processing {pretty}: ({tmdb_id}) {tmdb_name} ({amount} {media_type}{'' if amount == 1 else 's'})") - elif method == "tmdb_discover": - logger.info(f"Processing {pretty}: {amount} {media_type}{'' if amount == 1 else 's'}") - for attr, value in attrs.items(): - logger.info(f" {attr}: {value}") + item = self._keyword(int(data)) + results = item.movies if is_movie else item.tv_shows + ids = [(i.id, result_type) for i in results.get_results(results.total_results)] + logger.info(f"Processing {pretty}: ({data}) {item.name} ({len(results)} {media_type}{'' if len(results) == 1 else 's'})") + elif method == "tmdb_discover": + attrs = data.copy() + limit = int(attrs.pop("limit")) + for date_attr in discover_dates: + if date_attr in attrs: + attrs[date_attr] = util.validate_date(attrs[date_attr], f"tmdb_discover attribute {date_attr}", return_as="%Y-%m-%d") + if self.config.trace_mode: + logger.debug(f"Params: {attrs}") + results = self.TMDb.discover_movies(**attrs) if is_movie else self.TMDb.discover_tv_shows(**attrs) + amount = results.total_results if limit == 0 or results.total_results < limit else limit + ids = [(i.id, result_type) for i in results.get_results(amount)] + logger.info(f"Processing {pretty}: {amount} {media_type}{'' if amount == 1 else 's'}") + for attr, value in attrs.items(): + logger.info(f" {attr}: {value}") elif method in ["tmdb_popular", "tmdb_top_rated", "tmdb_now_playing", "tmdb_trending_daily", "tmdb_trending_weekly"]: - ids = self._pagenation(method, data, is_movie) + if method == "tmdb_popular": + results = self.TMDb.popular_movies() if is_movie else self.TMDb.popular_tv() + elif method == "tmdb_top_rated": + results = self.TMDb.top_rated_movies() if is_movie else self.TMDb.top_rated_tv() + elif method == "tmdb_now_playing": + results = self.TMDb.now_playing_movies() + else: + results = self.TMDb.trending("movie" if is_movie else "tv", "day" if method == "tmdb_trending_daily" else "week") + ids = [(i.id, result_type) for i in results.get_results(data)] logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") else: tmdb_id = int(data) if method == "tmdb_list": - tmdb_list = self.get_list(tmdb_id) - tmdb_name = tmdb_list.name - for tmdb_item in tmdb_list.items: - if tmdb_item.media_type == "movie": - ids.append((tmdb_item.id, "tmdb")) - elif tmdb_item.media_type == "tv": - try: - ids.append((tmdb_item.id, "tmdb_show")) - except Failed: - pass + results = self.get_list(tmdb_id) + tmdb_name = results.name + ids = [(i.id, result_type) for i in results.get_results(results.total_results)] elif method == "tmdb_movie": - tmdb_name = str(self.get_movie(tmdb_id).title) + tmdb_name = self.get_movie(tmdb_id).title ids.append((tmdb_id, "tmdb")) elif method == "tmdb_collection": - tmdb_items = self.get_collection(tmdb_id) - tmdb_name = str(tmdb_items.name) - for tmdb_item in tmdb_items.parts: - ids.append((tmdb_item["id"], "tmdb")) + collection = self.get_collection(tmdb_id) + tmdb_name = collection.name + ids = [(t.id, "tmdb") for t in collection.movies] elif method == "tmdb_show": - tmdb_name = str(self.get_show(tmdb_id).name) + tmdb_name = self.get_show(tmdb_id).name ids.append((tmdb_id, "tmdb_show")) else: - tmdb_name = str(self.get_person(tmdb_id).name) - if method == "tmdb_actor": ids = self._credits(tmdb_id, actor=True) - elif method == "tmdb_director": ids = self._credits(tmdb_id, director=True) - elif method == "tmdb_producer": ids = self._credits(tmdb_id, producer=True) - elif method == "tmdb_writer": ids = self._credits(tmdb_id, writer=True) - elif method == "tmdb_crew": ids = self._credits(tmdb_id, crew=True) - else: raise Failed(f"TMDb Error: Method {method} not supported") + person = self.get_person(tmdb_id) + tmdb_name = person.name + if method == "tmdb_actor": + ids = [(i.movie.id, "tmdb") for i in person.movie_cast] + ids.extend([(i.tv_show.id, "tmdb_show") for i in person.tv_cast]) + elif method == "tmdb_crew": + ids = [(i.movie.id, "tmdb") for i in person.movie_crew] + ids.extend([(i.tv_show.id, "tmdb_show") for i in person.tv_crew]) + elif method == "tmdb_director": + ids = [(i.movie.id, "tmdb") for i in person.movie_crew if i.department == "Directing"] + ids.extend([(i.tv_show.id, "tmdb_show") for i in person.tv_crew if i.department == "Directing"]) + elif method == "tmdb_writer": + ids = [(i.movie.id, "tmdb") for i in person.movie_crew if i.department == "Writing"] + ids.extend([(i.tv_show.id, "tmdb_show") for i in person.tv_crew if i.department == "Writing"]) + elif method == "tmdb_producer": + ids = [(i.movie.id, "tmdb") for i in person.movie_crew if i.department == "Production"] + ids.extend([(i.tv_show.id, "tmdb_show") for i in person.tv_crew if i.department == "Production"]) + else: + raise Failed(f"TMDb Error: Method {method} not supported") if len(ids) > 0: logger.info(f"Processing {pretty}: ({tmdb_id}) {tmdb_name} ({len(ids)} Item{'' if len(ids) == 1 else 's'})") return ids diff --git a/modules/webhooks.py b/modules/webhooks.py index ace4602c..a9e7b9df 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -73,7 +73,8 @@ class Webhooks: if playlist: json["playlist"] = str(playlist) self._request(self.error_webhooks, json) - def collection_hooks(self, webhooks, collection, poster_url=None, background_url=None, created=False, deleted=False, additions=None, removals=None, playlist=False): + def collection_hooks(self, webhooks, collection, poster_url=None, background_url=None, created=False, deleted=False, + additions=None, removals=None, radarr=None, sonarr=None, playlist=False): if self.library: thumb = None if not poster_url and collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None): @@ -93,4 +94,6 @@ class Webhooks: "background_url": background_url, "additions": additions if additions else [], "removals": removals if removals else [], + "radarr_adds": radarr if radarr else [], + "sonarr_adds": sonarr if sonarr else [], }) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index b8d96179..5d8751cd 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -34,6 +34,7 @@ parser.add_argument("-lo", "--library-only", "--libraries-only", dest="library_o parser.add_argument("-lf", "--library-first", "--libraries-first", dest="library_first", help="Run library operations before collections", action="store_true", default=False) parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collection", "--run-collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str) parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str) +parser.add_argument("-rm", "-m", "--metadata", "--metadata-files", "--run-metadata-files", dest="metadata", help="Process only specified Metadata files (comma-separated list)", type=str) parser.add_argument("-dc", "--delete", "--delete-collections", dest="delete", help="Deletes all Collections in the Plex Library before running", action="store_true", default=False) parser.add_argument("-nc", "--no-countdown", dest="no_countdown", help="Run without displaying the countdown", action="store_true", default=False) parser.add_argument("-nm", "--no-missing", dest="no_missing", help="Run without running the missing section", action="store_true", default=False) @@ -69,6 +70,7 @@ library_only = get_arg("PMM_LIBRARIES_ONLY", args.library_only, arg_bool=True) library_first = get_arg("PMM_LIBRARIES_FIRST", args.library_first, arg_bool=True) collections = get_arg("PMM_COLLECTIONS", args.collections) libraries = get_arg("PMM_LIBRARIES", args.libraries) +metadata_files = get_arg("PMM_METADATA_FILES", args.metadata) delete = get_arg("PMM_DELETE_COLLECTIONS", args.delete, arg_bool=True) resume = get_arg("PMM_RESUME", args.resume) no_countdown = get_arg("PMM_NO_COUNTDOWN", args.no_countdown, arg_bool=True) @@ -158,6 +160,7 @@ def start(attrs): logger.debug(f"--libraries-first (PMM_LIBRARIES_FIRST): {library_first}") logger.debug(f"--run-collections (PMM_COLLECTIONS): {collections}") logger.debug(f"--run-libraries (PMM_LIBRARIES): {libraries}") + logger.debug(f"--run-metadata-files (PMM_METADATA_FILES): {metadata_files}") logger.debug(f"--ignore-schedules (PMM_IGNORE_SCHEDULES): {ignore_schedules}") logger.debug(f"--delete-collections (PMM_DELETE_COLLECTIONS): {delete}") logger.debug(f"--resume (PMM_RESUME): {resume}") @@ -258,8 +261,11 @@ def update_libraries(config): logger.info("") library.map_guids() for metadata in library.metadata_files: + metadata_name = metadata.get_file_name() + if config.requested_metadata_files and metadata_name not in config.requested_metadata_files: + continue logger.info("") - util.separator(f"Running Metadata File\n{metadata.path}") + util.separator(f"Running {metadata_name} Metadata File\n{metadata.path}") if not config.test_mode and not config.resume_from and not collection_only: try: metadata.update_metadata() @@ -439,7 +445,7 @@ def library_operations(config, library): logger.debug(f"TMDb Collections: {library.tmdb_collections}") logger.debug(f"Genre Collections: {library.genre_collections}") logger.debug(f"Genre Mapper: {library.genre_mapper}") - logger.debug(f"TMDb Operation: {library.tmdb_library_operation}") + logger.debug(f"TMDb Operation: {library.items_library_operation}") if library.split_duplicates: items = library.search(**{"duplicate": True}) @@ -448,7 +454,7 @@ def library_operations(config, library): logger.info(util.adjust_space(f"{item.title[:25]:<25} | Splitting")) tmdb_collections = {} - if library.tmdb_library_operation: + if library.items_library_operation: items = library.get_all() radarr_adds = [] sonarr_adds = [] @@ -544,8 +550,8 @@ def library_operations(config, library): else: logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}")) - if library.tmdb_collections and tmdb_item and tmdb_item.belongs_to_collection: - tmdb_collections[tmdb_item.belongs_to_collection.id] = tmdb_item.belongs_to_collection.name + if library.tmdb_collections and tmdb_item and tmdb_item.collection: + tmdb_collections[tmdb_item.collection.id] = tmdb_item.collection.name if library.mass_genre_update: try: @@ -600,7 +606,8 @@ def library_operations(config, library): for genre in item.genres: if genre.tag in library.genre_mapper: deletes.append(genre.tag) - adds.append(library.genre_mapper[genre.tag]) + if library.genre_mapper[genre.tag]: + adds.append(library.genre_mapper[genre.tag]) library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) except Failed: pass @@ -652,6 +659,8 @@ def library_operations(config, library): new_collections[title] = {"template": template} metadata = MetadataFile(config, library, "Data", {"collections": new_collections, "templates": templates}) + if metadata.collections: + library.collections.extend([c for c in metadata.collections]) run_collection(config, library, metadata, metadata.get_collections(None)) if library.radarr_remove_by_tag: @@ -945,9 +954,11 @@ def run_playlists(config): else: server_check = pl_library.PlexServer.machineIdentifier - sync_to_users = config.general["playlist_sync_to_user"] + sync_to_users = config.general["playlist_sync_to_users"] if "sync_to_users" in playlist_attrs: sync_to_users = playlist_attrs["sync_to_users"] + elif "sync_to_user" in playlist_attrs: + sync_to_users = playlist_attrs["sync_to_user"] else: logger.warning(f"Playlist Error: sync_to_users attribute not found defaulting to playlist_sync_to_user: {sync_to_users}") @@ -992,7 +1003,16 @@ def run_playlists(config): logger.debug(f"Builder: {method}: {value}") logger.info("") items = [] - ids = builder.gather_ids(method, value) + if "plex" in method: + ids = [] + for pl_library in pl_libraries: + ids.extend(pl_library.get_rating_keys(method, value)) + elif "tautulli" in method: + ids = [] + for pl_library in pl_libraries: + ids.extend(pl_library.Tautulli.get_rating_keys(pl_library, value, True)) + else: + ids = builder.gather_ids(method, value) if len(ids) > 0: total_ids = len(ids) @@ -1052,7 +1072,7 @@ def run_playlists(config): try: input_id = config.Convert.tmdb_to_tvdb(input_id, fail=True) except Failed as e: - logger.error(e) + logger.warning(e) continue if input_id not in builder.ignore_ids: found = False @@ -1101,7 +1121,7 @@ def run_playlists(config): if tvdb_id not in builder.missing_shows: builder.missing_shows.append(tvdb_id) except Failed as e: - logger.error(e) + logger.warning(e) continue if not isinstance(rating_keys, list): rating_keys = [rating_keys] @@ -1205,7 +1225,7 @@ def run_playlists(config): return status, stats try: - if run or test or collections or libraries or resume: + if run or test or collections or libraries or metadata_files or resume: start({ "config_file": config_file, "test": test, @@ -1213,6 +1233,7 @@ try: "ignore_schedules": ignore_schedules, "collections": collections, "libraries": libraries, + "metadata_files": metadata_files, "library_first": library_first, "resume": resume, "trace": trace diff --git a/requirements.txt b/requirements.txt index 63e2fea6..30f159c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -PlexAPI==4.8.0 -tmdbv3api==1.7.6 +PlexAPI==4.9.1 +tmdbapis==0.1.8 arrapi==1.3.0 lxml==4.7.1 requests==2.27.1