From ab0dace8d5f71af89df0cddbad1e8c0998a63535 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 28 Mar 2021 15:24:02 -0400 Subject: [PATCH 01/28] fix for None rating --- modules/plex.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/plex.py b/modules/plex.py index bc482813..ae7e7032 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -210,6 +210,8 @@ class PlexAPI: attr = tmdb_item.vote_count else: attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name) + if attr is None: + attr = 0 if (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): match = False break From f304f31088b9b81f5f758a5e1660611db309f506 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 01:49:10 -0400 Subject: [PATCH 02/28] various fixes --- modules/builder.py | 42 ++++--- modules/config.py | 2 +- modules/imdb.py | 6 +- modules/plex.py | 268 +++++++++++++-------------------------------- modules/trakttv.py | 10 +- modules/util.py | 4 +- 6 files changed, 118 insertions(+), 214 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 4ced414e..fd4ec34d 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -213,14 +213,13 @@ class CollectionBuilder: raise Failed("Collection Error: tmdb_person attribute is blank") for method_name, method_data in self.data.items(): - if "tmdb" in method_name.lower() and not config.TMDb: raise Failed(f"Collection Error: {method_name} requires TMDb to be configured") - elif "trakt" in method_name.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_name} requires Trakt todo be configured") - elif "imdb" in method_name.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_name} requires TMDb or Trakt to be configured") - elif "tautulli" in method_name.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_name} requires Tautulli to be configured") - elif "mal" in method_name.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_name} requires MyAnimeList to be configured") + if "trakt" in method_name.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_name} requires Trakt todo be configured") + elif "imdb" in method_name.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_name} requires TMDb or Trakt to be configured") + elif "tautulli" in method_name.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_name} requires Tautulli to be configured") + elif "mal" in method_name.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_name} requires MyAnimeList to be configured") elif method_data is not None: logger.debug("") - logger.debug(f"Method: {method_name}") + logger.debug(f"Validating Method: {method_name}") logger.debug(f"Value: {method_data}") if method_name.lower() in util.method_alias: method_name = util.method_alias[method_name.lower()] @@ -370,12 +369,15 @@ class CollectionBuilder: if isinstance(imdb_list, dict): dict_methods = {dm.lower(): dm for dm in imdb_list} if "url" in dict_methods and imdb_list[dict_methods["url"]]: - imdb_url = imdb_list[dict_methods["url"]] + imdb_url = config.IMDb.validate_imdb_url(imdb_list[dict_methods["url"]]) else: raise Failed("Collection Error: imdb_list attribute url is required") - list_count = util.regex_first_int(imdb_list[dict_methods["limit"]], "List Limit", default=0) if "limit" in dict_methods and imdb_list[dict_methods["limit"]] else 0 + if "limit" in dict_methods and imdb_list[dict_methods["limit"]]: + list_count = util.regex_first_int(imdb_list[dict_methods["limit"]], "List Limit", default=0) + else: + list_count = 0 else: - imdb_url = str(imdb_list) + imdb_url = config.IMDb.validate_imdb_url(str(imdb_list)) list_count = 0 new_list.append({"url": imdb_url, "limit": list_count}) self.methods.append((method_name, new_list)) @@ -1061,15 +1063,19 @@ class CollectionBuilder: logger.warning(f"No Folder: {os.path.join(path, folder)}") def set_image(image_method, images, is_background=False): - if image_method in ["file_poster", "file_background", "asset_directory"]: - if is_background: collection.uploadArt(filepath=images[image_method]) - else: collection.uploadPoster(filepath=images[image_method]) - image_location = "File" - else: - if is_background: collection.uploadArt(url=images[image_method]) - else: collection.uploadPoster(url=images[image_method]) - image_location = "URL" - logger.info(f"Detail: {image_method} updated collection {'background' if is_background else 'poster'} to [{image_location}] {images[image_method]}") + message = f"{'background' if is_background else 'poster'} to [{'File' if image_method in image_file_details else 'URL'}] {images[image_method]}" + try: + if image_method in image_file_details and is_background: + collection.uploadArt(filepath=images[image_method]) + elif image_method in image_file_details: + collection.uploadPoster(filepath=images[image_method]) + elif is_background: + collection.uploadArt(url=images[image_method]) + else: + collection.uploadPoster(url=images[image_method]) + logger.info(f"Detail: {image_method} updated collection {message}") + except BadRequest: + logger.error(f"Detail: {image_method} failed to update {message}") if len(self.posters) > 1: logger.info(f"{len(self.posters)} posters found:") diff --git a/modules/config.py b/modules/config.py index 7ceae221..0598c178 100644 --- a/modules/config.py +++ b/modules/config.py @@ -122,11 +122,11 @@ class Config: if var_type == "path" and default and os.path.exists(os.path.abspath(default)): return default elif var_type == "path" and default: - default = None if attribute in data and data[attribute]: message = f"neither {data[attribute]} or the default path {default} could be found" else: message = f"no {text} found and the default path {default} could be found" + default = None if default is not None or default_is_none: message = message + f" using {default} as default" message = message + endline diff --git a/modules/imdb.py b/modules/imdb.py index 48fadf6f..e5a00717 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -14,11 +14,13 @@ class IMDbAPI: "search": "https://www.imdb.com/search/title/?" } - def get_imdb_ids_from_url(self, imdb_url, language, limit): + def validate_imdb_url(self, imdb_url): imdb_url = imdb_url.strip() if not imdb_url.startswith(self.urls["list"]) and not imdb_url.startswith(self.urls["search"]): - raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n| {self.urls['list']} (For Lists)\n| {self.urls['search']} (For Searches)") + raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n{self.urls['list']} (For Lists)\n{self.urls['search']} (For Searches)") + return imdb_url + def get_imdb_ids_from_url(self, imdb_url, language, limit): if imdb_url.startswith(self.urls["list"]): try: list_id = re.search("(\\d+)", str(imdb_url)).group(1) except AttributeError: raise Failed(f"IMDb Error: Failed to parse List ID from {imdb_url}") diff --git a/modules/plex.py b/modules/plex.py index ae7e7032..0d672d06 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -30,8 +30,11 @@ class PlexAPI: raise Failed(f"Plex Error: Plex Library {params['name']} not found") try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) - except yaml.scanner.ScannerError as e: - raise Failed(f"YAML Error: {util.tab_new_lines(e)}") + except yaml.scanner.ScannerError as ye: + raise Failed(f"YAML Error: {util.tab_new_lines(ye)}") + except Exception as e: + util.print_stacktrace() + raise Failed(f"YAML Error: {e}") def get_dict(attribute): if attribute in self.data: @@ -308,12 +311,7 @@ class PlexAPI: tagline = tmdb_item.tagline if tmdb_item and len(tmdb_item.tagline) > 0 else None summary = tmdb_item.overview if tmdb_item else None - details_updated = False - advance_details_updated = False - genre_updated = False - label_updated = False - season_updated = False - episode_updated = False + updated = False edits = {} def add_edit(name, current, group, alias, key=None, value=None): @@ -338,7 +336,7 @@ class PlexAPI: add_edit("summary", item.summary, meta, methods, value=summary) if len(edits) > 0: logger.debug(f"Details Update: {edits}") - details_updated = True + updated = True try: item.edit(**edits) item.reload() @@ -348,143 +346,35 @@ class PlexAPI: logger.error(f"{item_type}: {mapping_name} Details Update Failed") advance_edits = {} - if self.is_show: - - if "episode_sorting" in methods: - if meta[methods["episode_sorting"]]: - method_data = str(meta[methods["episode_sorting"]]).lower() - if method_data in ["default", "oldest", "newest"]: - if method_data == "default" and item.episodeSort != "-1": - advance_edits["episodeSort"] = "-1" - elif method_data == "oldest" and item.episodeSort != "0": - advance_edits["episodeSort"] = "0" - elif method_data == "newest" and item.episodeSort != "1": - advance_edits["episodeSort"] = "1" - if "episodeSort" in advance_edits: - logger.info(f"Detail: episode_sorting updated to {method_data}") - else: - logger.error(f"Metadata Error: {meta[methods['episode_sorting']]} episode_sorting attribute invalid") - else: - logger.error(f"Metadata Error: episode_sorting attribute is blank") - - if "keep_episodes" in methods: - if meta[methods["keep_episodes"]]: - method_data = str(meta[methods["keep_episodes"]]).lower() - if method_data in ["all", "5_latest", "3_latest", "latest", "past_3", "past_7", "past_30"]: - if method_data == "all" and item.autoDeletionItemPolicyUnwatchedLibrary != 0: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 0 - elif method_data == "5_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 5: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 5 - elif method_data == "3_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 3: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 3 - elif method_data == "latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 1: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 1 - elif method_data == "past_3" and item.autoDeletionItemPolicyUnwatchedLibrary != -3: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -3 - elif method_data == "past_7" and item.autoDeletionItemPolicyUnwatchedLibrary != -7: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -7 - elif method_data == "past_30" and item.autoDeletionItemPolicyUnwatchedLibrary != -30: - advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -30 - if "autoDeletionItemPolicyUnwatchedLibrary" in advance_edits: - logger.info(f"Detail: keep_episodes updated to {method_data}") - else: - logger.error(f"Metadata Error: {meta[methods['keep_episodes']]} keep_episodes attribute invalid") - else: - logger.error(f"Metadata Error: keep_episodes attribute is blank") - - if "delete_episodes" in methods: - if meta[methods["delete_episodes"]]: - method_data = str(meta[methods["delete_episodes"]]).lower() - if method_data in ["never", "day", "week", "refresh"]: - if method_data == "never" and item.autoDeletionItemPolicyWatchedLibrary != 0: - advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 0 - elif method_data == "day" and item.autoDeletionItemPolicyWatchedLibrary != 1: - advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 1 - elif method_data == "week" and item.autoDeletionItemPolicyWatchedLibrary != 7: - advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 7 - elif method_data == "refresh" and item.autoDeletionItemPolicyWatchedLibrary != 100: - advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 100 - if "autoDeletionItemPolicyWatchedLibrary" in advance_edits: - logger.info(f"Detail: delete_episodes updated to {method_data}") + def add_advanced_edit(attr, options, key=None, show_library=False): + if key is None: + key = attr + if show_library and not self.is_show: + logger.error(f"Metadata Error: {attr} attribute only works for show libraries") + elif attr in methods: + if meta[methods[attr]]: + method_data = str(meta[methods[attr]]).lower() + if method_data in options and getattr(item, 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: {meta[methods['delete_episodes']]} delete_episodes attribute invalid") - else: - logger.error(f"Metadata Error: delete_episodes attribute is blank") - - if "season_display" in methods: - if meta[methods["season_display"]]: - method_data = str(meta[methods["season_display"]]).lower() - if method_data in ["default", "hide", "show"]: - if method_data == "default" and item.flattenSeasons != -1: - advance_edits["flattenSeasons"] = -1 - elif method_data == "show" and item.flattenSeasons != 0: - advance_edits["flattenSeasons"] = 0 - elif method_data == "hide" and item.flattenSeasons != 1: - advance_edits["flattenSeasons"] = 1 - if "flattenSeasons" in advance_edits: - logger.info(f"Detail: season_display updated to {method_data}") - else: - logger.error(f"Metadata Error: {meta[methods['season_display']]} season_display attribute invalid") - else: - logger.error(f"Metadata Error: season_display attribute is blank") - - if "episode_ordering" in methods: - if meta[methods["episode_ordering"]]: - method_data = str(meta[methods["episode_ordering"]]).lower() - if method_data in ["default", "tmdb_aired", "tvdb_aired", "tvdb_dvd", "tvdb_absolute"]: - if method_data == "default" and item.showOrdering is not None: - advance_edits["showOrdering"] = None - elif method_data == "tmdb_aired" and item.showOrdering != "tmdbAiring": - advance_edits["showOrdering"] = "tmdbAiring" - elif method_data == "tvdb_aired" and item.showOrdering != "airing": - advance_edits["showOrdering"] = "airing" - elif method_data == "tvdb_dvd" and item.showOrdering != "dvd": - advance_edits["showOrdering"] = "dvd" - elif method_data == "tvdb_absolute" and item.showOrdering != "absolute": - advance_edits["showOrdering"] = "absolute" - if "showOrdering" in advance_edits: - logger.info(f"Detail: episode_ordering updated to {method_data}") - else: - logger.error(f"Metadata Error: {meta[methods['episode_ordering']]} episode_ordering attribute invalid") - else: - logger.error(f"Metadata Error: episode_ordering attribute is blank") - - if "metadata_language" in methods: - if meta[methods["metadata_language"]]: - method_data = str(meta[methods["metadata_language"]]).lower() - lower_languages = {la.lower(): la for la in util.plex_languages} - if method_data in lower_languages: - if method_data == "default" and item.languageOverride is None: - advance_edits["languageOverride"] = None - elif str(item.languageOverride).lower() != lower_languages[method_data]: - advance_edits["languageOverride"] = lower_languages[method_data] - if "languageOverride" in advance_edits: - logger.info(f"Detail: metadata_language updated to {method_data}") + logger.error(f"Metadata Error: {meta[methods[attr]]} {attr} attribute invalid") else: - logger.error(f"Metadata Error: {meta[methods['metadata_language']]} metadata_language attribute invalid") - else: - logger.error(f"Metadata Error: metadata_language attribute is blank") - - if "use_original_title" in methods: - if meta[methods["use_original_title"]]: - method_data = str(meta[methods["use_original_title"]]).lower() - if method_data in ["default", "no", "yes"]: - if method_data == "default" and item.useOriginalTitle != -1: - advance_edits["useOriginalTitle"] = -1 - elif method_data == "no" and item.useOriginalTitle != 0: - advance_edits["useOriginalTitle"] = 0 - elif method_data == "yes" and item.useOriginalTitle != 1: - advance_edits["useOriginalTitle"] = 1 - if "useOriginalTitle" in advance_edits: - logger.info(f"Detail: use_original_title updated to {method_data}") - else: - logger.error(f"Metadata Error: {meta[methods['use_original_title']]} use_original_title attribute invalid") - else: - logger.error(f"Metadata Error: use_original_title attribute is blank") + logger.error(f"Metadata Error: {attr} attribute is blank") + + add_advanced_edit("episode_sorting", episode_sorting_options, key="episodeSort", show_library=True) + add_advanced_edit("keep_episodes", keep_episodes_options, key="autoDeletionItemPolicyUnwatchedLibrary", show_library=True) + add_advanced_edit("delete_episodes", delete_episodes_options, key="autoDeletionItemPolicyWatchedLibrary", show_library=True) + add_advanced_edit("season_display", season_display_options, key="flattenSeasons", show_library=True) + add_advanced_edit("episode_ordering", episode_ordering_options, key="showOrdering", show_library=True) + add_advanced_edit("metadata_language", metadata_language_options, key="languageOverride") + + use_original_title_options = {"default": -1, "no": 0, "yes": 1} + add_advanced_edit("use_original_title", use_original_title_options, key="useOriginalTitle") if len(advance_edits) > 0: logger.debug(f"Details Update: {advance_edits}") - advance_details_updated = True + updated = True try: check_dict = {pref.id: list(pref.enumValues.keys()) for pref in item.preferences()} logger.info(check_dict) @@ -495,51 +385,44 @@ class PlexAPI: util.print_stacktrace() logger.error(f"{item_type}: {mapping_name} Advanced Details Update Failed") - genres = [] - if tmdb_item: - genres.extend([genre.name for genre in tmdb_item.genres]) - if "genre" in methods: - if meta[methods["genre"]]: - genres.extend(util.get_list(meta[methods["genre"]])) - else: - logger.error("Metadata Error: genre attribute is blank") - if len(genres) > 0: - item_genres = [genre.tag for genre in item.genres] - if "genre_sync_mode" in methods: - if meta[methods["genre_sync_mode"]] is None: - logger.error("Metadata Error: genre_sync_mode attribute is blank defaulting to append") - elif str(meta[methods["genre_sync_mode"]]).lower() not in ["append", "sync"]: - logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append") - elif str(meta["genre_sync_mode"]).lower() == "sync": - for genre in (g for g in item_genres if g not in genres): - genre_updated = True - item.removeGenre(genre) - logger.info(f"Detail: Genre {genre} removed") - for genre in (g for g in genres if g not in item_genres): - genre_updated = True - item.addGenre(genre) - logger.info(f"Detail: Genre {genre} added") - - if "label" in methods: - if meta[methods["label"]]: - item_labels = [label.tag for label in item.labels] - labels = util.get_list(meta[methods["label"]]) - if "label_sync_mode" in methods: - if meta[methods["label_sync_mode"]] is None: - logger.error("Metadata Error: label_sync_mode attribute is blank defaulting to append") - elif str(meta[methods["label_sync_mode"]]).lower() not in ["append", "sync"]: - logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append") - elif str(meta[methods["label_sync_mode"]]).lower() == "sync": - for label in (la for la in item_labels if la not in labels): - label_updated = True - item.removeLabel(label) - logger.info(f"Detail: Label {label} removed") - for label in (la for la in labels if la not in item_labels): - label_updated = True - item.addLabel(label) - logger.info(f"Detail: Label {label} added") - else: - logger.error("Metadata Error: label attribute is blank") + def edit_tags(attr, obj, key=None, extra=None, movie_library=False): + if key is None: + key = f"{attr}s" + if attr in methods and f"{attr}.sync" in methods: + logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together") + elif attr in methods or f"{attr}.sync" in methods: + attr_key = attr if attr in methods else f"{attr}.sync" + if movie_library and not self.is_movie: + logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries") + elif meta[methods[attr_key]] or extra: + item_tags = [item_tag.tag for item_tag in getattr(obj, key)] + input_tags = [] + if meta[methods[attr_key]]: + input_tags.extend(util.get_list(meta[methods[attr_key]])) + if extra: + input_tags.extend(extra) + if f"{attr}.sync" in methods: + remove_method = getattr(obj, f"remove{attr.capitalize()}") + for tag in (t for t in item_tags if t not in input_tags): + updated = True + remove_method(tag) + logger.info(f"Detail: {attr.capitalize()} {tag} removed") + add_method = getattr(obj, f"add{attr.capitalize()}") + for tag in (t for t in input_tags if t not in item_tags): + updated = True + add_method(tag) + logger.info(f"Detail: {attr.capitalize()} {tag} added") + else: + logger.error(f"Metadata Error: {attr} attribute is blank") + + genres = [genre.name for genre in tmdb_item.genres] if tmdb_item else [] + edit_tags("genre", item, extra=genres) + edit_tags("label", item) + edit_tags("collection", item) + edit_tags("country", item, key="countries", movie_library=True) + edit_tags("director", item, movie_library=True) + edit_tags("producer", item, movie_library=True) + edit_tags("writer", item, movie_library=True) if "seasons" in methods and self.is_show: if meta[methods["seasons"]]: @@ -572,7 +455,7 @@ class PlexAPI: add_edit("summary", season.summary, season_methods, season_dict) if len(edits) > 0: logger.debug(f"Season: {season_id} Details Update: {edits}") - season_updated = True + updated = True try: season.edit(**edits) season.reload() @@ -584,6 +467,8 @@ class PlexAPI: logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer") else: logger.error("Metadata Error: seasons attribute is blank") + elif "seasons" in methods: + logger.error("Metadata Error: seasons attribute only works for show libraries") if "episodes" in methods and self.is_show: if meta[methods["episodes"]]: @@ -622,7 +507,7 @@ class PlexAPI: add_edit("summary", episode.summary, episode_dict, episode_methods) if len(edits) > 0: logger.debug(f"Season: {season_id} Episode: {episode_id} Details Update: {edits}") - episode_updated = True + updated = True try: episode.edit(**edits) episode.reload() @@ -631,10 +516,15 @@ class PlexAPI: except BadRequest: util.print_stacktrace() logger.error(f"Season: {season_id} Episode: {episode_id} Details Update Failed") + edit_tags("director", episode) + edit_tags("writer", episode) + else: logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format") else: logger.error("Metadata Error: episodes attribute is blank") + elif "episodes" in methods: + logger.error("Metadata Error: episodes attribute only works for show libraries") - if not details_updated and not advance_details_updated and not genre_updated and not label_updated and not season_updated and not episode_updated: + if not updated: logger.info(f"{item_type}: {mapping_name} Details Update Not Needed") \ No newline at end of file diff --git a/modules/trakttv.py b/modules/trakttv.py index 1f48479d..aecbe6b3 100644 --- a/modules/trakttv.py +++ b/modules/trakttv.py @@ -94,9 +94,15 @@ class TraktAPI: return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source)) raise Failed(f"No {to_source.upper().replace('B', 'b')} ID found for {from_source.upper().replace('B', 'b')} ID {external_id}") - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) + def collection(self, data, is_movie): + return self.user_list("collection", data, is_movie) + def watchlist(self, data, is_movie): - items = Trakt[f"users/{data}/watchlist"].movies() if is_movie else Trakt[f"users/{data}/watchlist"].shows() + return self.user_list("watchlist", data, is_movie) + + @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) + def user_list(self, list_type, data, is_movie): + items = Trakt[f"users/{data}/{list_type}"].movies() if is_movie else Trakt[f"users/{data}/{list_type}"].shows() if items is None: raise Failed("Trakt Error: No List found") else: return [i for i in items] diff --git a/modules/util.py b/modules/util.py index 4c032273..24d3cba4 100644 --- a/modules/util.py +++ b/modules/util.py @@ -678,8 +678,8 @@ def check_number(value, method, number_type="int", minimum=None, maximum=None): return num_value def check_date(date_text, method, return_string=False, plex_date=False): - try: date_obg = datetime.strptime(str(date_text), "%Y/%m/%d" if plex_date else "%m/%d/%Y") - except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern {'YYYY/MM/DD e.g. 2020/12/25' if plex_date else 'MM/DD/YYYY e.g. 12/25/2020'}") + try: date_obg = datetime.strptime(str(date_text), "%Y-%m-%d" if plex_date else "%m/%d/%Y") + except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern {'YYYY-MM-DD e.g. 2020-12-25' if plex_date else 'MM/DD/YYYY e.g. 12/25/2020'}") return str(date_text) if return_string else date_obg def logger_input(prompt, timeout=60): From fac03aab3a04283397fbab05d04d6bc53bb2ed78 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 01:50:53 -0400 Subject: [PATCH 03/28] reorganized static lists --- modules/anidb.py | 2 + modules/anilist.py | 21 +- modules/builder.py | 215 +++++++++++++++++---- modules/imdb.py | 2 + modules/letterboxd.py | 2 + modules/mal.py | 75 +++++++- modules/plex.py | 89 ++++++++- modules/tautulli.py | 2 + modules/tmdb.py | 103 +++++++++- modules/trakttv.py | 12 ++ modules/tvdb.py | 9 + modules/util.py | 438 ------------------------------------------ 12 files changed, 482 insertions(+), 488 deletions(-) diff --git a/modules/anidb.py b/modules/anidb.py index b6138fae..1b0f6bf4 100644 --- a/modules/anidb.py +++ b/modules/anidb.py @@ -6,6 +6,8 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = ["anidb_id", "anidb_relation", "anidb_popular"] + class AniDBAPI: def __init__(self, config): self.config = config diff --git a/modules/anilist.py b/modules/anilist.py index 2ae98118..7f40598f 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -5,6 +5,21 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = [ + "anilist_genre", + "anilist_id", + "anilist_popular", + "anilist_relations", + "anilist_season", + "anilist_studio", + "anilist_tag", + "anilist_top_rated" +] +pretty_names = { + "score": "Average Score", + "popular": "Popularity" +} + class AniListAPI: def __init__(self, config): self.config = config @@ -223,15 +238,15 @@ class AniListAPI: elif method == "anilist_season": mal_ids = self.season(data["season"], data["year"], data["sort_by"], data["limit"]) if status_message: - logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.anilist_pretty[data['sort_by']]}") + logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}") elif method == "anilist_genre": mal_ids = self.genre(data["genre"], data["sort_by"], data["limit"]) if status_message: - logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {util.anilist_pretty[data['sort_by']]}") + logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {pretty_names[data['sort_by']]}") elif method == "anilist_tag": mal_ids = self.tag(data["tag"], data["sort_by"], data["limit"]) if status_message: - logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {util.anilist_pretty[data['sort_by']]}") + logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {pretty_names[data['sort_by']]}") elif method in ["anilist_studio", "anilist_relations"]: if method == "anilist_studio": mal_ids, name = self.studio(data) else: mal_ids, _, name = self.relations(data) diff --git a/modules/builder.py b/modules/builder.py index fd4ec34d..0668013e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,12 +1,147 @@ import glob, logging, os, re from datetime import datetime, timedelta -from modules import util +from modules import anidb, anilist, imdb, letterboxd, mal, plex, tautulli, tmdb, trakttv, tvdb, util from modules.util import Failed from plexapi.collection import Collections from plexapi.exceptions import BadRequest, NotFound logger = logging.getLogger("Plex Meta Manager") +image_file_details = ["file_poster", "file_background", "asset_directory"] +method_alias = { + "actors": "actor", "role": "actor", "roles": "actor", + "content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating", + "countries": "country", + "decades": "decade", + "directors": "director", + "genres": "genre", + "labels": "label", + "studios": "studio", "network": "studio", "networks": "studio", + "producers": "producer", + "writers": "writer", + "years": "year" +} +all_builders = anidb.builders + anilist.builders + imdb.builders + letterboxd.builders + mal.builders + plex.builders + tautulli.builders + tmdb.builders + trakttv.builders + tvdb.builders +dictionary_builders = [ + "filters", + "anilist_genre", + "anilist_season", + "anilist_tag", + "mal_season", + "mal_userlist", + "plex_collectionless", + "plex_search", + "tautulli_popular", + "tautulli_watched", + "tmdb_discover" +] +show_only_builders = [ + "tmdb_network", + "tmdb_show", + "tmdb_show_details", + "tvdb_show", + "tvdb_show_details" +] +movie_only_builders = [ + "letterboxd_list", + "letterboxd_list_details", + "tmdb_collection", + "tmdb_collection_details", + "tmdb_movie", + "tmdb_movie_details", + "tmdb_now_playing", + "tvdb_movie", + "tvdb_movie_details" +] +numbered_builders = [ + "anidb_popular", + "anilist_popular", + "anilist_top_rated", + "mal_all", + "mal_airing", + "mal_upcoming", + "mal_tv", + "mal_ova", + "mal_movie", + "mal_special", + "mal_popular", + "mal_favorite", + "mal_suggested", + "tmdb_popular", + "tmdb_top_rated", + "tmdb_now_playing", + "tmdb_trending_daily", + "tmdb_trending_weekly", + "trakt_trending", + "trakt_popular", + "trakt_recommended", + "trakt_watched", + "trakt_collected" +] +all_details = [ + "sort_title", "content_rating", + "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", "tvdb_description", "trakt_description", "letterboxd_description", + "collection_mode", "collection_order", + "url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster", + "url_background", "tmdb_background", "tvdb_background", "file_background", + "name_mapping", "add_to_arr", "arr_tag", "label", + "show_filtered", "show_missing", "save_missing" +] +collectionless_details = [ + "sort_title", "content_rating", + "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", + "collection_order", "plex_collectionless", + "url_poster", "tmdb_poster", "tmdb_profile", "file_poster", + "url_background", "file_background", + "name_mapping", "label", "label_sync_mode", "test" +] +ignored_details = [ + "run_again", + "schedule", + "sync_mode", + "template", + "test", + "tmdb_person" +] +boolean_details = [ + "add_to_arr", + "show_filtered", + "show_missing", + "save_missing" +] +all_filters = [ + "actor", "actor.not", + "audio_language", "audio_language.not", + "audio_track_title", "audio_track_title.not", + "collection", "collection.not", + "content_rating", "content_rating.not", + "country", "country.not", + "director", "director.not", + "genre", "genre.not", + "max_age", + "originally_available.gte", "originally_available.lte", + "tmdb_vote_count.gte", "tmdb_vote_count.lte", + "duration.gte", "duration.lte", + "original_language", "original_language.not", + "rating.gte", "rating.lte", + "studio", "studio.not", + "subtitle_language", "subtitle_language.not", + "video_resolution", "video_resolution.not", + "writer", "writer.not", + "year", "year.gte", "year.lte", "year.not" +] +movie_only_filters = [ + "audio_language", "audio_language.not", + "audio_track_title", "audio_track_title.not", + "country", "country.not", + "director", "director.not", + "duration.gte", "duration.lte", + "original_language", "original_language.not", + "subtitle_language", "subtitle_language.not", + "video_resolution", "video_resolution.not", + "writer", "writer.not" +] + class CollectionBuilder: def __init__(self, config, library, name, data): self.config = config @@ -221,18 +356,18 @@ class CollectionBuilder: logger.debug("") logger.debug(f"Validating Method: {method_name}") logger.debug(f"Value: {method_data}") - if method_name.lower() in util.method_alias: - method_name = util.method_alias[method_name.lower()] + if method_name.lower() in method_alias: + method_name = method_alias[method_name.lower()] logger.warning(f"Collection Warning: {method_name} attribute will run as {method_name}") else: method_name = method_name.lower() - if method_name in util.show_only_lists and self.library.is_movie: + if method_name in show_only_builders and self.library.is_movie: raise Failed(f"Collection Error: {method_name} attribute only works for show libraries") - elif method_name in util.movie_only_lists and self.library.is_show: + elif method_name in movie_only_builders and self.library.is_show: raise Failed(f"Collection Error: {method_name} attribute only works for movie libraries") - elif method_name in util.movie_only_searches and self.library.is_show: + elif method_name in plex.movie_only_searches and self.library.is_show: raise Failed(f"Collection Error: {method_name} plex search only works for movie libraries") - elif method_name not in util.collectionless_lists and self.collectionless: + elif method_name not in collectionless_details and self.collectionless: raise Failed(f"Collection Error: {method_name} attribute does not work for Collectionless collection") elif method_name == "summary": self.summaries[method_name] = method_data @@ -298,12 +433,12 @@ class CollectionBuilder: else: raise Failed("Collection Error: sync_mode attribute must be either 'append' or 'sync'") elif method_name in ["arr_tag", "label"]: self.details[method_name] = util.get_list(method_data) - elif method_name in util.boolean_details: + elif method_name in boolean_details: if isinstance(method_data, bool): self.details[method_name] = method_data elif str(method_data).lower() in ["t", "true"]: self.details[method_name] = True elif str(method_data).lower() in ["f", "false"]: self.details[method_name] = False else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false") - elif method_name in util.all_details: + elif method_name in all_details: self.details[method_name] = method_data elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]: self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}])) @@ -315,7 +450,7 @@ class CollectionBuilder: self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}])) elif method_name in ["year", "year.not"]: self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) - elif method_name in util.tmdb_searches: + elif method_name in plex.tmdb_searches: final_values = [] for value in util.get_list(method_data): if value.lower() == "tmdb" and "tmdb_person" in self.details: @@ -324,8 +459,8 @@ class CollectionBuilder: else: final_values.append(value) self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}])) - elif method_name in util.plex_searches: - if method_name in util.tmdb_searches: + elif method_name in plex.searches: + if method_name in plex.tmdb_searches: final_values = [] for value in util.get_list(method_data): if value.lower() == "tmdb" and "tmdb_person" in self.details: @@ -387,7 +522,7 @@ class CollectionBuilder: values = util.get_list(method_data, split=False) self.summaries[method_name] = config.Letterboxd.get_list_description(values[0], self.library.Plex.language) self.methods.append((method_name[:-8], values)) - elif method_name in util.dictionary_lists: + elif method_name in dictionary_builders: if isinstance(method_data, dict): def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum=None): if method not in methods_in: @@ -404,12 +539,12 @@ class CollectionBuilder: return default_in if method_name == "filters": for filter_name, filter_data in method_data.items(): - if filter_name.lower() in util.method_alias or (filter_name.lower().endswith(".not") and filter_name.lower()[:-4] in util.method_alias): - filter_method = (util.method_alias[filter_name.lower()[:-4]] + filter_name.lower()[-4:]) if filter_name.lower().endswith(".not") else util.method_alias[filter_name.lower()] + if filter_name.lower() in method_alias or (filter_name.lower().endswith(".not") and filter_name.lower()[:-4] in method_alias): + filter_method = (method_alias[filter_name.lower()[:-4]] + filter_name.lower()[-4:]) if filter_name.lower().endswith(".not") else method_alias[filter_name.lower()] logger.warning(f"Collection Warning: {filter_name} filter will run as {filter_method}") else: filter_method = filter_name.lower() - if filter_method in util.movie_only_filters and self.library.is_show: + if filter_method in movie_only_filters and self.library.is_show: raise Failed(f"Collection Error: {filter_method} filter only works for movie libraries") elif filter_data is None: raise Failed(f"Collection Error: {filter_method} filter is blank") @@ -427,7 +562,7 @@ class CollectionBuilder: valid_data = util.get_list(filter_data, lower=True) elif filter_method == "collection": valid_data = filter_data if isinstance(filter_data, list) else [filter_data] - elif filter_method in util.all_filters: + elif filter_method in all_filters: valid_data = util.get_list(filter_data) else: raise Failed(f"Collection Error: {filter_method} filter not supported") @@ -456,16 +591,16 @@ class CollectionBuilder: searches = {} for search_name, search_data in method_data.items(): search, modifier = os.path.splitext(str(search_name).lower()) - if search in util.method_alias: - search = util.method_alias[search] + if search in method_alias: + search = method_alias[search] logger.warning(f"Collection Warning: {str(search_name).lower()} plex search attribute will run as {search}{modifier if modifier else ''}") search_final = f"{search}{modifier}" - if search_final in util.movie_only_searches and self.library.is_show: + if search_final in plex.movie_only_searches and self.library.is_show: raise Failed(f"Collection Error: {search_final} plex search attribute only works for movie libraries") elif search_data is None: raise Failed(f"Collection Error: {search_final} plex search attribute is blank") elif search == "sort_by": - if str(search_data).lower() in util.plex_sort: + if str(search_data).lower() in plex.sorts: searches[search] = str(search_data).lower() else: logger.warning(f"Collection Error: {search_data} is not a valid plex search sort defaulting to title.asc") @@ -481,7 +616,7 @@ class CollectionBuilder: elif (search == "studio" and modifier in ["", ".and", ".not", ".begins", ".ends"]) \ or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \ or (search == "resolution" and modifier in [""]): - if search_final in util.tmdb_searches: + if search_final in plex.tmdb_searches: final_values = [] for value in util.get_list(search_data): if value.lower() == "tmdb" and "tmdb_person" in self.details: @@ -516,7 +651,7 @@ class CollectionBuilder: for discover_name, discover_data in method_data.items(): discover_final = discover_name.lower() if discover_data: - if (self.library.is_movie and discover_final in util.discover_movie) or (self.library.is_show and discover_final in util.discover_tv): + if (self.library.is_movie and discover_final in tmdb.discover_movie) or (self.library.is_show and discover_final in tmdb.discover_tv): if discover_final == "language": if re.compile("([a-z]{2})-([A-Z]{2})").match(str(discover_data)): new_dictionary[discover_final] = str(discover_data) @@ -528,7 +663,7 @@ class CollectionBuilder: else: raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} must match pattern ^[A-Z]{{2}}$ e.g. US") elif discover_final == "sort_by": - if (self.library.is_movie and discover_data in util.discover_movie_sort) or (self.library.is_show and discover_data in util.discover_tv_sort): + if (self.library.is_movie and discover_data in tmdb.discover_movie_sort) or (self.library.is_show and discover_data in tmdb.discover_tv_sort): new_dictionary[discover_final] = discover_data else: raise Failed(f"Collection Error: {method_name} attribute {discover_final}: {discover_data} is invalid") @@ -545,7 +680,7 @@ class CollectionBuilder: elif discover_final in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: if discover_data is True: new_dictionary[discover_final] = discover_data - elif discover_final in util.discover_dates: + elif discover_final in tmdb.discover_dates: new_dictionary[discover_final] = util.check_date(discover_data, f"{method_name} attribute {discover_final}", return_string=True) elif discover_final in ["primary_release_year", "year", "first_air_date_year"]: new_dictionary[discover_final] = util.check_number(discover_data, f"{method_name} attribute {discover_final}", minimum=1800, maximum=current_year + 1) @@ -588,10 +723,10 @@ class CollectionBuilder: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") elif not method_data[dict_methods["sort_by"]]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") - elif method_data[dict_methods["sort_by"]] not in util.mal_season_sort: + elif method_data[dict_methods["sort_by"]] not in mal.season_sort: logger.warning(f"Collection Warning: mal_season sort_by attribute {method_data[dict_methods['sort_by']]} invalid must be either 'members' or 'score' using members as default") else: - new_dictionary["sort_by"] = util.mal_season_sort[method_data[dict_methods["sort_by"]]] + new_dictionary["sort_by"] = mal.season_sort[method_data[dict_methods["sort_by"]]] if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter" elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring" @@ -624,19 +759,19 @@ class CollectionBuilder: logger.warning("Collection Warning: mal_season status attribute not found using all as default") elif not method_data[dict_methods["status"]]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") - elif method_data[dict_methods["status"]] not in util.mal_userlist_status: + elif method_data[dict_methods["status"]] not in mal.userlist_status: logger.warning(f"Collection Warning: mal_season status attribute {method_data[dict_methods['status']]} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default") else: - new_dictionary["status"] = util.mal_userlist_status[method_data[dict_methods["status"]]] + new_dictionary["status"] = mal.userlist_status[method_data[dict_methods["status"]]] if "sort_by" not in dict_methods: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") elif not method_data[dict_methods["sort_by"]]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") - elif method_data[dict_methods["sort_by"]] not in util.mal_userlist_sort: + elif method_data[dict_methods["sort_by"]] not in mal.userlist_sort: logger.warning(f"Collection Warning: mal_season sort_by attribute {method_data[dict_methods['sort_by']]} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default") else: - new_dictionary["sort_by"] = util.mal_userlist_sort[method_data[dict_methods["sort_by"]]] + new_dictionary["sort_by"] = mal.userlist_sort[method_data[dict_methods["sort_by"]]] new_dictionary["limit"] = get_int(method_name, "limit", method_data, dict_methods, 100, maximum=1000) self.methods.append((method_name, [new_dictionary])) @@ -688,7 +823,7 @@ class CollectionBuilder: self.methods.append((method_name, [new_dictionary])) else: raise Failed(f"Collection Error: {method_name} attribute is not a dictionary: {method_data}") - elif method_name in util.count_lists: + elif method_name in numbered_builders: list_count = util.regex_first_int(method_data, "List Size", default=10) if list_count < 1: logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 10") @@ -718,8 +853,8 @@ class CollectionBuilder: self.methods.append((method_name[:-8], values)) else: self.methods.append((method_name, values)) - elif method_name in util.tmdb_lists: - values = config.TMDb.validate_tmdb_list(util.get_int_list(method_data, f"TMDb {util.tmdb_type[method_name]} ID"), util.tmdb_type[method_name]) + elif method_name in tmdb.builders: + values = config.TMDb.validate_tmdb_list(util.get_int_list(method_data, f"TMDb {tmdb.type_map[method_name]} ID"), tmdb.type_map[method_name]) if method_name[-8:] == "_details": if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]: item = config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie) @@ -742,11 +877,11 @@ class CollectionBuilder: self.methods.append((method_name[:-8], values)) else: self.methods.append((method_name, values)) - elif method_name in util.all_lists: + elif method_name in all_builders: self.methods.append((method_name, util.get_list(method_data))) - elif method_name not in util.other_attributes: + elif method_name not in ignored_details: raise Failed(f"Collection Error: {method_name} attribute not supported") - elif method_name in util.all_lists or method_name in util.method_alias or method_name in util.plex_searches: + elif method_name in all_builders or method_name in method_alias or method_name in plex.searches: raise Failed(f"Collection Error: {method_name} attribute is blank") else: logger.warning(f"Collection Warning: {method_name} attribute is blank") @@ -814,11 +949,11 @@ class CollectionBuilder: if search_method == "limit": search_limit = search_data elif search_method == "sort_by": - search_sort = util.plex_sort[search_data] + search_sort = plex.sorts[search_data] else: search, modifier = os.path.splitext(str(search_method).lower()) - final_search = util.search_alias[search] if search in util.search_alias else search - final_mod = util.plex_modifiers[modifier] if modifier in util.plex_modifiers else "" + final_search = plex.search_translation[search] if search in plex.search_translation else search + final_mod = plex.modifiers[modifier] if modifier in plex.modifiers else "" final_method = f"{final_search}{final_mod}" search_terms[final_method] = search_data * 60000 if final_search == "duration" else search_data ors = "" diff --git a/modules/imdb.py b/modules/imdb.py index e5a00717..ad171288 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -6,6 +6,8 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = ["imdb_list", "imdb_id"] + class IMDbAPI: def __init__(self, config): self.config = config diff --git a/modules/letterboxd.py b/modules/letterboxd.py index 17ac2449..7b214a53 100644 --- a/modules/letterboxd.py +++ b/modules/letterboxd.py @@ -6,6 +6,8 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = ["letterboxd_list", "letterboxd_list_details"] + class LetterboxdAPI: def __init__(self, config): self.config = config diff --git a/modules/mal.py b/modules/mal.py index 063638fe..ec644a52 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -6,6 +6,73 @@ from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") +builders = [ + "mal_id", + "mal_all", + "mal_airing", + "mal_upcoming", + "mal_tv", + "mal_ova", + "mal_movie", + "mal_special", + "mal_popular", + "mal_favorite", + "mal_season", + "mal_suggested", + "mal_userlist" +] +mal_ranked_name = { + "mal_all": "all", + "mal_airing": "airing", + "mal_upcoming": "upcoming", + "mal_tv": "tv", + "mal_ova": "ova", + "mal_movie": "movie", + "mal_special": "special", + "mal_popular": "bypopularity", + "mal_favorite": "favorite" +} +season_sort = { + "anime_score": "anime_score", + "anime_num_list_users": "anime_num_list_users", + "score": "anime_score", + "members": "anime_num_list_users" +} +pretty_names = { + "anime_score": "Score", + "anime_num_list_users": "Members", + "list_score": "Score", + "list_updated_at": "Last Updated", + "anime_title": "Title", + "anime_start_date": "Start Date", + "all": "All Anime", + "watching": "Currently Watching", + "completed": "Completed", + "on_hold": "On Hold", + "dropped": "Dropped", + "plan_to_watch": "Plan to Watch" +} +userlist_sort = { + "score": "list_score", + "list_score": "list_score", + "last_updated": "list_updated_at", + "list_updated": "list_updated_at", + "list_updated_at": "list_updated_at", + "title": "anime_title", + "anime_title": "anime_title", + "start_date": "anime_start_date", + "anime_start_date": "anime_start_date" +} +userlist_status = [ + "all", + "watching", + "completed", + "on_hold", + "dropped", + "plan_to_watch" +] + + class MyAnimeListIDList: def __init__(self): self.ids = json.loads(requests.get("https://raw.githubusercontent.com/Fribb/anime-lists/master/animeMapping_full.json").content) @@ -155,14 +222,14 @@ class MyAnimeListAPI: mal_ids = [data] if status_message: logger.info(f"Processing {pretty}: {data}") - elif method in util.mal_ranked_name: - mal_ids = self.get_ranked(util.mal_ranked_name[method], data) + elif method in mal_ranked_name: + mal_ids = self.get_ranked(mal_ranked_name[method], data) if status_message: logger.info(f"Processing {pretty}: {data} Anime") elif method == "mal_season": mal_ids = self.get_season(data["season"], data["year"], data["sort_by"], data["limit"]) if status_message: - logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.mal_pretty[data['sort_by']]}") + logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}") elif method == "mal_suggested": mal_ids = self.get_suggestions(data) if status_message: @@ -170,7 +237,7 @@ class MyAnimeListAPI: elif method == "mal_userlist": mal_ids = self.get_userlist(data["username"], data["status"], data["sort_by"], data["limit"]) if status_message: - logger.info(f"Processing {pretty}: {data['limit']} Anime from {self.get_username() if data['username'] == '@me' else data['username']}'s {util.mal_pretty[data['status']]} list sorted by {util.mal_pretty[data['sort_by']]}") + logger.info(f"Processing {pretty}: {data['limit']} Anime from {self.get_username() if data['username'] == '@me' else data['username']}'s {pretty_names[data['status']]} list sorted by {pretty_names[data['sort_by']]}") else: raise Failed(f"MyAnimeList Error: Method {method} not supported") show_ids = [] diff --git a/modules/plex.py b/modules/plex.py index 0d672d06..34ad6c13 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -12,6 +12,91 @@ from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") +builders = ["plex_all", "plex_collection", "plex_collectionless", "plex_search",] +search_translation = { + "audio_language": "audioLanguage", + "content_rating": "contentRating", + "subtitle_language": "subtitleLanguage", + "added": "addedAt", + "originally_available": "originallyAvailableAt", + "rating": "userRating" +} +episode_sorting_options = {"default": "-1", "oldest": "0", "newest": "1"} +keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30} +delete_episodes_options = {"never": 0, "day": 1, "week": 7, "refresh": 100} +season_display_options = {"default": -1, "show": 0, "hide": 1} +episode_ordering_options = {"default": None, "tmdb_aired": "tmdbAiring", "tvdb_aired": "airing", "tvdb_dvd": "dvd", "tvdb_absolute": "absolute"} +plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", + "es-ES", "es-MX", "et-EE", "fa-IR", "fi-FI", "fr-CA", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID", + "it-IT", "ja-JP", "ko-KR", "lt-LT", "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", + "ru-RU", "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-HK", "zh-TW"] +metadata_language_options = {lang.lower(): lang for lang in plex_languages} +metadata_language_options["default"] = None +filter_alias = { + "actor": "actors", + "collection": "collections", + "content_rating": "contentRating", + "country": "countries", + "director": "directors", + "genre": "genres", + "originally_available": "originallyAvailableAt", + "tmdb_vote_count": "vote_count", + "writer": "writers" +} +searches = [ + "title", "title.and", "title.not", "title.begins", "title.ends", + "studio", "studio.and", "studio.not", "studio.begins", "studio.ends", + "actor", "actor.and", "actor.not", + "audio_language", "audio_language.and", "audio_language.not", + "collection", "collection.and", "collection.not", + "content_rating", "content_rating.and", "content_rating.not", + "country", "country.and", "country.not", + "director", "director.and", "director.not", + "genre", "genre.and", "genre.not", + "label", "label.and", "label.not", + "producer", "producer.and", "producer.not", + "subtitle_language", "subtitle_language.and", "subtitle_language.not", + "writer", "writer.and", "writer.not", + "decade", "resolution", + "added.before", "added.after", + "originally_available.before", "originally_available.after", + "duration.greater", "duration.less", + "rating.greater", "rating.less", + "year", "year.not", "year.greater", "year.less" +] +movie_only_searches = [ + "audio_language", "audio_language.and", "audio_language.not", + "country", "country.and", "country.not", + "subtitle_language", "subtitle_language.and", "subtitle_language.not", + "decade", "resolution", + "originally_available.before", "originally_available.after", + "duration.greater", "duration.less" +] +tmdb_searches = [ + "actor", "actor.and", "actor.not", + "director", "director.and", "director.not", + "producer", "producer.and", "producer.not", + "writer", "writer.and", "writer.not" +] +sorts = { + "title.asc": "titleSort:asc", "title.desc": "titleSort:desc", + "originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc", + "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", + "audience_rating.asc": "audienceRating:asc", "audience_rating.desc": "audienceRating:desc", + "duration.asc": "duration:asc", "duration.desc": "duration:desc", + "added.asc": "addedAt:asc", "added.desc": "addedAt:desc" +} +modifiers = { + ".and": "&", + ".not": "!", + ".begins": "<", + ".ends": ">", + ".before": "<<", + ".after": ">>", + ".greater": ">>", + ".less": "<<" +} + class PlexAPI: def __init__(self, params, TMDb, TVDb): try: @@ -98,7 +183,7 @@ class PlexAPI: else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)} def validate_search_list(self, data, search_name): - final_search = util.search_alias[search_name] if search_name in util.search_alias else search_name + final_search = search_translation[search_name] if search_name in search_translation else search_name search_choices = self.get_search_choices(final_search, key=final_search.endswith("Language")) valid_list = [] for value in util.get_list(data): @@ -160,7 +245,7 @@ class PlexAPI: for filter_method, filter_data in filters: modifier = filter_method[-4:] method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method - method_name = util.filter_alias[method] if method in util.filter_alias else method + method_name = filter_alias[method] if method in filter_alias else method if method_name == "max_age": threshold_date = datetime.now() - timedelta(days=filter_data) if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date: diff --git a/modules/tautulli.py b/modules/tautulli.py index 5b9bc517..c258c62e 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -5,6 +5,8 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = ["tautulli_popular", "tautulli_watched"] + class TautulliAPI: def __init__(self, params): try: diff --git a/modules/tmdb.py b/modules/tmdb.py index da8a98c9..4df51913 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -7,6 +7,107 @@ from tmdbv3api.exceptions import TMDbException logger = logging.getLogger("Plex Meta Manager") +builders = [ + "tmdb_actor", + "tmdb_actor_details", + "tmdb_collection", + "tmdb_collection_details", + "tmdb_company", + "tmdb_crew", + "tmdb_crew_details", + "tmdb_director", + "tmdb_director_details", + "tmdb_discover", + "tmdb_keyword", + "tmdb_list", + "tmdb_list_details", + "tmdb_movie", + "tmdb_movie_details", + "tmdb_network", + "tmdb_now_playing", + "tmdb_popular", + "tmdb_producer", + "tmdb_producer_details", + "tmdb_show", + "tmdb_show_details", + "tmdb_top_rated", + "tmdb_trending_daily", + "tmdb_trending_weekly", + "tmdb_writer", + "tmdb_writer_details" +] +type_map = { + "tmdb_actor": "Person", + "tmdb_actor_details": "Person", + "tmdb_collection": "Collection", + "tmdb_collection_details": "Collection", + "tmdb_company": "Company", + "tmdb_crew": "Person", + "tmdb_crew_details": "Person", + "tmdb_director": "Person", + "tmdb_director_details": "Person", + "tmdb_keyword": "Keyword", + "tmdb_list": "List", + "tmdb_list_details": "List", + "tmdb_movie": "Movie", + "tmdb_movie_details": "Movie", + "tmdb_network": "Network", + "tmdb_person": "Person", + "tmdb_producer": "Person", + "tmdb_producer_details": "Person", + "tmdb_show": "Show", + "tmdb_show_details": "Show", + "tmdb_writer": "Person", + "tmdb_writer_details": "Person" +} +discover_movie = [ + "language", "with_original_language", "region", "sort_by", + "certification_country", "certification", "certification.lte", "certification.gte", + "include_adult", + "primary_release_year", "primary_release_date.gte", "primary_release_date.lte", + "release_date.gte", "release_date.lte", "year", + "vote_count.gte", "vote_count.lte", + "vote_average.gte", "vote_average.lte", + "with_cast", "with_crew", "with_people", + "with_companies", + "with_genres", "without_genres", + "with_keywords", "without_keywords", + "with_runtime.gte", "with_runtime.lte" +] +discover_tv = [ + "language", "with_original_language", "timezone", "sort_by", + "air_date.gte", "air_date.lte", + "first_air_date.gte", "first_air_date.lte", "first_air_date_year", + "vote_count.gte", "vote_count.lte", + "vote_average.gte", "vote_average.lte", + "with_genres", "without_genres", + "with_keywords", "without_keywords", + "with_networks", "with_companies", + "with_runtime.gte", "with_runtime.lte", + "include_null_first_air_dates", + "screened_theatrically" +] +discover_dates = [ + "primary_release_date.gte", "primary_release_date.lte", + "release_date.gte", "release_date.lte", + "air_date.gte", "air_date.lte", + "first_air_date.gte", "first_air_date.lte" +] +discover_movie_sort = [ + "popularity.asc", "popularity.desc", + "release_date.asc", "release_date.desc", + "revenue.asc", "revenue.desc", + "primary_release_date.asc", "primary_release_date.desc", + "original_title.asc", "original_title.desc", + "vote_average.asc", "vote_average.desc", + "vote_count.asc", "vote_count.desc" +] +discover_tv_sort = [ + "vote_average.desc", "vote_average.asc", + "first_air_date.desc", "first_air_date.asc", + "popularity.desc", "popularity.asc" +] + class TMDbAPI: def __init__(self, params): self.TMDb = tmdbv3api.TMDb() @@ -156,7 +257,7 @@ class TMDbAPI: def get_discover(self, attrs, amount, is_movie): ids = [] count = 0 - for date_attr in util.discover_dates: + for date_attr in discover_dates: if date_attr in attrs: attrs[date_attr] = datetime.strftime(datetime.strptime(attrs[date_attr], "%m/%d/%Y"), "%Y-%m-%d") self.Discover.discover_movies(attrs) if is_movie else self.Discover.discover_tv_shows(attrs) diff --git a/modules/trakttv.py b/modules/trakttv.py index aecbe6b3..b24263ea 100644 --- a/modules/trakttv.py +++ b/modules/trakttv.py @@ -11,6 +11,18 @@ from trakt.objects.show import Show logger = logging.getLogger("Plex Meta Manager") +builders = [ + "trakt_collected", + "trakt_collection", + "trakt_list", + "trakt_list_details", + "trakt_popular", + "trakt_recommended", + "trakt_trending", + "trakt_watched", + "trakt_watchlist" +] + class TraktAPI: def __init__(self, params, authorization=None): self.base_url = "https://api.trakt.tv" diff --git a/modules/tvdb.py b/modules/tvdb.py index 4d7a5b8a..02e28a8c 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -6,6 +6,15 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +builders = [ + "tvdb_list", + "tvdb_list_details", + "tvdb_movie", + "tvdb_movie_details", + "tvdb_show", + "tvdb_show_details" +] + class TVDbObj: def __init__(self, tvdb_url, language, is_movie, TVDb): tvdb_url = tvdb_url.strip() diff --git a/modules/util.py b/modules/util.py index 24d3cba4..34bd631d 100644 --- a/modules/util.py +++ b/modules/util.py @@ -22,38 +22,6 @@ def retry_if_not_failed(exception): separating_character = "=" screen_width = 100 -method_alias = { - "actors": "actor", "role": "actor", "roles": "actor", - "content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating", - "countries": "country", - "decades": "decade", - "directors": "director", - "genres": "genre", - "labels": "label", - "studios": "studio", "network": "studio", "networks": "studio", - "producers": "producer", - "writers": "writer", - "years": "year" -} -search_alias = { - "audio_language": "audioLanguage", - "content_rating": "contentRating", - "subtitle_language": "subtitleLanguage", - "added": "addedAt", - "originally_available": "originallyAvailableAt", - "rating": "userRating" -} -filter_alias = { - "actor": "actors", - "collection": "collections", - "content_rating": "contentRating", - "country": "countries", - "director": "directors", - "genre": "genres", - "originally_available": "originallyAvailableAt", - "tmdb_vote_count": "vote_count", - "writer": "writers" -} days_alias = { "monday": 0, "mon": 0, "m": 0, "tuesday": 1, "tues": 1, "tue": 1, "tu": 1, "t": 1, @@ -170,64 +138,6 @@ pretty_names = { "tvdb_show": "TVDb Show", "tvdb_show_details": "TVDb Show" } -plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", "es-ES", - "es-MX", "et-EE", "fa-IR", "fi-FI", "fr-CA", "fr-FR", "he-IL", "hi-IN", "hu-HU", "id-ID", "it-IT", - "ja-JP", "ko-KR", "lt-LT", "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "ru-RU", - "sk-SK", "sv-SE", "th-TH", "tr-TR", "uk-UA", "vi-VN", "zh-CN", "zh-HK", "zh-TW"] -mal_ranked_name = { - "mal_all": "all", - "mal_airing": "airing", - "mal_upcoming": "upcoming", - "mal_tv": "tv", - "mal_ova": "ova", - "mal_movie": "movie", - "mal_special": "special", - "mal_popular": "bypopularity", - "mal_favorite": "favorite" -} -mal_season_sort = { - "anime_score": "anime_score", - "anime_num_list_users": "anime_num_list_users", - "score": "anime_score", - "members": "anime_num_list_users" -} -mal_pretty = { - "anime_score": "Score", - "anime_num_list_users": "Members", - "list_score": "Score", - "list_updated_at": "Last Updated", - "anime_title": "Title", - "anime_start_date": "Start Date", - "all": "All Anime", - "watching": "Currently Watching", - "completed": "Completed", - "on_hold": "On Hold", - "dropped": "Dropped", - "plan_to_watch": "Plan to Watch" -} -mal_userlist_sort = { - "score": "list_score", - "list_score": "list_score", - "last_updated": "list_updated_at", - "list_updated": "list_updated_at", - "list_updated_at": "list_updated_at", - "title": "anime_title", - "anime_title": "anime_title", - "start_date": "anime_start_date", - "anime_start_date": "anime_start_date" -} -mal_userlist_status = [ - "all", - "watching", - "completed", - "on_hold", - "dropped", - "plan_to_watch" -] -anilist_pretty = { - "score": "Average Score", - "popular": "Popularity" -} pretty_ids = { "anidbid": "AniDB", "imdbid": "IMDb", @@ -236,354 +146,6 @@ pretty_ids = { "thetvdb_id": "TVDb", "tvdbid": "TVDb" } -all_lists = [ - "anidb_id", - "anidb_relation", - "anidb_popular", - "anilist_genre", - "anilist_id", - "anilist_popular", - "anilist_relations", - "anilist_season", - "anilist_studio", - "anilist_tag", - "anilist_top_rated", - "imdb_list", - "imdb_id", - "letterboxd_list", - "letterboxd_list_details", - "mal_id", - "mal_all", - "mal_airing", - "mal_upcoming", - "mal_tv", - "mal_ova", - "mal_movie", - "mal_special", - "mal_popular", - "mal_favorite", - "mal_season", - "mal_suggested", - "mal_userlist", - "plex_collection", - "plex_search", - "tautulli_popular", - "tautulli_watched", - "tmdb_actor", - "tmdb_actor_details", - "tmdb_collection", - "tmdb_collection_details", - "tmdb_company", - "tmdb_crew", - "tmdb_crew_details", - "tmdb_director", - "tmdb_director_details", - "tmdb_discover", - "tmdb_keyword", - "tmdb_list", - "tmdb_list_details", - "tmdb_movie", - "tmdb_movie_details", - "tmdb_network", - "tmdb_now_playing", - "tmdb_popular", - "tmdb_producer", - "tmdb_producer_details", - "tmdb_show", - "tmdb_show_details", - "tmdb_top_rated", - "tmdb_trending_daily", - "tmdb_trending_weekly", - "tmdb_writer", - "tmdb_writer_details", - "trakt_collected", - "trakt_collection", - "trakt_list", - "trakt_list_details", - "trakt_popular", - "trakt_recommended", - "trakt_trending", - "trakt_watched", - "trakt_watchlist", - "tvdb_list", - "tvdb_list_details", - "tvdb_movie", - "tvdb_movie_details", - "tvdb_show", - "tvdb_show_details" -] -collectionless_lists = [ - "sort_title", "content_rating", - "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", - "collection_order", "plex_collectionless", - "url_poster", "tmdb_poster", "tmdb_profile", "file_poster", - "url_background", "file_background", - "name_mapping", "label", "label_sync_mode", "test" -] -other_attributes = [ - "run_again", - "schedule", - "sync_mode", - "template", - "test", - "tmdb_person" -] -dictionary_lists = [ - "filters", - "anilist_genre", - "anilist_season", - "anilist_tag", - "mal_season", - "mal_userlist", - "plex_collectionless", - "plex_search", - "tautulli_popular", - "tautulli_watched", - "tmdb_discover" -] -show_only_lists = [ - "tmdb_network", - "tmdb_show", - "tmdb_show_details", - "tvdb_show", - "tvdb_show_details" -] -movie_only_lists = [ - "letterboxd_list", - "letterboxd_list_details", - "tmdb_collection", - "tmdb_collection_details", - "tmdb_movie", - "tmdb_movie_details", - "tmdb_now_playing", - "tvdb_movie", - "tvdb_movie_details" -] -count_lists = [ - "anidb_popular", - "anilist_popular", - "anilist_top_rated", - "mal_all", - "mal_airing", - "mal_upcoming", - "mal_tv", - "mal_ova", - "mal_movie", - "mal_special", - "mal_popular", - "mal_favorite", - "mal_suggested", - "tmdb_popular", - "tmdb_top_rated", - "tmdb_now_playing", - "tmdb_trending_daily", - "tmdb_trending_weekly", - "trakt_trending", - "trakt_popular", - "trakt_recommended", - "trakt_watched", - "trakt_collected" -] -tmdb_lists = [ - "tmdb_actor", - "tmdb_actor_details", - "tmdb_collection", - "tmdb_collection_details", - "tmdb_company", - "tmdb_crew", - "tmdb_crew_details", - "tmdb_director", - "tmdb_director_details", - "tmdb_discover", - "tmdb_keyword", - "tmdb_list", - "tmdb_list_details", - "tmdb_movie", - "tmdb_movie_details", - "tmdb_network", - "tmdb_now_playing", - "tmdb_popular", - "tmdb_producer", - "tmdb_producer_details", - "tmdb_show", - "tmdb_show_details", - "tmdb_top_rated", - "tmdb_trending_daily", - "tmdb_trending_weekly", - "tmdb_writer", - "tmdb_writer_details" -] -tmdb_type = { - "tmdb_actor": "Person", - "tmdb_actor_details": "Person", - "tmdb_collection": "Collection", - "tmdb_collection_details": "Collection", - "tmdb_company": "Company", - "tmdb_crew": "Person", - "tmdb_crew_details": "Person", - "tmdb_director": "Person", - "tmdb_director_details": "Person", - "tmdb_keyword": "Keyword", - "tmdb_list": "List", - "tmdb_list_details": "List", - "tmdb_movie": "Movie", - "tmdb_movie_details": "Movie", - "tmdb_network": "Network", - "tmdb_person": "Person", - "tmdb_producer": "Person", - "tmdb_producer_details": "Person", - "tmdb_show": "Show", - "tmdb_show_details": "Show", - "tmdb_writer": "Person", - "tmdb_writer_details": "Person" -} -plex_searches = [ - "title", "title.and", "title.not", "title.begins", "title.ends", - "studio", "studio.and", "studio.not", "studio.begins", "studio.ends", - "actor", "actor.and", "actor.not", - "audio_language", "audio_language.and", "audio_language.not", - "collection", "collection.and", "collection.not", - "content_rating", "content_rating.and", "content_rating.not", - "country", "country.and", "country.not", - "director", "director.and", "director.not", - "genre", "genre.and", "genre.not", - "label", "label.and", "label.not", - "producer", "producer.and", "producer.not", - "subtitle_language", "subtitle_language.and", "subtitle_language.not", - "writer", "writer.and", "writer.not", - "decade", "resolution", - "added.before", "added.after", - "originally_available.before", "originally_available.after", - "duration.greater", "duration.less", - "rating.greater", "rating.less", - "year", "year.not", "year.greater", "year.less" -] -plex_sort = { - "title.asc": "titleSort:asc", "title.desc": "titleSort:desc", - "originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc", - "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", - "audience_rating.asc": "audienceRating:asc", "audience_rating.desc": "audienceRating:desc", - "duration.asc": "duration:asc", "duration.desc": "duration:desc", - "added.asc": "addedAt:asc", "added.desc": "addedAt:desc" -} -plex_modifiers = { - ".and": "&", - ".not": "!", - ".begins": "<", - ".ends": ">", - ".before": "<<", - ".after": ">>", - ".greater": ">>", - ".less": "<<" -} -movie_only_searches = [ - "audio_language", "audio_language.and", "audio_language.not", - "country", "country.and", "country.not", - "subtitle_language", "subtitle_language.and", "subtitle_language.not", - "decade", "resolution", - "originally_available.before", "originally_available.after", - "duration.greater", "duration.less" -] -tmdb_searches = [ - "actor", "actor.and", "actor.not", - "director", "director.and", "director.not", - "producer", "producer.and", "producer.not", - "writer", "writer.and", "writer.not" -] -all_filters = [ - "actor", "actor.not", - "audio_language", "audio_language.not", - "audio_track_title", "audio_track_title.not", - "collection", "collection.not", - "content_rating", "content_rating.not", - "country", "country.not", - "director", "director.not", - "genre", "genre.not", - "max_age", - "originally_available.gte", "originally_available.lte", - "tmdb_vote_count.gte", "tmdb_vote_count.lte", - "duration.gte", "duration.lte", - "original_language", "original_language.not", - "rating.gte", "rating.lte", - "studio", "studio.not", - "subtitle_language", "subtitle_language.not", - "video_resolution", "video_resolution.not", - "writer", "writer.not", - "year", "year.gte", "year.lte", "year.not" -] -movie_only_filters = [ - "audio_language", "audio_language.not", - "audio_track_title", "audio_track_title.not", - "country", "country.not", - "director", "director.not", - "duration.gte", "duration.lte", - "original_language", "original_language.not", - "subtitle_language", "subtitle_language.not", - "video_resolution", "video_resolution.not", - "writer", "writer.not" -] -boolean_details = [ - "add_to_arr", - "show_filtered", - "show_missing", - "save_missing" -] -all_details = [ - "sort_title", "content_rating", - "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", "tvdb_description", "trakt_description", "letterboxd_description", - "collection_mode", "collection_order", - "url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster", - "url_background", "tmdb_background", "tvdb_background", "file_background", - "name_mapping", "add_to_arr", "arr_tag", "label", - "show_filtered", "show_missing", "save_missing" -] -discover_movie = [ - "language", "with_original_language", "region", "sort_by", - "certification_country", "certification", "certification.lte", "certification.gte", - "include_adult", - "primary_release_year", "primary_release_date.gte", "primary_release_date.lte", - "release_date.gte", "release_date.lte", "year", - "vote_count.gte", "vote_count.lte", - "vote_average.gte", "vote_average.lte", - "with_cast", "with_crew", "with_people", - "with_companies", - "with_genres", "without_genres", - "with_keywords", "without_keywords", - "with_runtime.gte", "with_runtime.lte" -] -discover_tv = [ - "language", "with_original_language", "timezone", "sort_by", - "air_date.gte", "air_date.lte", - "first_air_date.gte", "first_air_date.lte", "first_air_date_year", - "vote_count.gte", "vote_count.lte", - "vote_average.gte", "vote_average.lte", - "with_genres", "without_genres", - "with_keywords", "without_keywords", - "with_networks", "with_companies", - "with_runtime.gte", "with_runtime.lte", - "include_null_first_air_dates", - "screened_theatrically" -] -discover_dates = [ - "primary_release_date.gte", "primary_release_date.lte", - "release_date.gte", "release_date.lte", - "air_date.gte", "air_date.lte", - "first_air_date.gte", "first_air_date.lte" -] -discover_movie_sort = [ - "popularity.asc", "popularity.desc", - "release_date.asc", "release_date.desc", - "revenue.asc", "revenue.desc", - "primary_release_date.asc", "primary_release_date.desc", - "original_title.asc", "original_title.desc", - "vote_average.asc", "vote_average.desc", - "vote_count.asc", "vote_count.desc" -] -discover_tv_sort = [ - "vote_average.desc", "vote_average.asc", - "first_air_date.desc", "first_air_date.asc", - "popularity.desc", "popularity.asc" -] def tab_new_lines(data): return str(data).replace("\n", "\n|\t ") if "\n" in str(data) else str(data) From 07821cff0ecf24256bd18ab2e0fc3934d7486dd3 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 16:50:34 -0400 Subject: [PATCH 04/28] fix for #151 --- modules/builder.py | 12 ++++++++---- modules/plex.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 0668013e..bf41d897 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -188,7 +188,7 @@ class CollectionBuilder: else: for tm in data_template: if not data_template[tm]: - raise Failed(f"Collection Error: template sub-attribute {data_template[tm]} is blank") + raise Failed(f"Collection Error: template sub-attribute {tm} is blank") template_name = data_template["name"] template = self.library.templates[template_name] @@ -230,18 +230,22 @@ class CollectionBuilder: if option not in data_template and f"<<{option}>>" in txt: raise Failed("remove attribute") for template_method in data_template: - if template_method != "name" and f"<<{template_method}>>" in txt: + if template_method != "name" and txt == f"<<{template_method}>>": + txt = data_template[template_method] + elif template_method != "name" and f"<<{template_method}>>" in txt: txt = txt.replace(f"<<{template_method}>>", str(data_template[template_method])) if "<>" in txt: txt = txt.replace("<>", str(self.name)) for dm in default: - if f"<<{dm}>>" in txt: + if txt == f"<<{dm}>>": + txt = default[dm] + elif f"<<{dm}>>" in txt: txt = txt.replace(f"<<{dm}>>", str(default[dm])) if txt in ["true", "True"]: return True elif txt in ["false", "False"]: return False else: try: return int(txt) - except ValueError: return txt + except (ValueError, TypeError): return txt try: if isinstance(attr_data, dict): final_data = {} diff --git a/modules/plex.py b/modules/plex.py index 34ad6c13..f242b563 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -190,7 +190,7 @@ class PlexAPI: if str(value).lower() in search_choices: valid_list.append(search_choices[str(value).lower()]) else: - raise Failed(f"Plex Error: {search_name}: {value} not found") + logger.error(f"Plex Error: {search_name}: {value} not found") return valid_list def get_all_collections(self): From d587ca3873a001685666bae82e0118d9b92d9502 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 16:52:24 -0400 Subject: [PATCH 05/28] fix for #154 --- modules/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/config.py b/modules/config.py index 0598c178..761b61d6 100644 --- a/modules/config.py +++ b/modules/config.py @@ -33,7 +33,7 @@ class Config: yaml.YAML().allow_duplicate_keys = True try: - new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) + new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8")) def replace_attr(all_data, attr, par): if "settings" not in all_data: all_data["settings"] = {} @@ -75,10 +75,13 @@ class Config: if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "mal" in new_config: new_config["mal"] = new_config.pop("mal") - yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) + yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=ind, block_seq_indent=bsi) self.data = new_config except yaml.scanner.ScannerError as e: raise Failed(f"YAML Error: {util.tab_new_lines(e)}") + except Exception as e: + util.print_stacktrace() + raise Failed(f"YAML Error: {e}") def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True): endline = "" From 68a2699ce2f8e8b7c208c715d1dc7e96a1ee2d28 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 16:53:23 -0400 Subject: [PATCH 06/28] doc edits --- README.md | 3 ++- modules/config.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 99915075..fcf0da53 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ The script is designed to work with most Metadata agents including the new Plex * [Wiki](https://github.com/meisnate12/Plex-Meta-Manager/wiki) * [Local Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Installation) -* [Docker Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker) +* [Docker Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker-Installation) +* [unRAID Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/unRAID-Installation) ## Support diff --git a/modules/config.py b/modules/config.py index 761b61d6..f339a6b7 100644 --- a/modules/config.py +++ b/modules/config.py @@ -128,7 +128,7 @@ class Config: if attribute in data and data[attribute]: message = f"neither {data[attribute]} or the default path {default} could be found" else: - message = f"no {text} found and the default path {default} could be found" + message = f"no {text} found and the default path {default} could not be found" default = None if default is not None or default_is_none: message = message + f" using {default} as default" From 840bd253641da63c254c536519e704c0804223a8 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 30 Mar 2021 20:33:08 -0400 Subject: [PATCH 07/28] output metadata path for each library --- modules/plex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/plex.py b/modules/plex.py index f242b563..771dd53e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -113,6 +113,7 @@ class PlexAPI: self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"] and ((self.is_movie and isinstance(s, MovieSection)) or (self.is_show and isinstance(s, ShowSection)))), None) if not self.Plex: raise Failed(f"Plex Error: Plex Library {params['name']} not found") + logger.info(f"Using Metadata File: {params['metadata_path']}") try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) except yaml.scanner.ScannerError as ye: From 19d7da9e7ace9445c355c495fc7cc2dbf57ef2a7 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 31 Mar 2021 00:09:56 -0400 Subject: [PATCH 08/28] fix for #151 --- modules/builder.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index bf41d897..fb58abbe 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -454,15 +454,6 @@ class CollectionBuilder: self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}])) elif method_name in ["year", "year.not"]: self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) - elif method_name in plex.tmdb_searches: - final_values = [] - for value in util.get_list(method_data): - if value.lower() == "tmdb" and "tmdb_person" in self.details: - for name in self.details["tmdb_person"]: - final_values.append(name) - else: - final_values.append(value) - self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}])) elif method_name in plex.searches: if method_name in plex.tmdb_searches: final_values = [] @@ -474,7 +465,12 @@ class CollectionBuilder: final_values.append(value) else: final_values = method_data - self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}])) + search = os.path.splitext(method_name)[0] + valid_values = self.library.validate_search_list(final_values, search) + if valid_values: + self.methods.append(("plex_search", [{method_name: valid_values}])) + else: + logger.warning(f"Collection Warning: No valid {search} values found in {final_values}") elif method_name == "plex_all": self.methods.append((method_name, [""])) elif method_name == "plex_collection": @@ -630,7 +626,11 @@ class CollectionBuilder: final_values.append(value) else: final_values = search_data - searches[search_final] = self.library.validate_search_list(final_values, search) + valid_values = self.library.validate_search_list(final_values, search) + if valid_values: + searches[search_final] = valid_values + else: + logger.warning(f"Collection Warning: No valid {search} values found in {final_values}") elif (search == "decade" and modifier in [""]) \ or (search == "year" and modifier in [".greater", ".less"]): searches[search_final] = [util.check_year(search_data, current_year, search_final)] From 0fef8d51d2a7767625581ac88d3ac8521ecb3688 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 31 Mar 2021 00:20:20 -0400 Subject: [PATCH 09/28] added --resume --- modules/config.py | 14 ++++++++++++-- modules/plex.py | 1 + plex_meta_manager.py | 12 +++++++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/modules/config.py b/modules/config.py index f339a6b7..1c23ef22 100644 --- a/modules/config.py +++ b/modules/config.py @@ -389,7 +389,7 @@ class Config: util.separator() - def update_libraries(self, test, requested_collections): + def update_libraries(self, test, requested_collections, resume_from): for library in self.libraries: os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") @@ -398,7 +398,7 @@ class Config: util.separator(f"Mapping {library.name} Library") logger.info("") movie_map, show_map = self.map_guids(library) - if not test: + if not test and not resume_from: if library.mass_genre_update: self.mass_metadata(library, movie_map, show_map) try: library.update_metadata(self.TMDb, test) @@ -406,6 +406,9 @@ class Config: logger.info("") util.separator(f"{library.name} Library {'Test ' if test else ''}Collections") collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections + if resume_from and resume_from not in collections: + logger.warning(f"Collection: {resume_from} not in {library.name}") + continue if collections: for mapping_name, collection_attrs in collections.items(): if test and ("test" not in collection_attrs or collection_attrs["test"] is not True): @@ -423,6 +426,13 @@ class Config: if no_template_test: continue try: + if resume_from and resume_from != mapping_name: + continue + elif resume_from == mapping_name: + resume_from = None + logger.info("") + util.separator(f"Resuming Collections") + logger.info("") util.separator(f"{mapping_name} Collection") logger.info("") diff --git a/modules/plex.py b/modules/plex.py index 771dd53e..5357e86a 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -113,6 +113,7 @@ class PlexAPI: self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"] and ((self.is_movie and isinstance(s, MovieSection)) or (self.is_show and isinstance(s, ShowSection)))), None) if not self.Plex: raise Failed(f"Plex Error: Plex Library {params['name']} not found") + logger.info(f"Using Metadata File: {params['metadata_path']}") try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 91f84025..c02e3a0c 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -13,6 +13,7 @@ parser.add_argument("--my-tests", dest="tests", help=argparse.SUPPRESS, action=" parser.add_argument("--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False) parser.add_argument("-c", "--config", dest="config", help="Run with desired *.yml file", type=str) parser.add_argument("-t", "--time", dest="time", help="Time to update each day use format HH:MM (Default: 03:00)", default="03:00", type=str) +parser.add_argument("-re", "--resume", dest="resume", help="Resume collection run from a specific collection", type=str) parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False) parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False) parser.add_argument("-cl", "--collection", "--collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str) @@ -37,6 +38,7 @@ test = check_bool("PMM_TEST", args.test) debug = check_bool("PMM_DEBUG", args.debug) run = check_bool("PMM_RUN", args.run) collections = os.environ.get("PMM_COLLECTIONS") if os.environ.get("PMM_COLLECTIONS") else args.collections +resume = os.environ.get("PMM_RESUME") if os.environ.get("PMM_RESUME") else args.resume time_to_run = os.environ.get("PMM_TIME") if os.environ.get("PMM_TIME") else args.time if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", time_to_run): @@ -94,7 +96,7 @@ if my_tests: tests.run_tests(default_dir) sys.exit(0) -def start(config_path, is_test, daily, collections_to_run): +def start(config_path, is_test, daily, collections_to_run, resume_from): if daily: start_type = "Daily " elif is_test: start_type = "Test " elif collections_to_run: start_type = "Collections " @@ -103,7 +105,7 @@ def start(config_path, is_test, daily, collections_to_run): util.separator(f"Starting {start_type}Run") try: config = Config(default_dir, config_path) - config.update_libraries(is_test, collections_to_run) + config.update_libraries(is_test, collections_to_run, resume_from) except Exception as e: util.print_stacktrace() logger.critical(e) @@ -111,11 +113,11 @@ def start(config_path, is_test, daily, collections_to_run): util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}") try: - if run or test or collections: - start(config_file, test, False, collections) + if run or test or collections or resume: + start(config_file, test, False, collections, resume) else: length = 0 - schedule.every().day.at(time_to_run).do(start, config_file, False, True, None) + schedule.every().day.at(time_to_run).do(start, config_file, False, True, None, None) while True: schedule.run_pending() current = datetime.now().strftime("%H:%M") From b08c7b7dc1c53a5e6a006b946fb7b6cd2c5fc0b5 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 31 Mar 2021 21:02:51 -0400 Subject: [PATCH 10/28] ignore None attributesin filters --- modules/plex.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/plex.py b/modules/plex.py index 5357e86a..7156cc4e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -300,9 +300,7 @@ class PlexAPI: attr = tmdb_item.vote_count else: attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name) - if attr is None: - attr = 0 - if (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): + if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): match = False break else: From 0839d2bb44a7f14b13c3580313a610ababdf75e5 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 1 Apr 2021 11:34:02 -0400 Subject: [PATCH 11/28] added network to plex_search --- modules/builder.py | 11 ++++++++--- modules/config.py | 2 +- modules/plex.py | 11 +++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index fb58abbe..4f9f07d7 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -16,7 +16,8 @@ method_alias = { "directors": "director", "genres": "genre", "labels": "label", - "studios": "studio", "network": "studio", "networks": "studio", + "studios": "studio", + "networks": "network", "producers": "producer", "writers": "writer", "years": "year" @@ -371,6 +372,8 @@ class CollectionBuilder: raise Failed(f"Collection Error: {method_name} attribute only works for movie libraries") elif method_name in plex.movie_only_searches and self.library.is_show: raise Failed(f"Collection Error: {method_name} plex search only works for movie libraries") + elif method_name in plex.show_only_searches and self.library.is_movie: + raise Failed(f"Collection Error: {method_name} plex search only works for show libraries") elif method_name not in collectionless_details and self.collectionless: raise Failed(f"Collection Error: {method_name} attribute does not work for Collectionless collection") elif method_name == "summary": @@ -597,6 +600,8 @@ class CollectionBuilder: search_final = f"{search}{modifier}" if search_final in plex.movie_only_searches and self.library.is_show: raise Failed(f"Collection Error: {search_final} plex search attribute only works for movie libraries") + if search_final in plex.show_only_searches and self.library.is_movie: + raise Failed(f"Collection Error: {search_final} plex search attribute only works for show libraries") elif search_data is None: raise Failed(f"Collection Error: {search_final} plex search attribute is blank") elif search == "sort_by": @@ -614,7 +619,7 @@ class CollectionBuilder: elif search == "title" and modifier in ["", ".and", ".not", ".begins", ".ends"]: searches[search_final] = util.get_list(search_data, split=False) elif (search == "studio" and modifier in ["", ".and", ".not", ".begins", ".ends"]) \ - or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \ + or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \ or (search == "resolution" and modifier in [""]): if search_final in plex.tmdb_searches: final_values = [] @@ -641,7 +646,7 @@ class CollectionBuilder: elif search == "year" and modifier in ["", ".not"]: searches[search_final] = util.get_year_list(search_data, current_year, search_final) elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ - or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ + or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ or (search in ["resolution", "decade"] and modifier not in [""]) \ or (search in ["added", "originally_available"] and modifier not in [".before", ".after"]) \ or (search in ["duration", "rating"] and modifier not in [".greater", ".less"]) \ diff --git a/modules/config.py b/modules/config.py index 1c23ef22..5c26dfd0 100644 --- a/modules/config.py +++ b/modules/config.py @@ -385,7 +385,7 @@ class Config: if len(self.libraries) > 0: logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful") else: - raise Failed("Plex Error: No Plex libraries were found") + raise Failed("Plex Error: No Plex libraries were connected to") util.separator() diff --git a/modules/plex.py b/modules/plex.py index 7156cc4e..6781fd5a 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -54,6 +54,7 @@ searches = [ "director", "director.and", "director.not", "genre", "genre.and", "genre.not", "label", "label.and", "label.not", + "network", "network.and", "network.not", "producer", "producer.and", "producer.not", "subtitle_language", "subtitle_language.and", "subtitle_language.not", "writer", "writer.and", "writer.not", @@ -72,6 +73,9 @@ movie_only_searches = [ "originally_available.before", "originally_available.after", "duration.greater", "duration.less" ] +show_only_searches = [ + "network", "network.and", "network.not", +] tmdb_searches = [ "actor", "actor.and", "actor.not", "director", "director.and", "director.not", @@ -181,8 +185,11 @@ class PlexAPI: return self.PlexServer.search(data) def get_search_choices(self, search_name, key=False): - if key: return {c.key.lower(): c.key for c in self.Plex.listFilterChoices(search_name)} - else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)} + try: + if key: return {c.key.lower(): c.key for c in self.Plex.listFilterChoices(search_name)} + else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)} + except NotFound: + raise Failed(f"Collection Error: plex search attribute: {search_name} only supported with Plex's New TV Agent") def validate_search_list(self, data, search_name): final_search = search_translation[search_name] if search_name in search_translation else search_name From 1f9e976eb34068c25e3b3f0ee8f57abc4ee8d4ac Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 1 Apr 2021 12:32:41 -0400 Subject: [PATCH 12/28] added critic_rating and audience_rating to plex_search and filters --- modules/builder.py | 18 +++++++++++------- modules/plex.py | 30 +++++++++++++++++++++--------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 4f9f07d7..5f2d46be 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -16,6 +16,7 @@ method_alias = { "directors": "director", "genres": "genre", "labels": "label", + "rating": "critic_rating", "studios": "studio", "networks": "network", "producers": "producer", @@ -124,7 +125,8 @@ all_filters = [ "tmdb_vote_count.gte", "tmdb_vote_count.lte", "duration.gte", "duration.lte", "original_language", "original_language.not", - "rating.gte", "rating.lte", + "audience_rating.gte", "audience_rating.lte", + "critic_rating.gte", "critic_rating.lte", "studio", "studio.not", "subtitle_language", "subtitle_language.not", "video_resolution", "video_resolution.not", @@ -542,11 +544,13 @@ class CollectionBuilder: return default_in if method_name == "filters": for filter_name, filter_data in method_data.items(): - if filter_name.lower() in method_alias or (filter_name.lower().endswith(".not") and filter_name.lower()[:-4] in method_alias): - filter_method = (method_alias[filter_name.lower()[:-4]] + filter_name.lower()[-4:]) if filter_name.lower().endswith(".not") else method_alias[filter_name.lower()] + modifier = filter_name[-4:].lower() + method = filter_name[:-4].lower() if modifier in [".not", ".lte", ".gte"] else filter_name.lower() + if method in method_alias: + filter_method = f"{method_alias[method]}{modifier}" logger.warning(f"Collection Warning: {filter_name} filter will run as {filter_method}") else: - filter_method = filter_name.lower() + filter_method = f"{method}{modifier}" if filter_method in movie_only_filters and self.library.is_show: raise Failed(f"Collection Error: {filter_method} filter only works for movie libraries") elif filter_data is None: @@ -557,7 +561,7 @@ class CollectionBuilder: valid_data = util.check_number(filter_data, f"{filter_method} filter", minimum=1) elif filter_method in ["year.gte", "year.lte"]: valid_data = util.check_year(filter_data, current_year, f"{filter_method} filter") - elif filter_method in ["rating.gte", "rating.lte"]: + elif filter_method in ["audience_rating.gte", "audience_rating.lte", "critic_rating.gte", "critic_rating.lte"]: valid_data = util.check_number(filter_data, f"{filter_method} filter", number_type="float", minimum=0.1, maximum=10) elif filter_method in ["originally_available.gte", "originally_available.lte"]: valid_data = util.check_date(filter_data, f"{filter_method} filter") @@ -639,7 +643,7 @@ class CollectionBuilder: elif (search == "decade" and modifier in [""]) \ or (search == "year" and modifier in [".greater", ".less"]): searches[search_final] = [util.check_year(search_data, current_year, search_final)] - elif search in ["added", "originally_available"] and modifier in [".before", ".after"]: + elif search in ["added", "originally_available"] and modifier in ["", ".not", ".before", ".after"]: searches[search_final] = [util.check_date(search_data, search_final, return_string=True, plex_date=True)] elif search in ["duration", "rating"] and modifier in [".greater", ".less"]: searches[search_final] = [util.check_number(search_data, search_final, minimum=0)] @@ -648,7 +652,7 @@ class CollectionBuilder: elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ or (search in ["resolution", "decade"] and modifier not in [""]) \ - or (search in ["added", "originally_available"] and modifier not in [".before", ".after"]) \ + or (search in ["added", "originally_available"] and modifier not in ["", ".not", ".before", ".after"]) \ or (search in ["duration", "rating"] and modifier not in [".greater", ".less"]) \ or (search in ["year"] and modifier not in ["", ".not", ".greater", ".less"]): raise Failed(f"Collection Error: modifier: {modifier} not supported with the {search} plex search attribute") diff --git a/modules/plex.py b/modules/plex.py index 6781fd5a..16159142 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -19,7 +19,8 @@ search_translation = { "subtitle_language": "subtitleLanguage", "added": "addedAt", "originally_available": "originallyAvailableAt", - "rating": "userRating" + "audience_rating": "audienceRating", + "critic_rating": "rating" } episode_sorting_options = {"default": "-1", "oldest": "0", "newest": "1"} keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30} @@ -62,7 +63,8 @@ searches = [ "added.before", "added.after", "originally_available.before", "originally_available.after", "duration.greater", "duration.less", - "rating.greater", "rating.less", + "audience_rating.greater", "audience_rating.less", + "critic_rating.greater", "critic_rating.less", "year", "year.not", "year.greater", "year.less" ] movie_only_searches = [ @@ -406,21 +408,31 @@ class PlexAPI: updated = False edits = {} - def add_edit(name, current, group, alias, key=None, value=None): + def add_edit(name, current, group, alias, key=None, value=None, var_type="str"): if value or name in alias: if value or group[alias[name]]: if key is None: key = name if value is None: value = group[alias[name]] - if str(current) != str(value): - edits[f"{key}.value"] = value - edits[f"{key}.locked"] = 1 - logger.info(f"Detail: {name} updated to {value}") + try: + if var_type == "date": + final_value = util.check_date(value, name, return_string=True, plex_date=True) + elif var_type == "float": + final_value = util.check_number(value, name, number_type="float", minimum=0, maximum=10) + else: + final_value = value + if str(current) != str(final_value): + edits[f"{key}.value"] = final_value + edits[f"{key}.locked"] = 1 + logger.info(f"Detail: {name} updated to {final_value}") + except Failed as ee: + logger.error(ee) else: logger.error(f"Metadata Error: {name} attribute is blank") add_edit("title", item.title, meta, methods, value=title) add_edit("sort_title", item.titleSort, meta, methods, key="titleSort") - add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available) - add_edit("rating", item.rating, meta, methods, value=rating) + add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date") + add_edit("critic_rating", item.rating, meta, methods, value=rating, key="rating", var_type="float") + add_edit("audience_rating", item.audienceRating, meta, methods, key="audienceRating", var_type="float") add_edit("content_rating", item.contentRating, meta, methods, key="contentRating") add_edit("original_title", item.originalTitle, meta, methods, key="originalTitle", value=original_title) add_edit("studio", item.studio, meta, methods, value=studio) From 2bf33bc0cfec858d41162d6711b237fce8b0009c Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 1 Apr 2021 15:07:12 -0400 Subject: [PATCH 13/28] fix for #157 --- modules/builder.py | 2 +- modules/plex.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 5f2d46be..bc04c0df 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -502,7 +502,7 @@ class CollectionBuilder: self.summaries[method_name] = item.description self.methods.append((method_name[:-8], valid_list)) elif method_name in ["trakt_watchlist", "trakt_collection"]: - self.methods.append((method_name, config.Trakt.validate_trakt(method_name[6:], util.get_list(method_data), self.library.is_movie))) + self.methods.append((method_name, config.Trakt.validate_trakt(util.get_list(method_data), trakt_type=method_name[6:], is_movie=self.library.is_movie))) elif method_name == "imdb_list": new_list = [] for imdb_list in util.get_list(method_data, split=False): diff --git a/modules/plex.py b/modules/plex.py index 16159142..815b42b2 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -453,10 +453,10 @@ class PlexAPI: def add_advanced_edit(attr, options, key=None, show_library=False): if key is None: key = attr - if show_library and not self.is_show: - logger.error(f"Metadata Error: {attr} attribute only works for show libraries") - elif attr in methods: - if meta[methods[attr]]: + if attr in methods: + if show_library and not self.is_show: + logger.error(f"Metadata Error: {attr} attribute only works for show libraries") + elif meta[methods[attr]]: method_data = str(meta[methods[attr]]).lower() if method_data in options and getattr(item, key) != options[method_data]: advance_edits[key] = options[method_data] From 81071cfda0718ce1f5b116b261f64719fc12a7e3 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 1 Apr 2021 16:11:35 -0400 Subject: [PATCH 14/28] minor fixes --- modules/builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index bc04c0df..032be01e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -234,7 +234,7 @@ class CollectionBuilder: raise Failed("remove attribute") for template_method in data_template: if template_method != "name" and txt == f"<<{template_method}>>": - txt = data_template[template_method] + return data_template[template_method] elif template_method != "name" and f"<<{template_method}>>" in txt: txt = txt.replace(f"<<{template_method}>>", str(data_template[template_method])) if "<>" in txt: @@ -531,7 +531,7 @@ class CollectionBuilder: if isinstance(method_data, dict): def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum=None): if method not in methods_in: - logger.warning(f"Collection Warning: {parent} {methods_in[method]} attribute not found using {default_in} as default") + logger.warning(f"Collection Warning: {parent} {method} attribute not found using {default_in} as default") elif not data_in[methods_in[method]]: logger.warning(f"Collection Warning: {parent} {methods_in[method]} attribute is blank using {default_in} as default") elif isinstance(data_in[methods_in[method]], int) and data_in[methods_in[method]] >= minimum: From 5ce3f3986af8b825b0f466f19db1e43de97f775d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 1 Apr 2021 16:31:16 -0400 Subject: [PATCH 15/28] added arr_folder collection detail #158 --- modules/builder.py | 7 ++++--- modules/radarr.py | 4 ++-- modules/sonarr.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 032be01e..f2114182 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -86,7 +86,7 @@ all_details = [ "collection_mode", "collection_order", "url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster", "url_background", "tmdb_background", "tvdb_background", "file_background", - "name_mapping", "add_to_arr", "arr_tag", "label", + "name_mapping", "add_to_arr", "arr_tag", "arr_folder", "label", "show_filtered", "show_missing", "save_missing" ] collectionless_details = [ @@ -153,6 +153,7 @@ class CollectionBuilder: self.data = data self.details = { "arr_tag": None, + "arr_folder": None, "show_filtered": library.show_filtered, "show_missing": library.show_missing, "save_missing": library.save_missing @@ -1058,7 +1059,7 @@ class CollectionBuilder: if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_movies_with_names, True) if self.do_arr and self.library.Radarr: - self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"]) + self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"], folder=self.details["arr_folder"]) if self.run_again: self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names]) if len(missing_shows) > 0 and self.library.is_show: @@ -1087,7 +1088,7 @@ class CollectionBuilder: if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_shows_with_names, False) if self.do_arr and self.library.Sonarr: - self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"]) + self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"], folder=self.details["arr_folder"]) if self.run_again: self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names]) diff --git a/modules/radarr.py b/modules/radarr.py index f1a891c4..9f996f6a 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -37,7 +37,7 @@ class RadarrAPI: self.search = params["search"] self.tag = params["tag"] - def add_tmdb(self, tmdb_ids, tag=None): + def add_tmdb(self, tmdb_ids, tag=None, folder=None): logger.info("") logger.debug(f"TMDb IDs: {tmdb_ids}") tag_nums = [] @@ -81,7 +81,7 @@ class RadarrAPI: "tmdbid": int(tmdb_id), "titleslug": titleslug, "monitored": True, - "rootFolderPath": self.root_folder_path, + "rootFolderPath": self.root_folder_path if folder is None else folder, "images": [{"covertype": "poster", "url": poster}], "addOptions": {"searchForMovie": self.search} } diff --git a/modules/sonarr.py b/modules/sonarr.py index a13c5c27..4ae3268b 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -39,7 +39,7 @@ class SonarrAPI: self.season_folder = params["season_folder"] self.tag = params["tag"] - def add_tvdb(self, tvdb_ids, tag=None): + def add_tvdb(self, tvdb_ids, tag=None, folder=None): logger.info("") logger.debug(f"TVDb IDs: {tvdb_ids}") tag_nums = [] @@ -73,7 +73,7 @@ class SonarrAPI: "language": self.language, "monitored": True, "seasonFolder": self.season_folder, - "rootFolderPath": self.root_folder_path, + "rootFolderPath": self.root_folder_path if folder is None else folder, "seasons": [], "images": [{"covertype": "poster", "url": show.poster_path}], "addOptions": {"searchForMissingEpisodes": self.search} From e72979e6874ba1d7948ed3871106a5bf8e91b416 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 2 Apr 2021 00:48:40 -0400 Subject: [PATCH 16/28] fixed plex_search display error --- modules/builder.py | 56 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index f2114182..3b44eccf 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -452,13 +452,15 @@ class CollectionBuilder: self.details[method_name] = method_data elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]: self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}])) - elif method_name in ["decade", "year.greater", "year.less"]: - self.methods.append(("plex_search", [{method_name: [util.check_year(method_data, current_year, method_name)]}])) + elif method_name in ["year.greater", "year.less"]: + self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}])) elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]: - self.methods.append(("plex_search", [{method_name: [util.check_date(method_data, method_name, return_string=True, plex_date=True)]}])) + self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}])) + elif method_name in ["added", "added.not", "originally_available", "originally_available.not"]: + self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=1)}])) elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]: - self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}])) - elif method_name in ["year", "year.not"]: + self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=0)}])) + elif method_name in ["decade", "year", "year.not"]: self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) elif method_name in plex.searches: if method_name in plex.tmdb_searches: @@ -641,14 +643,15 @@ class CollectionBuilder: searches[search_final] = valid_values else: logger.warning(f"Collection Warning: No valid {search} values found in {final_values}") - elif (search == "decade" and modifier in [""]) \ - or (search == "year" and modifier in [".greater", ".less"]): - searches[search_final] = [util.check_year(search_data, current_year, search_final)] - elif search in ["added", "originally_available"] and modifier in ["", ".not", ".before", ".after"]: - searches[search_final] = [util.check_date(search_data, search_final, return_string=True, plex_date=True)] + elif search == "year" and modifier in [".greater", ".less"]: + searches[search_final] = util.check_year(search_data, current_year, search_final) + elif search in ["added", "originally_available"] and modifier in [".before", ".after"]: + searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True) + elif search in ["added", "originally_available"] and modifier in ["", ".not"]: + searches[search_final] = util.check_number(search_data, search_final, minimum=1) elif search in ["duration", "rating"] and modifier in [".greater", ".less"]: - searches[search_final] = [util.check_number(search_data, search_final, minimum=0)] - elif search == "year" and modifier in ["", ".not"]: + searches[search_final] = util.check_number(search_data, search_final, minimum=0) + elif (search == "decade" and modifier in [""]) or (search == "year" and modifier in ["", ".not"]): searches[search_final] = util.get_year_list(search_data, current_year, search_final) elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ @@ -967,14 +970,29 @@ class CollectionBuilder: else: search, modifier = os.path.splitext(str(search_method).lower()) final_search = plex.search_translation[search] if search in plex.search_translation else search - final_mod = plex.modifiers[modifier] if modifier in plex.modifiers else "" + if search == "originally_available" and modifier == "": + final_mod = ">>" + elif search == "originally_available" and modifier == ".not": + final_mod = "<<" + else: + final_mod = plex.modifiers[modifier] if modifier in plex.modifiers else "" final_method = f"{final_search}{final_mod}" - search_terms[final_method] = search_data * 60000 if final_search == "duration" else search_data - ors = "" - conjunction = " AND " if final_mod == "&" else " OR " - for o, param in enumerate(search_data): - or_des = conjunction if o > 0 else f"{search_method}(" - ors += f"{or_des}{param}" + + if search == "duration": + search_terms[final_method] = search_data * 60000 + elif search in ["added", "originally_available"] and modifier in ["", ".not"]: + search_terms[final_method] = f"{search_data}d" + else: + search_terms[final_method] = search_data + + if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]: + ors = f"{search_method}({search_data}" + else: + ors = "" + conjunction = " AND " if final_mod == "&" else " OR " + for o, param in enumerate(search_data): + or_des = conjunction if o > 0 else f"{search_method}(" + ors += f"{or_des}{param}" if has_processed: logger.info(f"\t\t AND {ors})") else: From ecfc3c378ee6bc97c87846aeaf2ec181d9005966 Mon Sep 17 00:00:00 2001 From: Joris Date: Fri, 2 Apr 2021 11:33:19 +0200 Subject: [PATCH 17/28] feat: add language_profile_id option for sonarr --- modules/config.py | 2 ++ modules/sonarr.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/config.py b/modules/config.py index 5c26dfd0..aac91544 100644 --- a/modules/config.py +++ b/modules/config.py @@ -258,6 +258,7 @@ class Config: self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default="v2") self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) + self.general["sonarr"]["language_profile_id"] = check_for_attribute(self.data, "language_profile_id", parent="sonarr", default=1) self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["season_folder"] = check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True) @@ -358,6 +359,7 @@ class Config: sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default=self.general["sonarr"]["version"], save=False) sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) + sonarr_params["language_profile_id"] = check_for_attribute(lib, "language_profile_id", parent="sonarr", default=self.general["sonarr"]["language_profile_id"], save=False) sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) diff --git a/modules/sonarr.py b/modules/sonarr.py index 4ae3268b..91617d65 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -34,6 +34,7 @@ class SonarrAPI: self.version = params["version"] self.token = params["token"] self.root_folder_path = params["root_folder_path"] + self.language_profile_id = params["language_profile_id"] self.add = params["add"] self.search = params["search"] self.season_folder = params["season_folder"] @@ -67,7 +68,7 @@ class SonarrAPI: url_json = { "title": show.title, f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id, - "languageProfileId": 1, + "languageProfileId": self.language_profile_id, "tvdbId": int(tvdb_id), "titleslug": titleslug, "language": self.language, From 1e9c90146643b22221dd44bfa3451c2e08124e3f Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 2 Apr 2021 11:45:29 -0400 Subject: [PATCH 18/28] sonarr language profile validation --- modules/config.py | 7 +++++-- modules/radarr.py | 12 ++++++------ modules/sonarr.py | 29 ++++++++++++++++++++++------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/modules/config.py b/modules/config.py index aac91544..e208210d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -258,7 +258,7 @@ class Config: self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default="v2") self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) - self.general["sonarr"]["language_profile_id"] = check_for_attribute(self.data, "language_profile_id", parent="sonarr", default=1) + self.general["sonarr"]["language_profile"] = check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["season_folder"] = check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True) @@ -359,7 +359,10 @@ class Config: sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default=self.general["sonarr"]["version"], save=False) sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) - sonarr_params["language_profile_id"] = check_for_attribute(lib, "language_profile_id", parent="sonarr", default=self.general["sonarr"]["language_profile_id"], save=False) + if self.general["sonarr"]["language_profile"]: + sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) + else: + sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False) sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) diff --git a/modules/radarr.py b/modules/radarr.py index 9f996f6a..602d8fb4 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -20,7 +20,7 @@ class RadarrAPI: raise Failed("Radarr Error: Unexpected Response Check URL") self.quality_profile_id = None profiles = "" - for profile in self.send_get(f"{self.base_url}{'qualityProfile' if params['version'] == 'v3' else 'profile'}"): + for profile in self.send_get("qualityProfile" if params["version"] == "v3" else "profile"): if len(profiles) > 0: profiles += ", " profiles += profile["name"] @@ -47,8 +47,8 @@ class RadarrAPI: if tag: tag_cache = {} for label in tag: - self.send_post(f"{self.base_url}tag", {"label": str(label)}) - for t in self.send_get(f"{self.base_url}tag"): + self.send_post("tag", {"label": str(label)}) + for t in self.send_get("tag"): tag_cache[t["label"]] = t["id"] for label in tag: if label in tag_cache: @@ -87,7 +87,7 @@ class RadarrAPI: } if tag_nums: url_json["tags"] = tag_nums - response = self.send_post(f"{self.base_url}movie", url_json) + response = self.send_post("movie", url_json) if response.status_code < 400: logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie.title}") add_count += 1 @@ -101,8 +101,8 @@ class RadarrAPI: @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_get(self, url): - return requests.get(url, params=self.url_params).json() + return requests.get(f"{self.base_url}{url}", params=self.url_params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): - return requests.post(url, json=url_json, params=self.url_params) + return requests.post(f"{self.base_url}{url}", json=url_json, params=self.url_params) diff --git a/modules/sonarr.py b/modules/sonarr.py index 91617d65..fd79d589 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -20,7 +20,7 @@ class SonarrAPI: raise Failed("Sonarr Error: Unexpected Response Check URL") self.quality_profile_id = None profiles = "" - for profile in self.send_get(f"{self.base_url}{'qualityProfile' if params['version'] == 'v3' else 'profile'}"): + for profile in self.send_get("qualityProfile" if params["version"] == "v3" else "profile"): if len(profiles) > 0: profiles += ", " profiles += profile["name"] @@ -28,13 +28,28 @@ class SonarrAPI: self.quality_profile_id = profile["id"] if not self.quality_profile_id: raise Failed(f"Sonarr Error: quality_profile: {params['quality_profile']} does not exist in sonarr. Profiles available: {profiles}") + + self.language_profile_id = None + if params["version"] == "v3" and params["language_profile"] is not None: + profiles = "" + for profile in self.send_get("languageProfile"): + if len(profiles) > 0: + profiles += ", " + profiles += profile["name"] + if profile["name"] == params["language_profile"]: + self.language_profile_id = profile["id"] + if not self.quality_profile_id: + raise Failed(f"Sonarr Error: language_profile: {params['language_profile']} does not exist in sonarr. Profiles available: {profiles}") + + if self.language_profile_id is None: + self.language_profile_id = 1 + self.tvdb = tvdb self.language = language self.url = params["url"] self.version = params["version"] self.token = params["token"] self.root_folder_path = params["root_folder_path"] - self.language_profile_id = params["language_profile_id"] self.add = params["add"] self.search = params["search"] self.season_folder = params["season_folder"] @@ -50,8 +65,8 @@ class SonarrAPI: if tag: tag_cache = {} for label in tag: - self.send_post(f"{self.base_url}tag", {"label": str(label)}) - for t in self.send_get(f"{self.base_url}tag"): + self.send_post("tag", {"label": str(label)}) + for t in self.send_get("tag"): tag_cache[t["label"]] = t["id"] for label in tag: if label in tag_cache: @@ -81,7 +96,7 @@ class SonarrAPI: } if tag_nums: url_json["tags"] = tag_nums - response = self.send_post(f"{self.base_url}series", url_json) + response = self.send_post("series", url_json) if response.status_code < 400: logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show.title}") add_count += 1 @@ -95,8 +110,8 @@ class SonarrAPI: @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_get(self, url): - return requests.get(url, params=self.url_params).json() + return requests.get(f"{self.base_url}{url}", params=self.url_params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): - return requests.post(url, json=url_json, params=self.url_params) + return requests.post(f"{self.base_url}{url}", json=url_json, params=self.url_params) From f5fc9a8509831d3b751d5becec29c16297093223 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 2 Apr 2021 14:15:43 -0400 Subject: [PATCH 19/28] less radarr/sonarr calls --- modules/builder.py | 4 ++-- modules/radarr.py | 31 +++++++++++++++++++------------ modules/sonarr.py | 31 +++++++++++++++++++------------ 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 3b44eccf..0c38bd76 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1077,7 +1077,7 @@ class CollectionBuilder: if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_movies_with_names, True) if self.do_arr and self.library.Radarr: - self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"], folder=self.details["arr_folder"]) + self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tags=self.details["arr_tag"], folder=self.details["arr_folder"]) if self.run_again: self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names]) if len(missing_shows) > 0 and self.library.is_show: @@ -1106,7 +1106,7 @@ class CollectionBuilder: if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_shows_with_names, False) if self.do_arr and self.library.Sonarr: - self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"], folder=self.details["arr_folder"]) + self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tags=self.details["arr_tag"], folder=self.details["arr_folder"]) if self.run_again: self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names]) diff --git a/modules/radarr.py b/modules/radarr.py index 602d8fb4..0ae3fcb9 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -28,6 +28,7 @@ class RadarrAPI: self.quality_profile_id = profile["id"] if not self.quality_profile_id: raise Failed(f"Radarr Error: quality_profile: {params['quality_profile']} does not exist in radarr. Profiles available: {profiles}") + self.tags = self.get_tags() self.tmdb = tmdb self.url = params["url"] self.version = params["version"] @@ -37,22 +38,28 @@ class RadarrAPI: self.search = params["search"] self.tag = params["tag"] - def add_tmdb(self, tmdb_ids, tag=None, folder=None): + def get_tags(self): + return {tag["label"]: tag["id"] for tag in self.send_get("tag")} + + def add_tags(self, tags): + added = False + for label in tags: + if label not in self.tags: + added = True + self.send_post("tag", {"label": str(label)}) + if added: + self.tags = self.get_tags() + + def add_tmdb(self, tmdb_ids, tags=None, folder=None): logger.info("") logger.debug(f"TMDb IDs: {tmdb_ids}") tag_nums = [] add_count = 0 - if tag is None: - tag = self.tag - if tag: - tag_cache = {} - for label in tag: - self.send_post("tag", {"label": str(label)}) - for t in self.send_get("tag"): - tag_cache[t["label"]] = t["id"] - for label in tag: - if label in tag_cache: - tag_nums.append(tag_cache[label]) + if tags is None: + tags = self.tag + if tags: + self.add_tags(tags) + tag_nums = [self.tags[label] for label in tags if label in self.tags] for tmdb_id in tmdb_ids: try: movie = self.tmdb.get_movie(tmdb_id) diff --git a/modules/sonarr.py b/modules/sonarr.py index fd79d589..d1c92934 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -44,6 +44,7 @@ class SonarrAPI: if self.language_profile_id is None: self.language_profile_id = 1 + self.tags = self.get_tags() self.tvdb = tvdb self.language = language self.url = params["url"] @@ -55,22 +56,28 @@ class SonarrAPI: self.season_folder = params["season_folder"] self.tag = params["tag"] - def add_tvdb(self, tvdb_ids, tag=None, folder=None): + def get_tags(self): + return {tag["label"]: tag["id"] for tag in self.send_get("tag")} + + def add_tags(self, tags): + added = False + for label in tags: + if label not in self.tags: + added = True + self.send_post("tag", {"label": str(label)}) + if added: + self.tags = self.get_tags() + + def add_tvdb(self, tvdb_ids, tags=None, folder=None): logger.info("") logger.debug(f"TVDb IDs: {tvdb_ids}") tag_nums = [] add_count = 0 - if tag is None: - tag = self.tag - if tag: - tag_cache = {} - for label in tag: - self.send_post("tag", {"label": str(label)}) - for t in self.send_get("tag"): - tag_cache[t["label"]] = t["id"] - for label in tag: - if label in tag_cache: - tag_nums.append(tag_cache[label]) + if tags is None: + tags = self.tag + if tags: + self.add_tags(tags) + tag_nums = [self.tags[label] for label in tags if label in self.tags] for tvdb_id in tvdb_ids: try: show = self.tvdb.get_series(self.language, tvdb_id) From eaee4d9abd56c4834a01e1dd97e2567cca5fedcf Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 2 Apr 2021 16:15:59 -0400 Subject: [PATCH 20/28] using arr for lookups now --- modules/config.py | 4 ++-- modules/radarr.py | 55 ++++++++++++++++++++++++----------------------- modules/sonarr.py | 43 ++++++++++++++++++++++-------------- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/modules/config.py b/modules/config.py index e208210d..f1ba6e6d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -345,7 +345,7 @@ class Config: radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) - library.Radarr = RadarrAPI(self.TMDb, radarr_params) + library.Radarr = RadarrAPI(radarr_params) except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") @@ -367,7 +367,7 @@ class Config: sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) - library.Sonarr = SonarrAPI(self.TVDb, sonarr_params, library.Plex.language) + library.Sonarr = SonarrAPI(sonarr_params, library.Plex.language) except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") diff --git a/modules/radarr.py b/modules/radarr.py index 0ae3fcb9..944834c4 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -6,11 +6,11 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") class RadarrAPI: - def __init__(self, tmdb, params): - self.url_params = {"apikey": f"{params['token']}"} + def __init__(self, params): self.base_url = f"{params['url']}/api{'/v3' if params['version'] == 'v3' else ''}/" + self.token = params["token"] try: - result = requests.get(f"{self.base_url}system/status", params=self.url_params).json() + result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() except Exception: util.print_stacktrace() raise Failed(f"Radarr Error: Could not connect to Radarr at {params['url']}") @@ -29,7 +29,6 @@ class RadarrAPI: if not self.quality_profile_id: raise Failed(f"Radarr Error: quality_profile: {params['quality_profile']} does not exist in radarr. Profiles available: {profiles}") self.tags = self.get_tags() - self.tmdb = tmdb self.url = params["url"] self.version = params["version"] self.token = params["token"] @@ -50,6 +49,13 @@ class RadarrAPI: if added: self.tags = self.get_tags() + def lookup(self, tmdb_id): + results = self.send_get("movie/lookup", params={"term": f"tmdb:{tmdb_id}"}) + if results: + return results[0] + else: + raise Failed(f"Sonarr Error: TMDb ID: {tmdb_id} not found") + def add_tmdb(self, tmdb_ids, tags=None, folder=None): logger.info("") logger.debug(f"TMDb IDs: {tmdb_ids}") @@ -62,54 +68,49 @@ class RadarrAPI: tag_nums = [self.tags[label] for label in tags if label in self.tags] for tmdb_id in tmdb_ids: try: - movie = self.tmdb.get_movie(tmdb_id) + movie_info = self.lookup(tmdb_id) except Failed as e: logger.error(e) continue - try: - year = movie.release_date.split("-")[0] - except AttributeError: - logger.error(f"TMDb Error: No year for ({tmdb_id}) {movie.title}") - continue - - if year.isdigit() is False: - logger.error(f"TMDb Error: No release date yet for ({tmdb_id}) {movie.title}") - continue - - poster = f"https://image.tmdb.org/t/p/original{movie.poster_path}" - - titleslug = re.sub(r"([^\s\w]|_)+", "", f"{movie.title} {year}").replace(" ", "-").lower() + poster_url = None + for image in movie_info["images"]: + if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image: + poster_url = image["remoteUrl"] url_json = { - "title": movie.title, + "title": movie_info["title"], f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id, - "year": int(year), + "year": int(movie_info["year"]), "tmdbid": int(tmdb_id), - "titleslug": titleslug, + "titleslug": movie_info["titleSlug"], "monitored": True, "rootFolderPath": self.root_folder_path if folder is None else folder, - "images": [{"covertype": "poster", "url": poster}], + "images": [{"covertype": "poster", "url": poster_url}], "addOptions": {"searchForMovie": self.search} } if tag_nums: url_json["tags"] = tag_nums response = self.send_post("movie", url_json) if response.status_code < 400: - logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie.title}") + logger.info(f"Added to Radarr | {tmdb_id:<6} | {movie_info['title']}") add_count += 1 else: try: - logger.error(f"Radarr Error: ({tmdb_id}) {movie.title}: ({response.status_code}) {response.json()[0]['errorMessage']}") + logger.error(f"Radarr Error: ({tmdb_id}) {movie_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}") except KeyError: logger.debug(url_json) logger.error(f"Radarr Error: {response.json()}") logger.info(f"{add_count} Movie{'s' if add_count > 1 else ''} added to Radarr") @retry(stop_max_attempt_number=6, wait_fixed=10000) - def send_get(self, url): - return requests.get(f"{self.base_url}{url}", params=self.url_params).json() + def send_get(self, url, params=None): + url_params = {"apikey": f"{self.token}"} + if params: + for param in params: + url_params[param] = params[param] + return requests.get(f"{self.base_url}{url}", params=url_params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): - return requests.post(f"{self.base_url}{url}", json=url_json, params=self.url_params) + return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"}) diff --git a/modules/sonarr.py b/modules/sonarr.py index d1c92934..0b501756 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -6,11 +6,11 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") class SonarrAPI: - def __init__(self, tvdb, params, language): - self.url_params = {"apikey": f"{params['token']}"} + def __init__(self, params, language): self.base_url = f"{params['url']}/api{'/v3/' if params['version'] == 'v3' else '/'}" + self.token = params["token"] try: - result = requests.get(f"{self.base_url}system/status", params=self.url_params).json() + result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() except Exception: util.print_stacktrace() raise Failed(f"Sonarr Error: Could not connect to Sonarr at {params['url']}") @@ -45,11 +45,8 @@ class SonarrAPI: self.language_profile_id = 1 self.tags = self.get_tags() - self.tvdb = tvdb self.language = language - self.url = params["url"] self.version = params["version"] - self.token = params["token"] self.root_folder_path = params["root_folder_path"] self.add = params["add"] self.search = params["search"] @@ -68,6 +65,13 @@ class SonarrAPI: if added: self.tags = self.get_tags() + def lookup(self, tvdb_id): + results = self.send_get("series/lookup", params={"term": f"tvdb:{tvdb_id}"}) + if results: + return results[0] + else: + raise Failed(f"Sonarr Error: TVDb ID: {tvdb_id} not found") + def add_tvdb(self, tvdb_ids, tags=None, folder=None): logger.info("") logger.debug(f"TVDb IDs: {tvdb_ids}") @@ -80,45 +84,52 @@ class SonarrAPI: tag_nums = [self.tags[label] for label in tags if label in self.tags] for tvdb_id in tvdb_ids: try: - show = self.tvdb.get_series(self.language, tvdb_id) + show_info = self.lookup(tvdb_id) except Failed as e: logger.error(e) continue - titleslug = re.sub(r"([^\s\w]|_)+", "", show.title).replace(" ", "-").lower() + poster_url = None + for image in show_info["images"]: + if "coverType" in image and image["coverType"] == "poster" and "remoteUrl" in image: + poster_url = image["remoteUrl"] url_json = { - "title": show.title, + "title": show_info["title"], f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id, "languageProfileId": self.language_profile_id, "tvdbId": int(tvdb_id), - "titleslug": titleslug, + "titleslug": show_info["titleSlug"], "language": self.language, "monitored": True, "seasonFolder": self.season_folder, "rootFolderPath": self.root_folder_path if folder is None else folder, "seasons": [], - "images": [{"covertype": "poster", "url": show.poster_path}], + "images": [{"covertype": "poster", "url": poster_url}], "addOptions": {"searchForMissingEpisodes": self.search} } if tag_nums: url_json["tags"] = tag_nums response = self.send_post("series", url_json) if response.status_code < 400: - logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show.title}") + logger.info(f"Added to Sonarr | {tvdb_id:<6} | {show_info['title']}") add_count += 1 else: try: - logger.error(f"Sonarr Error: ({tvdb_id}) {show.title}: ({response.status_code}) {response.json()[0]['errorMessage']}") + logger.error(f"Sonarr Error: ({tvdb_id}) {show_info['title']}: ({response.status_code}) {response.json()[0]['errorMessage']}") except KeyError: logger.debug(url_json) logger.error(f"Sonarr Error: {response.json()}") logger.info(f"{add_count} Show{'s' if add_count > 1 else ''} added to Sonarr") @retry(stop_max_attempt_number=6, wait_fixed=10000) - def send_get(self, url): - return requests.get(f"{self.base_url}{url}", params=self.url_params).json() + def send_get(self, url, params=None): + url_params = {"apikey": f"{self.token}"} + if params: + for param in params: + url_params[param] = params[param] + return requests.get(f"{self.base_url}{url}", params=url_params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): - return requests.post(f"{self.base_url}{url}", json=url_json, params=self.url_params) + return requests.post(f"{self.base_url}{url}", json=url_json, params={"apikey": f"{self.token}"}) From b843ef2a9734d476017c99e7154c2f39dadeb214 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 3 Apr 2021 14:00:05 -0400 Subject: [PATCH 21/28] fixed critic_rating and audience_rating --- modules/builder.py | 95 ++++------------------------------------- modules/plex.py | 103 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 86 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 0c38bd76..e43ef3f9 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -456,10 +456,10 @@ class CollectionBuilder: self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}])) elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]: self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}])) - elif method_name in ["added", "added.not", "originally_available", "originally_available.not"]: + elif method_name in ["added", "added.not", "originally_available", "originally_available.not", "duration.greater", "duration.less"]: self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=1)}])) - elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]: - self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=0)}])) + elif method_name in ["critic_rating.greater", "critic_rating.less", "audience_rating.greater", "audience_rating.less"]: + self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, number_type="float", minimum=0, maximum=10)}])) elif method_name in ["decade", "year", "year.not"]: self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) elif method_name in plex.searches: @@ -647,17 +647,17 @@ class CollectionBuilder: searches[search_final] = util.check_year(search_data, current_year, search_final) elif search in ["added", "originally_available"] and modifier in [".before", ".after"]: searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True) - elif search in ["added", "originally_available"] and modifier in ["", ".not"]: + elif (search in ["added", "originally_available"] and modifier in ["", ".not"]) or (search in ["duration"] and modifier in [".greater", ".less"]): searches[search_final] = util.check_number(search_data, search_final, minimum=1) - elif search in ["duration", "rating"] and modifier in [".greater", ".less"]: - searches[search_final] = util.check_number(search_data, search_final, minimum=0) + elif search in ["critic_rating", "audience_rating"] and modifier in [".greater", ".less"]: + searches[search_final] = util.check_number(search_data, search_final, number_type="float", minimum=0, maximum=10) elif (search == "decade" and modifier in [""]) or (search == "year" and modifier in ["", ".not"]): searches[search_final] = util.get_year_list(search_data, current_year, search_final) elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ or (search in ["resolution", "decade"] and modifier not in [""]) \ or (search in ["added", "originally_available"] and modifier not in ["", ".not", ".before", ".after"]) \ - or (search in ["duration", "rating"] and modifier not in [".greater", ".less"]) \ + or (search in ["duration", "critic_rating", "audience_rating"] and modifier not in [".greater", ".less"]) \ or (search in ["year"] and modifier not in ["", ".not", ".greater", ".less"]): raise Failed(f"Collection Error: modifier: {modifier} not supported with the {search} plex search attribute") else: @@ -929,7 +929,6 @@ class CollectionBuilder: logger.debug("") logger.debug(f"Method: {method}") logger.debug(f"Values: {values}") - pretty = util.pretty_names[method] if method in util.pretty_names else method for value in values: items = [] missing_movies = [] @@ -950,85 +949,9 @@ class CollectionBuilder: return items_found_inside logger.info("") logger.debug(f"Value: {value}") - if method == "plex_all": - logger.info(f"Processing {pretty} {'Movies' if self.library.is_movie else 'Shows'}") - items = self.library.Plex.all() + if "plex" in method: + items = self.library.get_items(method, value) items_found += len(items) - elif method == "plex_collection": - items = value.items() - items_found += len(items) - elif method == "plex_search": - search_terms = {} - has_processed = False - search_limit = None - search_sort = None - for search_method, search_data in value.items(): - if search_method == "limit": - search_limit = search_data - elif search_method == "sort_by": - search_sort = plex.sorts[search_data] - else: - search, modifier = os.path.splitext(str(search_method).lower()) - final_search = plex.search_translation[search] if search in plex.search_translation else search - if search == "originally_available" and modifier == "": - final_mod = ">>" - elif search == "originally_available" and modifier == ".not": - final_mod = "<<" - else: - final_mod = plex.modifiers[modifier] if modifier in plex.modifiers else "" - final_method = f"{final_search}{final_mod}" - - if search == "duration": - search_terms[final_method] = search_data * 60000 - elif search in ["added", "originally_available"] and modifier in ["", ".not"]: - search_terms[final_method] = f"{search_data}d" - else: - search_terms[final_method] = search_data - - if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]: - ors = f"{search_method}({search_data}" - else: - ors = "" - conjunction = " AND " if final_mod == "&" else " OR " - for o, param in enumerate(search_data): - or_des = conjunction if o > 0 else f"{search_method}(" - ors += f"{or_des}{param}" - if has_processed: - logger.info(f"\t\t AND {ors})") - else: - logger.info(f"Processing {pretty}: {ors})") - has_processed = True - items = self.library.Plex.search(sort=search_sort, maxresults=search_limit, **search_terms) - items_found += len(items) - elif method == "plex_collectionless": - good_collections = [] - for col in self.library.get_all_collections(): - keep_collection = True - for pre in value["exclude_prefix"]: - if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): - keep_collection = False - break - if keep_collection: - for ext in value["exclude"]: - if col.title == ext or (col.titleSort and col.titleSort == ext): - keep_collection = False - break - if keep_collection: - good_collections.append(col.index) - all_items = self.library.Plex.all() - length = 0 - for i, item in enumerate(all_items, 1): - length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}") - add_item = True - item.reload() - for collection in item.collections: - if collection.id in good_collections: - add_item = False - break - if add_item: - items.append(item) - items_found += len(items) - util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.library.is_movie else 'Shows'}") elif "tautulli" in method: items = self.library.Tautulli.get_items(self.library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"]) items_found += len(items) diff --git a/modules/plex.py b/modules/plex.py index 815b42b2..d2c3ce21 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -85,6 +85,7 @@ tmdb_searches = [ "writer", "writer.and", "writer.not" ] sorts = { + None: None, "title.asc": "titleSort:asc", "title.desc": "titleSort:desc", "originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc", "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", @@ -221,6 +222,108 @@ class PlexAPI: raise Failed(f"Collection Error: No valid Plex Collections in {collections}") return valid_collections + def get_items(self, method, data, status_message=True): + if status_message: + logger.debug(f"Data: {data}") + pretty = util.pretty_names[method] if method in util.pretty_names else method + media_type = "Movie" if self.is_movie else "Show" + items = [] + if method == "plex_all": + if status_message: + logger.info(f"Processing {pretty} {media_type}s") + items = self.Plex.all() + elif method == "plex_collection": + if status_message: + logger.info(f"Processing {pretty} {data}") + items = data.items() + elif method == "plex_search": + search_terms = {} + has_processed = False + search_limit = None + search_sort = None + for search_method, search_data in data.items(): + if search_method == "limit": + search_limit = search_data + elif search_method == "sort_by": + search_sort = search_data + else: + search, modifier = os.path.splitext(str(search_method).lower()) + final_search = search_translation[search] if search in search_translation else search + if search == "originally_available" and modifier == "": + final_mod = ">>" + elif search == "originally_available" and modifier == ".not": + final_mod = "<<" + elif search in ["critic_rating", "audience_rating"] and modifier == ".greater": + final_mod = "__gte" + elif search in ["critic_rating", "audience_rating"] and modifier == ".less": + final_mod = "__lt" + else: + final_mod = modifiers[modifier] if modifier in modifiers else "" + final_method = f"{final_search}{final_mod}" + + if search == "duration": + search_terms[final_method] = search_data * 60000 + elif search in ["added", "originally_available"] and modifier in ["", ".not"]: + search_terms[final_method] = f"{search_data}d" + else: + search_terms[final_method] = search_data + + if status_message: + if search in ["added", "originally_available"] or modifier in [".greater", ".less", ".before", ".after"]: + ors = f"{search_method}({search_data}" + else: + ors = "" + conjunction = " AND " if final_mod == "&" else " OR " + for o, param in enumerate(search_data): + or_des = conjunction if o > 0 else f"{search_method}(" + ors += f"{or_des}{param}" + if has_processed: + logger.info(f"\t\t AND {ors})") + else: + logger.info(f"Processing {pretty}: {ors})") + has_processed = True + if status_message: + if search_sort: + logger.info(f"\t\t SORT BY {search_sort})") + if search_limit: + logger.info(f"\t\t LIMIT {search_limit})") + logger.debug(f"Search: {search_terms}") + return self.Plex.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms) + elif method == "plex_collectionless": + good_collections = [] + for col in self.get_all_collections(): + keep_collection = True + for pre in data["exclude_prefix"]: + if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): + keep_collection = False + break + if keep_collection: + for ext in data["exclude"]: + if col.title == ext or (col.titleSort and col.titleSort == ext): + keep_collection = False + break + if keep_collection: + good_collections.append(col.index) + all_items = self.Plex.all() + length = 0 + for i, item in enumerate(all_items, 1): + length = util.print_return(length, f"Processing: {i}/{len(all_items)} {item.title}") + add_item = True + item.reload() + for collection in item.collections: + if collection.id in good_collections: + add_item = False + break + if add_item: + items.append(item) + util.print_end(length, f"Processed {len(all_items)} {'Movies' if self.is_movie else 'Shows'}") + else: + raise Failed(f"Plex Error: Method {method} not supported") + if len(items) > 0: + return items + else: + raise Failed("Plex Error: No Items found in Plex") + def add_missing(self, collection, items, is_movie): col_name = collection.encode("ascii", "replace").decode() if col_name not in self.missing: From 1be819a3b9b964adb00c449eb2e5ba5e356ac787 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 3 Apr 2021 16:48:27 -0400 Subject: [PATCH 22/28] fix for #164 --- modules/builder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index e43ef3f9..001c86b9 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -294,7 +294,7 @@ class CollectionBuilder: run_time = str(schedule).lower() if run_time.startswith("day") or run_time.startswith("daily"): skip_collection = False - if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): + elif run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): match = re.search("\\(([^)]+)\\)", run_time) if match: param = match.group(1) @@ -939,8 +939,10 @@ class CollectionBuilder: if len(movie_ids) > 0: items_found_inside += len(movie_ids) for movie_id in movie_ids: - if movie_id in movie_map: items.append(movie_map[movie_id]) - else: missing_movies.append(movie_id) + if movie_id in movie_map: + items.append(movie_map[movie_id]) + else: + missing_movies.append(movie_id) if len(show_ids) > 0: items_found_inside += len(show_ids) for show_id in show_ids: From 569554e61b74bd67635174b5cdcf4673f887a5db Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 3 Apr 2021 16:54:34 -0400 Subject: [PATCH 23/28] fix for #163 --- modules/plex.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/plex.py b/modules/plex.py index d2c3ce21..ac7af4e5 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -35,9 +35,11 @@ metadata_language_options = {lang.lower(): lang for lang in plex_languages} metadata_language_options["default"] = None filter_alias = { "actor": "actors", + "audience_rating": "audienceRating", "collection": "collections", "content_rating": "contentRating", "country": "countries", + "critic_rating": "rating", "director": "directors", "genre": "genres", "originally_available": "originallyAvailableAt", From 16de1a722edd55e2b4ff4a0c4ac2f8671ad684af Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 4 Apr 2021 11:42:31 -0400 Subject: [PATCH 24/28] update test_lists --- modules/config.py | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/modules/config.py b/modules/config.py index f1ba6e6d..cabab956 100644 --- a/modules/config.py +++ b/modules/config.py @@ -22,6 +22,12 @@ from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") +sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"} +radarr_versions = {"v2": "For Radarr 0.2", "v3": "For Radarr 3.0"} +sonarr_versions = {"v2": "For Sonarr 0.2", "v3": "For Sonarr 3.0"} +mass_genre_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} +library_types = {"movie": "For Movie Libraries", "show": "For Show Libraries"} + class Config: def __init__(self, default_dir, config_path=None): logger.info("Locating config...") @@ -83,7 +89,7 @@ class Config: util.print_stacktrace() raise Failed(f"YAML Error: {e}") - def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True): + def check_for_attribute(data, attribute, parent=None, test_list=None, default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True): endline = "" if parent is not None: if parent in data: @@ -135,6 +141,12 @@ class Config: message = message + endline if req_default and default is None: raise Failed(f"Config Error: {attribute} attribute must be set under {parent} globally or under this specific Library") + options = "" + if test_list: + for option, description in test_list.items(): + if len(options) > 0: + options = f"{options}\n" + options = f"{options} {option} ({description})" if (default is None and not default_is_none) or throw: if len(options) > 0: message = message + "\n" + options @@ -146,7 +158,7 @@ class Config: return default self.general = {} - self.general["cache"] = check_for_attribute(self.data, "cache", parent="settings", options=" true (Create a cache to store ids)\n false (Do not create a cache to store ids)", var_type="bool", default=True) + self.general["cache"] = check_for_attribute(self.data, "cache", parent="settings", var_type="bool", default=True) self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) if self.general["cache"]: util.separator() @@ -154,7 +166,7 @@ class Config: else: self.Cache = None self.general["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="settings", var_type="list_path", default=[os.path.join(default_dir, "assets")]) - self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)") + self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=sync_modes) self.general["run_again_delay"] = check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0) self.general["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True) self.general["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False) @@ -244,18 +256,18 @@ class Config: self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) - self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default="v2") self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) - self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) - self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) + self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v2") self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) - self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) + self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) + self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) self.general["radarr"]["tag"] = check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True) + self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) self.general["sonarr"] = {} self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) - self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default="v2") + self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v2") self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) self.general["sonarr"]["language_profile"] = check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True) @@ -286,9 +298,9 @@ class Config: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") if "settings" in lib and lib["settings"] and "sync_mode" in lib["settings"]: - params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False) + params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) else: - params["sync_mode"] = check_for_attribute(lib, "sync_mode", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False) + params["sync_mode"] = check_for_attribute(lib, "sync_mode", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) if "settings" in lib and lib["settings"] and "show_unmanaged" in lib["settings"]: params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) @@ -311,7 +323,7 @@ class Config: params["save_missing"] = check_for_attribute(lib, "save_missing", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) if "mass_genre_update" in lib and lib["mass_genre_update"]: - params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=["tmdb", "omdb"], options=" tmdb (Use TMDb Metadata)\n omdb (Use IMDb Metadata through OMDb)", default_is_none=True, save=False) + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_genre_update_options, default_is_none=True, save=False) else: params["mass_genre_update"] = None @@ -321,7 +333,7 @@ class Config: try: params["metadata_path"] = check_for_attribute(lib, "metadata_path", var_type="path", default=os.path.join(default_dir, f"{library_name}.yml"), throw=True) - params["library_type"] = check_for_attribute(lib, "library_type", test_list=["movie", "show"], options=" movie (For Movie Libraries)\n show (For Show Libraries)", throw=True) + params["library_type"] = check_for_attribute(lib, "library_type", test_list=library_types, throw=True) params["plex"] = {} params["plex"]["url"] = check_for_attribute(lib, "url", parent="plex", default=self.general["plex"]["url"], req_default=True, save=False) params["plex"]["token"] = check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False) @@ -339,12 +351,12 @@ class Config: try: radarr_params["url"] = check_for_attribute(lib, "url", parent="radarr", default=self.general["radarr"]["url"], req_default=True, save=False) radarr_params["token"] = check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False) - radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default=self.general["radarr"]["version"], save=False) - radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) - radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) + radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=radarr_versions, default=self.general["radarr"]["version"], save=False) radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) - radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) + radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) + radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) + radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) library.Radarr = RadarrAPI(radarr_params) except Failed as e: util.print_multiline(e, error=True) @@ -356,7 +368,7 @@ class Config: try: sonarr_params["url"] = check_for_attribute(lib, "url", parent="sonarr", default=self.general["sonarr"]["url"], req_default=True, save=False) sonarr_params["token"] = check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False) - sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default=self.general["sonarr"]["version"], save=False) + sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=sonarr_versions, default=self.general["sonarr"]["version"], save=False) sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) if self.general["sonarr"]["language_profile"]: From 18dd05754800cf8f2dacf92a29235873de926cb5 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 4 Apr 2021 22:25:41 -0400 Subject: [PATCH 25/28] added radarr/sonarr attributes and split add_to_arr --- config/Movies.yml.template | 1 - config/config.yml.template | 21 +++++-- modules/builder.py | 126 +++++++++++++++++++++++++++---------- modules/config.py | 47 +++++++++++--- modules/radarr.py | 60 +++++++++++------- modules/sonarr.py | 104 ++++++++++++++++++------------ 6 files changed, 248 insertions(+), 111 deletions(-) diff --git a/config/Movies.yml.template b/config/Movies.yml.template index 18631dc8..739286c6 100644 --- a/config/Movies.yml.template +++ b/config/Movies.yml.template @@ -1292,5 +1292,4 @@ collections: - + - ~ sort_title: ~_Collectionless - collection_order: alpha collection_order: alpha \ No newline at end of file diff --git a/config/config.yml.template b/config/config.yml.template index 74bfefa2..f9bb7211 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -30,19 +30,28 @@ tautulli: # Can be individually specified radarr: # Can be individually specified per library as well url: http://192.168.1.12:7878 token: ################################ - version: v2 - quality_profile: HD-1080p - root_folder_path: S:/Movies + version: v3 add: false + root_folder_path: S:/Movies + monitor: true + availability: announced + quality_profile: HD-1080p + tag: search: false sonarr: # Can be individually specified per library as well url: http://192.168.1.12:8989 token: ################################ - version: v2 - quality_profile: HD-1080p - root_folder_path: "S:/TV Shows" + version: v3 add: false + root_folder_path: "S:/TV Shows" + monitor: all + quality_profile: HD-1080p + language_profile: English + series_type: standard + season_folder: true + tag: search: false + cutoff_search: false omdb: apikey: ######## trakt: diff --git a/modules/builder.py b/modules/builder.py index 001c86b9..5a8ff554 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,6 +1,6 @@ import glob, logging, os, re from datetime import datetime, timedelta -from modules import anidb, anilist, imdb, letterboxd, mal, plex, tautulli, tmdb, trakttv, tvdb, util +from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util from modules.util import Failed from plexapi.collection import Collections from plexapi.exceptions import BadRequest, NotFound @@ -81,13 +81,12 @@ numbered_builders = [ "trakt_collected" ] all_details = [ - "sort_title", "content_rating", - "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", "tvdb_description", "trakt_description", "letterboxd_description", - "collection_mode", "collection_order", + "sort_title", "content_rating", "collection_mode", "collection_order", + "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", + "tvdb_description", "trakt_description", "letterboxd_description", "url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster", "url_background", "tmdb_background", "tvdb_background", "file_background", - "name_mapping", "add_to_arr", "arr_tag", "arr_folder", "label", - "show_filtered", "show_missing", "save_missing" + "name_mapping", "label", "show_filtered", "show_missing", "save_missing" ] collectionless_details = [ "sort_title", "content_rating", @@ -106,7 +105,6 @@ ignored_details = [ "tmdb_person" ] boolean_details = [ - "add_to_arr", "show_filtered", "show_missing", "save_missing" @@ -152,12 +150,12 @@ class CollectionBuilder: self.name = name self.data = data self.details = { - "arr_tag": None, - "arr_folder": None, "show_filtered": library.show_filtered, "show_missing": library.show_missing, "save_missing": library.save_missing } + self.radarr_options = {} + self.sonarr_options = {} self.missing_movies = [] self.missing_shows = [] self.methods = [] @@ -167,6 +165,8 @@ class CollectionBuilder: self.summaries = {} self.schedule = "" self.rating_key_map = {} + self.add_to_radarr = None + self.add_to_sonarr = None current_time = datetime.now() current_year = current_time.year @@ -355,20 +355,28 @@ class CollectionBuilder: else: raise Failed("Collection Error: tmdb_person attribute is blank") - for method_name, method_data in self.data.items(): - if "trakt" in method_name.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_name} requires Trakt todo be configured") - elif "imdb" in method_name.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_name} requires TMDb or Trakt to be configured") - elif "tautulli" in method_name.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_name} requires Tautulli to be configured") - elif "mal" in method_name.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_name} requires MyAnimeList to be configured") + for method_key, method_data in self.data.items(): + if "trakt" in method_key.lower() and not config.Trakt: raise Failed(f"Collection Error: {method_key} requires Trakt todo be configured") + elif "imdb" in method_key.lower() and not config.IMDb: raise Failed(f"Collection Error: {method_key} requires TMDb or Trakt to be configured") + elif "radarr" in method_key.lower() and not self.library.Radarr: raise Failed(f"Collection Error: {method_key} requires Radarr to be configured") + elif "sonarr" in method_key.lower() and not self.library.Sonarr: raise Failed(f"Collection Error: {method_key} requires Sonarr to be configured") + elif "tautulli" in method_key.lower() and not self.library.Tautulli: raise Failed(f"Collection Error: {method_key} requires Tautulli to be configured") + elif "mal" in method_key.lower() and not config.MyAnimeList: raise Failed(f"Collection Error: {method_key} requires MyAnimeList to be configured") elif method_data is not None: logger.debug("") - logger.debug(f"Validating Method: {method_name}") + logger.debug(f"Validating Method: {method_key}") logger.debug(f"Value: {method_data}") - if method_name.lower() in method_alias: - method_name = method_alias[method_name.lower()] - logger.warning(f"Collection Warning: {method_name} attribute will run as {method_name}") + if method_key.lower() in method_alias: + method_name = method_alias[method_key.lower()] + logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}") + elif method_key.lower() == "add_to_arr": + method_name = "radarr_add" if self.library.is_movie else "sonarr_add" + logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}") + elif method_key.lower() in ["arr_tag", "arr_folder"]: + method_name = f"{'rad' if self.library.is_movie else 'son'}{method_key.lower()}" + logger.warning(f"Collection Warning: {method_key} attribute will run as {method_name}") else: - method_name = method_name.lower() + method_name = method_key.lower() if method_name in show_only_builders and self.library.is_movie: raise Failed(f"Collection Error: {method_name} attribute only works for show libraries") elif method_name in movie_only_builders and self.library.is_show: @@ -441,7 +449,7 @@ class CollectionBuilder: elif method_name == "sync_mode": if str(method_data).lower() in ["append", "sync"]: self.details[method_name] = method_data.lower() else: raise Failed("Collection Error: sync_mode attribute must be either 'append' or 'sync'") - elif method_name in ["arr_tag", "label"]: + elif method_name == "label": self.details[method_name] = util.get_list(method_data) elif method_name in boolean_details: if isinstance(method_data, bool): self.details[method_name] = method_data @@ -450,6 +458,52 @@ class CollectionBuilder: else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false") elif method_name in all_details: self.details[method_name] = method_data + elif method_name == "radarr_add": + self.add_to_radarr = True + elif method_name == "radarr_folder": + self.radarr_options["folder"] = method_data + elif method_name in ["radarr_monitor", "radarr_search"]: + if isinstance(method_data, bool): self.radarr_options[method_name[7:]] = method_data + elif str(method_data).lower() in ["t", "true"]: self.radarr_options[method_name[7:]] = True + elif str(method_data).lower() in ["f", "false"]: self.radarr_options[method_name[7:]] = False + else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false") + elif method_name == "radarr_availability": + if str(method_data).lower() in radarr.availability_translation: + self.radarr_options["availability"] = str(method_data).lower() + else: + raise Failed(f"Collection Error: {method_name} attribute must be either announced, cinemas, released or db") + elif method_name == "radarr_quality": + self.library.Radarr.get_profile_id(method_data) + self.radarr_options["quality"] = method_data + elif method_name == "radarr_tag": + self.radarr_options["tag"] = util.get_list(method_data) + elif method_name == "sonarr_add": + self.add_to_sonarr = True + elif method_name == "sonarr_folder": + self.sonarr_options["folder"] = method_data + elif method_name == "sonarr_monitor": + if str(method_data).lower() in sonarr.monitor_translation: + self.sonarr_options["monitor"] = str(method_data).lower() + else: + raise Failed(f"Collection Error: {method_name} attribute must be either all, future, missing, existing, pilot, first, latest or none") + elif method_name == "sonarr_quality": + self.library.Sonarr.get_profile_id(method_data, "quality_profile") + self.sonarr_options["quality"] = method_data + elif method_name == "sonarr_language": + self.library.Sonarr.get_profile_id(method_data, "language_profile") + self.sonarr_options["language"] = method_data + elif method_name == "sonarr_series": + if str(method_data).lower() in sonarr.series_type: + self.sonarr_options["series"] = str(method_data).lower() + else: + raise Failed(f"Collection Error: {method_name} attribute must be either standard, daily, or anime") + elif method_name in ["sonarr_season", "sonarr_search", "sonarr_cutoff_search"]: + if isinstance(method_data, bool): self.sonarr_options[method_name[7:]] = method_data + elif str(method_data).lower() in ["t", "true"]: self.sonarr_options[method_name[7:]] = True + elif str(method_data).lower() in ["f", "false"]: self.sonarr_options[method_name[7:]] = False + else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false") + elif method_name == "sonarr_tag": + self.sonarr_options["tag"] = util.get_list(method_data) elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]: self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}])) elif method_name in ["year.greater", "year.less"]: @@ -898,10 +952,10 @@ class CollectionBuilder: self.methods.append((method_name, util.get_list(method_data))) elif method_name not in ignored_details: raise Failed(f"Collection Error: {method_name} attribute not supported") - elif method_name in all_builders or method_name in method_alias or method_name in plex.searches: - raise Failed(f"Collection Error: {method_name} attribute is blank") + elif method_key.lower() in all_builders or method_key.lower() in method_alias or method_key.lower() in plex.searches: + raise Failed(f"Collection Error: {method_key} attribute is blank") else: - logger.warning(f"Collection Warning: {method_name} attribute is blank") + logger.warning(f"Collection Warning: {method_key} attribute is blank") self.sync = self.library.sync_mode == "sync" if "sync_mode" in methods: @@ -912,14 +966,14 @@ class CollectionBuilder: else: self.sync = self.data[methods["sync_mode"]].lower() == "sync" - self.do_arr = False - if self.library.Radarr: - self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add - if self.library.Sonarr: - self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add + if self.add_to_radarr is None: + self.add_to_radarr = self.library.Radarr.add if self.library.Radarr else False + if self.add_to_sonarr is None: + self.add_to_sonarr = self.library.Sonarr.add if self.library.Sonarr else False if self.collectionless: - self.details["add_to_arr"] = False + self.add_to_radarr = False + self.add_to_sonarr = False self.details["collection_mode"] = "hide" self.sync = True @@ -1001,8 +1055,11 @@ class CollectionBuilder: logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing") if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_movies_with_names, True) - if self.do_arr and self.library.Radarr: - self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tags=self.details["arr_tag"], folder=self.details["arr_folder"]) + if self.add_to_radarr and self.library.Radarr: + try: + self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], **self.radarr_options) + except Failed as e: + logger.error(e) if self.run_again: self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names]) if len(missing_shows) > 0 and self.library.is_show: @@ -1030,8 +1087,11 @@ class CollectionBuilder: logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing") if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_shows_with_names, False) - if self.do_arr and self.library.Sonarr: - self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tags=self.details["arr_tag"], folder=self.details["arr_folder"]) + if self.add_to_sonarr and self.library.Sonarr: + try: + self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], **self.sonarr_options) + except Failed as e: + logger.error(e) if self.run_again: self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names]) diff --git a/modules/config.py b/modules/config.py index cabab956..3bba2ab2 100644 --- a/modules/config.py +++ b/modules/config.py @@ -24,7 +24,28 @@ logger = logging.getLogger("Plex Meta Manager") sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"} radarr_versions = {"v2": "For Radarr 0.2", "v3": "For Radarr 3.0"} +radarr_availabilities = { + "announced": "For Announced", + "cinemas": "For In Cinemas", + "released": "For Released", + "db": "For PreDB" +} sonarr_versions = {"v2": "For Sonarr 0.2", "v3": "For Sonarr 3.0"} +sonarr_monitors = { + "all": "Monitor all episodes except specials", + "future": "Monitor episodes that have not aired yet", + "missing": "Monitor episodes that do not have files or have not aired yet", + "existing": "Monitor episodes that have files or have not aired yet", + "pilot": "Monitor the first episode. All other episodes will be ignored", + "first": "Monitor all episodes of the first season. All other seasons will be ignored", + "latest": "Monitor all episodes of the latest season and future seasons", + "none": "No episodes will be monitored" +} +sonarr_series_types = { + "standard": "Episodes released with SxxEyy pattern", + "daily": "Episodes released daily or less frequently that use year-month-day (2017-05-25)", + "anime": "Episodes released using an absolute episode number" +} mass_genre_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} library_types = {"movie": "For Movie Libraries", "show": "For Show Libraries"} @@ -257,9 +278,11 @@ class Config: self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) - self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v2") + self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v3") self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) + self.general["radarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True) + self.general["radarr"]["availability"] = check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr_availabilities, default="announced") self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) self.general["radarr"]["tag"] = check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True) self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) @@ -267,14 +290,17 @@ class Config: self.general["sonarr"] = {} self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) - self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v2") - self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) + self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v3") + self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) + self.general["sonarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr_monitors, default="all") + self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["language_profile"] = check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True) - self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) - self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) + self.general["sonarr"]["series_type"] = check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr_series_types, default="standard") self.general["sonarr"]["season_folder"] = check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True) self.general["sonarr"]["tag"] = check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True) + self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) + self.general["sonarr"]["cutoff_search"] = check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) self.general["tautulli"] = {} self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) @@ -354,6 +380,8 @@ class Config: radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=radarr_versions, default=self.general["radarr"]["version"], save=False) radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) + radarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False) + radarr_params["availability"] = check_for_attribute(lib, "availability", parent="radarr", test_list=radarr_availabilities, default=self.general["radarr"]["availability"], save=False) radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) @@ -369,16 +397,19 @@ class Config: sonarr_params["url"] = check_for_attribute(lib, "url", parent="sonarr", default=self.general["sonarr"]["url"], req_default=True, save=False) sonarr_params["token"] = check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False) sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=sonarr_versions, default=self.general["sonarr"]["version"], save=False) - sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) + sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) + sonarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr_monitors, default=self.general["sonarr"]["monitor"], save=False) + sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) if self.general["sonarr"]["language_profile"]: sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) else: sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False) - sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) - sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) + sonarr_params["series_type"] = check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr_series_types, default=self.general["sonarr"]["series_type"], save=False) sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) + sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) + sonarr_params["cutoff_search"] = check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) library.Sonarr = SonarrAPI(sonarr_params, library.Plex.language) except Failed as e: util.print_multiline(e, error=True) diff --git a/modules/radarr.py b/modules/radarr.py index 944834c4..48ec8994 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -5,37 +5,46 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +availability_translation = { + "announced": "announced", + "cinemas": "inCinemas", + "released": "released", + "db": "preDB" +} + class RadarrAPI: def __init__(self, params): - self.base_url = f"{params['url']}/api{'/v3' if params['version'] == 'v3' else ''}/" + self.url = params["url"] self.token = params["token"] + self.version = params["version"] + self.base_url = f"{self.url}/api{'/v3' if self.version == 'v3' else ''}/" try: result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() except Exception: util.print_stacktrace() - raise Failed(f"Radarr Error: Could not connect to Radarr at {params['url']}") + raise Failed(f"Radarr Error: Could not connect to Radarr at {self.url}") if "error" in result and result["error"] == "Unauthorized": raise Failed("Radarr Error: Invalid API Key") if "version" not in result: raise Failed("Radarr Error: Unexpected Response Check URL") - self.quality_profile_id = None + self.add = params["add"] + self.root_folder_path = params["root_folder_path"] + self.monitor = params["monitor"] + self.availability = params["availability"] + self.quality_profile_id = self.get_profile_id(params["quality_profile"]) + self.tag = params["tag"] + self.tags = self.get_tags() + self.search = params["search"] + + def get_profile_id(self, profile_name): profiles = "" - for profile in self.send_get("qualityProfile" if params["version"] == "v3" else "profile"): + for profile in self.send_get("qualityProfile" if self.version == "v3" else "profile"): if len(profiles) > 0: profiles += ", " profiles += profile["name"] - if profile["name"] == params["quality_profile"]: - self.quality_profile_id = profile["id"] - if not self.quality_profile_id: - raise Failed(f"Radarr Error: quality_profile: {params['quality_profile']} does not exist in radarr. Profiles available: {profiles}") - self.tags = self.get_tags() - self.url = params["url"] - self.version = params["version"] - self.token = params["token"] - self.root_folder_path = params["root_folder_path"] - self.add = params["add"] - self.search = params["search"] - self.tag = params["tag"] + if profile["name"] == profile_name: + return profile["id"] + raise Failed(f"Radarr Error: quality_profile: {profile_name} does not exist in radarr. Profiles available: {profiles}") def get_tags(self): return {tag["label"]: tag["id"] for tag in self.send_get("tag")} @@ -56,13 +65,17 @@ class RadarrAPI: else: raise Failed(f"Sonarr Error: TMDb ID: {tmdb_id} not found") - def add_tmdb(self, tmdb_ids, tags=None, folder=None): + def add_tmdb(self, tmdb_ids, **options): logger.info("") logger.debug(f"TMDb IDs: {tmdb_ids}") tag_nums = [] add_count = 0 - if tags is None: - tags = self.tag + folder = options["folder"] if "folder" in options else self.root_folder_path + monitor = options["monitor"] if "monitor" in options else self.monitor + availability = options["availability"] if "availability" in options else self.availability + quality_profile_id = self.get_profile_id(options["quality"]) if "quality" in options else self.quality_profile_id + tags = options["tag"] if "tag" in options else self.tag + search = options["search"] if "search" in options else self.search if tags: self.add_tags(tags) tag_nums = [self.tags[label] for label in tags if label in self.tags] @@ -80,14 +93,15 @@ class RadarrAPI: url_json = { "title": movie_info["title"], - f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id, + f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id, "year": int(movie_info["year"]), "tmdbid": int(tmdb_id), "titleslug": movie_info["titleSlug"], - "monitored": True, - "rootFolderPath": self.root_folder_path if folder is None else folder, + "minimumAvailability": availability_translation[availability], + "monitored": monitor, + "rootFolderPath": folder, "images": [{"covertype": "poster", "url": poster_url}], - "addOptions": {"searchForMovie": self.search} + "addOptions": {"searchForMovie": search} } if tag_nums: url_json["tags"] = tag_nums diff --git a/modules/sonarr.py b/modules/sonarr.py index 0b501756..fdcb0dba 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -5,53 +5,65 @@ from retrying import retry logger = logging.getLogger("Plex Meta Manager") +series_type = ["standard", "daily", "anime"] +monitor_translation = { + "all": "all", + "future": "future", + "missing": "missing", + "existing": "existing", + "pilot": "pilot", + "first": "firstSeason", + "latest": "latestSeason", + "none": "none" +} + class SonarrAPI: def __init__(self, params, language): - self.base_url = f"{params['url']}/api{'/v3/' if params['version'] == 'v3' else '/'}" + self.url = params["url"] self.token = params["token"] + self.version = params["version"] + self.base_url = f"{self.url}/api{'/v3/' if self.version == 'v3' else '/'}" try: result = requests.get(f"{self.base_url}system/status", params={"apikey": f"{self.token}"}).json() except Exception: util.print_stacktrace() - raise Failed(f"Sonarr Error: Could not connect to Sonarr at {params['url']}") + raise Failed(f"Sonarr Error: Could not connect to Sonarr at {self.url}") if "error" in result and result["error"] == "Unauthorized": raise Failed("Sonarr Error: Invalid API Key") if "version" not in result: raise Failed("Sonarr Error: Unexpected Response Check URL") - self.quality_profile_id = None - profiles = "" - for profile in self.send_get("qualityProfile" if params["version"] == "v3" else "profile"): - if len(profiles) > 0: - profiles += ", " - profiles += profile["name"] - if profile["name"] == params["quality_profile"]: - self.quality_profile_id = profile["id"] - if not self.quality_profile_id: - raise Failed(f"Sonarr Error: quality_profile: {params['quality_profile']} does not exist in sonarr. Profiles available: {profiles}") - + self.add = params["add"] + self.root_folder_path = params["root_folder_path"] + self.monitor = params["monitor"] + self.quality_profile_id = self.get_profile_id(params["quality_profile"], "quality_profile") self.language_profile_id = None - if params["version"] == "v3" and params["language_profile"] is not None: - profiles = "" - for profile in self.send_get("languageProfile"): - if len(profiles) > 0: - profiles += ", " - profiles += profile["name"] - if profile["name"] == params["language_profile"]: - self.language_profile_id = profile["id"] - if not self.quality_profile_id: - raise Failed(f"Sonarr Error: language_profile: {params['language_profile']} does not exist in sonarr. Profiles available: {profiles}") - + if self.version == "v3" and params["language_profile"] is not None: + self.language_profile_id = self.get_profile_id(params["language_profile"], "language_profile") if self.language_profile_id is None: self.language_profile_id = 1 - - self.tags = self.get_tags() - self.language = language - self.version = params["version"] - self.root_folder_path = params["root_folder_path"] - self.add = params["add"] - self.search = params["search"] + self.series_type = params["series_type"] self.season_folder = params["season_folder"] self.tag = params["tag"] + self.tags = self.get_tags() + self.search = params["search"] + self.cutoff_search = params["cutoff_search"] + self.language = language + + def get_profile_id(self, profile_name, profile_type): + profiles = "" + if profile_type == "quality_profile" and self.version == "v3": + endpoint = "qualityProfile" + elif profile_type == "language_profile": + endpoint = "languageProfile" + else: + endpoint = "profile" + for profile in self.send_get(endpoint): + if len(profiles) > 0: + profiles += ", " + profiles += profile["name"] + if profile["name"] == profile_name: + return profile["id"] + raise Failed(f"Sonarr Error: {profile_type}: {profile_name} does not exist in sonarr. Profiles available: {profiles}") def get_tags(self): return {tag["label"]: tag["id"] for tag in self.send_get("tag")} @@ -72,13 +84,20 @@ class SonarrAPI: else: raise Failed(f"Sonarr Error: TVDb ID: {tvdb_id} not found") - def add_tvdb(self, tvdb_ids, tags=None, folder=None): + def add_tvdb(self, tvdb_ids, **options): logger.info("") logger.debug(f"TVDb IDs: {tvdb_ids}") tag_nums = [] add_count = 0 - if tags is None: - tags = self.tag + folder = options["folder"] if "folder" in options else self.root_folder_path + monitor = options["monitor"] if "monitor" in options else self.monitor + quality_profile_id = self.get_profile_id(options["quality"], "quality_profile") if "quality" in options else self.quality_profile_id + language_profile_id = self.get_profile_id(options["language"], "language_profile") if "quality" in options else self.quality_profile_id + series = options["series"] if "series" in options else self.series_type + season = options["season"] if "season" in options else self.season_folder + tags = options["tag"] if "tag" in options else self.tag + search = options["search"] if "search" in options else self.search + cutoff_search = options["cutoff_search"] if "cutoff_search" in options else self.cutoff_search if tags: self.add_tags(tags) tag_nums = [self.tags[label] for label in tags if label in self.tags] @@ -96,17 +115,22 @@ class SonarrAPI: url_json = { "title": show_info["title"], - f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": self.quality_profile_id, - "languageProfileId": self.language_profile_id, + f"{'qualityProfileId' if self.version == 'v3' else 'profileId'}": quality_profile_id, + "languageProfileId": language_profile_id, "tvdbId": int(tvdb_id), "titleslug": show_info["titleSlug"], "language": self.language, - "monitored": True, - "seasonFolder": self.season_folder, - "rootFolderPath": self.root_folder_path if folder is None else folder, + "monitored": monitor != "none", + "seasonFolder": season, + "seriesType": series, + "rootFolderPath": folder, "seasons": [], "images": [{"covertype": "poster", "url": poster_url}], - "addOptions": {"searchForMissingEpisodes": self.search} + "addOptions": { + "searchForMissingEpisodes": search, + "searchForCutoffUnmetEpisodes": cutoff_search, + "monitor": monitor_translation[monitor] + } } if tag_nums: url_json["tags"] = tag_nums From 9376fcf1693f66ed609b61e3cd8c3cb5cf2dea5a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 4 Apr 2021 22:29:11 -0400 Subject: [PATCH 26/28] removed unused imports --- modules/radarr.py | 2 +- modules/sonarr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/radarr.py b/modules/radarr.py index 48ec8994..b8b52e17 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -1,4 +1,4 @@ -import logging, re, requests +import logging, requests from modules import util from modules.util import Failed from retrying import retry diff --git a/modules/sonarr.py b/modules/sonarr.py index fdcb0dba..c0fe97c8 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -1,4 +1,4 @@ -import logging, re, requests +import logging, requests from modules import util from modules.util import Failed from retrying import retry From 64faafc7ac5e608c6027d10df937d71a5cf737ba Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 5 Apr 2021 00:01:30 -0400 Subject: [PATCH 27/28] v1.7.0 --- README.md | 23 ++++++++++++----------- plex_meta_manager.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fcf0da53..878a821a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Plex Meta Manager -#### Version 1.6.4 +#### Version 1.7.0 The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services. @@ -11,16 +11,17 @@ The script is designed to work with most Metadata agents including the new Plex ## Getting Started -* [Wiki](https://github.com/meisnate12/Plex-Meta-Manager/wiki) -* [Local Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Installation) -* [Docker Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker-Installation) -* [unRAID Installation](https://github.com/meisnate12/Plex-Meta-Manager/wiki/unRAID-Installation) +1. Install Plex Meta Manager either by installing Python3 and following the [Local Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Local-Installation) + or by installing Docker and following the [Docker Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Docker-Installation) or the [unRAID Installation Guide](https://github.com/meisnate12/Plex-Meta-Manager/wiki/unRAID-Installation) +2. Once installed, you have to create a [Configuration File](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Configuration-File) filled with all your values to connect to the various services. +3. After that you can start updating Metadata and building automatic Collections by creating a [Metadata File](https://github.com/meisnate12/Plex-Meta-Manager/wiki/Metadata-File) for each Library you want to interact with. +4. Explore the [Wiki](https://github.com/meisnate12/Plex-Meta-Manager/wiki) to see all the different Collection Builders that can be used to create collections. ## Support -* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB) -* If you're getting an error or have an enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues) -* If you have a configuration question visit the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions) -* To see user submitted Metadata configuration files and you could even add your own go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs) -* Pull Request are welcome but please submit them to the develop branch -* If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki) +* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB). +* If you're getting an Error or have an Enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues). +* If you have a configuration question post in the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions). +* To see user submitted Metadata configuration files, and you to even add your own, go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs). +* Pull Request are welcome but please submit them to the develop branch. +* If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki). diff --git a/plex_meta_manager.py b/plex_meta_manager.py index c02e3a0c..475ad391 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -89,7 +89,7 @@ util.centered("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_ util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ") util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ") util.centered(" |___/ ") -util.centered(" Version: 1.6.4 ") +util.centered(" Version: 1.7.0 ") util.separator() if my_tests: From 1292ea0ccb58d933335d7bd405f2a9bc4a6a00bd Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 5 Apr 2021 00:09:26 -0400 Subject: [PATCH 28/28] catch #150 --- modules/sonarr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/sonarr.py b/modules/sonarr.py index c0fe97c8..26936005 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -1,4 +1,5 @@ import logging, requests +from json.decoder import JSONDecodeError from modules import util from modules.util import Failed from retrying import retry @@ -144,6 +145,10 @@ class SonarrAPI: except KeyError: logger.debug(url_json) logger.error(f"Sonarr Error: {response.json()}") + except JSONDecodeError: + logger.debug(url_json) + logger.error(f"Sonarr Error: {response}") + logger.info(f"{add_count} Show{'s' if add_count > 1 else ''} added to Sonarr") @retry(stop_max_attempt_number=6, wait_fixed=10000)