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 bc482813a..ae7e70329 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -210,6 +210,8 @@ def add_to_collection(self, collection, items, filters, show_filtered, rating_ke 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 4ced414eb..fd4ec34d6 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -213,14 +213,13 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def get_summary(summary_method, summaries): 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 7ceae2211..0598c1782 100644 --- a/modules/config.py +++ b/modules/config.py @@ -122,11 +122,11 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 48fadf6fb..e5a007174 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -14,11 +14,13 @@ def __init__(self, config): "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 ae7e70329..0d672d06c 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -30,8 +30,11 @@ def __init__(self, params, TMDb, TVDb): 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 @@ def update_metadata(self, TMDb, test): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 @@ def add_edit(name, current, group, alias, key=None, value=None): 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 1f48479d2..aecbe6b3a 100644 --- a/modules/trakttv.py +++ b/modules/trakttv.py @@ -94,9 +94,15 @@ def convert_id(self, external_id, from_source, to_source, media_type): 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 4c032273d..24d3cba4a 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 b6138fae9..1b0f6bf4a 100644 --- a/modules/anidb.py +++ b/modules/anidb.py @@ -6,6 +6,8 @@ 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 2ae98118d..7f40598f3 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -5,6 +5,21 @@ 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 @@ def get_items(self, method, data, status_message=True): 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 fd4ec34d6..0668013e7 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def check_map(input_ids): 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 e5a007174..ad171288f 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -6,6 +6,8 @@ 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 17ac2449e..7b214a534 100644 --- a/modules/letterboxd.py +++ b/modules/letterboxd.py @@ -6,6 +6,8 @@ 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 063638feb..ec644a52f 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -6,6 +6,73 @@ 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 @@ def get_items(self, method, data, status_message=True): 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 @@ def get_items(self, method, data, status_message=True): 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 0d672d06c..34ad6c134 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -12,6 +12,91 @@ 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 @@ def get_search_choices(self, search_name, key=False): 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 @@ def add_to_collection(self, collection, items, filters, show_filtered, rating_ke 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 5b9bc5172..c258c62e2 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -5,6 +5,8 @@ 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 da8a98c92..4df51913a 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -7,6 +7,107 @@ 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 @@ def get_pagenation(self, method, amount, is_movie): 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 aecbe6b3a..b24263eaf 100644 --- a/modules/trakttv.py +++ b/modules/trakttv.py @@ -11,6 +11,18 @@ 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 4d7a5b8a8..02e28a8c0 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -6,6 +6,15 @@ 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 24d3cba4a..34bd631d8 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 @@ def retry_if_not_failed(exception): "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 @@ def retry_if_not_failed(exception): "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 0668013e7..bf41d8971 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -188,7 +188,7 @@ def __init__(self, config, library, name, data): 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 @@ def replace_txt(txt): 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 34ad6c134..f242b563a 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -190,7 +190,7 @@ def validate_search_list(self, data, search_name): 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 0598c1782..761b61d68 100644 --- a/modules/config.py +++ b/modules/config.py @@ -33,7 +33,7 @@ def __init__(self, default_dir, config_path=None): 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 @@ def replace_attr(all_data, attr, par): 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 99915075c..fcf0da53c 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 761b61d68..f339a6b7d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -128,7 +128,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 f242b563a..771dd53ed 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -113,6 +113,7 @@ def __init__(self, params, TMDb, TVDb): 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 bf41d8971..fb58abbea 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -454,15 +454,6 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 f339a6b7d..1c23ef22e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -389,7 +389,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def update_libraries(self, test, requested_collections): 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 @@ def update_libraries(self, test, requested_collections): 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 @@ def update_libraries(self, test, requested_collections): 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 771dd53ed..5357e86a5 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -113,6 +113,7 @@ def __init__(self, params, TMDb, TVDb): 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 91f84025f..c02e3a0ce 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -13,6 +13,7 @@ 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 @@ def check_bool(env_str, default): 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 @@ def fmt_filter(record): 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 5357e86a5..7156cc4eb 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -300,9 +300,7 @@ def add_to_collection(self, collection, items, filters, show_filtered, rating_ke 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 fb58abbea..4f9f07d75 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -16,7 +16,8 @@ "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 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 1c23ef22e..5c26dfd0f 100644 --- a/modules/config.py +++ b/modules/config.py @@ -385,7 +385,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 7156cc4eb..6781fd5a3 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -54,6 +54,7 @@ "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 @@ "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 @@ def server_search(self, data): 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 4f9f07d75..5f2d46bea 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -16,6 +16,7 @@ "directors": "director", "genres": "genre", "labels": "label", + "rating": "critic_rating", "studios": "studio", "networks": "network", "producers": "producer", @@ -124,7 +125,8 @@ "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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 6781fd5a3..16159142a 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -19,7 +19,8 @@ "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 @@ "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 @@ def update_metadata(self, TMDb, test): 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 5f2d46bea..bc04c0df9 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -502,7 +502,7 @@ def replace_txt(txt): 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 16159142a..815b42b2d 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -453,10 +453,10 @@ def add_edit(name, current, group, alias, key=None, value=None, var_type="str"): 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 bc04c0df9..032be01e8 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -234,7 +234,7 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 032be01e8..f21141821 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -86,7 +86,7 @@ "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 @@ def __init__(self, config, library, name, data): 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 @@ def check_map(input_ids): 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 @@ def check_map(input_ids): 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 f1a891c43..9f996f6a6 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -37,7 +37,7 @@ def __init__(self, tmdb, params): 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 @@ def add_tmdb(self, tmdb_ids, tag=None): "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 a13c5c278..4ae3268b2 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -39,7 +39,7 @@ def __init__(self, tvdb, params, language): 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 @@ def add_tvdb(self, tvdb_ids, tag=None): "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 f21141821..3b44eccf5 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -452,13 +452,15 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def check_map(input_ids): 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 5c26dfd0f..aac91544b 100644 --- a/modules/config.py +++ b/modules/config.py @@ -258,6 +258,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 4ae3268b2..91617d658 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -34,6 +34,7 @@ def __init__(self, tvdb, params, language): 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 @@ def add_tvdb(self, tvdb_ids, tag=None, folder=None): 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 aac91544b..e208210d1 100644 --- a/modules/config.py +++ b/modules/config.py @@ -258,7 +258,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 9f996f6a6..602d8fb4c 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -20,7 +20,7 @@ def __init__(self, tmdb, params): 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 @@ def add_tmdb(self, tmdb_ids, tag=None, folder=None): 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 @@ def add_tmdb(self, tmdb_ids, tag=None, folder=None): } 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 @@ def add_tmdb(self, tmdb_ids, tag=None, folder=None): @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 91617d658..fd79d589c 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -20,7 +20,7 @@ def __init__(self, tvdb, params, language): 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 @@ def __init__(self, tvdb, params, language): 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 @@ def add_tvdb(self, tvdb_ids, tag=None, folder=None): 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 @@ def add_tvdb(self, tvdb_ids, tag=None, folder=None): } 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 @@ def add_tvdb(self, tvdb_ids, tag=None, folder=None): @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 3b44eccf5..0c38bd76d 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1077,7 +1077,7 @@ def check_map(input_ids): 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 @@ def check_map(input_ids): 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 602d8fb4c..0ae3fcb9b 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -28,6 +28,7 @@ def __init__(self, tmdb, params): 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 @@ def __init__(self, tmdb, params): 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 fd79d589c..d1c92934c 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -44,6 +44,7 @@ def __init__(self, tvdb, params, language): 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 @@ def __init__(self, tvdb, params, language): 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 e208210d1..f1ba6e6df 100644 --- a/modules/config.py +++ b/modules/config.py @@ -345,7 +345,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 0ae3fcb9b..944834c45 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -6,11 +6,11 @@ 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 @@ def __init__(self, tmdb, params): 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 @@ def add_tags(self, tags): 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 @@ def add_tmdb(self, tmdb_ids, tags=None, folder=None): 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 d1c92934c..0b501756c 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -6,11 +6,11 @@ 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 @@ def __init__(self, tvdb, params, language): 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 @@ def add_tags(self, tags): 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 @@ def add_tvdb(self, tvdb_ids, tags=None, folder=None): 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 0c38bd76d..e43ef3f9f 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -456,10 +456,10 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def run_methods(self, collection_obj, collection_name, rating_key_map, movie_map 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 @@ def check_map(input_ids): 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 815b42b2d..d2c3ce21e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -85,6 +85,7 @@ "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 @@ def validate_collections(self, collections): 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 e43ef3f9f..001c86b94 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -294,7 +294,7 @@ def replace_txt(txt): 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 @@ def check_map(input_ids): 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 d2c3ce21e..ac7af4e53 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -35,9 +35,11 @@ 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 f1ba6e6df..cabab956d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -22,6 +22,12 @@ 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 @@ def replace_attr(all_data, attr, par): 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" 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 18631dc86..739286c61 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 74bfefa29..f9bb7211d 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 001c86b94..5a8ff554a 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 @@ "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 @@ "tmdb_person" ] boolean_details = [ - "add_to_arr", "show_filtered", "show_missing", "save_missing" @@ -152,12 +150,12 @@ def __init__(self, config, library, name, data): 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 @@ def __init__(self, config, library, name, data): 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def replace_txt(txt): 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= 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 @@ def check_map(input_ids): 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 @@ def check_map(input_ids): 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 cabab956d..3bba2ab2c 100644 --- a/modules/config.py +++ b/modules/config.py @@ -24,7 +24,28 @@ 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No 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 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No 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 944834c45..48ec8994c 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -5,37 +5,46 @@ 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 @@ def lookup(self, tmdb_id): 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 @@ def add_tmdb(self, tmdb_ids, tags=None, folder=None): 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 0b501756c..fdcb0dba8 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -5,53 +5,65 @@ 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 @@ def lookup(self, tvdb_id): 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 @@ def add_tvdb(self, tvdb_ids, tags=None, folder=None): 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 48ec8994c..b8b52e172 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 fdcb0dba8..c0fe97c80 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 fcf0da53c..878a821a4 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 c02e3a0ce..475ad3918 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -89,7 +89,7 @@ def fmt_filter(record): 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 c0fe97c80..269360055 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 @@ def add_tvdb(self, tvdb_ids, **options): 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)