From 6cacd7c8f69b5a1cce0b824066f1099c062d2fdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 06:09:26 +0000 Subject: [PATCH 01/57] Bump plexapi from 4.7.0 to 4.7.1 Bumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.7.0 to 4.7.1. - [Release notes](https://github.com/pkkid/python-plexapi/releases) - [Commits](https://github.com/pkkid/python-plexapi/compare/4.7.0...4.7.1) --- updated-dependencies: - dependency-name: plexapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15e501aa6..28b18adda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -PlexAPI==4.7.0 +PlexAPI==4.7.1 tmdbv3api==1.7.6 arrapi==1.1.3 lxml==4.6.3 From 39ed4a283abb483b3cfe24fdd8cc344962d66567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Sep 2021 06:09:36 +0000 Subject: [PATCH 02/57] Bump ruamel-yaml from 0.17.10 to 0.17.16 Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.10 to 0.17.16. --- updated-dependencies: - dependency-name: ruamel-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15e501aa6..27c6dd55a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ tmdbv3api==1.7.6 arrapi==1.1.3 lxml==4.6.3 requests==2.26.0 -ruamel.yaml==0.17.10 +ruamel.yaml==0.17.16 schedule==1.1.0 retrying==1.3.3 pathvalidate==2.4.1 From 3daeb795d3ea425d2c6a123931a2a1663f664cbe Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 14 Sep 2021 08:57:40 -0400 Subject: [PATCH 03/57] MyAnimeList fix --- modules/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/convert.py b/modules/convert.py index 7c376189b..f3b0a1011 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -230,8 +230,8 @@ def get_id(self, item, library): raise Failed(f"Hama Agent ID: {check_id} not supported") elif item_type == "myanimelist": library.mal_map[int(check_id)] = item.ratingKey - if check_id in self.mal_to_anidb: - anidb_id = self.mal_to_anidb[check_id] + if int(check_id) in self.mal_to_anidb: + anidb_id = self.mal_to_anidb[int(check_id)] else: raise Failed(f"Convert Error: AniDB ID not found for MyAnimeList ID: {check_id}") elif item_type == "local": raise Failed("No match in Plex") From a0f0b6e3e6857617fde8000c530891cda36cc7f0 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 14 Sep 2021 11:10:24 -0400 Subject: [PATCH 04/57] fix MyanimeList --- modules/convert.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/convert.py b/modules/convert.py index f3b0a1011..489a2a712 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -62,10 +62,10 @@ def anilist_to_ids(self, anilist_ids, library): def myanimelist_to_ids(self, mal_ids, library): ids = [] for mal_id in mal_ids: - if mal_id in library.mal_map: - ids.append((library.mal_map[mal_id], "ratingKey")) - elif mal_id in self.mal_to_anidb: - ids.extend(self.anidb_to_ids(self.mal_to_anidb[mal_id], library)) + if int(mal_id) in library.mal_map: + ids.append((library.mal_map[int(mal_id)], "ratingKey")) + elif int(mal_id) in self.mal_to_anidb: + ids.extend(self.anidb_to_ids(self.mal_to_anidb[int(mal_id)], library)) else: logger.error(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}") return ids From a862d79fa247684d52827d785fc56f3fae60968a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 14 Sep 2021 17:50:39 -0400 Subject: [PATCH 05/57] fix mal and anidb caches --- modules/convert.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/convert.py b/modules/convert.py index 489a2a712..8f817cf65 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -193,16 +193,20 @@ def get_id(self, item, library): tvdb_id = [] imdb_id = [] anidb_id = None + guid = requests.utils.urlparse(item.guid) + item_type = guid.scheme.split(".")[-1] + check_id = guid.netloc if self.config.Cache: cache_id, imdb_check, media_type, expired = self.config.Cache.query_guid_map(item.guid) if cache_id and not expired: media_id_type = "movie" if "movie" in media_type else "show" + if item_type == "hama" and check_id.startswith("anidb"): + anidb_id = int(re.search("-(.*)", check_id).group(1)) + library.anidb_map[anidb_id] = item.ratingKey + elif item_type == "myanimelist": + library.mal_map[int(check_id)] = item.ratingKey return media_id_type, cache_id, imdb_check try: - guid = requests.utils.urlparse(item.guid) - item_type = guid.scheme.split(".")[-1] - check_id = guid.netloc - if item_type == "plex": try: for guid_tag in library.get_guids(item): From 9b5b92c536b2cbc02928870ace43532bd9ee34e6 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 14 Sep 2021 23:16:59 -0400 Subject: [PATCH 06/57] fix for delete_below_minimum --- modules/builder.py | 48 +++++++++++++++++++++++++++----------------- plex_meta_manager.py | 38 +++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 259e9deca..5e42a98cf 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -76,7 +76,7 @@ ] poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"] background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"] -boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "item_assets", "missing_only_released", "revert_overlay", "delete_below_minimum"] +boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", "delete_below_minimum"] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", @@ -85,7 +85,7 @@ details = ["collection_mode", "collection_order", "collection_level", "collection_minimum", "label"] + boolean_details + string_details collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ poster_details + background_details + summary_details + string_details -item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay"] + list(plex.item_advance_keys.keys()) +item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_assets", "revert_overlay"] + list(plex.item_advance_keys.keys()) radarr_details = ["radarr_add", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"] sonarr_details = [ "sonarr_add", "sonarr_add_existing", "sonarr_folder", "sonarr_monitor", "sonarr_language", "sonarr_series", @@ -168,7 +168,7 @@ def __init__(self, config, library, metadata, name, no_missing, data): "save_missing": self.library.save_missing, "missing_only_released": self.library.missing_only_released, "create_asset_folders": self.library.create_asset_folders, - "item_assets": False + "delete_below_minimum": self.library.delete_below_minimum } self.item_details = {} self.radarr_details = {} @@ -183,12 +183,12 @@ def __init__(self, config, library, metadata, name, no_missing, data): self.filtered_keys = {} self.run_again_movies = [] self.run_again_shows = [] + self.items = [] self.posters = {} self.backgrounds = {} self.summaries = {} self.schedule = "" self.minimum = self.library.collection_minimum - self.delete_below_minimum = self.library.delete_below_minimum self.current_time = datetime.now() self.current_year = self.current_time.year @@ -722,6 +722,9 @@ def _item_details(self, method_name, method_data, method_mod, method_final, meth raise Failed("Each Overlay can only be used once per Library") self.library.overlays.append(method_data) self.item_details[method_name] = method_data + elif method_name in ["item_assets", "revert_overlay"]: + if util.parse(method_name, method_data, datatype="bool", default=False): + self.item_details[method_name] = True elif method_name in plex.item_advance_keys: key, options = plex.item_advance_keys[method_name] if method_name in advance_new_agent and self.library.agent not in plex.new_plex_agents: @@ -1721,15 +1724,10 @@ def sync_collection(self): logger.info("") logger.info(f"{count_removed} {self.collection_level.capitalize()}{'s' if count_removed == 1 else ''} Removed") - def update_item_details(self): - add_tags = self.item_details["item_label"] if "item_label" in self.item_details else None - remove_tags = self.item_details["item_label.remove"] if "item_label.remove" in self.item_details else None - sync_tags = self.item_details["item_label.sync"] if "item_label.sync" in self.item_details else None - - if self.build_collection: - items = self.library.get_collection_items(self.obj, self.smart_label_collection) - else: - items = [] + def load_collection_items(self): + if self.build_collection and self.obj: + self.items = self.library.get_collection_items(self.obj, self.smart_label_collection) + elif not self.build_collection: logger.info("") util.separator(f"Items Found for {self.name} Collection", space=False, border=False) logger.info("") @@ -1737,10 +1735,13 @@ def update_item_details(self): try: item = self.fetch_item(rk) logger.info(f"{item.title} (Rating Key: {rk})") - items.append(item) + self.items.append(item) except Failed as e: logger.error(e) + if not self.items: + raise Failed(f"Plex Error: No Collection items found") + def update_item_details(self): logger.info("") util.separator(f"Updating Details of the Items in {self.name} Collection", space=False, border=False) logger.info("") @@ -1767,16 +1768,20 @@ def update_item_details(self): temp_image = os.path.join(overlay_folder, f"temp.png") overlay = (overlay_name, overlay_folder, overlay_image, temp_image) - revert = "revert_overlay" in self.details and self.details["revert_overlay"] + revert = "revert_overlay" in self.item_details if revert: overlay = None + add_tags = self.item_details["item_label"] if "item_label" in self.item_details else None + remove_tags = self.item_details["item_label.remove"] if "item_label.remove" in self.item_details else None + sync_tags = self.item_details["item_label.sync"] if "item_label.sync" in self.item_details else None + tmdb_ids = [] tvdb_ids = [] - for item in items: + for item in self.items: if int(item.ratingKey) in rating_keys and not revert: rating_keys.remove(int(item.ratingKey)) - if self.details["item_assets"] or overlay is not None: + if "item_assets" in self.item_details or overlay is not None: try: self.library.update_item_from_assets(item, overlay=overlay) except Failed as e: @@ -1823,7 +1828,7 @@ def delete_collection(self): if self.obj: self.library.query(self.obj.delete) - def update_details(self): + def load_collection(self): if not self.obj and self.smart_url: self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url) elif self.smart_label_collection: @@ -1835,6 +1840,10 @@ def update_details(self): raise Failed(f"Collection Error: Label: {self.name} was not added to any items in the Library") self.obj = self.library.get_collection(self.name) + def update_details(self): + logger.info("") + util.separator(f"Updating Details of {self.name} Collection", space=False, border=False) + logger.info("") if self.smart_url and self.smart_url != self.library.smart_filter(self.obj): self.library.update_smart_collection(self.obj, self.smart_url) logger.info(f"Detail: Smart Filter updated to {self.smart_url}") @@ -1983,6 +1992,9 @@ def get_summary(summary_method, summaries): self.library.upload_images(self.obj, poster=poster, background=background) def sort_collection(self): + logger.info("") + util.separator(f"Sorting {self.name} Collection", space=False, border=False) + logger.info("") items = self.library.get_collection_items(self.obj, self.smart_label_collection) keys = {item.ratingKey: item for item in items} previous = None diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 47935d44e..eb0f37026 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -504,10 +504,10 @@ def run_collection(config, library, metadata, requested_collections): builder.add_to_collection() elif len(builder.rating_keys) < builder.minimum and builder.build_collection: logger.info("") - logger.info(f"Collection minimum: {builder.minimum} not met for {mapping_name} Collection") - logger.info("") - if library.delete_below_minimum and builder.obj: + logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") + if builder.details["delete_below_minimum"] and builder.obj: builder.delete_collection() + logger.info("") logger.info(f"Collection {builder.obj.title} deleted") if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): if builder.details["show_missing"] is True: @@ -518,20 +518,28 @@ def run_collection(config, library, metadata, requested_collections): if builder.sync and len(builder.rating_keys) > 0 and builder.build_collection: builder.sync_collection() + run_item_details = True if builder.build_collection: - logger.info("") - util.separator(f"Updating Details of {mapping_name} Collection", space=False, border=False) - logger.info("") - builder.update_details() - - if builder.custom_sort: - library.run_sort.append(builder) - # logger.info("") - # util.separator(f"Sorting {mapping_name} Collection", space=False, border=False) - # logger.info("") - # builder.sort_collection() + try: + builder.load_collection() + except Failed: + run_item_details = False + logger.info("") + util.separator("No Collection to Update", space=False, border=False) + else: + builder.update_details() + if builder.custom_sort: + library.run_sort.append(builder) + #builder.sort_collection() - builder.update_item_details() + if builder.item_details and run_item_details: + try: + builder.load_collection_items() + except Failed: + logger.info("") + util.separator("No Items Found", space=False, border=False) + else: + builder.update_item_details() if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0): library.run_again.append(builder) From b281639ed34fc873562f4f6c7788de95f6aa16bd Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 17 Sep 2021 22:07:30 -0400 Subject: [PATCH 07/57] fix for no id --- modules/trakt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trakt.py b/modules/trakt.py index ee5772ac8..1bec27c68 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -158,7 +158,7 @@ def _parse(self, items, typeless=False, item_type=None): else: continue id_type = "tmdb" if current_type == "movie" else "tvdb" - if data["ids"][id_type]: + if id_type in data["ids"] and data["ids"][id_type]: final_id = data["ids"][id_type] if current_type == "episode": final_id = f"{final_id}_{item[current_type]['season']}" From 008c0fe5ffca8a69a0d34a74c29a5d96e8e06600 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 20 Sep 2021 22:24:07 -0400 Subject: [PATCH 08/57] fix for bad rating key --- config/config.yml.template | 2 -- modules/builder.py | 3 ++- plex_meta_manager.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/config/config.yml.template b/config/config.yml.template index d6b0d9251..fb7951a73 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -45,7 +45,6 @@ tautulli: # Can be individually specified radarr: # Can be individually specified per library as well url: http://192.168.1.12:7878 token: ################################ - version: v3 add: false root_folder_path: S:/Movies monitor: true @@ -56,7 +55,6 @@ radarr: # Can be individually specified sonarr: # Can be individually specified per library as well url: http://192.168.1.12:8989 token: ################################ - version: v3 add: false root_folder_path: "S:/TV Shows" monitor: all diff --git a/modules/builder.py b/modules/builder.py index 5e42a98cf..8e0aa1c61 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1760,7 +1760,8 @@ def update_item_details(self): except Failed as e: logger.error(e) continue - self.library.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"]) + if isinstance(item, (Movie, Show)): + self.library.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"]) self.config.Cache.update_remove_overlay(self.library.image_table_name, overlay_name) rating_keys = [int(item.ratingKey) for item in self.library.get_labeled_items(f"{overlay_name} Overlay")] overlay_folder = os.path.join(self.config.default_dir, "overlays", overlay_name) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index eb0f37026..67f573dbb 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -530,7 +530,7 @@ def run_collection(config, library, metadata, requested_collections): builder.update_details() if builder.custom_sort: library.run_sort.append(builder) - #builder.sort_collection() + # builder.sort_collection() if builder.item_details and run_item_details: try: From 5a37bca5dda3c4a3671f37a2cf85700fa20fa4da Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 20 Sep 2021 23:57:16 -0400 Subject: [PATCH 09/57] reorg --- modules/library.py | 259 +++++++++++++++++++++++++++++++++++++++++++++ modules/plex.py | 254 +++----------------------------------------- 2 files changed, 275 insertions(+), 238 deletions(-) create mode 100644 modules/library.py diff --git a/modules/library.py b/modules/library.py new file mode 100644 index 000000000..7f420253c --- /dev/null +++ b/modules/library.py @@ -0,0 +1,259 @@ +import logging, os, requests, shutil, time +from abc import ABC, abstractmethod +from modules import util +from modules.meta import Metadata +from modules.util import Failed, ImageData +from PIL import Image +from ruamel import yaml + +logger = logging.getLogger("Plex Meta Manager") + +class Library(ABC): + def __init__(self, config, params): + self.Radarr = None + self.Sonarr = None + self.Tautulli = None + self.collections = [] + self.metadatas = [] + self.metadata_files = [] + self.missing = {} + self.movie_map = {} + self.show_map = {} + self.imdb_map = {} + self.anidb_map = {} + self.mal_map = {} + self.movie_rating_key_map = {} + self.show_rating_key_map = {} + self.run_again = [] + self.run_sort = [] + self.overlays = [] + self.type = "" + self.config = config + self.name = params["name"] + self.original_mapping_name = params["mapping_name"] + self.metadata_path = params["metadata_path"] + self.asset_directory = params["asset_directory"] + self.default_dir = params["default_dir"] + self.mapping_name, output = util.validate_filename(self.original_mapping_name) + self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None + self.missing_path = os.path.join(self.default_dir, f"{self.original_mapping_name}_missing.yml") + self.asset_folders = params["asset_folders"] + self.assets_for_all = params["assets_for_all"] + self.sync_mode = params["sync_mode"] + self.show_unmanaged = params["show_unmanaged"] + self.show_filtered = params["show_filtered"] + self.show_missing = params["show_missing"] + self.save_missing = params["save_missing"] + self.missing_only_released = params["missing_only_released"] + self.create_asset_folders = params["create_asset_folders"] + self.mass_genre_update = params["mass_genre_update"] + self.mass_audience_rating_update = params["mass_audience_rating_update"] + self.mass_critic_rating_update = params["mass_critic_rating_update"] + self.mass_trakt_rating_update = params["mass_trakt_rating_update"] + self.radarr_add_all = params["radarr_add_all"] + self.sonarr_add_all = params["sonarr_add_all"] + self.collection_minimum = params["collection_minimum"] + self.delete_below_minimum = params["delete_below_minimum"] + self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? + self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? + self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? + self.optimize = params["plex"]["optimize"] # TODO: Here or just in Plex? + + self.mass_update = self.mass_genre_update or self.mass_audience_rating_update or self.mass_critic_rating_update \ + or self.mass_trakt_rating_update or self.split_duplicates or self.radarr_add_all or self.sonarr_add_all + + metadata = [] + for file_type, metadata_file in self.metadata_path: + if file_type == "Folder": + if os.path.isdir(metadata_file): + yml_files = util.glob_filter(os.path.join(metadata_file, "*.yml")) + if yml_files: + metadata.extend([("File", yml) for yml in yml_files]) + else: + logger.error(f"Config Error: No YAML (.yml) files found in {metadata_file}") + else: + logger.error(f"Config Error: Folder not found: {metadata_file}") + else: + metadata.append((file_type, metadata_file)) + for file_type, metadata_file in metadata: + try: + meta_obj = Metadata(config, self, file_type, metadata_file) + if meta_obj.collections: + self.collections.extend([c for c in meta_obj.collections]) + if meta_obj.metadata: + self.metadatas.extend([c for c in meta_obj.metadata]) + self.metadata_files.append(meta_obj) + except Failed as e: + util.print_multiline(e, error=True) + + if len(self.metadata_files) == 0: + logger.info("") + raise Failed("Metadata File Error: No valid metadata files found") + + if self.asset_directory: + logger.info("") + for ad in self.asset_directory: + logger.info(f"Using Asset Directory: {ad}") + + if output: + logger.info(output) + + def upload_images(self, item, poster=None, background=None, overlay=None): + image = None + image_compare = None + poster_uploaded = False + if self.config.Cache: + image, image_compare = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name) + + if poster is not None: + try: + if image_compare and str(poster.compare) != str(image_compare): + image = None + if image is None or image != item.thumb: + self._upload_image(item, poster) + poster_uploaded = True + logger.info(f"Detail: {poster.attribute} updated {poster.message}") + else: + logger.info(f"Detail: {poster.prefix}poster update not needed") + except Failed: + util.print_stacktrace() + logger.error(f"Detail: {poster.attribute} failed to update {poster.message}") + + if overlay is not None: + overlay_name, overlay_folder, overlay_image, temp_image = overlay + self.reload(item) + item_labels = {item_tag.tag.lower(): item_tag.tag for item_tag in item.labels} + for item_label in item_labels: + if item_label.endswith(" overlay") and item_label != f"{overlay_name.lower()} overlay": + raise Failed(f"Overlay Error: Poster already has an existing Overlay: {item_labels[item_label]}") + if poster_uploaded or image is None or image != item.thumb or f"{overlay_name.lower()} overlay" not in item_labels: + if not item.posterUrl: + raise Failed(f"Overlay Error: No existing poster to Overlay for {item.title}") + response = requests.get(item.posterUrl) + if response.status_code >= 400: + raise Failed(f"Overlay Error: Overlay Failed for {item.title}") + og_image = response.content + with open(temp_image, "wb") as handler: + handler.write(og_image) + shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.png")) + while util.is_locked(temp_image): + time.sleep(1) + try: + new_poster = Image.open(temp_image).convert("RGBA") + new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS) + new_poster.paste(overlay_image, (0, 0), overlay_image) + new_poster.save(temp_image) + self.upload_file_poster(item, temp_image) + self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"]) + poster_uploaded = True + logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}") + except OSError as e: + util.print_stacktrace() + logger.error(f"Overlay Error: {e}") + + background_uploaded = False + if background is not None: + try: + image = None + if self.config.Cache: + image, image_compare = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds") + if str(background.compare) != str(image_compare): + image = None + if image is None or image != item.art: + self._upload_image(item, background) + background_uploaded = True + logger.info(f"Detail: {background.attribute} updated {background.message}") + else: + logger.info(f"Detail: {background.prefix}background update not needed") + except Failed: + util.print_stacktrace() + logger.error(f"Detail: {background.attribute} failed to update {background.message}") + + if self.config.Cache: + if poster_uploaded: + self.config.Cache.update_image_map(item.ratingKey, self.image_table_name, item.thumb, poster.compare if poster else "") + if background_uploaded: + self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) + + @abstractmethod + def _upload_image(self, item, image): + pass + + @abstractmethod + def upload_file_poster(self, item, image): + pass + + @abstractmethod + def reload(self, item): + pass + + @abstractmethod + def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None): + pass + + @abstractmethod + def get_all(self): + pass + + def add_missing(self, collection, items, is_movie): + col_name = collection.encode("ascii", "replace").decode() + if col_name not in self.missing: + self.missing[col_name] = {} + section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)" + if section not in self.missing[col_name]: + self.missing[col_name][section] = {} + for title, item_id in items: + self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode() + with open(self.missing_path, "w"): pass + try: + yaml.round_trip_dump(self.missing, open(self.missing_path, "w")) + except yaml.scanner.ScannerError as e: + util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True) + + def map_guids(self): + items = self.get_all() + logger.info(f"Mapping {self.type} Library: {self.name}") + logger.info("") + for i, item in enumerate(items, 1): + util.print_return(f"Processing: {i}/{len(items)} {item.title}") + if item.ratingKey not in self.movie_rating_key_map and item.ratingKey not in self.show_rating_key_map: + id_type, main_id, imdb_id = self.config.Convert.get_id(item, self) + if main_id: + if id_type == "movie": + self.movie_rating_key_map[item.ratingKey] = main_id[0] + util.add_dict_list(main_id, item.ratingKey, self.movie_map) + elif id_type == "show": + self.show_rating_key_map[item.ratingKey] = main_id[0] + util.add_dict_list(main_id, item.ratingKey, self.show_map) + if imdb_id: + util.add_dict_list(imdb_id, item.ratingKey, self.imdb_map) + logger.info("") + logger.info(util.adjust_space(f"Processed {len(items)} {self.type}s")) + return items + + def find_collection_assets(self, item, name=None, create=False): + if name is None: + name = item.title + for ad in self.asset_directory: + poster = None + background = None + if self.asset_folders: + if not os.path.isdir(os.path.join(ad, name)): + continue + poster_filter = os.path.join(ad, name, "poster.*") + background_filter = os.path.join(ad, name, "background.*") + else: + poster_filter = os.path.join(ad, f"{name}.*") + background_filter = os.path.join(ad, f"{name}_background.*") + matches = util.glob_filter(poster_filter) + if len(matches) > 0: + poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title}'s ", is_url=False) + matches = util.glob_filter(background_filter) + if len(matches) > 0: + background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title}'s ", is_poster=False, is_url=False) + if poster or background: + return poster, background + if create and self.asset_folders and not os.path.isdir(os.path.join(self.asset_directory[0], name)): + os.makedirs(os.path.join(self.asset_directory[0], name), exist_ok=True) + logger.info(f"Asset Directory Created: {os.path.join(self.asset_directory[0], name)}") + return None, None diff --git a/modules/plex.py b/modules/plex.py index b8e2df77a..5410de938 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1,14 +1,12 @@ -import logging, os, plexapi, requests, shutil, time +import logging, os, plexapi, requests from modules import builder, util -from modules.meta import Metadata +from modules.library import Library from modules.util import Failed, ImageData from plexapi import utils from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.collection import Collection from plexapi.server import PlexServer -from PIL import Image from retrying import retry -from ruamel import yaml from urllib import parse from xml.etree.ElementTree import ParseError @@ -225,9 +223,9 @@ } sort_types = {"movies": (1, movie_sorts), "shows": (2, show_sorts), "seasons": (3, season_sorts), "episodes": (4, episode_sorts)} -class Plex: +class Plex(Library): def __init__(self, config, params): - self.config = config + super().__init__(config, params) self.plex = params["plex"] self.url = params["plex"]["url"] self.token = params["plex"]["token"] @@ -255,89 +253,6 @@ def __init__(self, config, params): self.is_other = self.agent == "com.plexapp.agents.none" if self.is_other: self.type = "Video" - self.collections = [] - self.metadatas = [] - - self.metadata_files = [] - metadata = [] - for file_type, metadata_file in params["metadata_path"]: - if file_type == "Folder": - if os.path.isdir(metadata_file): - yml_files = util.glob_filter(os.path.join(metadata_file, "*.yml")) - if yml_files: - metadata.extend([("File", yml) for yml in yml_files]) - else: - logger.error(f"Config Error: No YAML (.yml) files found in {metadata_file}") - else: - logger.error(f"Config Error: Folder not found: {metadata_file}") - else: - metadata.append((file_type, metadata_file)) - for file_type, metadata_file in metadata: - try: - meta_obj = Metadata(config, self, file_type, metadata_file) - if meta_obj.collections: - self.collections.extend([c for c in meta_obj.collections]) - if meta_obj.metadata: - self.metadatas.extend([c for c in meta_obj.metadata]) - self.metadata_files.append(meta_obj) - except Failed as e: - util.print_multiline(e, error=True) - - if len(self.metadata_files) == 0: - logger.info("") - raise Failed("Metadata File Error: No valid metadata files found") - - if params["asset_directory"]: - logger.info("") - for ad in params["asset_directory"]: - logger.info(f"Using Asset Directory: {ad}") - - self.Radarr = None - self.Sonarr = None - self.Tautulli = None - self.name = params["name"] - self.original_mapping_name = params["mapping_name"] - self.mapping_name, output = util.validate_filename(self.original_mapping_name) - if output: - logger.info(output) - self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None - self.missing_path = os.path.join(params["default_dir"], f"{self.name}_missing.yml") - self.collection_minimum = params["collection_minimum"] - self.delete_below_minimum = params["delete_below_minimum"] - self.metadata_path = params["metadata_path"] - self.asset_directory = params["asset_directory"] - self.asset_folders = params["asset_folders"] - self.assets_for_all = params["assets_for_all"] - self.sync_mode = params["sync_mode"] - self.show_unmanaged = params["show_unmanaged"] - self.show_filtered = params["show_filtered"] - self.show_missing = params["show_missing"] - self.save_missing = params["save_missing"] - self.missing_only_released = params["missing_only_released"] - self.create_asset_folders = params["create_asset_folders"] - self.mass_genre_update = params["mass_genre_update"] - self.mass_audience_rating_update = params["mass_audience_rating_update"] - self.mass_critic_rating_update = params["mass_critic_rating_update"] - self.mass_trakt_rating_update = params["mass_trakt_rating_update"] - self.split_duplicates = params["split_duplicates"] - self.radarr_add_all = params["radarr_add_all"] - self.sonarr_add_all = params["sonarr_add_all"] - self.mass_update = self.mass_genre_update or self.mass_audience_rating_update or self.mass_critic_rating_update \ - or self.mass_trakt_rating_update or self.split_duplicates or self.radarr_add_all or self.sonarr_add_all - self.clean_bundles = params["plex"]["clean_bundles"] - self.empty_trash = params["plex"]["empty_trash"] - self.optimize = params["plex"]["optimize"] - self.missing = {} - self.movie_map = {} - self.show_map = {} - self.imdb_map = {} - self.anidb_map = {} - self.mal_map = {} - self.movie_rating_key_map = {} - self.show_rating_key_map = {} - self.run_again = [] - self.run_sort = [] - self.overlays = [] def get_all_collections(self): return self.search(libtype="collection") @@ -422,98 +337,24 @@ def edit_query(self, item, edits, advanced=False): @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def _upload_image(self, item, image): - if image.is_poster and image.is_url: - item.uploadPoster(url=image.location) - elif image.is_poster: - item.uploadPoster(filepath=image.location) - elif image.is_url: - item.uploadArt(url=image.location) - else: - item.uploadArt(filepath=image.location) - self.reload(item) + try: + if image.is_poster and image.is_url: + item.uploadPoster(url=image.location) + elif image.is_poster: + item.uploadPoster(filepath=image.location) + elif image.is_url: + item.uploadArt(url=image.location) + else: + item.uploadArt(filepath=image.location) + self.reload(item) + except BadRequest as e: + raise Failed(e) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def upload_file_poster(self, item, image): item.uploadPoster(filepath=image) self.reload(item) - def upload_images(self, item, poster=None, background=None, overlay=None): - image = None - image_compare = None - poster_uploaded = False - if self.config.Cache: - image, image_compare = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name) - - if poster is not None: - try: - if image_compare and str(poster.compare) != str(image_compare): - image = None - if image is None or image != item.thumb: - self._upload_image(item, poster) - poster_uploaded = True - logger.info(f"Detail: {poster.attribute} updated {poster.message}") - else: - logger.info(f"Detail: {poster.prefix}poster update not needed") - except BadRequest: - util.print_stacktrace() - logger.error(f"Detail: {poster.attribute} failed to update {poster.message}") - - if overlay is not None: - overlay_name, overlay_folder, overlay_image, temp_image = overlay - self.reload(item) - item_labels = {item_tag.tag.lower(): item_tag.tag for item_tag in item.labels} - for item_label in item_labels: - if item_label.endswith(" overlay") and item_label != f"{overlay_name.lower()} overlay": - raise Failed(f"Overlay Error: Poster already has an existing Overlay: {item_labels[item_label]}") - if poster_uploaded or image is None or image != item.thumb or f"{overlay_name.lower()} overlay" not in item_labels: - if not item.posterUrl: - raise Failed(f"Overlay Error: No existing poster to Overlay for {item.title}") - response = requests.get(item.posterUrl) - if response.status_code >= 400: - raise Failed(f"Overlay Error: Overlay Failed for {item.title}") - og_image = response.content - with open(temp_image, "wb") as handler: - handler.write(og_image) - shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.png")) - while util.is_locked(temp_image): - time.sleep(1) - try: - new_poster = Image.open(temp_image).convert("RGBA") - new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS) - new_poster.paste(overlay_image, (0, 0), overlay_image) - new_poster.save(temp_image) - self.upload_file_poster(item, temp_image) - self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"]) - poster_uploaded = True - logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}") - except OSError as e: - util.print_stacktrace() - logger.error(f"Overlay Error: {e}") - - background_uploaded = False - if background is not None: - try: - image = None - if self.config.Cache: - image, image_compare = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds") - if str(background.compare) != str(image_compare): - image = None - if image is None or image != item.art: - self._upload_image(item, background) - background_uploaded = True - logger.info(f"Detail: {background.attribute} updated {background.message}") - else: - logger.info(f"Detail: {background.prefix}background update not needed") - except BadRequest: - util.print_stacktrace() - logger.error(f"Detail: {background.attribute} failed to update {background.message}") - - if self.config.Cache: - if poster_uploaded: - self.config.Cache.update_image_map(item.ratingKey, self.image_table_name, item.thumb, poster.compare if poster else "") - if background_uploaded: - self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_search_choices(self, search_name, title=True): final_search = search_translation[search_name] if search_name in search_translation else search_name @@ -681,21 +522,6 @@ def get_rating_keys(self, method, data): 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: - self.missing[col_name] = {} - section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)" - if section not in self.missing[col_name]: - self.missing[col_name][section] = {} - for title, item_id in items: - self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode() - with open(self.missing_path, "w"): pass - try: - yaml.round_trip_dump(self.missing, open(self.missing_path, "w")) - except yaml.scanner.ScannerError as e: - util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True) - def get_collection_items(self, collection, smart_label_collection): if smart_label_collection: return self.get_labeled_items(collection.title if isinstance(collection, Collection) else str(collection)) @@ -715,27 +541,6 @@ def get_collection_name_and_items(self, collection, smart_label_collection): name = collection.title if isinstance(collection, Collection) else str(collection) return name, self.get_collection_items(collection, smart_label_collection) - def map_guids(self): - items = self.get_all() - logger.info(f"Mapping {self.type} Library: {self.name}") - logger.info("") - for i, item in enumerate(items, 1): - util.print_return(f"Processing: {i}/{len(items)} {item.title}") - if item.ratingKey not in self.movie_rating_key_map and item.ratingKey not in self.show_rating_key_map: - id_type, main_id, imdb_id = self.config.Convert.get_id(item, self) - if main_id: - if id_type == "movie": - self.movie_rating_key_map[item.ratingKey] = main_id[0] - util.add_dict_list(main_id, item.ratingKey, self.movie_map) - elif id_type == "show": - self.show_rating_key_map[item.ratingKey] = main_id[0] - util.add_dict_list(main_id, item.ratingKey, self.show_map) - if imdb_id: - util.add_dict_list(imdb_id, item.ratingKey, self.imdb_map) - logger.info("") - logger.info(util.adjust_space(f"Processed {len(items)} {self.type}s")) - return items - def get_tmdb_from_map(self, item): return self.movie_rating_key_map[item.ratingKey] if item.ratingKey in self.movie_rating_key_map else None @@ -848,30 +653,3 @@ def update_item_from_assets(self, item, overlay=None, create=False): logger.error(f"Asset Warning: No asset folder found called '{name}'") elif not poster and not background: logger.error(f"Asset Warning: No poster or background found in an assets folder for '{name}'") - - def find_collection_assets(self, item, name=None, create=False): - if name is None: - name = item.title - for ad in self.asset_directory: - poster = None - background = None - if self.asset_folders: - if not os.path.isdir(os.path.join(ad, name)): - continue - poster_filter = os.path.join(ad, name, "poster.*") - background_filter = os.path.join(ad, name, "background.*") - else: - poster_filter = os.path.join(ad, f"{name}.*") - background_filter = os.path.join(ad, f"{name}_background.*") - matches = util.glob_filter(poster_filter) - if len(matches) > 0: - poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title}'s ", is_url=False) - matches = util.glob_filter(background_filter) - if len(matches) > 0: - background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title}'s ", is_poster=False, is_url=False) - if poster or background: - return poster, background - if create and self.asset_folders and not os.path.isdir(os.path.join(self.asset_directory[0], name)): - os.makedirs(os.path.join(self.asset_directory[0], name), exist_ok=True) - logger.info(f"Asset Directory Created: {os.path.join(self.asset_directory[0], name)}") - return None, None From aa6fea52e3964ed3937c2726b582454cc7712741 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 21 Sep 2021 14:42:32 -0400 Subject: [PATCH 10/57] fix for wrong rating keys --- modules/tautulli.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/tautulli.py b/modules/tautulli.py index a953c8a07..8a665c39e 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -1,4 +1,7 @@ import logging + +from plexapi.video import Movie, Show + from modules import util from modules.util import Failed from plexapi.exceptions import BadRequest, NotFound @@ -40,7 +43,9 @@ def get_rating_keys(self, library, params): for item in items: if item["section_id"] == section_id and count < int(params['list_size']): try: - library.fetchItem(int(item["rating_key"])) + item = library.fetchItem(int(item["rating_key"])) + if not isinstance(item, (Movie, Show)): + raise BadRequest rating_keys.append(item["rating_key"]) except (BadRequest, NotFound): new_item = library.exact_search(item["title"], year=item["year"]) @@ -65,5 +70,5 @@ def _section_id(self, library_name): else: raise Failed(f"Tautulli Error: No Library named {library_name} in the response") def _request(self, url): - logger.debug(f"Tautulli URL: {url.replace(self.apikey, '###############')}") + logger.debug(f"Tautulli URL: {url.replace(self.apikey, 'APIKEY').replace(self.url, 'URL')}") return self.config.get_json(url) From b4d786cb34f59afd20c20ee3b02982b582c50f17 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 22 Sep 2021 08:43:21 -0400 Subject: [PATCH 11/57] check for None Seconds --- plex_meta_manager.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 67f573dbb..1a1e3457c 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -583,11 +583,14 @@ def run_collection(config, library, metadata, requested_collections): if (seconds is None or new_seconds < seconds) and new_seconds > 0: seconds = new_seconds og_time_str = time_to_run - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else "" - time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}" - util.print_return(f"Current Time: {current} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}") + if seconds is not None: + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else "" + time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}" + util.print_return(f"Current Time: {current} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}") + else: + logger.error(f"Time Error: {valid_times}") time.sleep(60) except KeyboardInterrupt: util.separator("Exiting Plex Meta Manager") From 14b7f5c57faf2848a11ab31c45f2e7f93ba8e84e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Sep 2021 04:25:13 +0000 Subject: [PATCH 12/57] Bump pathvalidate from 2.4.1 to 2.5.0 Bumps [pathvalidate](https://github.com/thombashi/pathvalidate) from 2.4.1 to 2.5.0. - [Release notes](https://github.com/thombashi/pathvalidate/releases) - [Commits](https://github.com/thombashi/pathvalidate/compare/v2.4.1...v2.5.0) --- updated-dependencies: - dependency-name: pathvalidate dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c633f2966..cc64f92c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ requests==2.26.0 ruamel.yaml==0.17.16 schedule==1.1.0 retrying==1.3.3 -pathvalidate==2.4.1 +pathvalidate==2.5.0 pillow==8.3.2 \ No newline at end of file From b049ef0e60dc2e311a55b6f5e6cfc8e0652781ac Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 30 Sep 2021 09:48:28 -0400 Subject: [PATCH 13/57] fixes issue with tautulli --- modules/tautulli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tautulli.py b/modules/tautulli.py index 8a665c39e..826690653 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -43,8 +43,8 @@ def get_rating_keys(self, library, params): for item in items: if item["section_id"] == section_id and count < int(params['list_size']): try: - item = library.fetchItem(int(item["rating_key"])) - if not isinstance(item, (Movie, Show)): + plex_item = library.fetchItem(int(item["rating_key"])) + if not isinstance(plex_item, (Movie, Show)): raise BadRequest rating_keys.append(item["rating_key"]) except (BadRequest, NotFound): From f40da259bfe6f911cee70d3a0ef8641737d4dd4a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 30 Sep 2021 10:22:03 -0400 Subject: [PATCH 14/57] New Agents will not have the collection tag locked state altered --- modules/builder.py | 6 ++---- modules/plex.py | 14 ++++++++++++++ plex_meta_manager.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 8e0aa1c61..4ce5d5783 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1484,10 +1484,8 @@ def add_to_collection(self): logger.info(util.adjust_space(f"{name} Collection | {current_operation} | {self.item_title(current)}")) if current in collection_items: self.plex_map[current.ratingKey] = None - elif self.smart_label_collection: - self.library.query_data(current.addLabel, name) else: - self.library.query_data(current.addCollection, name) + self.library.add_to_collection(current, name, smart_label_collection=self.smart_label_collection) util.print_end() logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") @@ -2028,7 +2026,7 @@ def run_collections_again(self): if current in collection_items: logger.info(f"{name} Collection | = | {self.item_title(current)}") else: - self.library.query_data(current.addLabel if self.smart_label_collection else current.addCollection, name) + self.library.add_to_collection(current, name, smart_label_collection=self.smart_label_collection) logger.info(f"{name} Collection | + | {self.item_title(current)}") logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed") diff --git a/modules/plex.py b/modules/plex.py index 5410de938..1991639b0 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -302,6 +302,10 @@ def query(self, method): def query_data(self, method, data): return method(data) + @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) + def query_collection(self, item, collection, locked=True): + item.addCollection(collection, locked=locked) + @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def collection_mode_query(self, collection, data): collection.modeUpdate(mode=data) @@ -380,6 +384,16 @@ def _query(self, key, post=False, put=False): else: method = None return self.Plex._server.query(key, method=method) + def add_to_collection(self, item, collection, smart_label_collection=False): + if smart_label_collection: + self.query_data(item.addLabel, collection) + else: + locked = True + if self.agent in ["tv.plex.agents.movie", "tv.plex.agents.series"]: + field = next((f for f in item.fields if f.name == "collection"), None) + locked = field is not None + self.query_collection(item, collection, locked=locked) + def move_item(self, collection, item, after=None): key = f"{collection.key}/items/{item}/move" if after: diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 1a1e3457c..fd976de03 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -115,7 +115,7 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")) logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) - logger.info(util.centered(" Version: 1.12.2 ")) + logger.info(util.centered(" Version: 1.12.2-develop0930 ")) if time_scheduled: start_type = f"{time_scheduled} " elif is_test: start_type = "Test " elif requested_collections: start_type = "Collections " From 462bfe41fe63855b4e4d3e122071a5a4430415ec Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 30 Sep 2021 10:34:19 -0400 Subject: [PATCH 15/57] added more reporting --- plex_meta_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index fd976de03..cd33f1839 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -523,6 +523,7 @@ def run_collection(config, library, metadata, requested_collections): try: builder.load_collection() except Failed: + util.print_stacktrace() run_item_details = False logger.info("") util.separator("No Collection to Update", space=False, border=False) From cef150bec0e8e8e0c480fa95822552f0ce2e0008 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 30 Sep 2021 16:55:29 -0400 Subject: [PATCH 16/57] fixed collection tags on remove --- modules/builder.py | 9 +++------ modules/plex.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 4ce5d5783..96cbe2b51 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1485,7 +1485,7 @@ def add_to_collection(self): if current in collection_items: self.plex_map[current.ratingKey] = None else: - self.library.add_to_collection(current, name, smart_label_collection=self.smart_label_collection) + self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) util.print_end() logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") @@ -1713,10 +1713,7 @@ def sync_collection(self): logger.info("") self.library.reload(item) logger.info(f"{self.name} Collection | - | {self.item_title(item)}") - if self.smart_label_collection: - self.library.query_data(item.removeLabel, self.name) - else: - self.library.query_data(item.removeCollection, self.name) + self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) count_removed += 1 if count_removed > 0: logger.info("") @@ -2026,7 +2023,7 @@ def run_collections_again(self): if current in collection_items: logger.info(f"{name} Collection | = | {self.item_title(current)}") else: - self.library.add_to_collection(current, name, smart_label_collection=self.smart_label_collection) + self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) logger.info(f"{name} Collection | + | {self.item_title(current)}") logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed") diff --git a/modules/plex.py b/modules/plex.py index 1991639b0..f44d7cf03 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -303,8 +303,11 @@ def query_data(self, method, data): return method(data) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def query_collection(self, item, collection, locked=True): - item.addCollection(collection, locked=locked) + def query_collection(self, item, collection, locked=True, add=True): + if add: + item.addCollection(collection, locked=locked) + else: + item.removeCollection(collection, locked=locked) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def collection_mode_query(self, collection, data): @@ -384,15 +387,15 @@ def _query(self, key, post=False, put=False): else: method = None return self.Plex._server.query(key, method=method) - def add_to_collection(self, item, collection, smart_label_collection=False): + def alter_collection(self, item, collection, smart_label_collection=False, add=True): if smart_label_collection: - self.query_data(item.addLabel, collection) + self.query_data(item.addLabel if add else item.removeLabel, collection) else: locked = True if self.agent in ["tv.plex.agents.movie", "tv.plex.agents.series"]: field = next((f for f in item.fields if f.name == "collection"), None) locked = field is not None - self.query_collection(item, collection, locked=locked) + self.query_collection(item, collection, locked=locked, add=add) def move_item(self, collection, item, after=None): key = f"{collection.key}/items/{item}/move" From 8516ac10db99fb7176d4547a1fc373993ecbe66d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 4 Oct 2021 13:51:32 -0400 Subject: [PATCH 17/57] push notifiarr update --- modules/builder.py | 65 ++++- modules/config.py | 638 ++++++++++++++++++++++++------------------- modules/imdb.py | 4 +- modules/library.py | 7 + modules/meta.py | 1 + modules/notifiarr.py | 76 ++++++ modules/tmdb.py | 9 +- modules/util.py | 3 + plex_meta_manager.py | 228 +++++++++------- 9 files changed, 634 insertions(+), 397 deletions(-) create mode 100644 modules/notifiarr.py diff --git a/modules/builder.py b/modules/builder.py index 96cbe2b51..49385abc9 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -76,7 +76,10 @@ ] poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"] background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"] -boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", "delete_below_minimum"] +boolean_details = [ + "visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", + "delete_below_minimum", "notifiarr_collection_creation", "notifiarr_collection_addition", "notifiarr_collection_removing" +] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", @@ -168,7 +171,10 @@ def __init__(self, config, library, metadata, name, no_missing, data): "save_missing": self.library.save_missing, "missing_only_released": self.library.missing_only_released, "create_asset_folders": self.library.create_asset_folders, - "delete_below_minimum": self.library.delete_below_minimum + "delete_below_minimum": self.library.delete_below_minimum, + "notifiarr_collection_creation": self.library.notifiarr_collection_creation, + "notifiarr_collection_addition": self.library.notifiarr_collection_addition, + "notifiarr_collection_removing": self.library.notifiarr_collection_removing, } self.item_details = {} self.radarr_details = {} @@ -183,6 +189,8 @@ def __init__(self, config, library, metadata, name, no_missing, data): self.filtered_keys = {} self.run_again_movies = [] self.run_again_shows = [] + self.notifiarr_additions = [] + self.notifiarr_removals = [] self.items = [] self.posters = {} self.backgrounds = {} @@ -191,6 +199,8 @@ def __init__(self, config, library, metadata, name, no_missing, data): self.minimum = self.library.collection_minimum self.current_time = datetime.now() self.current_year = self.current_time.year + self.exists = False + self.created = False methods = {m.lower(): m for m in self.data} @@ -537,6 +547,7 @@ def cant_interact(attr1, attr2, fail=False): elif not self.library.Sonarr and "sonarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Sonarr to be configured") elif not self.library.Tautulli and "tautulli" in method_name: raise Failed(f"Collection Error: {method_final} requires Tautulli to be configured") elif not self.config.MyAnimeList and "mal" in method_name: raise Failed(f"Collection Error: {method_final} requires MyAnimeList to be configured") + elif not self.library.Notifiarr and "notifiarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Notifiarr to be configured") elif self.library.is_movie and method_name in show_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for show libraries") elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for movie libraries") elif self.library.is_show and method_name in plex.movie_only_searches: raise Failed(f"Collection Error: {method_final} plex search only works for movie libraries") @@ -617,6 +628,8 @@ def cant_interact(attr1, attr2, fail=False): if self.sync and self.obj: for item in self.library.get_collection_items(self.obj, self.smart_label_collection): self.plex_map[item.ratingKey] = item + if self.obj: + self.exists = True else: self.obj = None self.sync = False @@ -1122,7 +1135,7 @@ def find_rating_keys(self): rating_keys.append(input_id) elif id_type == "tmdb" and not self.parts_collection: if input_id in self.library.movie_map: - rating_keys.append(self.library.movie_map[input_id][0]) + rating_keys.extend(self.library.movie_map[input_id]) elif input_id not in self.missing_movies: self.missing_movies.append(input_id) elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection: @@ -1133,12 +1146,12 @@ def find_rating_keys(self): logger.error(e) continue if input_id in self.library.show_map: - rating_keys.append(self.library.show_map[input_id][0]) + rating_keys.extend(self.library.show_map[input_id]) elif input_id not in self.missing_shows: self.missing_shows.append(input_id) elif id_type == "imdb" and not self.parts_collection: if input_id in self.library.imdb_map: - rating_keys.append(self.library.imdb_map[input_id][0]) + rating_keys.extend(self.library.imdb_map[input_id]) else: if self.do_missing: try: @@ -1486,6 +1499,14 @@ def add_to_collection(self): self.plex_map[current.ratingKey] = None else: self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) + if self.details["notifiarr_collection_addition"]: + if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: + add_id = self.library.movie_rating_key_map[current.ratingKey] + elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: + add_id = self.library.show_rating_key_map[current.ratingKey] + else: + add_id = None + self.notifiarr_additions.append({"title": current.title, "id": add_id}) util.print_end() logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") @@ -1714,6 +1735,14 @@ def sync_collection(self): self.library.reload(item) logger.info(f"{self.name} Collection | - | {self.item_title(item)}") self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) + if self.details["notifiarr_collection_removing"]: + if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: + remove_id = self.library.movie_rating_key_map[item.ratingKey] + elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: + remove_id = self.library.show_rating_key_map[item.ratingKey] + else: + remove_id = None + self.notifiarr_removals.append({"title": item.title, "id": remove_id}) count_removed += 1 if count_removed > 0: logger.info("") @@ -1835,6 +1864,8 @@ def load_collection(self): except Failed: raise Failed(f"Collection Error: Label: {self.name} was not added to any items in the Library") self.obj = self.library.get_collection(self.name) + if not self.exists: + self.created = True def update_details(self): logger.info("") @@ -2002,10 +2033,26 @@ def sort_collection(self): self.library.move_item(self.obj, key, after=previous) previous = key + def send_notifications(self): + if self.obj and ( + (self.details["notifiarr_collection_creation"] and self.created) or + (self.details["notifiarr_collection_addition"] and len(self.notifiarr_additions) > 0) or + (self.details["notifiarr_collection_removing"] and len(self.notifiarr_removals) > 0) + ): + self.obj.reload() + self.library.Notifiarr.plex_collection( + self.obj, + created=self.created, + additions=self.notifiarr_additions, + removals=self.notifiarr_removals + ) + def run_collections_again(self): self.obj = self.library.get_collection(self.name) name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection) + self.created = False rating_keys = [] + self.notifiarr_additions = [] for mm in self.run_again_movies: if mm in self.library.movie_map: rating_keys.extend(self.library.movie_map[mm]) @@ -2025,6 +2072,14 @@ def run_collections_again(self): else: self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) logger.info(f"{name} Collection | + | {self.item_title(current)}") + if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: + add_id = self.library.movie_rating_key_map[current.ratingKey] + elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: + add_id = self.library.show_rating_key_map[current.ratingKey] + else: + add_id = None + self.notifiarr_additions.append({"title": current.title, "id": add_id}) + self.send_notifications() logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed") if len(self.run_again_movies) > 0: diff --git a/modules/config.py b/modules/config.py index c2b8ff36e..e8e8477fd 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,4 @@ -import logging, os, requests +import base64, logging, os, requests from datetime import datetime from lxml import html from modules import util, radarr, sonarr @@ -10,6 +10,7 @@ from modules.imdb import IMDb from modules.letterboxd import Letterboxd from modules.mal import MyAnimeList +from modules.notifiarr import NotifiarrFactory from modules.omdb import OMDb from modules.plex import Plex from modules.radarr import Radarr @@ -29,21 +30,22 @@ mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} class Config: - def __init__(self, default_dir, config_path=None, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): + def __init__(self, default_dir, attrs): logger.info("Locating config...") - if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path) - elif config_path and not os.path.exists(config_path): raise Failed(f"Config Error: config not found at {os.path.abspath(config_path)}") + config_file = attrs["config_file"] + if config_file and os.path.exists(config_file): self.config_path = os.path.abspath(config_file) + elif config_file and not os.path.exists(config_file): raise Failed(f"Config Error: config not found at {os.path.abspath(config_file)}") elif os.path.exists(os.path.join(default_dir, "config.yml")): self.config_path = os.path.abspath(os.path.join(default_dir, "config.yml")) else: raise Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}") logger.info(f"Using {self.config_path} as config") self.default_dir = default_dir - self.test_mode = is_test - self.run_start_time = time_scheduled - self.run_hour = datetime.strptime(time_scheduled, "%H:%M").hour - self.requested_collections = util.get_list(requested_collections) - self.requested_libraries = util.get_list(requested_libraries) - self.resume_from = resume_from + self.test_mode = attrs["test"] + self.run_start_time = attrs["time"] + self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour + self.requested_collections = util.get_list(attrs["collections"]) + self.requested_libraries = util.get_list(attrs["libraries"]) + self.resume_from = attrs["resume"] yaml.YAML().allow_duplicate_keys = True try: @@ -87,6 +89,7 @@ def replace_attr(all_data, attr, par): if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") + if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "mal" in new_config: new_config["mal"] = new_config.pop("mal") if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") @@ -186,7 +189,10 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "missing_only_released": check_for_attribute(self.data, "missing_only_released", parent="settings", var_type="bool", default=False), "create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False), "collection_minimum": check_for_attribute(self.data, "collection_minimum", parent="settings", var_type="int", default=1), - "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False) + "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), + "notifiarr_collection_creation": check_for_attribute(self.data, "notifiarr_collection_creation", parent="settings", var_type="bool", default=False), + "notifiarr_collection_addition": check_for_attribute(self.data, "notifiarr_collection_addition", parent="settings", var_type="bool", default=False), + "notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False) } if self.general["cache"]: util.separator() @@ -196,323 +202,383 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No util.separator() - self.TMDb = None - if "tmdb" in self.data: - logger.info("Connecting to TMDb...") - self.TMDb = TMDb(self, { - "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), - "language": check_for_attribute(self.data, "language", parent="tmdb", default="en") - }) - logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") - else: - raise Failed("Config Error: tmdb attribute not found") - - util.separator() - - self.OMDb = None - if "omdb" in self.data: - logger.info("Connecting to OMDb...") - try: - self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)}) - except Failed as e: - logger.error(e) - logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") - else: - logger.warning("omdb attribute not found") - - util.separator() - - self.Trakt = None - if "trakt" in self.data: - logger.info("Connecting to Trakt...") + self.NotifiarrFactory = None + if "notifiarr" in self.data: + logger.info("Connecting to Notifiarr...") try: - self.Trakt = Trakt(self, { - "client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True), - "client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True), - "config_path": self.config_path, - "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None + self.NotifiarrFactory = NotifiarrFactory(self, { + "apikey": check_for_attribute(self.data, "apikey", parent="notifiarr", throw=True), + "error_notification": check_for_attribute(self.data, "error_notification", parent="notifiarr", var_type="bool", default=True), + "develop": check_for_attribute(self.data, "develop", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False), + "test": check_for_attribute(self.data, "test", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False) }) except Failed as e: logger.error(e) - logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") + logger.info(f"Notifiarr Connection {'Failed' if self.NotifiarrFactory is None else 'Successful'}") else: - logger.warning("trakt attribute not found") + logger.warning("notifiarr attribute not found") + + self.errors = [] util.separator() - self.MyAnimeList = None - if "mal" in self.data: - logger.info("Connecting to My Anime List...") - try: - self.MyAnimeList = MyAnimeList(self, { - "client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True), - "client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True), - "config_path": self.config_path, - "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None + try: + self.TMDb = None + if "tmdb" in self.data: + logger.info("Connecting to TMDb...") + self.TMDb = TMDb(self, { + "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), + "language": check_for_attribute(self.data, "language", parent="tmdb", default="en") }) - except Failed as e: - logger.error(e) - logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") - else: - logger.warning("mal attribute not found") + logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") + else: + raise Failed("Config Error: tmdb attribute not found") - util.separator() + util.separator() + + self.OMDb = None + if "omdb" in self.data: + logger.info("Connecting to OMDb...") + try: + self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)}) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") + else: + logger.warning("omdb attribute not found") - self.AniDB = None - if "anidb" in self.data: util.separator() - logger.info("Connecting to AniDB...") - try: - self.AniDB = AniDB(self, { - "username": check_for_attribute(self.data, "username", parent="anidb", throw=True), - "password": check_for_attribute(self.data, "password", parent="anidb", throw=True) + + self.Trakt = None + if "trakt" in self.data: + logger.info("Connecting to Trakt...") + try: + self.Trakt = Trakt(self, { + "client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True), + "client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True), + "config_path": self.config_path, + "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None }) - except Failed as e: - logger.error(e) - logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") - if self.AniDB is None: - self.AniDB = AniDB(self, None) - - self.TVDb = TVDb(self) - self.IMDb = IMDb(self) - self.Convert = Convert(self) - self.AniList = AniList(self) - self.Letterboxd = Letterboxd(self) - self.ICheckMovies = ICheckMovies(self) - self.StevenLu = StevenLu(self) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") + else: + logger.warning("trakt attribute not found") - util.separator() + util.separator() - logger.info("Connecting to Plex Libraries...") + self.MyAnimeList = None + if "mal" in self.data: + logger.info("Connecting to My Anime List...") + try: + self.MyAnimeList = MyAnimeList(self, { + "client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True), + "client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True), + "config_path": self.config_path, + "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None + }) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") + else: + logger.warning("mal attribute not found") - self.general["plex"] = { - "url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True), - "timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60), - "clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False), - "empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False), - "optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False) - } - self.general["radarr"] = { - "url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True), - "add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False), - "add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False), - "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True), - "monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True), - "availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"), - "quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True), - "tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True), - "search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) - } - self.general["sonarr"] = { - "url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True), - "add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False), - "add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False), - "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True), - "monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"), - "quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True), - "language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True), - "series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"), - "season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True), - "tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True), - "search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False), - "cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) - } - self.general["tautulli"] = { - "url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True), - "apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) - } + util.separator() - self.libraries = [] - libs = check_for_attribute(self.data, "libraries", throw=True) + self.AniDB = None + if "anidb" in self.data: + util.separator() + logger.info("Connecting to AniDB...") + try: + self.AniDB = AniDB(self, { + "username": check_for_attribute(self.data, "username", parent="anidb", throw=True), + "password": check_for_attribute(self.data, "password", parent="anidb", throw=True) + }) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") + if self.AniDB is None: + self.AniDB = AniDB(self, None) + + self.TVDb = TVDb(self) + self.IMDb = IMDb(self) + self.Convert = Convert(self) + self.AniList = AniList(self) + self.Letterboxd = Letterboxd(self) + self.ICheckMovies = ICheckMovies(self) + self.StevenLu = StevenLu(self) - for library_name, lib in libs.items(): - if self.requested_libraries and library_name not in self.requested_libraries: - continue util.separator() - params = { - "mapping_name": str(library_name), - "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name) + + logger.info("Connecting to Plex Libraries...") + + self.general["plex"] = { + "url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True), + "timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60), + "clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False), + "empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False), + "optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False) + } + self.general["radarr"] = { + "url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True), + "add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False), + "add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False), + "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True), + "monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True), + "availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"), + "quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True), + "tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True), + "search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) + } + self.general["sonarr"] = { + "url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True), + "add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False), + "add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False), + "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True), + "monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"), + "quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True), + "language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True), + "series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"), + "season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True), + "tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True), + "search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False), + "cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) + } + self.general["tautulli"] = { + "url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True), + "apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) } - display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] - - util.separator(f"{display_name} Configuration") - logger.info("") - logger.info(f"Connecting to {display_name} Library...") - - params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False) - if params["asset_directory"] is None: - logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") - - params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) - params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], 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) - params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) - params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) - params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) - params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) - params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False) - params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) - params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) - params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) - - params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) - if self.OMDb is None and params["mass_genre_update"] == "omdb": - params["mass_genre_update"] = None - logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection") - - params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) - if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": - params["mass_audience_rating_update"] = None - logger.error("Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection") - - params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) - if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": - params["mass_critic_rating_update"] = None - logger.error("Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection") - - params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib) - if self.Trakt is None and params["mass_trakt_rating_update"]: - params["mass_trakt_rating_update"] = None - logger.error("Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection") - - params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib) - params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib) - params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib) - try: - if lib and "metadata_path" in lib: - params["metadata_path"] = [] - if lib["metadata_path"] is None: - raise Failed("Config Error: metadata_path attribute is blank") - paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]] - for path in paths_to_check: - if isinstance(path, dict): - def check_dict(attr, name): - if attr in path: - if path[attr] is None: - logger.error(f"Config Error: metadata_path {attr} is blank") - else: - params["metadata_path"].append((name, path[attr])) - check_dict("url", "URL") - check_dict("git", "Git") - check_dict("file", "File") - check_dict("folder", "Folder") - else: - params["metadata_path"].append(("File", path)) - else: - params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))] - params["default_dir"] = default_dir - params["plex"] = { - "url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False), - "timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False), - "clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False), - "empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False), - "optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False) + self.libraries = [] + libs = check_for_attribute(self.data, "libraries", throw=True) + + for library_name, lib in libs.items(): + if self.requested_libraries and library_name not in self.requested_libraries: + continue + util.separator() + params = { + "mapping_name": str(library_name), + "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name) } - library = Plex(self, params) - logger.info("") - logger.info(f"{display_name} Library Connection Successful") - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info(f"{display_name} Library Connection Failed") - continue + display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] - if self.general["radarr"]["url"] or (lib and "radarr" in lib): + util.separator(f"{display_name} Configuration") logger.info("") - util.separator("Radarr Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Radarr...") - logger.info("") - try: - library.Radarr = Radarr(self, { - "url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False), - "add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False), - "add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False), - "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False), - "monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False), - "availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False), - "quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False), - "tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False), - "search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) - }) - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info("") - logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") + logger.info(f"Connecting to {display_name} Library...") + + params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False) + if params["asset_directory"] is None: + logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") + + params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) + params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], 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) + params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) + params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) + params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) + params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) + params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False) + params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) + params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) + params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) + params["notifiarr_collection_creation"] = check_for_attribute(lib, "notifiarr_collection_creation", parent="settings", var_type="bool", default=self.general["notifiarr_collection_creation"], do_print=False, save=False) + params["notifiarr_collection_addition"] = check_for_attribute(lib, "notifiarr_collection_addition", parent="settings", var_type="bool", default=self.general["notifiarr_collection_addition"], do_print=False, save=False) + params["notifiarr_collection_removing"] = check_for_attribute(lib, "notifiarr_collection_removing", parent="settings", var_type="bool", default=self.general["notifiarr_collection_removing"], do_print=False, save=False) + + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) + if self.OMDb is None and params["mass_genre_update"] == "omdb": + params["mass_genre_update"] = None + e = "Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": + params["mass_audience_rating_update"] = None + e = "Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": + params["mass_critic_rating_update"] = None + e = "Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib) + if self.Trakt is None and params["mass_trakt_rating_update"]: + params["mass_trakt_rating_update"] = None + e = "Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection" + self.errors.append(e) + logger.error(e) + + params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib) + params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib) + params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib) - if self.general["sonarr"]["url"] or (lib and "sonarr" in lib): - logger.info("") - util.separator("Sonarr Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Sonarr...") - logger.info("") try: - library.Sonarr = Sonarr(self, { - "url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False), - "add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False), - "add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False), - "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False), - "monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False), - "quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False), - "language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False), - "series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False), - "season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False), - "tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False), - "search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False), - "cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) - }) + if lib and "metadata_path" in lib: + params["metadata_path"] = [] + if lib["metadata_path"] is None: + raise Failed("Config Error: metadata_path attribute is blank") + paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]] + for path in paths_to_check: + if isinstance(path, dict): + def check_dict(attr, name): + if attr in path: + if path[attr] is None: + e = f"Config Error: metadata_path {attr} is blank" + self.errors.append(e) + logger.error(e) + else: + params["metadata_path"].append((name, path[attr])) + check_dict("url", "URL") + check_dict("git", "Git") + check_dict("file", "File") + check_dict("folder", "Folder") + else: + params["metadata_path"].append(("File", path)) + else: + params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))] + params["default_dir"] = default_dir + params["plex"] = { + "url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False), + "timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False), + "clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False), + "empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False), + "optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False) + } + library = Plex(self, params) + logger.info("") + logger.info(f"{display_name} Library Connection Successful") except Failed as e: + self.errors.append(e) util.print_stacktrace() util.print_multiline(e, error=True) + logger.info(f"{display_name} Library Connection Failed") + continue + + if self.general["radarr"]["url"] or (lib and "radarr" in lib): + logger.info("") + util.separator("Radarr Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Radarr...") logger.info("") - logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") + try: + library.Radarr = Radarr(self, { + "url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False), + "add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False), + "add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False), + "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False), + "monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False), + "availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False), + "quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False), + "tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False), + "search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") + + if self.general["sonarr"]["url"] or (lib and "sonarr" in lib): + logger.info("") + util.separator("Sonarr Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Sonarr...") + logger.info("") + try: + library.Sonarr = Sonarr(self, { + "url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False), + "add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False), + "add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False), + "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False), + "monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False), + "quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False), + "language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False), + "series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False), + "season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False), + "tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False), + "search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False), + "cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") + + if self.general["tautulli"]["url"] or (lib and "tautulli" in lib): + logger.info("") + util.separator("Tautulli Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Tautulli...") + logger.info("") + try: + library.Tautulli = Tautulli(self, { + "url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False), + "apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") + + library.Notifiarr = self.NotifiarrFactory.getNotifiarr(library) if self.NotifiarrFactory else None - if self.general["tautulli"]["url"] or (lib and "tautulli" in lib): logger.info("") - util.separator("Tautulli Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Tautulli...") - logger.info("") - try: - library.Tautulli = Tautulli(self, { - "url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False), - "apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) - }) - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info("") - logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") + self.libraries.append(library) - logger.info("") - self.libraries.append(library) + util.separator() - util.separator() + 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 connected to") - 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 connected to") + util.separator() - util.separator() + if self.errors: + self.notify(self.errors) + except Exception as e: + self.notify(e) + raise + + def notify(self, text, library=None, collection=None, critical=True): + if self.NotifiarrFactory: + if not isinstance(text, list): + text = [text] + for t in text: + self.NotifiarrFactory.error(t, library=library, collection=collection, critical=critical) def get_html(self, url, headers=None, params=None): return html.fromstring(self.get(url, headers=headers, params=params).content) - def get_json(self, url, headers=None): - return self.get(url, headers=headers).json() + def get_json(self, url, json=None, headers=None, params=None): + return self.get(url, json=json, headers=headers, params=params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) - def get(self, url, headers=None, params=None): - return self.session.get(url, headers=headers, params=params) + def get(self, url, json=None, headers=None, params=None): + return self.session.get(url, json=json, headers=headers, params=params) + + def get_image_encoded(self, url): + return base64.b64encode(self.get(url).content).decode('utf-8') def post_html(self, url, data=None, json=None, headers=None): return html.fromstring(self.post(url, data=data, json=json, headers=headers).content) diff --git a/modules/imdb.py b/modules/imdb.py index e1fb9f3db..dcc1e9178 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -57,7 +57,7 @@ def _total(self, imdb_url, language): pass if total > 0: return total, item_counts[page_type] - raise ValueError(f"IMDb Error: Failed to parse URL: {imdb_url}") + raise Failed(f"IMDb Error: Failed to parse URL: {imdb_url}") def _ids_from_url(self, imdb_url, language, limit): total, item_count = self._total(imdb_url, language) @@ -93,7 +93,7 @@ def _ids_from_url(self, imdb_url, language, limit): if len(imdb_ids) > 0: logger.debug(f"{len(imdb_ids)} IMDb IDs Found: {imdb_ids}") return imdb_ids - raise ValueError(f"IMDb Error: No IMDb IDs Found at {imdb_url}") + raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}") def get_imdb_ids(self, method, data, language): if method == "imdb_id": diff --git a/modules/library.py b/modules/library.py index 7f420253c..92cd1b2c4 100644 --- a/modules/library.py +++ b/modules/library.py @@ -13,6 +13,7 @@ def __init__(self, config, params): self.Radarr = None self.Sonarr = None self.Tautulli = None + self.Notifiarr = None self.collections = [] self.metadatas = [] self.metadata_files = [] @@ -54,6 +55,9 @@ def __init__(self, config, params): self.sonarr_add_all = params["sonarr_add_all"] self.collection_minimum = params["collection_minimum"] self.delete_below_minimum = params["delete_below_minimum"] + self.notifiarr_collection_creation = params["notifiarr_collection_creation"] + self.notifiarr_collection_addition = params["notifiarr_collection_addition"] + self.notifiarr_collection_removing = params["notifiarr_collection_removing"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? @@ -175,6 +179,9 @@ def upload_images(self, item, poster=None, background=None, overlay=None): if background_uploaded: self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) + def notify(self, text, collection=None, critical=True): + self.config.notify(text, library=self, collection=collection, critical=critical) + @abstractmethod def _upload_image(self, item, image): pass diff --git a/modules/meta.py b/modules/meta.py index fdca9a16d..e83f6e249 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -249,6 +249,7 @@ def set_images(obj, group, alias): add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date") add_edit("critic_rating", item, meta, methods, value=rating, key="rating", var_type="float") add_edit("audience_rating", item, meta, methods, key="audienceRating", var_type="float") + add_edit("user_rating", item, meta, methods, key="userRating", var_type="float") add_edit("content_rating", item, meta, methods, key="contentRating") add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title) add_edit("studio", item, meta, methods, value=studio) diff --git a/modules/notifiarr.py b/modules/notifiarr.py new file mode 100644 index 000000000..d729aa3c5 --- /dev/null +++ b/modules/notifiarr.py @@ -0,0 +1,76 @@ +import logging + +from modules.util import Failed + +logger = logging.getLogger("Plex Meta Manager") + +base_url = "https://notifiarr.com/api/v1/" +dev_url = "https://dev.notifiarr.com/api/v1/" + +class NotifiarrBase: + def __init__(self, config, apikey, develop, test, error_notification): + self.config = config + self.apikey = apikey + self.develop = develop + self.test = test + self.error_notification = error_notification + + def _request(self, path, json=None, params=None): + url = f"{dev_url if self.develop else base_url}" + \ + ("notification/test" if self.test else f"{path}{self.apikey}") + logger.debug(url) + response = self.config.get(url, json=json, params={"event": "pmm"} if self.test else params) + response_json = response.json() + if self.develop or self.test: + logger.debug(json) + logger.debug("") + logger.debug(response_json) + if response.status_code >= 400 or ("response" in response_json and response_json["response"] == "error"): + raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") + return response_json + + def error(self, text, library=None, collection=None, critical=True): + if self.error_notification: + json = {"error": str(text), "critical": critical} + if library: + json["server_name"] = library.PlexServer.friendlyName + json["library_name"] = library.name + if collection: + json["collection"] = str(collection) + self._request("notification/plex/", json=json, params={"event": "collections"}) + +class NotifiarrFactory(NotifiarrBase): + def __init__(self, config, params): + super().__init__(config, params["apikey"], params["develop"], params["test"], params["error_notification"]) + if not params["test"] and not self._request("user/validate/")["message"]["response"]: + raise Failed("Notifiarr Error: Invalid apikey") + + def getNotifiarr(self, library): + return Notifiarr(self.config, library, self.apikey, self.develop, self.test, self.error_notification) + +class Notifiarr(NotifiarrBase): + def __init__(self, config, library, apikey, develop, test, error_notification): + super().__init__(config, apikey, develop, test, error_notification) + self.library = library + + def plex_collection(self, collection, created=False, additions=None, removals=None): + thumb = None + if collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None): + thumb = self.config.get_image_encoded(f"{self.library.url}{collection.thumb}?X-Plex-Token={self.library.token}") + art = None + if collection.art and next((f for f in collection.fields if f.name == "art"), None): + art = self.config.get_image_encoded(f"{self.library.url}{collection.art}?X-Plex-Token={self.library.token}") + json = { + "server_name": self.library.PlexServer.friendlyName, + "library_name": self.library.name, + "type": "movie" if self.library.is_movie else "show", + "collection": collection.title, + "created": created, + "poster": thumb, + "background": art + } + if additions: + json["additions"] = additions + if removals: + json["removals"] = removals + self._request("notification/plex/", json=json, params={"event": "collections"}) diff --git a/modules/tmdb.py b/modules/tmdb.py index 6bc9b28d1..7fee81096 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -56,9 +56,12 @@ def __init__(self, config, params): self.TMDb = tmdbv3api.TMDb(session=self.config.session) self.TMDb.api_key = params["apikey"] self.TMDb.language = params["language"] - response = tmdbv3api.Configuration().info() - if hasattr(response, "status_message"): - raise Failed(f"TMDb Error: {response.status_message}") + try: + response = tmdbv3api.Configuration().info() + if hasattr(response, "status_message"): + raise Failed(f"TMDb Error: {response.status_message}") + except TMDbException as e: + raise Failed(f"TMDb Error: {e}") self.apikey = params["apikey"] self.language = params["language"] self.Movie = tmdbv3api.Movie() diff --git a/modules/util.py b/modules/util.py index 1eb6dc0bc..4fab89d59 100644 --- a/modules/util.py +++ b/modules/util.py @@ -29,6 +29,9 @@ def __init__(self, attribute, location, prefix="", is_poster=True, is_url=True): self.compare = location if is_url else os.stat(location).st_size self.message = f"{prefix}{'poster' if is_poster else 'background'} to [{'URL' if is_url else 'File'}] {location}" + def __str__(self): + return str(self.__dict__) + def retry_if_not_failed(exception): return not isinstance(exception, Failed) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index cd33f1839..091bb8ebe 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -98,7 +98,7 @@ def fmt_filter(record): sys.excepthook = util.my_except_hook -def start(config_path, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): +def start(attrs): file_logger = os.path.join(default_dir, "logs", "meta.log") should_roll_over = os.path.isfile(file_logger) file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8") @@ -115,107 +115,117 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")) logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) - logger.info(util.centered(" Version: 1.12.2-develop0930 ")) - if time_scheduled: start_type = f"{time_scheduled} " - elif is_test: start_type = "Test " - elif requested_collections: start_type = "Collections " - elif requested_libraries: start_type = "Libraries " + logger.info(util.centered(" Version: 1.12.2-develop1004 ")) + if "time" in attrs: start_type = f"{attrs['time']} " + elif "test" in attrs: start_type = "Test " + elif "collections" in attrs: start_type = "Collections " + elif "libraries" in attrs: start_type = "Libraries " else: start_type = "" start_time = datetime.now() - if time_scheduled is None: - time_scheduled = start_time.strftime("%H:%M") + if "time" not in attrs: + attrs["time"] = start_time.strftime("%H:%M") util.separator(f"Starting {start_type}Run") try: - config = Config(default_dir, config_path=config_path, is_test=is_test, - time_scheduled=time_scheduled, requested_collections=requested_collections, - requested_libraries=requested_libraries, resume_from=resume_from) - update_libraries(config) + config = Config(default_dir, attrs) except Exception as e: util.print_stacktrace() util.print_multiline(e, critical=True) + else: + try: + update_libraries(config) + except Exception as e: + config.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) logger.info("") util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}") logger.removeHandler(file_handler) def update_libraries(config): for library in config.libraries: - os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) - col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log") - should_roll_over = os.path.isfile(col_file_logger) - library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") - util.apply_formatter(library_handler) - if should_roll_over: - library_handler.doRollover() - logger.addHandler(library_handler) - - os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) - logger.info("") - util.separator(f"{library.name} Library") - items = None - if not library.is_other: - logger.info("") - util.separator(f"Mapping {library.name} Library", space=False, border=False) - logger.info("") - items = library.map_guids() - if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: - mass_metadata(config, library, items=items) - for metadata in library.metadata_files: - logger.info("") - util.separator(f"Running Metadata File\n{metadata.path}") - if not config.test_mode and not config.resume_from and not collection_only: - try: - metadata.update_metadata() - except Failed as e: - logger.error(e) - collections_to_run = metadata.get_collections(config.requested_collections) - if config.resume_from and config.resume_from not in collections_to_run: - logger.info("") - logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}") - continue - if collections_to_run and not library_only: - logger.info("") - util.separator(f"{'Test ' if config.test_mode else ''}Collections") - logger.removeHandler(library_handler) - run_collection(config, library, metadata, collections_to_run) - logger.addHandler(library_handler) - if library.run_sort: - logger.info("") - util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False) + try: + os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) + col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log") + should_roll_over = os.path.isfile(col_file_logger) + library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") + util.apply_formatter(library_handler) + if should_roll_over: + library_handler.doRollover() + logger.addHandler(library_handler) + + os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") - for builder in library.run_sort: + util.separator(f"{library.name} Library") + items = None + if not library.is_other: logger.info("") - util.separator(f"Sorting {builder.name} Collection", space=False, border=False) + util.separator(f"Mapping {library.name} Library", space=False, border=False) logger.info("") - builder.sort_collection() - - if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): - logger.info("") - util.separator(f"Other {library.name} Library Operations") - unmanaged_collections = [] - for col in library.get_all_collections(): - if col.title not in library.collections: - unmanaged_collections.append(col) - - if library.show_unmanaged and not library_only: + items = library.map_guids() + if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: + mass_metadata(config, library, items=items) + for metadata in library.metadata_files: logger.info("") - util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) + util.separator(f"Running Metadata File\n{metadata.path}") + if not config.test_mode and not config.resume_from and not collection_only: + try: + metadata.update_metadata() + except Failed as e: + library.notify(e) + logger.error(e) + collections_to_run = metadata.get_collections(config.requested_collections) + if config.resume_from and config.resume_from not in collections_to_run: + logger.info("") + logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}") + continue + if collections_to_run and not library_only: + logger.info("") + util.separator(f"{'Test ' if config.test_mode else ''}Collections") + logger.removeHandler(library_handler) + run_collection(config, library, metadata, collections_to_run) + logger.addHandler(library_handler) + if library.run_sort: logger.info("") - for col in unmanaged_collections: - logger.info(col.title) + util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False) logger.info("") - logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") + for builder in library.run_sort: + logger.info("") + util.separator(f"Sorting {builder.name} Collection", space=False, border=False) + logger.info("") + builder.sort_collection() - if library.assets_for_all and not collection_only: - logger.info("") - util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False) + if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): logger.info("") - for col in unmanaged_collections: - poster, background = library.find_collection_assets(col, create=library.create_asset_folders) - library.upload_images(col, poster=poster, background=background) - for item in library.get_all(): - library.update_item_from_assets(item, create=library.create_asset_folders) + util.separator(f"Other {library.name} Library Operations") + unmanaged_collections = [] + for col in library.get_all_collections(): + if col.title not in library.collections: + unmanaged_collections.append(col) + + if library.show_unmanaged and not library_only: + logger.info("") + util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + logger.info(col.title) + logger.info("") + logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") - logger.removeHandler(library_handler) + if library.assets_for_all and not collection_only: + logger.info("") + util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + poster, background = library.find_collection_assets(col, create=library.create_asset_folders) + library.upload_images(col, poster=poster, background=background) + for item in library.get_all(): + library.update_item_from_assets(item, create=library.create_asset_folders) + + logger.removeHandler(library_handler) + except Exception as e: + library.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) has_run_again = False for library in config.libraries: @@ -234,26 +244,32 @@ def update_libraries(config): util.print_end() for library in config.libraries: if library.run_again: - col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log") - library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8") - util.apply_formatter(library_handler) - logger.addHandler(library_handler) - library_handler.addFilter(fmt_filter) - os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) - logger.info("") - util.separator(f"{library.name} Library Run Again") - logger.info("") - library.map_guids() - for builder in library.run_again: + try: + col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log") + library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8") + util.apply_formatter(library_handler) + logger.addHandler(library_handler) + library_handler.addFilter(fmt_filter) + os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") - util.separator(f"{builder.name} Collection") + util.separator(f"{library.name} Library Run Again") logger.info("") - try: - builder.run_collections_again() - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.removeHandler(library_handler) + library.map_guids() + for builder in library.run_again: + logger.info("") + util.separator(f"{builder.name} Collection") + logger.info("") + try: + builder.run_collections_again() + except Failed as e: + library.notify(e, collection=builder.name, critical=False) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.removeHandler(library_handler) + except Exception as e: + library.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) used_url = [] for library in config.libraries: @@ -457,7 +473,7 @@ def run_collection(config, library, metadata, requested_collections): collection_log_name, output_str = util.validate_filename(mapping_name) collection_log_folder = os.path.join(default_dir, "logs", library.mapping_name, "collections", collection_log_name) os.makedirs(collection_log_folder, exist_ok=True) - col_file_logger = os.path.join(collection_log_folder, f"collection.log") + col_file_logger = os.path.join(collection_log_folder, "collection.log") should_roll_over = os.path.isfile(col_file_logger) collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") util.apply_formatter(collection_handler) @@ -533,6 +549,8 @@ def run_collection(config, library, metadata, requested_collections): library.run_sort.append(builder) # builder.sort_collection() + builder.send_notifications() + if builder.item_details and run_item_details: try: builder.load_collection_items() @@ -546,9 +564,11 @@ def run_collection(config, library, metadata, requested_collections): library.run_again.append(builder) except Failed as e: + library.notify(e, collection=mapping_name) util.print_stacktrace() util.print_multiline(e, error=True) except Exception as e: + library.notify(f"Unknown Error: {e}", collection=mapping_name) util.print_stacktrace() logger.error(f"Unknown Error: {e}") logger.info("") @@ -557,7 +577,13 @@ def run_collection(config, library, metadata, requested_collections): try: if run or test or collections or libraries or resume: - start(config_file, is_test=test, requested_collections=collections, requested_libraries=libraries, resume_from=resume) + start({ + "config_file": config_file, + "test": test, + "collections": collections, + "libraries": libraries, + "resume": resume + }) else: times_to_run = util.get_list(times) valid_times = [] @@ -570,7 +596,7 @@ def run_collection(config, library, metadata, requested_collections): else: raise Failed(f"Argument Error: blank time argument") for time_to_run in valid_times: - schedule.every().day.at(time_to_run).do(start, config_file, time_scheduled=time_to_run) + schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run}) while True: schedule.run_pending() if not no_countdown: From 0ed61cec798ee734f7593a4ab30e213482eedeb9 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 5 Oct 2021 00:30:04 -0400 Subject: [PATCH 18/57] include guids update --- modules/convert.py | 2 +- modules/plex.py | 7 +------ requirements.txt | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/modules/convert.py b/modules/convert.py index 8f817cf65..58fbdcb22 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -209,7 +209,7 @@ def get_id(self, item, library): try: if item_type == "plex": try: - for guid_tag in library.get_guids(item): + for guid_tag in item.guids: url_parsed = requests.utils.urlparse(guid_tag.id) if url_parsed.scheme == "tvdb": tvdb_id.append(int(url_parsed.netloc)) elif url_parsed.scheme == "imdb": imdb_id.append(url_parsed.netloc) diff --git a/modules/plex.py b/modules/plex.py index f44d7cf03..1e8153a0e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -279,7 +279,7 @@ def fetchItem(self, data): def get_all(self): logger.info(f"Loading All {self.type}s from Library: {self.name}") - key = f"/library/sections/{self.Plex.key}/all?type={utils.searchType(self.Plex.TYPE)}" + key = f"/library/sections/{self.Plex.key}/all?includeGuids=1&type={utils.searchType(self.Plex.TYPE)}" container_start = 0 container_size = plexapi.X_PLEX_CONTAINER_SIZE results = [] @@ -317,11 +317,6 @@ def collection_mode_query(self, collection, data): def collection_order_query(self, collection, data): collection.sortUpdate(sort=data) - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) - def get_guids(self, item): - self.reload(item) - return item.guids - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def reload(self, item): try: diff --git a/requirements.txt b/requirements.txt index cc64f92c4..c8aebe909 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -PlexAPI==4.7.1 +PlexAPI==4.7.2 tmdbv3api==1.7.6 arrapi==1.1.3 lxml==4.6.3 From 014d6c859c68e561f57a36489b8aee3e7b9f70cc Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 6 Oct 2021 11:51:35 -0400 Subject: [PATCH 19/57] fixed series issue with sonarr --- modules/builder.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 49385abc9..9c47f0c7e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -2038,7 +2038,7 @@ def send_notifications(self): (self.details["notifiarr_collection_creation"] and self.created) or (self.details["notifiarr_collection_addition"] and len(self.notifiarr_additions) > 0) or (self.details["notifiarr_collection_removing"] and len(self.notifiarr_removals) > 0) - ): + ): self.obj.reload() self.library.Notifiarr.plex_collection( self.obj, diff --git a/requirements.txt b/requirements.txt index c8aebe909..674938e62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PlexAPI==4.7.2 tmdbv3api==1.7.6 -arrapi==1.1.3 +arrapi==1.1.6 lxml==4.6.3 requests==2.26.0 ruamel.yaml==0.17.16 From 2cd489ea628e0e02cd74511862fd004141105377 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 7 Oct 2021 11:17:20 -0400 Subject: [PATCH 20/57] fix attr issue --- modules/config.py | 8 ++++---- plex_meta_manager.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/config.py b/modules/config.py index e8e8477fd..6596be074 100644 --- a/modules/config.py +++ b/modules/config.py @@ -40,12 +40,12 @@ def __init__(self, default_dir, attrs): logger.info(f"Using {self.config_path} as config") self.default_dir = default_dir - self.test_mode = attrs["test"] + self.test_mode = attrs["test"] if "test" in attrs else False self.run_start_time = attrs["time"] self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour - self.requested_collections = util.get_list(attrs["collections"]) - self.requested_libraries = util.get_list(attrs["libraries"]) - self.resume_from = attrs["resume"] + self.requested_collections = util.get_list(attrs["collections"]) if "collections" in attrs else None + self.requested_libraries = util.get_list(attrs["libraries"]) if "libraries" in attrs else None + self.resume_from = attrs["resume"] if "resume" in attrs else None yaml.YAML().allow_duplicate_keys = True try: diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 091bb8ebe..baefcec5c 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -116,11 +116,11 @@ def start(attrs): logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) logger.info(util.centered(" Version: 1.12.2-develop1004 ")) - if "time" in attrs: start_type = f"{attrs['time']} " - elif "test" in attrs: start_type = "Test " - elif "collections" in attrs: start_type = "Collections " - elif "libraries" in attrs: start_type = "Libraries " - else: start_type = "" + if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} " + elif "test" in attrs and attrs["test"]: start_type = "Test " + elif "collections" in attrs and attrs["collections"]: start_type = "Collections " + elif "libraries" in attrs and attrs["libraries"]: start_type = "Libraries " + else: start_type = "" start_time = datetime.now() if "time" not in attrs: attrs["time"] = start_time.strftime("%H:%M") From 5d1c73b21ce52b7903eca1687ba2c17008323653 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 19 Oct 2021 00:19:33 -0400 Subject: [PATCH 21/57] add actions --- .github/workflows/develop.yml | 38 ++++++++++++++++++++++++++++++++++ .github/workflows/latest.yml | 36 ++++++++++++++++++++++++++++++++ .github/workflows/tag.yml | 18 ++++++++++++++++ .github/workflows/version.yml | 39 +++++++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 .github/workflows/develop.yml create mode 100644 .github/workflows/latest.yml create mode 100644 .github/workflows/tag.yml create mode 100644 .github/workflows/version.yml diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 000000000..049c1eae2 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,38 @@ +name: Docker Develop Release + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + + docker-develop: + runs-on: ubuntu-latest + + steps: + + - name: Check Out Repo + uses: actions/checkout@v2 + with: + ref: develop + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/plex-meta-manager:develop diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml new file mode 100644 index 000000000..c458a4318 --- /dev/null +++ b/.github/workflows/latest.yml @@ -0,0 +1,36 @@ +name: Docker Latest Release + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + docker-latest: + runs-on: ubuntu-latest + + steps: + + - name: Check Out Repo + uses: actions/checkout@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/plex-meta-manager:latest diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 000000000..8a6aa3ab5 --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,18 @@ +name: Tag + +on: + push: + branches: [ master ] + +jobs: + tag-new-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + token: ${{ secrets.PAT }} + fetch-depth: 2 + - uses: salsify/action-detect-and-tag-new-version@v1.0.3 + with: + version-command: | + cat VERSION diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 000000000..fdfa7384b --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,39 @@ +name: Docker Version Release + +on: + create: + tags: + - v* + +jobs: + + docker-develop: + runs-on: ubuntu-latest + + steps: + + - name: Check Out Repo + uses: actions/checkout@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + context: ./ + file: ./Dockerfile + push: true + tags: ${{ secrets.DOCKER_HUB_USERNAME }}/plex-meta-manager:${{ steps.get_version.outputs.VERSION }} From 4034b8ee1074d6bb4eabb34d5c03790f3e8cec2a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 19 Oct 2021 00:23:52 -0400 Subject: [PATCH 22/57] sonarr fix --- VERSION | 1 + modules/builder.py | 11 ++--------- plex_meta_manager.py | 12 ++++++++++-- 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..125d2e931 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.12.2-develop1019 \ No newline at end of file diff --git a/modules/builder.py b/modules/builder.py index 9c47f0c7e..762f16538 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -761,7 +761,6 @@ def _radarr(self, method_name, method_data): 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_details["quality"] = method_data elif method_name == "radarr_tag": self.radarr_details["tag"] = util.get_list(method_data) @@ -769,19 +768,13 @@ def _radarr(self, method_name, method_data): def _sonarr(self, method_name, method_data): if method_name in ["sonarr_add", "sonarr_add_existing", "sonarr_season", "sonarr_search", "sonarr_cutoff_search"]: self.sonarr_details[method_name[7:]] = util.parse(method_name, method_data, datatype="bool") - elif method_name == "sonarr_folder": - self.sonarr_details["folder"] = method_data + elif method_name in ["sonarr_folder", "sonarr_quality", "sonarr_language"]: + self.sonarr_details[method_name[7:]] = method_data elif method_name == "sonarr_monitor": if str(method_data).lower() in sonarr.monitor_translation: self.sonarr_details["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_details["quality"] = method_data - elif method_name == "sonarr_language": - self.library.Sonarr.get_profile_id(method_data, "language_profile") - self.sonarr_details["language"] = method_data elif method_name == "sonarr_series": if str(method_data).lower() in sonarr.series_type: self.sonarr_details["series"] = str(method_data).lower() diff --git a/plex_meta_manager.py b/plex_meta_manager.py index baefcec5c..50fc24db7 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -98,6 +98,14 @@ def fmt_filter(record): sys.excepthook = util.my_except_hook +version = "Unknown" +with open("VERSION") as handle: + for line in handle.readlines(): + line = line.strip() + if len(line) > 0: + version = line + break + def start(attrs): file_logger = os.path.join(default_dir, "logs", "meta.log") should_roll_over = os.path.isfile(file_logger) @@ -108,14 +116,14 @@ def start(attrs): file_handler.doRollover() logger.addHandler(file_handler) util.separator() - logger.info(util.centered(" ")) + logger.info("") logger.info(util.centered(" ____ _ __ __ _ __ __ ")) logger.info(util.centered("| _ \\| | _____ __ | \\/ | ___| |_ __ _ | \\/ | __ _ _ __ __ _ __ _ ___ _ __ ")) logger.info(util.centered("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_ \\ / _` |/ _` |/ _ \\ '__|")) logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")) logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) - logger.info(util.centered(" Version: 1.12.2-develop1004 ")) + logger.info(f" Version: {version}") if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} " elif "test" in attrs and attrs["test"]: start_type = "Test " elif "collections" in attrs and attrs["collections"]: start_type = "Collections " From 9d0f8c2dab98ec2b9a94ee284a87a4c2de549382 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 20 Oct 2021 15:58:25 -0400 Subject: [PATCH 23/57] #398 title.is fix --- VERSION | 2 +- modules/builder.py | 53 ++++++++++++++++++++++---------------------- plex_meta_manager.py | 5 +++-- requirements.txt | 4 ++-- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/VERSION b/VERSION index 125d2e931..11614fd05 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2-develop1019 \ No newline at end of file +1.12.2-develop1020 \ No newline at end of file diff --git a/modules/builder.py b/modules/builder.py index 762f16538..9ff1cb6c6 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1327,11 +1327,12 @@ def build_url_arg(arg, mod=None, arg_s=None, mod_s=None): bool_mod = "" if validation else "!" bool_arg = "true" if validation else "false" results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") - elif (attr in ["title", "episode_title", "studio", "decade", "year", "episode_year"] or attr in plex.tags) and modifier in ["", ".not", ".begins", ".ends"]: + elif (attr in ["title", "episode_title", "studio", "decade", "year", "episode_year"] or attr in plex.tags) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]: results = "" display_add = "" for og_value, result in validation: - built_arg = build_url_arg(quote(result) if attr in string_filters else result, arg_s=og_value) + print(og_value, result) + built_arg = build_url_arg(quote(str(result)) if attr in string_filters else result, arg_s=og_value) display_add += built_arg[1] results += f"{conjunction if len(results) > 0 else ''}{built_arg[0]}" else: @@ -1504,6 +1505,30 @@ def add_to_collection(self): logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") + def sync_collection(self): + count_removed = 0 + for ratingKey, item in self.plex_map.items(): + if item is not None: + if count_removed == 0: + logger.info("") + util.separator(f"Removed from {self.name} Collection", space=False, border=False) + logger.info("") + self.library.reload(item) + logger.info(f"{self.name} Collection | - | {self.item_title(item)}") + self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) + if self.details["notifiarr_collection_removing"]: + if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: + remove_id = self.library.movie_rating_key_map[item.ratingKey] + elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: + remove_id = self.library.show_rating_key_map[item.ratingKey] + else: + remove_id = None + self.notifiarr_removals.append({"title": item.title, "id": remove_id}) + count_removed += 1 + if count_removed > 0: + logger.info("") + logger.info(f"{count_removed} {self.collection_level.capitalize()}{'s' if count_removed == 1 else ''} Removed") + def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False): if self.tmdb_filters or check_released: try: @@ -1717,30 +1742,6 @@ def item_title(self, item): else: return item.title - def sync_collection(self): - count_removed = 0 - for ratingKey, item in self.plex_map.items(): - if item is not None: - if count_removed == 0: - logger.info("") - util.separator(f"Removed from {self.name} Collection", space=False, border=False) - logger.info("") - self.library.reload(item) - logger.info(f"{self.name} Collection | - | {self.item_title(item)}") - self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) - if self.details["notifiarr_collection_removing"]: - if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: - remove_id = self.library.movie_rating_key_map[item.ratingKey] - elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: - remove_id = self.library.show_rating_key_map[item.ratingKey] - else: - remove_id = None - self.notifiarr_removals.append({"title": item.title, "id": remove_id}) - count_removed += 1 - if count_removed > 0: - logger.info("") - logger.info(f"{count_removed} {self.collection_level.capitalize()}{'s' if count_removed == 1 else ''} Removed") - def load_collection_items(self): if self.build_collection and self.obj: self.items = self.library.get_collection_items(self.obj, self.smart_label_collection) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 50fc24db7..faf6d0d77 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -526,6 +526,8 @@ def run_collection(config, library, metadata, requested_collections): util.separator(f"Adding to {mapping_name} Collection", space=False, border=False) logger.info("") builder.add_to_collection() + if builder.sync: + builder.sync_collection() elif len(builder.rating_keys) < builder.minimum and builder.build_collection: logger.info("") logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") @@ -533,14 +535,13 @@ def run_collection(config, library, metadata, requested_collections): builder.delete_collection() logger.info("") logger.info(f"Collection {builder.obj.title} deleted") + if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): if builder.details["show_missing"] is True: logger.info("") util.separator(f"Missing from Library", space=False, border=False) logger.info("") builder.run_missing() - if builder.sync and len(builder.rating_keys) > 0 and builder.build_collection: - builder.sync_collection() run_item_details = True if builder.build_collection: diff --git a/requirements.txt b/requirements.txt index 674938e62..a29f49d7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ PlexAPI==4.7.2 tmdbv3api==1.7.6 -arrapi==1.1.6 +arrapi==1.1.7 lxml==4.6.3 requests==2.26.0 ruamel.yaml==0.17.16 schedule==1.1.0 retrying==1.3.3 pathvalidate==2.5.0 -pillow==8.3.2 \ No newline at end of file +pillow==8.4.0 \ No newline at end of file From f34634f5d9512174b2e930eb9a722c41c9615d70 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 20 Oct 2021 16:09:51 -0400 Subject: [PATCH 24/57] #397 added item_refresh as a collection detail --- modules/builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 9ff1cb6c6..934a66a44 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -88,7 +88,7 @@ details = ["collection_mode", "collection_order", "collection_level", "collection_minimum", "label"] + boolean_details + string_details collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ poster_details + background_details + summary_details + string_details -item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_assets", "revert_overlay"] + list(plex.item_advance_keys.keys()) +item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_assets", "revert_overlay", "item_refresh"] + list(plex.item_advance_keys.keys()) radarr_details = ["radarr_add", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"] sonarr_details = [ "sonarr_add", "sonarr_add_existing", "sonarr_folder", "sonarr_monitor", "sonarr_language", "sonarr_series", @@ -735,7 +735,7 @@ def _item_details(self, method_name, method_data, method_mod, method_final, meth raise Failed("Each Overlay can only be used once per Library") self.library.overlays.append(method_data) self.item_details[method_name] = method_data - elif method_name in ["item_assets", "revert_overlay"]: + elif method_name in ["item_assets", "revert_overlay", "item_refresh"]: if util.parse(method_name, method_data, datatype="bool", default=False): self.item_details[method_name] = True elif method_name in plex.item_advance_keys: @@ -1817,6 +1817,8 @@ def update_item_details(self): if getattr(item, key) != options[method_data]: advance_edits[key] = options[method_data] self.library.edit_item(item, item.title, self.collection_level.capitalize(), advance_edits, advanced=True) + if "item_refresh" in self.item_details: + item.refresh() if len(tmdb_ids) > 0: if "item_radarr_tag" in self.item_details: From d10aa91b8c0359e13fda053eea77ca72ef01350c Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 21 Oct 2021 10:00:16 -0400 Subject: [PATCH 25/57] not scheduled collection no longer error --- modules/builder.py | 4 ++-- modules/util.py | 3 +++ plex_meta_manager.py | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 934a66a44..92ad48b1e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,7 +1,7 @@ import logging, os, re from datetime import datetime, timedelta from modules import anidb, anilist, icheckmovies, imdb, letterboxd, mal, plex, radarr, sonarr, stevenlu, tautulli, tmdb, trakt, tvdb, util -from modules.util import Failed, ImageData +from modules.util import Failed, ImageData, NotScheduled from PIL import Image from plexapi.exceptions import BadRequest, NotFound from plexapi.video import Movie, Show, Season, Episode @@ -391,7 +391,7 @@ def scan_text(og_txt, var, var_value): if len(self.schedule) == 0: skip_collection = False if skip_collection: - raise Failed(f"{self.schedule}\n\nCollection {self.name} not scheduled to run") + raise NotScheduled(f"{self.schedule}\n\nCollection {self.name} not scheduled to run") self.collectionless = "plex_collectionless" in methods diff --git a/modules/util.py b/modules/util.py index 4fab89d59..a307b0d0b 100644 --- a/modules/util.py +++ b/modules/util.py @@ -19,6 +19,9 @@ class TimeoutExpired(Exception): class Failed(Exception): pass +class NotScheduled(Exception): + pass + class ImageData: def __init__(self, attribute, location, prefix="", is_poster=True, is_url=True): self.attribute = attribute diff --git a/plex_meta_manager.py b/plex_meta_manager.py index faf6d0d77..451a416d6 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -6,7 +6,7 @@ from modules import util from modules.builder import CollectionBuilder from modules.config import Config - from modules.util import Failed + from modules.util import Failed, NotScheduled except ModuleNotFoundError: print("Requirements Error: Requirements are not installed") sys.exit(0) @@ -572,6 +572,9 @@ def run_collection(config, library, metadata, requested_collections): if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0): library.run_again.append(builder) + + except NotScheduled as e: + util.print_multiline(e, info=True) except Failed as e: library.notify(e, collection=mapping_name) util.print_stacktrace() From 3f67a67fd5b7e822524730dd0f3be425aa234d73 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 21 Oct 2021 12:00:00 -0400 Subject: [PATCH 26/57] fix Version error --- plex_meta_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 451a416d6..69c3f1a12 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -99,7 +99,7 @@ def fmt_filter(record): sys.excepthook = util.my_except_hook version = "Unknown" -with open("VERSION") as handle: +with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) as handle: for line in handle.readlines(): line = line.strip() if len(line) > 0: From 309f2d5fbfff501be3588992801abcabc50ac069 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 25 Oct 2021 11:40:49 -0400 Subject: [PATCH 27/57] #418 Added period to trakt_recommended, trakt_watched , and trakt_collected --- VERSION | 2 +- modules/builder.py | 5 +++-- modules/trakt.py | 11 +++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 11614fd05..54461f879 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2-develop1020 \ No newline at end of file +1.12.2-develop1025 \ No newline at end of file diff --git a/modules/builder.py b/modules/builder.py index 92ad48b1e..8d0764dee 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -41,7 +41,8 @@ "seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score", "minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent", "anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search", - "mal_producer": "mal_studio", "mal_licensor": "mal_studio" + "mal_producer": "mal_studio", "mal_licensor": "mal_studio", + "trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly" } filter_translation = { "actor": "actors", @@ -1027,7 +1028,7 @@ def _trakt(self, method_name, method_data): self.builders.append(("trakt_list", trakt_list)) if method_name.endswith("_details"): self.summaries[method_name] = self.config.Trakt.list_description(trakt_lists[0]) - elif method_name in ["trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected"]: + elif method_name.startswith(("trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected")): self.builders.append((method_name, util.parse(method_name, method_data, datatype="int", default=10))) elif method_name in ["trakt_watchlist", "trakt_collection"]: for trakt_list in self.config.Trakt.validate_trakt(method_data, self.library.is_movie, trakt_type=method_name[6:]): diff --git a/modules/trakt.py b/modules/trakt.py index 1bec27c68..899fd2f1e 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -9,8 +9,10 @@ redirect_uri_encoded = redirect_uri.replace(":", "%3A") base_url = "https://api.trakt.tv" builders = [ - "trakt_collected", "trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", - "trakt_recommended", "trakt_trending", "trakt_watched", "trakt_watchlist" + "trakt_collected_daily", "trakt_collected_weekly", "trakt_collected_monthly", "trakt_collected_yearly", "trakt_collected_all", + "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", + "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", + "trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", "trakt_trending", "trakt_watchlist" ] sorts = [ "rank", "added", "title", "released", "runtime", "popularity", @@ -216,9 +218,10 @@ def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"): def get_trakt_ids(self, method, data, is_movie): pretty = method.replace("_", " ").title() media_type = "Movie" if is_movie else "Show" - if method in ["trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected"]: + if method.startswith(("trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected")): logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") - return self._pagenation(method[6:], data, is_movie) + terms = method.split("_") + return self._pagenation(f"{terms[1]}{f'/{terms[2]}' if len(terms) > 2 else ''}", data, is_movie) elif method in ["trakt_collection", "trakt_watchlist"]: logger.info(f"Processing {pretty} {media_type}s for {data}") return self._user_items(method[6:], data, is_movie) From eee977f9cb4e40c35a03726ab7460c3c484b9162 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 25 Oct 2021 12:43:13 -0400 Subject: [PATCH 28/57] allow custom sort to work for new trakt builders --- modules/builder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/builder.py b/modules/builder.py index 8d0764dee..4195ed428 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -146,7 +146,10 @@ "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated", "tmdb_trending_daily", "tmdb_trending_weekly", "tmdb_discover", "tvdb_list", "imdb_list", "stevenlu_popular", "anidb_popular", - "trakt_list", "trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected", + "trakt_list", "trakt_trending", "trakt_popular", + "trakt_collected_daily", "trakt_collected_weekly", "trakt_collected_monthly", "trakt_collected_yearly", "trakt_collected_all", + "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", + "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", "tautulli_popular", "tautulli_watched", "letterboxd_list", "icheckmovies_list", "anilist_top_rated", "anilist_popular", "anilist_season", "anilist_studio", "anilist_genre", "anilist_tag", "anilist_search", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", From f93ed6a1b914a7dea91784f8fc4a8c5226e02e6e Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 25 Oct 2021 16:48:35 -0400 Subject: [PATCH 29/57] update key --- modules/notifiarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/notifiarr.py b/modules/notifiarr.py index d729aa3c5..51a549140 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -42,7 +42,7 @@ def error(self, text, library=None, collection=None, critical=True): class NotifiarrFactory(NotifiarrBase): def __init__(self, config, params): super().__init__(config, params["apikey"], params["develop"], params["test"], params["error_notification"]) - if not params["test"] and not self._request("user/validate/")["message"]["response"]: + if not params["test"] and not self._request("user/validate/")["details"]["response"]: raise Failed("Notifiarr Error: Invalid apikey") def getNotifiarr(self, library): From eb3740b739be1f0a9e2ad8971b942d2e0592a202 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 25 Oct 2021 16:51:51 -0400 Subject: [PATCH 30/57] updated notifiarr key --- modules/notifiarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 51a549140..380588aab 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -25,7 +25,7 @@ def _request(self, path, json=None, params=None): logger.debug(json) logger.debug("") logger.debug(response_json) - if response.status_code >= 400 or ("response" in response_json and response_json["response"] == "error"): + if response.status_code >= 400 or ("result" in response_json and response_json["result"] == "error"): raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") return response_json From f7fad1ca2a7e697cfd14225903681caed05e863d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 25 Oct 2021 18:01:11 -0400 Subject: [PATCH 31/57] timeout fix --- plex_meta_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 69c3f1a12..7a2c202ca 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -2,7 +2,7 @@ from datetime import datetime from logging.handlers import RotatingFileHandler try: - import schedule + import plexapi, schedule from modules import util from modules.builder import CollectionBuilder from modules.config import Config @@ -161,7 +161,7 @@ def update_libraries(config): library_handler.doRollover() logger.addHandler(library_handler) - os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) + plexapi.TIMEOUT = library.timeout logger.info("") util.separator(f"{library.name} Library") items = None From 13ed5caed37fe5a9dd90058a8d5663aa163e109f Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 26 Oct 2021 01:40:23 -0400 Subject: [PATCH 32/57] timeout fix --- plex_meta_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 7a2c202ca..d49b1f7f7 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -161,7 +161,7 @@ def update_libraries(config): library_handler.doRollover() logger.addHandler(library_handler) - plexapi.TIMEOUT = library.timeout + plexapi.server.TIMEOUT = library.timeout logger.info("") util.separator(f"{library.name} Library") items = None From 39c9bfa28d8797296d4bd3750be7e0b5481e976a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 26 Oct 2021 09:39:30 -0400 Subject: [PATCH 33/57] save missing supports utf-8 --- modules/builder.py | 9 ++++----- modules/library.py | 13 ++++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 4195ed428..039f7d420 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1697,14 +1697,13 @@ def run_missing(self): except Failed as e: logger.error(e) continue - current_title = str(show.title.encode("ascii", "replace").decode()) if self.check_tmdb_filter(missing_id, False, check_released=self.details["missing_only_released"]): - missing_shows_with_names.append((current_title, missing_id)) + missing_shows_with_names.append((show.title, missing_id)) if self.details["show_missing"] is True: - logger.info(f"{self.name} Collection | ? | {current_title} (TVDB: {missing_id})") + logger.info(f"{self.name} Collection | ? | {show.title} (TVDB: {missing_id})") else: if self.details["show_filtered"] is True and self.details["show_missing"] is True: - logger.info(f"{self.name} Collection | X | {current_title} (TVDb: {missing_id})") + logger.info(f"{self.name} Collection | X | {show.title} (TVDb: {missing_id})") logger.info("") logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing") if len(missing_shows_with_names) > 0: @@ -2102,7 +2101,7 @@ def run_collections_again(self): for missing_id in self.run_again_shows: if missing_id not in self.library.show_map: try: - title = str(self.config.TVDb.get_series(self.language, missing_id).title.encode("ascii", "replace").decode()) + title = self.config.TVDb.get_series(self.language, missing_id).title except Failed as e: logger.error(e) continue diff --git a/modules/library.py b/modules/library.py index 92cd1b2c4..71aa2f11f 100644 --- a/modules/library.py +++ b/modules/library.py @@ -203,17 +203,16 @@ def get_all(self): pass def add_missing(self, collection, items, is_movie): - col_name = collection.encode("ascii", "replace").decode() - if col_name not in self.missing: - self.missing[col_name] = {} + if collection not in self.missing: + self.missing[collection] = {} section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)" - if section not in self.missing[col_name]: - self.missing[col_name][section] = {} + if section not in self.missing[collection]: + self.missing[collection][section] = {} for title, item_id in items: - self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode() + self.missing[collection][section][int(item_id)] = title with open(self.missing_path, "w"): pass try: - yaml.round_trip_dump(self.missing, open(self.missing_path, "w")) + yaml.round_trip_dump(self.missing, open(self.missing_path, "w", encoding="utf-8")) except yaml.scanner.ScannerError as e: util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True) From 89beded78211921f4049b389a2d2f94dfcdca43c Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 26 Oct 2021 11:01:08 -0400 Subject: [PATCH 34/57] added tvdb_language --- config/config.yml.template | 12 +++++++++ modules/builder.py | 18 ++++++------- modules/config.py | 5 ++-- modules/tvdb.py | 53 +++++++++++++++++++------------------- modules/util.py | 2 +- 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/config/config.yml.template b/config/config.yml.template index fb7951a73..8c97e60e0 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -29,6 +29,15 @@ settings: # Can be individually specified show_missing: true save_missing: true run_again_delay: 2 + released_missing_only: false + create_asset_folders: false + missing_only_released: false + collection_minimum: 1 + delete_below_minimum: true + notifiarr_collection_creation: false + notifiarr_collection_addition: false + notifiarr_collection_removing: false + tvdb_language: eng plex: # Can be individually specified per library as well url: http://192.168.1.12:32400 token: #################### @@ -67,6 +76,9 @@ sonarr: # Can be individually specified cutoff_search: false omdb: apikey: ######## +notifiarr: + apikey: #################################### + error_notification: true trakt: client_id: ################################################################ client_secret: ################################################################ diff --git a/modules/builder.py b/modules/builder.py index 039f7d420..998b2181b 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -651,9 +651,9 @@ def _summary(self, method_name, method_data): elif method_name == "tmdb_biography": self.summaries[method_name] = self.config.TMDb.get_person(util.regex_first_int(method_data, "TMDb Person ID")).biography elif method_name == "tvdb_summary": - self.summaries[method_name] = self.config.TVDb.get_movie_or_show(method_data, self.language, self.library.is_movie).summary + self.summaries[method_name] = self.config.TVDb.get_item(method_data, self.library.is_movie).summary elif method_name == "tvdb_description": - self.summaries[method_name] = self.config.TVDb.get_list_description(method_data, self.language) + self.summaries[method_name] = self.config.TVDb.get_list_description(method_data) elif method_name == "trakt_description": self.summaries[method_name] = self.config.Trakt.list_description(self.config.Trakt.validate_trakt(method_data, self.library.is_movie)[0]) elif method_name == "letterboxd_description": @@ -671,7 +671,7 @@ def _poster(self, method_name, method_data): url_slug = self.config.TMDb.get_person(util.regex_first_int(method_data, 'TMDb Person ID')).profile_path self.posters[method_name] = f"{self.config.TMDb.image_url}{url_slug}" elif method_name == "tvdb_poster": - self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.language, self.library.is_movie).poster_path}" + self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.library.is_movie).poster_path}" elif method_name == "file_poster": if os.path.exists(method_data): self.posters[method_name] = os.path.abspath(method_data) @@ -685,7 +685,7 @@ def _background(self, method_name, method_data): url_slug = self.config.TMDb.get_movie_show_or_collection(util.regex_first_int(method_data, 'TMDb ID'), self.library.is_movie).poster_path self.backgrounds[method_name] = f"{self.config.TMDb.image_url}{url_slug}" elif method_name == "tvdb_background": - self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.language, self.library.is_movie).background_path}" + self.posters[method_name] = f"{self.config.TVDb.get_item(method_data, self.library.is_movie).background_path}" elif method_name == "file_background": if os.path.exists(method_data): self.backgrounds[method_name] = os.path.abspath(method_data) @@ -1041,7 +1041,7 @@ def _tvdb(self, method_name, method_data): values = util.get_list(method_data) if method_name.endswith("_details"): if method_name.startswith(("tvdb_movie", "tvdb_show")): - item = self.config.TVDb.get_item(self.language, values[0], method_name.startswith("tvdb_movie")) + item = self.config.TVDb.get_item(values[0], method_name.startswith("tvdb_movie")) if hasattr(item, "description") and item.description: self.summaries[method_name] = item.description if hasattr(item, "background_path") and item.background_path: @@ -1049,7 +1049,7 @@ def _tvdb(self, method_name, method_data): if hasattr(item, "poster_path") and item.poster_path: self.posters[method_name] = f"{self.config.TMDb.image_url}{item.poster_path}" elif method_name.startswith("tvdb_list"): - self.summaries[method_name] = self.config.TVDb.get_list_description(values[0], self.language) + self.summaries[method_name] = self.config.TVDb.get_list_description(values[0]) for value in values: self.builders.append((method_name[:-8] if method_name.endswith("_details") else method_name, value)) @@ -1104,7 +1104,7 @@ def find_rating_keys(self): mal_ids = self.config.MyAnimeList.get_mal_ids(method, value) ids = self.config.Convert.myanimelist_to_ids(mal_ids, self.library) elif "tvdb" in method: - ids = self.config.TVDb.get_tvdb_ids(method, value, self.language) + ids = self.config.TVDb.get_tvdb_ids(method, value) elif "imdb" in method: ids = self.config.IMDb.get_imdb_ids(method, value, self.language) elif "icheckmovies" in method: @@ -1693,7 +1693,7 @@ def run_missing(self): missing_shows_with_names = [] for missing_id in self.missing_shows: try: - show = self.config.TVDb.get_series(self.language, missing_id) + show = self.config.TVDb.get_series(missing_id) except Failed as e: logger.error(e) continue @@ -2101,7 +2101,7 @@ def run_collections_again(self): for missing_id in self.run_again_shows: if missing_id not in self.library.show_map: try: - title = self.config.TVDb.get_series(self.language, missing_id).title + title = self.config.TVDb.get_series(missing_id).title except Failed as e: logger.error(e) continue diff --git a/modules/config.py b/modules/config.py index 6596be074..eecfab525 100644 --- a/modules/config.py +++ b/modules/config.py @@ -192,7 +192,8 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), "notifiarr_collection_creation": check_for_attribute(self.data, "notifiarr_collection_creation", parent="settings", var_type="bool", default=False), "notifiarr_collection_addition": check_for_attribute(self.data, "notifiarr_collection_addition", parent="settings", var_type="bool", default=False), - "notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False) + "notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False), + "tvdb_language": check_for_attribute(self.data, "tvdb_language", parent="settings", default="default") } if self.general["cache"]: util.separator() @@ -304,7 +305,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No if self.AniDB is None: self.AniDB = AniDB(self, None) - self.TVDb = TVDb(self) + self.TVDb = TVDb(self, self.general["tvdb_language"]) self.IMDb = IMDb(self) self.Convert = Convert(self) self.AniList = AniList(self) diff --git a/modules/tvdb.py b/modules/tvdb.py index 5cbc0c2d9..532f8d9e9 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -38,20 +38,18 @@ def __init__(self, tvdb_url, language, is_movie, config): else: raise Failed(f"TVDb Error: Could not find a TVDb {self.media_type} ID at the URL {self.tvdb_url}") - def parse_page(xpath, fail=None, multi=False): + def parse_page(xpath): parse_results = response.xpath(xpath) if len(parse_results) > 0: parse_results = [r.strip() for r in parse_results if len(r) > 0] - if not multi and len(parse_results) > 0: - return parse_results[0] - elif len(parse_results) > 0: - return parse_results - elif fail is not None: - raise Failed(f"TVDb Error: {fail} not found from TVDb URL: {self.tvdb_url}") - else: - return None + return parse_results[0] if len(parse_results) > 0 else None + + self.title = parse_page(f"//div[@class='change_translation_text' and @data-language='{self.language}']/@data-title") + if not self.title: + self.title = parse_page("//div[@class='change_translation_text' and not(@style='display:none')]/@data-title") + if not self.title: + raise Failed(f"TVDb Error: Name not found from TVDb URL: {self.tvdb_url}") - self.title = parse_page("//div[@class='change_translation_text' and not(@style='display:none')]/@data-title", fail="Name") self.poster_path = parse_page("//div[@class='row hidden-xs hidden-sm']/div/img/@src") self.background_path = parse_page("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]") self.summary = parse_page("//div[@class='change_translation_text' and not(@style='display:none')]/p/text()[normalize-space()]") @@ -84,49 +82,50 @@ def parse_page(xpath, fail=None, multi=False): self.imdb_id = imdb_id class TVDb: - def __init__(self, config): + def __init__(self, config, tvdb_language): self.config = config + self.tvdb_language = tvdb_language - def get_item(self, language, tvdb_url, is_movie): - return self.get_movie(language, tvdb_url) if is_movie else self.get_series(language, tvdb_url) + def get_item(self, tvdb_url, is_movie): + return self.get_movie(tvdb_url) if is_movie else self.get_series(tvdb_url) - def get_series(self, language, tvdb_url): + def get_series(self, tvdb_url): try: tvdb_url = f"{urls['series_id']}{int(tvdb_url)}" except ValueError: pass - return TVDbObj(tvdb_url, language, False, self.config) + return TVDbObj(tvdb_url, self.tvdb_language, False, self.config) - def get_movie(self, language, tvdb_url): + def get_movie(self, tvdb_url): try: tvdb_url = f"{urls['movie_id']}{int(tvdb_url)}" except ValueError: pass - return TVDbObj(tvdb_url, language, True, self.config) + return TVDbObj(tvdb_url, self.tvdb_language, True, self.config) - def get_list_description(self, tvdb_url, language): - response = self.config.get_html(tvdb_url, headers=util.header(language)) + def get_list_description(self, tvdb_url): + response = self.config.get_html(tvdb_url, headers=util.header(self.tvdb_language)) description = response.xpath("//div[@class='block']/div[not(@style='display:none')]/p/text()") return description[0] if len(description) > 0 and len(description[0]) > 0 else "" - def _ids_from_url(self, tvdb_url, language): + def _ids_from_url(self, tvdb_url): ids = [] tvdb_url = tvdb_url.strip() if tvdb_url.startswith((urls["list"], urls["alt_list"])): try: - response = self.config.get_html(tvdb_url, headers=util.header(language)) + response = self.config.get_html(tvdb_url, headers=util.header(self.tvdb_language)) items = response.xpath("//div[@class='col-xs-12 col-sm-12 col-md-8 col-lg-8 col-md-pull-4']/div[@class='row']") for item in items: title = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/text()")[0] item_url = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/@href")[0] if item_url.startswith("/series/"): try: - ids.append((self.get_series(language, f"{base_url}{item_url}").id, "tvdb")) + ids.append((self.get_series(f"{base_url}{item_url}").id, "tvdb")) except Failed as e: logger.error(f"{e} for series {title}") elif item_url.startswith("/movies/"): try: - movie = self.get_movie(language, f"{base_url}{item_url}") + movie = self.get_movie(f"{base_url}{item_url}") if movie.tmdb_id: ids.append((movie.tmdb_id, "tmdb")) elif movie.imdb_id: @@ -145,19 +144,19 @@ def _ids_from_url(self, tvdb_url, language): else: raise Failed(f"TVDb Error: {tvdb_url} must begin with {urls['list']}") - def get_tvdb_ids(self, method, data, language): + def get_tvdb_ids(self, method, data): if method == "tvdb_show": logger.info(f"Processing TVDb Show: {data}") - return [(self.get_series(language, data).id, "tvdb")] + return [(self.get_series(data).id, "tvdb")] elif method == "tvdb_movie": logger.info(f"Processing TVDb Movie: {data}") - movie = self.get_movie(language, data) + movie = self.get_movie(data) if movie.tmdb_id: return [(movie.tmdb_id, "tmdb")] elif movie.imdb_id: return [(movie.imdb_id, "imdb")] elif method == "tvdb_list": logger.info(f"Processing TVDb List: {data}") - return self._ids_from_url(data, language) + return self._ids_from_url(data) else: raise Failed(f"TVDb Error: Method {method} not supported") diff --git a/modules/util.py b/modules/util.py index a307b0d0b..477d32914 100644 --- a/modules/util.py +++ b/modules/util.py @@ -109,7 +109,7 @@ def logger_input(prompt, timeout=60): else: raise SystemError("Input Timeout not supported on this system") def header(language="en-US,en;q=0.5"): - return {"Accept-Language": language, "User-Agent": "Mozilla/5.0 x64"} + return {"Accept-Language": "eng" if language == "default" else language, "User-Agent": "Mozilla/5.0 x64"} def alarm_handler(signum, frame): raise TimeoutExpired From c3738f0442c45167c07b6a7647af33a7be035ccb Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 26 Oct 2021 11:01:35 -0400 Subject: [PATCH 35/57] version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 54461f879..6adc0ea8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2-develop1025 \ No newline at end of file +1.12.2-develop1026 \ No newline at end of file From d8d20b07a5861fc9601de714922e10b8cc1198e0 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 27 Oct 2021 02:07:33 -0400 Subject: [PATCH 36/57] added delete_collections_with_less and delete_unmanaged_collections --- modules/config.py | 8 ++++++-- modules/library.py | 4 +++- plex_meta_manager.py | 16 +++++++++++++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/modules/config.py b/modules/config.py index eecfab525..300766916 100644 --- a/modules/config.py +++ b/modules/config.py @@ -179,7 +179,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "cache_expiration": check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60), "asset_directory": check_for_attribute(self.data, "asset_directory", parent="settings", var_type="list_path", default=[os.path.join(default_dir, "assets")]), "asset_folders": check_for_attribute(self.data, "asset_folders", parent="settings", var_type="bool", default=True), - "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False), + "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False, save=False, do_print=False), "sync_mode": check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=sync_modes), "run_again_delay": check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0), "show_unmanaged": check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True), @@ -379,7 +379,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) - params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) + assets_for_all = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], 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) params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) @@ -393,6 +393,10 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["notifiarr_collection_addition"] = check_for_attribute(lib, "notifiarr_collection_addition", parent="settings", var_type="bool", default=self.general["notifiarr_collection_addition"], do_print=False, save=False) params["notifiarr_collection_removing"] = check_for_attribute(lib, "notifiarr_collection_removing", parent="settings", var_type="bool", default=self.general["notifiarr_collection_removing"], do_print=False, save=False) + params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", var_type="bool", default=assets_for_all, save=False, do_print=lib and "assets_for_all" in lib) + params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", var_type="bool", default=False, save=False, do_print=lib and "delete_unmanaged_collections" in lib) + params["delete_collections_with_less"] = check_for_attribute(lib, "delete_collections_with_less", var_type="int", default_is_none=True, save=False, do_print=lib and "delete_collections_with_less" in lib) + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) if self.OMDb is None and params["mass_genre_update"] == "omdb": params["mass_genre_update"] = None diff --git a/modules/library.py b/modules/library.py index 71aa2f11f..4335a6304 100644 --- a/modules/library.py +++ b/modules/library.py @@ -39,7 +39,6 @@ def __init__(self, config, params): self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None self.missing_path = os.path.join(self.default_dir, f"{self.original_mapping_name}_missing.yml") self.asset_folders = params["asset_folders"] - self.assets_for_all = params["assets_for_all"] self.sync_mode = params["sync_mode"] self.show_unmanaged = params["show_unmanaged"] self.show_filtered = params["show_filtered"] @@ -47,6 +46,9 @@ def __init__(self, config, params): self.save_missing = params["save_missing"] self.missing_only_released = params["missing_only_released"] self.create_asset_folders = params["create_asset_folders"] + self.assets_for_all = params["assets_for_all"] + self.delete_unmanaged_collections = params["delete_unmanaged_collections"] + self.delete_collections_with_less = params["delete_collections_with_less"] self.mass_genre_update = params["mass_genre_update"] self.mass_audience_rating_update = params["mass_audience_rating_update"] self.mass_critic_rating_update = params["mass_critic_rating_update"] diff --git a/plex_meta_manager.py b/plex_meta_manager.py index d49b1f7f7..837f69cb4 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -203,10 +203,16 @@ def update_libraries(config): builder.sort_collection() if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): - logger.info("") - util.separator(f"Other {library.name} Library Operations") + if library.delete_collections_with_less is not None: + logger.info("") + text = f" with less then {library.delete_collections_with_less} item{'s' if library.delete_collections_with_less > 1 else ''}" + util.separator(f"Deleting All Collections{text if library.delete_collections_with_less > 0 else ''}", space=False, border=False) + logger.info("") unmanaged_collections = [] for col in library.get_all_collections(): + if library.delete_collections_with_less is not None and (library.delete_collections_with_less == 0 or col.childCount < library.delete_collections_with_less): + library.query(col.delete) + logger.info(f"{col.title} Deleted") if col.title not in library.collections: unmanaged_collections.append(col) @@ -215,7 +221,11 @@ def update_libraries(config): util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) logger.info("") for col in unmanaged_collections: - logger.info(col.title) + if library.delete_unmanaged_collections: + library.query(col.delete) + logger.info(f"{col.title} Deleted") + else: + logger.info(col.title) logger.info("") logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") From cc8f925afc34f5fa76a8c6480e321a2f6238e994 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 28 Oct 2021 02:07:14 -0400 Subject: [PATCH 37/57] hide notifiarr apikey --- modules/notifiarr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 380588aab..6a2037046 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -18,7 +18,7 @@ def __init__(self, config, apikey, develop, test, error_notification): def _request(self, path, json=None, params=None): url = f"{dev_url if self.develop else base_url}" + \ ("notification/test" if self.test else f"{path}{self.apikey}") - logger.debug(url) + logger.debug(url.replace(self.apikey, "APIKEY")) response = self.config.get(url, json=json, params={"event": "pmm"} if self.test else params) response_json = response.json() if self.develop or self.test: From cc262647c28649c07baa598789ff948e0285fb5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 04:43:32 +0000 Subject: [PATCH 38/57] Bump ruamel-yaml from 0.17.16 to 0.17.17 Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.16 to 0.17.17. --- updated-dependencies: - dependency-name: ruamel-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a29f49d7a..393ef4daa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ tmdbv3api==1.7.6 arrapi==1.1.7 lxml==4.6.3 requests==2.26.0 -ruamel.yaml==0.17.16 +ruamel.yaml==0.17.17 schedule==1.1.0 retrying==1.3.3 pathvalidate==2.5.0 From fab98f4fa4f13478661a66c9750ae64d0513e5ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 04:28:28 +0000 Subject: [PATCH 39/57] Bump lxml from 4.6.3 to 4.6.4 Bumps [lxml](https://github.com/lxml/lxml) from 4.6.3 to 4.6.4. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.3...lxml-4.6.4) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a29f49d7a..448fcd9fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PlexAPI==4.7.2 tmdbv3api==1.7.6 arrapi==1.1.7 -lxml==4.6.3 +lxml==4.6.4 requests==2.26.0 ruamel.yaml==0.17.16 schedule==1.1.0 From 9312c5ec350d4662c7c56e4f0e447f294a2cd8ed Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 3 Nov 2021 10:36:11 -0400 Subject: [PATCH 40/57] added webhooks --- config/config.yml.template | 10 +++-- modules/builder.py | 66 +++++++++++++++++------------- modules/config.py | 38 +++++++++-------- modules/library.py | 11 +++-- modules/notifiarr.py | 75 +++++++--------------------------- modules/radarr.py | 2 + modules/sonarr.py | 2 + modules/util.py | 2 +- modules/webhooks.py | 83 ++++++++++++++++++++++++++++++++++++++ plex_meta_manager.py | 29 +++++++++++-- 10 files changed, 201 insertions(+), 117 deletions(-) create mode 100644 modules/webhooks.py diff --git a/config/config.yml.template b/config/config.yml.template index 8c97e60e0..89284dac1 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -34,9 +34,12 @@ settings: # Can be individually specified missing_only_released: false collection_minimum: 1 delete_below_minimum: true - notifiarr_collection_creation: false - notifiarr_collection_addition: false - notifiarr_collection_removing: false + error_webhooks: + run_start_webhooks: + run_end_webhooks: + collection_creation_webhooks: + collection_addition_webhooks: + collection_removing_webhooks: tvdb_language: eng plex: # Can be individually specified per library as well url: http://192.168.1.12:32400 @@ -78,7 +81,6 @@ omdb: apikey: ######## notifiarr: apikey: #################################### - error_notification: true trakt: client_id: ################################################################ client_secret: ################################################################ diff --git a/modules/builder.py b/modules/builder.py index 998b2181b..4c58cfc0c 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -78,15 +78,16 @@ poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"] background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"] boolean_details = [ - "visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", - "delete_below_minimum", "notifiarr_collection_creation", "notifiarr_collection_addition", "notifiarr_collection_removing" + "visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", + "missing_only_released", "delete_below_minimum" ] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name" ] -details = ["collection_mode", "collection_order", "collection_level", "collection_minimum", "label"] + boolean_details + string_details +notification_details = ["collection_creation_webhooks", "collection_addition_webhooks", "collection_removing_webhooks"] +details = ["collection_mode", "collection_order", "collection_level", "collection_minimum", "label"] + boolean_details + string_details + notification_details collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ poster_details + background_details + summary_details + string_details item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay", "item_assets", "revert_overlay", "item_refresh"] + list(plex.item_advance_keys.keys()) @@ -176,9 +177,9 @@ def __init__(self, config, library, metadata, name, no_missing, data): "missing_only_released": self.library.missing_only_released, "create_asset_folders": self.library.create_asset_folders, "delete_below_minimum": self.library.delete_below_minimum, - "notifiarr_collection_creation": self.library.notifiarr_collection_creation, - "notifiarr_collection_addition": self.library.notifiarr_collection_addition, - "notifiarr_collection_removing": self.library.notifiarr_collection_removing, + "collection_creation_webhooks": self.library.collection_creation_webhooks, + "collection_addition_webhooks": self.library.collection_addition_webhooks, + "collection_removing_webhooks": self.library.collection_removing_webhooks, } self.item_details = {} self.radarr_details = {} @@ -193,8 +194,8 @@ def __init__(self, config, library, metadata, name, no_missing, data): self.filtered_keys = {} self.run_again_movies = [] self.run_again_shows = [] - self.notifiarr_additions = [] - self.notifiarr_removals = [] + self.notification_additions = [] + self.notification_removals = [] self.items = [] self.posters = {} self.backgrounds = {} @@ -551,7 +552,6 @@ def cant_interact(attr1, attr2, fail=False): elif not self.library.Sonarr and "sonarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Sonarr to be configured") elif not self.library.Tautulli and "tautulli" in method_name: raise Failed(f"Collection Error: {method_final} requires Tautulli to be configured") elif not self.config.MyAnimeList and "mal" in method_name: raise Failed(f"Collection Error: {method_final} requires MyAnimeList to be configured") - elif not self.library.Notifiarr and "notifiarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Notifiarr to be configured") elif self.library.is_movie and method_name in show_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for show libraries") elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for movie libraries") elif self.library.is_show and method_name in plex.movie_only_searches: raise Failed(f"Collection Error: {method_final} plex search only works for movie libraries") @@ -709,6 +709,8 @@ def _details(self, method_name, method_data, method_final, methods): self.details["label.sync"] = util.get_list(method_data) else: self.details[method_final] = util.get_list(method_data) + elif method_name in notification_details: + self.details[method_name] = util.parse(method_name, method_data, datatype="list") elif method_name in boolean_details: default = self.details[method_name] if method_name in self.details else None self.details[method_name] = util.parse(method_name, method_data, datatype="bool", default=default) @@ -1485,6 +1487,7 @@ def fetch_item(self, item): def add_to_collection(self): name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection) total = len(self.rating_keys) + amount_added = 0 for i, item in enumerate(self.rating_keys, 1): try: current = self.fetch_item(item) @@ -1497,41 +1500,44 @@ def add_to_collection(self): self.plex_map[current.ratingKey] = None else: self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) - if self.details["notifiarr_collection_addition"]: + amount_added += 1 + if self.details["collection_addition_webhooks"]: if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: add_id = self.library.movie_rating_key_map[current.ratingKey] elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: add_id = self.library.show_rating_key_map[current.ratingKey] else: add_id = None - self.notifiarr_additions.append({"title": current.title, "id": add_id}) + self.notification_additions.append({"title": current.title, "id": add_id}) util.print_end() logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") + return amount_added def sync_collection(self): - count_removed = 0 + amount_removed = 0 for ratingKey, item in self.plex_map.items(): if item is not None: - if count_removed == 0: + if amount_removed == 0: logger.info("") util.separator(f"Removed from {self.name} Collection", space=False, border=False) logger.info("") self.library.reload(item) logger.info(f"{self.name} Collection | - | {self.item_title(item)}") self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) - if self.details["notifiarr_collection_removing"]: + if self.details["collection_removing_webhooks"]: if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: remove_id = self.library.movie_rating_key_map[item.ratingKey] elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: remove_id = self.library.show_rating_key_map[item.ratingKey] else: remove_id = None - self.notifiarr_removals.append({"title": item.title, "id": remove_id}) - count_removed += 1 - if count_removed > 0: + self.notification_removals.append({"title": item.title, "id": remove_id}) + amount_removed += 1 + if amount_removed > 0: logger.info("") - logger.info(f"{count_removed} {self.collection_level.capitalize()}{'s' if count_removed == 1 else ''} Removed") + logger.info(f"{amount_removed} {self.collection_level.capitalize()}{'s' if amount_removed == 1 else ''} Removed") + return amount_removed def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False): if self.tmdb_filters or check_released: @@ -1653,6 +1659,8 @@ def check_filters(self, current, display): return True def run_missing(self): + added_to_radarr = 0 + added_to_sonarr = 0 if len(self.missing_movies) > 0: missing_movies_with_names = [] for missing_id in self.missing_movies: @@ -1679,7 +1687,7 @@ def run_missing(self): if self.library.Radarr: if self.radarr_details["add"]: try: - self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_details) + added_to_radarr += self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_details) except Failed as e: logger.error(e) if "item_radarr_tag" in self.item_details: @@ -1714,7 +1722,7 @@ def run_missing(self): if self.library.Sonarr: if self.sonarr_details["add"]: try: - self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_details) + added_to_sonarr += self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_details) except Failed as e: logger.error(e) if "item_sonarr_tag" in self.item_details: @@ -1727,6 +1735,7 @@ def run_missing(self): if len(self.missing_parts) > 0 and self.library.is_show and self.details["save_missing"] is True: for missing in self.missing_parts: logger.info(f"{self.name} Collection | X | {missing}") + return added_to_radarr, added_to_sonarr def item_title(self, item): if self.collection_level == "season": @@ -2034,16 +2043,17 @@ def sort_collection(self): def send_notifications(self): if self.obj and ( - (self.details["notifiarr_collection_creation"] and self.created) or - (self.details["notifiarr_collection_addition"] and len(self.notifiarr_additions) > 0) or - (self.details["notifiarr_collection_removing"] and len(self.notifiarr_removals) > 0) + (self.details["collection_creation_webhooks"] and self.created) or + (self.details["collection_addition_webhooks"] and len(self.notification_additions) > 0) or + (self.details["collection_removing_webhooks"] and len(self.notification_removals) > 0) ): self.obj.reload() - self.library.Notifiarr.plex_collection( + self.library.Webhooks.collection_hooks( + self.details["collection_creation_webhooks"] + self.details["collection_addition_webhooks"] + self.details["collection_removing_webhooks"], self.obj, created=self.created, - additions=self.notifiarr_additions, - removals=self.notifiarr_removals + additions=self.notification_additions, + removals=self.notification_removals ) def run_collections_again(self): @@ -2051,7 +2061,7 @@ def run_collections_again(self): name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection) self.created = False rating_keys = [] - self.notifiarr_additions = [] + self.notification_additions = [] for mm in self.run_again_movies: if mm in self.library.movie_map: rating_keys.extend(self.library.movie_map[mm]) @@ -2077,7 +2087,7 @@ def run_collections_again(self): add_id = self.library.show_rating_key_map[current.ratingKey] else: add_id = None - self.notifiarr_additions.append({"title": current.title, "id": add_id}) + self.notification_additions.append({"title": current.title, "id": add_id}) self.send_notifications() logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed") diff --git a/modules/config.py b/modules/config.py index 300766916..9b29f1e69 100644 --- a/modules/config.py +++ b/modules/config.py @@ -10,7 +10,7 @@ from modules.imdb import IMDb from modules.letterboxd import Letterboxd from modules.mal import MyAnimeList -from modules.notifiarr import NotifiarrFactory +from modules.notifiarr import Notifiarr from modules.omdb import OMDb from modules.plex import Plex from modules.radarr import Radarr @@ -21,6 +21,7 @@ from modules.trakt import Trakt from modules.tvdb import TVDb from modules.util import Failed +from modules.webhooks import Webhooks from retrying import retry from ruamel import yaml @@ -135,9 +136,9 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No elif var_type == "path": if os.path.exists(os.path.abspath(data[attribute])): return data[attribute] else: message = f"Path {os.path.abspath(data[attribute])} does not exist" - elif var_type == "list": return util.get_list(data[attribute]) + elif var_type == "list": return util.get_list(data[attribute], split=False) elif var_type == "list_path": - temp_list = [p for p in util.get_list(data[attribute], split=True) if os.path.exists(os.path.abspath(p))] + temp_list = [p for p in util.get_list(data[attribute], split=False) if os.path.exists(os.path.abspath(p))] if len(temp_list) > 0: return temp_list else: message = "No Paths exist" elif var_type == "lower_list": return util.get_list(data[attribute], lower=True) @@ -190,9 +191,12 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False), "collection_minimum": check_for_attribute(self.data, "collection_minimum", parent="settings", var_type="int", default=1), "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), - "notifiarr_collection_creation": check_for_attribute(self.data, "notifiarr_collection_creation", parent="settings", var_type="bool", default=False), - "notifiarr_collection_addition": check_for_attribute(self.data, "notifiarr_collection_addition", parent="settings", var_type="bool", default=False), - "notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False), + "error_webhooks": check_for_attribute(self.data, "error_webhooks", parent="settings", var_type="list", default_is_none=True), + "run_start_webhooks": check_for_attribute(self.data, "run_start_webhooks", parent="settings", var_type="list", default_is_none=True), + "run_end_webhooks": check_for_attribute(self.data, "run_end_webhooks", parent="settings", var_type="list", default_is_none=True), + "collection_creation_webhooks": check_for_attribute(self.data, "collection_creation_webhooks", parent="settings", var_type="list", default_is_none=True), + "collection_addition_webhooks": check_for_attribute(self.data, "collection_addition_webhooks", parent="settings", var_type="list", default_is_none=True), + "collection_removing_webhooks": check_for_attribute(self.data, "collection_removing_webhooks", parent="settings", var_type="list", default_is_none=True), "tvdb_language": check_for_attribute(self.data, "tvdb_language", parent="settings", default="default") } if self.general["cache"]: @@ -207,9 +211,8 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No if "notifiarr" in self.data: logger.info("Connecting to Notifiarr...") try: - self.NotifiarrFactory = NotifiarrFactory(self, { + self.NotifiarrFactory = Notifiarr(self, { "apikey": check_for_attribute(self.data, "apikey", parent="notifiarr", throw=True), - "error_notification": check_for_attribute(self.data, "error_notification", parent="notifiarr", var_type="bool", default=True), "develop": check_for_attribute(self.data, "develop", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False), "test": check_for_attribute(self.data, "test", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False) }) @@ -219,6 +222,9 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No else: logger.warning("notifiarr attribute not found") + self.Webhooks = Webhooks(self, self.general, notifiarr=self.NotifiarrFactory) + self.Webhooks.start_time_hooks(self.run_start_time) + self.errors = [] util.separator() @@ -389,9 +395,10 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) - params["notifiarr_collection_creation"] = check_for_attribute(lib, "notifiarr_collection_creation", parent="settings", var_type="bool", default=self.general["notifiarr_collection_creation"], do_print=False, save=False) - params["notifiarr_collection_addition"] = check_for_attribute(lib, "notifiarr_collection_addition", parent="settings", var_type="bool", default=self.general["notifiarr_collection_addition"], do_print=False, save=False) - params["notifiarr_collection_removing"] = check_for_attribute(lib, "notifiarr_collection_removing", parent="settings", var_type="bool", default=self.general["notifiarr_collection_removing"], do_print=False, save=False) + params["error_webhooks"] = check_for_attribute(lib, "error_webhooks", parent="settings", var_type="list", default=self.general["error_webhooks"], do_print=False, save=False) + params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation_webhooks", parent="settings", var_type="list", default=self.general["collection_creation_webhooks"], do_print=False, save=False) + params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition_webhooks", parent="settings", var_type="list", default=self.general["collection_addition_webhooks"], do_print=False, save=False) + params["collection_removing_webhooks"] = check_for_attribute(lib, "collection_removing_webhooks", parent="settings", var_type="list", default=self.general["collection_removing_webhooks"], do_print=False, save=False) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", var_type="bool", default=assets_for_all, save=False, do_print=lib and "assets_for_all" in lib) params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", var_type="bool", default=False, save=False, do_print=lib and "delete_unmanaged_collections" in lib) @@ -545,7 +552,7 @@ def check_dict(attr, name): logger.info("") logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") - library.Notifiarr = self.NotifiarrFactory.getNotifiarr(library) if self.NotifiarrFactory else None + library.Webhooks = Webhooks(self, {"error_webhooks": library.error_webhooks}, library=library, notifiarr=self.NotifiarrFactory) logger.info("") self.libraries.append(library) @@ -566,11 +573,8 @@ def check_dict(attr, name): raise def notify(self, text, library=None, collection=None, critical=True): - if self.NotifiarrFactory: - if not isinstance(text, list): - text = [text] - for t in text: - self.NotifiarrFactory.error(t, library=library, collection=collection, critical=critical) + for error in util.get_list(text, split=False): + self.Webhooks.error_hooks(error, library=library, collection=collection, critical=critical) def get_html(self, url, headers=None, params=None): return html.fromstring(self.get(url, headers=headers, params=params).content) diff --git a/modules/library.py b/modules/library.py index 4335a6304..5c47ce715 100644 --- a/modules/library.py +++ b/modules/library.py @@ -13,6 +13,7 @@ def __init__(self, config, params): self.Radarr = None self.Sonarr = None self.Tautulli = None + self.Webhooks = None self.Notifiarr = None self.collections = [] self.metadatas = [] @@ -57,9 +58,10 @@ def __init__(self, config, params): self.sonarr_add_all = params["sonarr_add_all"] self.collection_minimum = params["collection_minimum"] self.delete_below_minimum = params["delete_below_minimum"] - self.notifiarr_collection_creation = params["notifiarr_collection_creation"] - self.notifiarr_collection_addition = params["notifiarr_collection_addition"] - self.notifiarr_collection_removing = params["notifiarr_collection_removing"] + self.error_webhooks = params["error_webhooks"] + self.collection_creation_webhooks = params["collection_creation_webhooks"] + self.collection_addition_webhooks = params["collection_addition_webhooks"] + self.collection_removing_webhooks = params["collection_removing_webhooks"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? @@ -182,6 +184,9 @@ def upload_images(self, item, poster=None, background=None, overlay=None): self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) def notify(self, text, collection=None, critical=True): + for error in util.get_list(text, split=False): + self.Webhooks.error_hooks(error, library=self, collection=collection, critical=critical) + self.config.notify(text, library=self, collection=collection, critical=critical) @abstractmethod diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 6a2037046..1c0e8e408 100644 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -7,70 +7,25 @@ base_url = "https://notifiarr.com/api/v1/" dev_url = "https://dev.notifiarr.com/api/v1/" -class NotifiarrBase: - def __init__(self, config, apikey, develop, test, error_notification): - self.config = config - self.apikey = apikey - self.develop = develop - self.test = test - self.error_notification = error_notification - def _request(self, path, json=None, params=None): - url = f"{dev_url if self.develop else base_url}" + \ - ("notification/test" if self.test else f"{path}{self.apikey}") - logger.debug(url.replace(self.apikey, "APIKEY")) - response = self.config.get(url, json=json, params={"event": "pmm"} if self.test else params) +class Notifiarr: + def __init__(self, config, params): + self.config = config + self.apikey = params["apikey"] + self.develop = params["develop"] + self.test = params["test"] + url, _ = self.get_url("user/validate/") + response = self.config.get(url) response_json = response.json() - if self.develop or self.test: - logger.debug(json) - logger.debug("") - logger.debug(response_json) if response.status_code >= 400 or ("result" in response_json and response_json["result"] == "error"): + logger.debug(f"Response: {response_json}") raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") - return response_json - - def error(self, text, library=None, collection=None, critical=True): - if self.error_notification: - json = {"error": str(text), "critical": critical} - if library: - json["server_name"] = library.PlexServer.friendlyName - json["library_name"] = library.name - if collection: - json["collection"] = str(collection) - self._request("notification/plex/", json=json, params={"event": "collections"}) - -class NotifiarrFactory(NotifiarrBase): - def __init__(self, config, params): - super().__init__(config, params["apikey"], params["develop"], params["test"], params["error_notification"]) - if not params["test"] and not self._request("user/validate/")["details"]["response"]: + if not params["test"] and not response_json["details"]["response"]: raise Failed("Notifiarr Error: Invalid apikey") - def getNotifiarr(self, library): - return Notifiarr(self.config, library, self.apikey, self.develop, self.test, self.error_notification) - -class Notifiarr(NotifiarrBase): - def __init__(self, config, library, apikey, develop, test, error_notification): - super().__init__(config, apikey, develop, test, error_notification) - self.library = library + def get_url(self, path): + url = f"{dev_url if self.develop else base_url}{'notification/test' if self.test else f'{path}{self.apikey}'}" + logger.debug(url.replace(self.apikey, "APIKEY")) + params = {"event": "pmm" if self.test else "collections"} + return url, params - def plex_collection(self, collection, created=False, additions=None, removals=None): - thumb = None - if collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None): - thumb = self.config.get_image_encoded(f"{self.library.url}{collection.thumb}?X-Plex-Token={self.library.token}") - art = None - if collection.art and next((f for f in collection.fields if f.name == "art"), None): - art = self.config.get_image_encoded(f"{self.library.url}{collection.art}?X-Plex-Token={self.library.token}") - json = { - "server_name": self.library.PlexServer.friendlyName, - "library_name": self.library.name, - "type": "movie" if self.library.is_movie else "show", - "collection": collection.title, - "created": created, - "poster": thumb, - "background": art - } - if additions: - json["additions"] = additions - if removals: - json["removals"] = removals - self._request("notification/plex/", json=json, params={"event": "collections"}) diff --git a/modules/radarr.py b/modules/radarr.py index 7f704d204..426eba0c6 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -61,6 +61,8 @@ def add_tmdb(self, tmdb_ids, **options): for tmdb_id in invalid: logger.info(f"Invalid TMDb ID | {tmdb_id}") + return len(added) + def edit_tags(self, tmdb_ids, tags, apply_tags): logger.info("") logger.info(f"{apply_tags_translation[apply_tags].capitalize()} Radarr Tags: {tags}") diff --git a/modules/sonarr.py b/modules/sonarr.py index 2386e3f84..201544402 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -87,6 +87,8 @@ def add_tvdb(self, tvdb_ids, **options): logger.info("") logger.info(f"Invalid TVDb ID | {tvdb_id}") + return len(added) + def edit_tags(self, tvdb_ids, tags, apply_tags): logger.info("") logger.info(f"{apply_tags_translation[apply_tags].capitalize()} Sonarr Tags: {tags}") diff --git a/modules/util.py b/modules/util.py index 477d32914..0cf0f73a5 100644 --- a/modules/util.py +++ b/modules/util.py @@ -304,7 +304,7 @@ def parse(attribute, data, datatype=None, methods=None, parent=None, default=Non value = data[methods[attribute]] if methods and attribute in methods else data if datatype == "list": - if methods and attribute in methods and data[methods[attribute]]: + if value: return [v for v in value if v] if isinstance(value, list) else [str(value)] return [] elif datatype == "dictlist": diff --git a/modules/webhooks.py b/modules/webhooks.py new file mode 100644 index 000000000..3a79f334f --- /dev/null +++ b/modules/webhooks.py @@ -0,0 +1,83 @@ +import logging + +from modules.util import Failed + +logger = logging.getLogger("Plex Meta Manager") + +class Webhooks: + def __init__(self, config, system_webhooks, library=None, notifiarr=None): + self.config = config + self.error_webhooks = system_webhooks["error_webhooks"] if "error_webhooks" in system_webhooks else [] + self.run_start_webhooks = system_webhooks["run_start_webhooks"] if "run_start_webhooks" in system_webhooks else [] + self.run_end_webhooks = system_webhooks["run_end_webhooks"] if "run_end_webhooks" in system_webhooks else [] + self.library = library + self.notifiarr = notifiarr + + def _request(self, webhooks, json): + if self.config.trace_mode: + logger.debug("") + logger.debug(f"JSON: {json}") + for webhook in webhooks: + if self.config.trace_mode: + logger.debug(f"Webhook: {webhook}") + if webhook == "notifiarr": + url, params = self.notifiarr.get_url("notification/plex/") + response = self.config.get(url, json=json, params=params) + else: + response = self.config.post(webhook, json=json) + response_json = response.json() + if self.config.trace_mode: + logger.debug(f"Response: {response_json}") + if response.status_code >= 400 or ("result" in response_json and response_json["result"] == "error"): + raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") + + def start_time_hooks(self, start_time): + if self.run_start_webhooks: + self._request(self.run_start_webhooks, {"start_time": start_time}) + + def end_time_hooks(self, start_time, run_time, stats): + if self.run_end_webhooks: + self._request(self.run_end_webhooks, { + "start_time": start_time, + "run_time": run_time, + "collections_created": stats["created"], + "collections_modified": stats["modified"], + "collections_deleted": stats["deleted"], + "items_added": stats["added"], + "items_removed": stats["removed"], + "added_to_radarr": stats["radarr"], + "added_to_sonarr": stats["sonarr"], + }) + + def error_hooks(self, text, library=None, collection=None, critical=True): + if self.error_webhooks: + json = {"error": str(text), "critical": critical} + if library: + json["server_name"] = library.PlexServer.friendlyName + json["library_name"] = library.name + if collection: + json["collection"] = str(collection) + self._request(self.error_webhooks, json) + + def collection_hooks(self, webhooks, collection, created=False, additions=None, removals=None): + if self.library: + thumb = None + if collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None): + thumb = self.config.get_image_encoded(f"{self.library.url}{collection.thumb}?X-Plex-Token={self.library.token}") + art = None + if collection.art and next((f for f in collection.fields if f.name == "art"), None): + art = self.config.get_image_encoded(f"{self.library.url}{collection.art}?X-Plex-Token={self.library.token}") + json = { + "server_name": self.library.PlexServer.friendlyName, + "library_name": self.library.name, + "type": "movie" if self.library.is_movie else "show", + "collection": collection.title, + "created": created, + "poster": thumb, + "background": art + } + if additions: + json["additions"] = additions + if removals: + json["removals"] = removals + self._request(webhooks, json) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 837f69cb4..38b1429d1 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -63,6 +63,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): divider = get_arg("PMM_DIVIDER", args.divider) screen_width = get_arg("PMM_WIDTH", args.width) config_file = get_arg("PMM_CONFIG", args.config) +stats = {} util.separating_character = divider[0] @@ -133,6 +134,9 @@ def start(attrs): if "time" not in attrs: attrs["time"] = start_time.strftime("%H:%M") util.separator(f"Starting {start_type}Run") + config = None + global stats + stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "removed": 0, "radarr": 0, "sonarr": 0} try: config = Config(default_dir, attrs) except Exception as e: @@ -146,10 +150,14 @@ def start(attrs): util.print_stacktrace() util.print_multiline(e, critical=True) logger.info("") - util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}") + run_time = str(datetime.now() - start_time).split('.')[0] + if config: + config.Webhooks.end_time_hooks(start_time, run_time, stats) + util.separator(f"Finished {start_type}Run\nRun Time: {run_time}") logger.removeHandler(file_handler) def update_libraries(config): + global stats for library in config.libraries: try: os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) @@ -460,6 +468,7 @@ def mass_metadata(config, library, items=None): logger.error(e) def run_collection(config, library, metadata, requested_collections): + global stats logger.info("") for mapping_name, collection_attrs in requested_collections.items(): collection_start = datetime.now() @@ -520,6 +529,8 @@ def run_collection(config, library, metadata, requested_collections): logger.info("") util.print_multiline(builder.smart_filter_details, info=True) + items_added = 0 + items_removed = 0 if not builder.smart_url: logger.info("") logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") @@ -535,14 +546,18 @@ def run_collection(config, library, metadata, requested_collections): logger.info("") util.separator(f"Adding to {mapping_name} Collection", space=False, border=False) logger.info("") - builder.add_to_collection() + items_added = builder.add_to_collection() + stats["added"] += items_added + items_removed = 0 if builder.sync: - builder.sync_collection() + items_removed = builder.sync_collection() + stats["removed"] += items_removed elif len(builder.rating_keys) < builder.minimum and builder.build_collection: logger.info("") logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") if builder.details["delete_below_minimum"] and builder.obj: builder.delete_collection() + stats["deleted"] += 1 logger.info("") logger.info(f"Collection {builder.obj.title} deleted") @@ -551,12 +566,18 @@ def run_collection(config, library, metadata, requested_collections): logger.info("") util.separator(f"Missing from Library", space=False, border=False) logger.info("") - builder.run_missing() + radarr_add, sonarr_add = builder.run_missing() + stats["radarr"] += radarr_add + stats["sonarr"] += sonarr_add run_item_details = True if builder.build_collection: try: builder.load_collection() + if builder.created: + stats["created"] += 1 + elif items_added > 0 or items_removed > 0: + stats["modified"] += 1 except Failed: util.print_stacktrace() run_item_details = False From 54b9d532623ce5732dfdbc8f80c30b08631111a6 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 3 Nov 2021 10:38:43 -0400 Subject: [PATCH 41/57] added trace --- modules/anidb.py | 16 +++++++++++----- modules/anilist.py | 5 +++++ modules/config.py | 1 + modules/icheckmovies.py | 2 ++ modules/imdb.py | 15 +++++++++------ modules/letterboxd.py | 6 ++++++ modules/mal.py | 4 ++++ modules/omdb.py | 2 ++ modules/tmdb.py | 2 ++ modules/trakt.py | 4 ++++ modules/tvdb.py | 4 ++++ plex_meta_manager.py | 9 ++++++--- 12 files changed, 56 insertions(+), 14 deletions(-) diff --git a/modules/anidb.py b/modules/anidb.py index 315a9cd8c..713e312f8 100644 --- a/modules/anidb.py +++ b/modules/anidb.py @@ -22,15 +22,21 @@ def __init__(self, config, params): if params and not self._login(self.username, self.password).xpath("//li[@class='sub-menu my']/@title"): raise Failed("AniDB Error: Login failed") - def _request(self, url, language=None, post=None): - if post: - return self.config.post_html(url, post, headers=util.header(language)) + def _request(self, url, language=None, data=None): + if self.config.trace_mode: + logger.debug(f"URL: {url}") + if data: + return self.config.post_html(url, data=data, headers=util.header(language)) else: return self.config.get_html(url, headers=util.header(language)) def _login(self, username, password): - data = {"show": "main", "xuser": username, "xpass": password, "xdoautologin": "on"} - return self._request(urls["login"], post=data) + return self._request(urls["login"], data={ + "show": "main", + "xuser": username, + "xpass": password, + "xdoautologin": "on" + }) def _popular(self, language): response = self._request(urls["popular"], language=language) diff --git a/modules/anilist.py b/modules/anilist.py index c743cf917..eeff1c004 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -61,8 +61,13 @@ def __init__(self, config): self.options["Tag Category"][media_tag["category"].lower().replace(" ", "-")] = media_tag["category"] def _request(self, query, variables, level=1): + if self.config.trace_mode: + logger.debug(f"Query: {query}") + logger.debug(f"Variables: {variables}") response = self.config.post(base_url, json={"query": query, "variables": variables}) json_obj = response.json() + if self.config.trace_mode: + logger.debug(f"Response: {json_obj}") if "errors" in json_obj: if json_obj['errors'][0]['message'] == "Too Many Requests.": wait_time = int(response.headers["Retry-After"]) if "Retry-After" in response.headers else 0 diff --git a/modules/config.py b/modules/config.py index 9b29f1e69..47531f361 100644 --- a/modules/config.py +++ b/modules/config.py @@ -42,6 +42,7 @@ def __init__(self, default_dir, attrs): self.default_dir = default_dir self.test_mode = attrs["test"] if "test" in attrs else False + self.trace_mode = attrs["trace"] if "trace" in attrs else False self.run_start_time = attrs["time"] self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour self.requested_collections = util.get_list(attrs["collections"]) if "collections" in attrs else None diff --git a/modules/icheckmovies.py b/modules/icheckmovies.py index ebccc9bc6..8bfaaef2c 100644 --- a/modules/icheckmovies.py +++ b/modules/icheckmovies.py @@ -12,6 +12,8 @@ def __init__(self, config): self.config = config def _request(self, url, language, xpath): + if self.config.trace_mode: + logger.debug(f"URL: {url}") return self.config.get_html(url, headers=util.header(language)).xpath(xpath) def _parse_list(self, list_url, language): diff --git a/modules/imdb.py b/modules/imdb.py index dcc1e9178..1a131125a 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -66,9 +66,12 @@ def _ids_from_url(self, imdb_url, language, limit): parsed_url = urlparse(imdb_url) params = parse_qs(parsed_url.query) imdb_base = parsed_url._replace(query=None).geturl() - params.pop("start", None) - params.pop("count", None) - params.pop("page", None) + params.pop("start", None) # noqa + params.pop("count", None) # noqa + params.pop("page", None) # noqa + if self.config.trace_mode: + logger.debug(f"URL: {imdb_base}") + logger.debug(f"Params: {params}") if limit < 1 or total < limit: limit = total @@ -80,10 +83,10 @@ def _ids_from_url(self, imdb_url, language, limit): start_num = (i - 1) * item_count + 1 util.print_return(f"Parsing Page {i}/{num_of_pages} {start_num}-{limit if i == num_of_pages else i * item_count}") if imdb_base.startswith((urls["list"], urls["keyword"])): - params["page"] = i + params["page"] = i # noqa else: - params["count"] = remainder if i == num_of_pages else item_count - params["start"] = start_num + params["count"] = remainder if i == num_of_pages else item_count # noqa + params["start"] = start_num # noqa ids_found = self.config.get_html(imdb_base, headers=headers, params=params).xpath(xpath["imdb_id"]) if imdb_base.startswith((urls["list"], urls["keyword"])) and i == num_of_pages: ids_found = ids_found[:remainder] diff --git a/modules/letterboxd.py b/modules/letterboxd.py index 7ad7d0c59..d577646cb 100644 --- a/modules/letterboxd.py +++ b/modules/letterboxd.py @@ -12,6 +12,8 @@ def __init__(self, config): self.config = config def _parse_list(self, list_url, language): + if self.config.trace_mode: + logger.debug(f"URL: {list_url}") response = self.config.get_html(list_url, headers=util.header(language)) letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container')]/div/@data-film-id") items = [] @@ -25,6 +27,8 @@ def _parse_list(self, list_url, language): return items def _tmdb(self, letterboxd_url, language): + if self.config.trace_mode: + logger.debug(f"URL: {letterboxd_url}") response = self.config.get_html(letterboxd_url, headers=util.header(language)) ids = response.xpath("//a[@data-track-action='TMDb']/@href") if len(ids) > 0 and ids[0]: @@ -34,6 +38,8 @@ def _tmdb(self, letterboxd_url, language): raise Failed(f"Letterboxd Error: TMDb Movie ID not found at {letterboxd_url}") def get_list_description(self, list_url, language): + if self.config.trace_mode: + logger.debug(f"URL: {list_url}") response = self.config.get_html(list_url, headers=util.header(language)) descriptions = response.xpath("//meta[@property='og:description']/@content") return descriptions[0] if len(descriptions) > 0 and len(descriptions[0]) > 0 else None diff --git a/modules/mal.py b/modules/mal.py index 196f21359..192e541d1 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -128,7 +128,11 @@ def _oauth(self, data): def _request(self, url, authorization=None): new_authorization = authorization if authorization else self.authorization + if self.config.trace_mode: + logger.debug(f"URL: {url}") response = self.config.get_json(url, headers={"Authorization": f"Bearer {new_authorization['access_token']}"}) + if self.config.trace_mode: + logger.debug(f"Response: {response}") if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}") else: return response diff --git a/modules/omdb.py b/modules/omdb.py index 45051c92e..fa388d965 100644 --- a/modules/omdb.py +++ b/modules/omdb.py @@ -48,6 +48,8 @@ def get_omdb(self, imdb_id): omdb_dict, expired = self.config.Cache.query_omdb(imdb_id) if omdb_dict and expired is False: return OMDbObj(imdb_id, omdb_dict) + if self.config.trace_mode: + logger.debug(f"IMDb ID: {imdb_id}") response = self.config.get(base_url, params={"i": imdb_id, "apikey": self.apikey}) if response.status_code < 400: omdb = OMDbObj(imdb_id, response.json()) diff --git a/modules/tmdb.py b/modules/tmdb.py index 7fee81096..b5c3bf4c8 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -202,6 +202,8 @@ def _discover(self, attrs, amount, is_movie): for date_attr in discover_dates: if date_attr in attrs: attrs[date_attr] = util.validate_date(attrs[date_attr], f"tmdb_discover attribute {date_attr}", return_as="%Y-%m-%d") + if self.config.trace_mode: + logger.debug(f"Params: {attrs}") self.Discover.discover_movies(attrs) if is_movie else self.Discover.discover_tv_shows(attrs) total_pages = int(self.TMDb.total_pages) total_results = int(self.TMDb.total_results) diff --git a/modules/trakt.py b/modules/trakt.py index 899fd2f1e..d0bc2d047 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -107,6 +107,8 @@ def _request(self, url): output_json = [] pages = 1 current = 1 + if self.config.trace_mode: + logger.debug(f"URL: {base_url}{url}") while current <= pages: if pages == 1: response = self.config.get(f"{base_url}{url}", headers=headers) @@ -116,6 +118,8 @@ def _request(self, url): response = self.config.get(f"{base_url}{url}?page={current}", headers=headers) if response.status_code == 200: json_data = response.json() + if self.config.trace_mode: + logger.debug(f"Response: {json_data}") if isinstance(json_data, dict): return json_data else: diff --git a/modules/tvdb.py b/modules/tvdb.py index 532f8d9e9..d9a053223 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -27,6 +27,8 @@ def __init__(self, tvdb_url, language, is_movie, config): else: raise Failed(f"TVDb Error: {self.tvdb_url} must begin with {urls['movies'] if self.is_movie else urls['series']}") + if self.config.trace_mode: + logger.debug(f"URL: {tvdb_url}") response = self.config.get_html(self.tvdb_url, headers=util.header(self.language)) results = response.xpath(f"//*[text()='TheTVDB.com {self.media_type} ID']/parent::node()/span/text()") if len(results) > 0: @@ -111,6 +113,8 @@ def get_list_description(self, tvdb_url): def _ids_from_url(self, tvdb_url): ids = [] tvdb_url = tvdb_url.strip() + if self.config.trace_mode: + logger.debug(f"URL: {tvdb_url}") if tvdb_url.startswith((urls["list"], urls["alt_list"])): try: response = self.config.get_html(tvdb_url, headers=util.header(self.tvdb_language)) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 38b1429d1..b75c205fc 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -17,6 +17,7 @@ parser = argparse.ArgumentParser() parser.add_argument("-db", "--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False) +parser.add_argument("-tr", "--trace", dest="trace", 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", "--times", dest="times", help="Times to update each day use format HH:MM (Default: 03:00) (comma-separated list)", default="03:00", type=str) parser.add_argument("-re", "--resume", dest="resume", help="Resume collection run from a specific collection", type=str) @@ -51,6 +52,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): test = get_arg("PMM_TEST", args.test, arg_bool=True) debug = get_arg("PMM_DEBUG", args.debug, arg_bool=True) +trace = get_arg("PMM_TRACE", args.trace, arg_bool=True) run = get_arg("PMM_RUN", args.run, arg_bool=True) no_countdown = get_arg("PMM_NO_COUNTDOWN", args.no_countdown, arg_bool=True) no_missing = get_arg("PMM_NO_MISSING", args.no_missing, arg_bool=True) @@ -93,7 +95,7 @@ def fmt_filter(record): return True cmd_handler = logging.StreamHandler() -cmd_handler.setLevel(logging.DEBUG if test or debug else logging.INFO) +cmd_handler.setLevel(logging.DEBUG if test or debug or trace else logging.INFO) logger.addHandler(cmd_handler) @@ -625,7 +627,8 @@ def run_collection(config, library, metadata, requested_collections): "test": test, "collections": collections, "libraries": libraries, - "resume": resume + "resume": resume, + "trace": trace }) else: times_to_run = util.get_list(times) @@ -639,7 +642,7 @@ def run_collection(config, library, metadata, requested_collections): else: raise Failed(f"Argument Error: blank time argument") for time_to_run in valid_times: - schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run}) + schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run, "trace": trace}) while True: schedule.run_pending() if not no_countdown: From d52ae82a01d479919719081b70501d119e16af51 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 3 Nov 2021 11:52:41 -0400 Subject: [PATCH 42/57] #423 Added IMDb Filmography Search --- modules/imdb.py | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/modules/imdb.py b/modules/imdb.py index 1a131125a..e0deb48d6 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -8,17 +8,11 @@ builders = ["imdb_list", "imdb_id"] base_url = "https://www.imdb.com" urls = { - "list": f"{base_url}/list/ls", - "search": f"{base_url}/search/title/", - "keyword": f"{base_url}/search/keyword/" + "lists": f"{base_url}/list/ls", + "searches": f"{base_url}/search/title/", + "keyword_searches": f"{base_url}/search/keyword/", + "filmography_searches": f"{base_url}/filmosearch/" } -xpath = { - "imdb_id": "//div[contains(@class, 'lister-item-image')]//a/img//@data-tconst", - "list": "//div[@class='desc lister-total-num-results']/text()", - "search": "//div[@class='desc']/span/text()", - "keyword": "//div[@class='desc']/text()" -} -item_counts = {"list": 100, "search": 250, "keyword": 50} class IMDb: def __init__(self, config): @@ -31,22 +25,25 @@ def validate_imdb_lists(self, imdb_lists, language): imdb_dict = {"url": imdb_dict} dict_methods = {dm.lower(): dm for dm in imdb_dict} imdb_url = util.parse("url", imdb_dict, methods=dict_methods, parent="imdb_list").strip() - if not imdb_url.startswith((urls["list"], urls["search"], urls["keyword"])): - raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n{urls['list']} (For Lists)\n{urls['search']} (For Searches)\n{urls['keyword']} (For Keyword Searches)") + if not imdb_url.startswith((v for k, v in urls.items())): + fails = "\n".join([f"{v} (For {k.replace('_', ' ').title()})" for k, v in urls.items()]) + raise Failed(f"IMDb Error: {imdb_url} must begin with either:{fails}") self._total(imdb_url, language) list_count = util.parse("limit", imdb_dict, datatype="int", methods=dict_methods, default=0, parent="imdb_list", minimum=0) if "limit" in dict_methods else 0 valid_lists.append({"url": imdb_url, "limit": list_count}) return valid_lists def _total(self, imdb_url, language): - headers = util.header(language) - if imdb_url.startswith(urls["keyword"]): - page_type = "keyword" - elif imdb_url.startswith(urls["list"]): - page_type = "list" + if imdb_url.startswith(urls["lists"]): + xpath_total = "//div[@class='desc lister-total-num-results']/text()" + per_page = 100 + elif imdb_url.startswith(urls["searches"]): + xpath_total = "//div[@class='desc']/span/text()" + per_page = 250 else: - page_type = "search" - results = self.config.get_html(imdb_url, headers=headers).xpath(xpath[page_type]) + xpath_total = "//div[@class='desc']/text()" + per_page = 50 + results = self.config.get_html(imdb_url, headers=util.header(language)).xpath(xpath_total) total = 0 for result in results: if "title" in result: @@ -56,7 +53,7 @@ def _total(self, imdb_url, language): except IndexError: pass if total > 0: - return total, item_counts[page_type] + return total, per_page raise Failed(f"IMDb Error: Failed to parse URL: {imdb_url}") def _ids_from_url(self, imdb_url, language, limit): @@ -72,7 +69,7 @@ def _ids_from_url(self, imdb_url, language, limit): if self.config.trace_mode: logger.debug(f"URL: {imdb_base}") logger.debug(f"Params: {params}") - + search_url = imdb_base.startswith(urls["searches"]) if limit < 1 or total < limit: limit = total remainder = limit % item_count @@ -82,13 +79,14 @@ def _ids_from_url(self, imdb_url, language, limit): for i in range(1, num_of_pages + 1): start_num = (i - 1) * item_count + 1 util.print_return(f"Parsing Page {i}/{num_of_pages} {start_num}-{limit if i == num_of_pages else i * item_count}") - if imdb_base.startswith((urls["list"], urls["keyword"])): - params["page"] = i # noqa - else: + if search_url: params["count"] = remainder if i == num_of_pages else item_count # noqa params["start"] = start_num # noqa - ids_found = self.config.get_html(imdb_base, headers=headers, params=params).xpath(xpath["imdb_id"]) - if imdb_base.startswith((urls["list"], urls["keyword"])) and i == num_of_pages: + else: + params["page"] = i # noqa + response = self.config.get_html(imdb_base, headers=headers, params=params) + ids_found = response.xpath("//div[contains(@class, 'lister-item-image')]//a/img//@data-tconst") + if not search_url and i == num_of_pages: ids_found = ids_found[:remainder] imdb_ids.extend(ids_found) time.sleep(2) From 203f0e6cf565d2dd52804cdd0a6a1a6adfc12df9 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 5 Nov 2021 23:54:12 -0400 Subject: [PATCH 43/57] add operations --- config/config.yml.template | 10 +- modules/config.py | 80 ++++---- modules/library.py | 3 - modules/tvdb.py | 34 +++- modules/webhooks.py | 2 +- plex_meta_manager.py | 363 +++++++++++++++++++------------------ 6 files changed, 269 insertions(+), 223 deletions(-) diff --git a/config/config.yml.template b/config/config.yml.template index 89284dac1..457eca316 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -3,19 +3,15 @@ libraries: # Library mappings must have a colon (:) placed after them Movies: metadata_path: - - file: config/Movies.yml # You have to create this file the other are online + - file: config/Movies.yml # You have to create this file the other is online - git: meisnate12/MovieCharts - - git: meisnate12/Studios - - git: meisnate12/IMDBGenres - - git: meisnate12/People TV Shows: metadata_path: - - file: config/TV Shows.yml # You have to create this file the other are online + - file: config/TV Shows.yml # You have to create this file the other is online - git: meisnate12/ShowCharts - - git: meisnate12/Networks Anime: metadata_path: - - file: config/Anime.yml # You have to create this file the other are online + - file: config/Anime.yml # You have to create this file the other is online - git: meisnate12/AnimeCharts settings: # Can be individually specified per library as well cache: true diff --git a/modules/config.py b/modules/config.py index 47531f361..368e3b0b1 100644 --- a/modules/config.py +++ b/modules/config.py @@ -385,8 +385,6 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No if params["asset_directory"] is None: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") - params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) - assets_for_all = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], 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) params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) @@ -400,42 +398,54 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation_webhooks", parent="settings", var_type="list", default=self.general["collection_creation_webhooks"], do_print=False, save=False) params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition_webhooks", parent="settings", var_type="list", default=self.general["collection_addition_webhooks"], do_print=False, save=False) params["collection_removing_webhooks"] = check_for_attribute(lib, "collection_removing_webhooks", parent="settings", var_type="list", default=self.general["collection_removing_webhooks"], do_print=False, save=False) + params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) + params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) + params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) + params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False) + params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=False) + params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=False) + params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=False) + + if lib and "operations" in lib and lib["operations"]: + if isinstance(lib["operations"], dict): + if "assets_for_all" in lib["operations"]: + params["assets_for_all"] = check_for_attribute(lib["operations"], "assets_for_all", var_type="bool", default=False, save=False) + if "delete_unmanaged_collections" in lib["operations"]: + params["delete_unmanaged_collections"] = check_for_attribute(lib["operations"], "delete_unmanaged_collections", var_type="bool", default=False, save=False) + if "delete_collections_with_less" in lib["operations"]: + params["delete_collections_with_less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False) + if "mass_genre_update" in lib["operations"]: + params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False) + if "mass_audience_rating_update" in lib["operations"]: + params["mass_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False) + if "mass_critic_rating_update" in lib["operations"]: + params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False) + if "mass_trakt_rating_update" in lib["operations"]: + params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False) + if "split_duplicates" in lib["operations"]: + params["split_duplicates"] = check_for_attribute(lib["operations"], "split_duplicates", var_type="bool", default=False, save=False) + if "radarr_add_all" in lib["operations"]: + params["radarr_add_all"] = check_for_attribute(lib["operations"], "radarr_add_all", var_type="bool", default=False, save=False) + if "sonarr_add_all" in lib["operations"]: + params["sonarr_add_all"] = check_for_attribute(lib["operations"], "sonarr_add_all", var_type="bool", default=False, save=False) + else: + logger.error("Config Error: operations must be a dictionary") - params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", var_type="bool", default=assets_for_all, save=False, do_print=lib and "assets_for_all" in lib) - params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", var_type="bool", default=False, save=False, do_print=lib and "delete_unmanaged_collections" in lib) - params["delete_collections_with_less"] = check_for_attribute(lib, "delete_collections_with_less", var_type="int", default_is_none=True, save=False, do_print=lib and "delete_collections_with_less" in lib) + def error_check(attr, service): + params[attr] = None + err = f"Config Error: {attr} cannot be omdb without a successful {service} Connection" + self.errors.append(err) + logger.error(err) - params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) if self.OMDb is None and params["mass_genre_update"] == "omdb": - params["mass_genre_update"] = None - e = "Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection" - self.errors.append(e) - logger.error(e) - - params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + error_check("mass_genre_update", "OMDb") if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": - params["mass_audience_rating_update"] = None - e = "Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection" - self.errors.append(e) - logger.error(e) - - params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + error_check("mass_audience_rating_update", "OMDb") if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": - params["mass_critic_rating_update"] = None - e = "Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection" - self.errors.append(e) - logger.error(e) - - params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib) + error_check("mass_critic_rating_update", "OMDb") if self.Trakt is None and params["mass_trakt_rating_update"]: - params["mass_trakt_rating_update"] = None - e = "Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection" - self.errors.append(e) - logger.error(e) - - params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib) - params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib) - params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib) + error_check("mass_trakt_rating_update", "Trakt") try: if lib and "metadata_path" in lib: @@ -448,9 +458,9 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No def check_dict(attr, name): if attr in path: if path[attr] is None: - e = f"Config Error: metadata_path {attr} is blank" - self.errors.append(e) - logger.error(e) + err = f"Config Error: metadata_path {attr} is blank" + self.errors.append(err) + logger.error(err) else: params["metadata_path"].append((name, path[attr])) check_dict("url", "URL") diff --git a/modules/library.py b/modules/library.py index 5c47ce715..ff3b0ed51 100644 --- a/modules/library.py +++ b/modules/library.py @@ -67,9 +67,6 @@ def __init__(self, config, params): self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? self.optimize = params["plex"]["optimize"] # TODO: Here or just in Plex? - self.mass_update = self.mass_genre_update or self.mass_audience_rating_update or self.mass_critic_rating_update \ - or self.mass_trakt_rating_update or self.split_duplicates or self.radarr_add_all or self.sonarr_add_all - metadata = [] for file_type, metadata_file in self.metadata_path: if file_type == "Folder": diff --git a/modules/tvdb.py b/modules/tvdb.py index d9a053223..24f1f5094 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -13,6 +13,28 @@ "movies": f"{base_url}/movies/", "alt_movies": f"{alt_url}/movies/", "series_id": f"{base_url}/dereferrer/series/", "movie_id": f"{base_url}/dereferrer/movie/" } +language_translation = { + "ab": "abk", "aa": "aar", "af": "afr", "ak": "aka", "sq": "sqi", "am": "amh", "ar": "ara", "an": "arg", "hy": "hye", + "as": "asm", "av": "ava", "ae": "ave", "ay": "aym", "az": "aze", "bm": "bam", "ba": "bak", "eu": "eus", "be": "bel", + "bn": "ben", "bi": "bis", "bs": "bos", "br": "bre", "bg": "bul", "my": "mya", "ca": "cat", "ch": "cha", "ce": "che", + "ny": "nya", "zh": "zho", "cv": "chv", "kw": "cor", "co": "cos", "cr": "cre", "hr": "hrv", "cs": "ces", "da": "dan", + "dv": "div", "nl": "nld", "dz": "dzo", "en": "eng", "eo": "epo", "et": "est", "ee": "ewe", "fo": "fao", "fj": "fij", + "fi": "fin", "fr": "fra", "ff": "ful", "gl": "glg", "ka": "kat", "de": "deu", "el": "ell", "gn": "grn", "gu": "guj", + "ht": "hat", "ha": "hau", "he": "heb", "hz": "her", "hi": "hin", "ho": "hmo", "hu": "hun", "ia": "ina", "id": "ind", + "ie": "ile", "ga": "gle", "ig": "ibo", "ik": "ipk", "io": "ido", "is": "isl", "it": "ita", "iu": "iku", "ja": "jpn", + "jv": "jav", "kl": "kal", "kn": "kan", "kr": "kau", "ks": "kas", "kk": "kaz", "km": "khm", "ki": "kik", "rw": "kin", + "ky": "kir", "kv": "kom", "kg": "kon", "ko": "kor", "ku": "kur", "kj": "kua", "la": "lat", "lb": "ltz", "lg": "lug", + "li": "lim", "ln": "lin", "lo": "lao", "lt": "lit", "lu": "lub", "lv": "lav", "gv": "glv", "mk": "mkd", "mg": "mlg", + "ms": "msa", "ml": "mal", "mt": "mlt", "mi": "mri", "mr": "mar", "mh": "mah", "mn": "mon", "na": "nau", "nv": "nav", + "nd": "nde", "ne": "nep", "ng": "ndo", "nb": "nob", "nn": "nno", "no": "nor", "ii": "iii", "nr": "nbl", "oc": "oci", + "oj": "oji", "cu": "chu", "om": "orm", "or": "ori", "os": "oss", "pa": "pan", "pi": "pli", "fa": "fas", "pl": "pol", + "ps": "pus", "pt": "por", "qu": "que", "rm": "roh", "rn": "run", "ro": "ron", "ru": "rus", "sa": "san", "sc": "srd", + "sd": "snd", "se": "sme", "sm": "smo", "sg": "sag", "sr": "srp", "gd": "gla", "sn": "sna", "si": "sin", "sk": "slk", + "sl": "slv", "so": "som", "st": "sot", "es": "spa", "su": "sun", "sw": "swa", "ss": "ssw", "sv": "swe", "ta": "tam", + "te": "tel", "tg": "tgk", "th": "tha", "ti": "tir", "bo": "bod", "tk": "tuk", "tl": "tgl", "tn": "tsn", "to": "ton", + "tr": "tur", "ts": "tso", "tt": "tat", "tw": "twi", "ty": "tah", "ug": "uig", "uk": "ukr", "ur": "urd", "uz": "uzb", + "ve": "ven", "vi": "vie", "vo": "vol", "wa": "wln", "cy": "cym", "wo": "wol", "fy": "fry", "xh": "xho", "yi": "yid", + "yo": "yor", "za": "zha", "zu": "zul"} class TVDbObj: def __init__(self, tvdb_url, language, is_movie, config): @@ -46,15 +68,21 @@ def parse_page(xpath): parse_results = [r.strip() for r in parse_results if len(r) > 0] return parse_results[0] if len(parse_results) > 0 else None - self.title = parse_page(f"//div[@class='change_translation_text' and @data-language='{self.language}']/@data-title") + def parse_title_summary(lang=None): + place = "//div[@class='change_translation_text' and " + place += f"@data-language='{lang}'" if lang else "not(@style='display:none')" + return parse_page(f"{place}/@data-title"), parse_page(f"{place}]/p/text()[normalize-space()]") + + self.title, self.summary = parse_title_summary(lang=self.language) + if not self.title and self.language in language_translation: + self.title, self.summary = parse_title_summary(lang=language_translation[self.language]) if not self.title: - self.title = parse_page("//div[@class='change_translation_text' and not(@style='display:none')]/@data-title") + self.title, self.summary = parse_title_summary() if not self.title: raise Failed(f"TVDb Error: Name not found from TVDb URL: {self.tvdb_url}") self.poster_path = parse_page("//div[@class='row hidden-xs hidden-sm']/div/img/@src") self.background_path = parse_page("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]") - self.summary = parse_page("//div[@class='change_translation_text' and not(@style='display:none')]/p/text()[normalize-space()]") if self.is_movie: self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]") self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]") diff --git a/modules/webhooks.py b/modules/webhooks.py index 3a79f334f..44e20c317 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -38,7 +38,7 @@ def start_time_hooks(self, start_time): def end_time_hooks(self, start_time, run_time, stats): if self.run_end_webhooks: self._request(self.run_end_webhooks, { - "start_time": start_time, + "start_time": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), "run_time": run_time, "collections_created": stats["created"], "collections_modified": stats["modified"], diff --git a/plex_meta_manager.py b/plex_meta_manager.py index b75c205fc..9d5804524 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -180,8 +180,6 @@ def update_libraries(config): util.separator(f"Mapping {library.name} Library", space=False, border=False) logger.info("") items = library.map_guids() - if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: - mass_metadata(config, library, items=items) for metadata in library.metadata_files: logger.info("") util.separator(f"Running Metadata File\n{metadata.path}") @@ -212,42 +210,8 @@ def update_libraries(config): logger.info("") builder.sort_collection() - if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): - if library.delete_collections_with_less is not None: - logger.info("") - text = f" with less then {library.delete_collections_with_less} item{'s' if library.delete_collections_with_less > 1 else ''}" - util.separator(f"Deleting All Collections{text if library.delete_collections_with_less > 0 else ''}", space=False, border=False) - logger.info("") - unmanaged_collections = [] - for col in library.get_all_collections(): - if library.delete_collections_with_less is not None and (library.delete_collections_with_less == 0 or col.childCount < library.delete_collections_with_less): - library.query(col.delete) - logger.info(f"{col.title} Deleted") - if col.title not in library.collections: - unmanaged_collections.append(col) - - if library.show_unmanaged and not library_only: - logger.info("") - util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) - logger.info("") - for col in unmanaged_collections: - if library.delete_unmanaged_collections: - library.query(col.delete) - logger.info(f"{col.title} Deleted") - else: - logger.info(col.title) - logger.info("") - logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") - - if library.assets_for_all and not collection_only: - logger.info("") - util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False) - logger.info("") - for col in unmanaged_collections: - poster, background = library.find_collection_assets(col, create=library.create_asset_folders) - library.upload_images(col, poster=poster, background=background) - for item in library.get_all(): - library.update_item_from_assets(item, create=library.create_asset_folders) + if not config.test_mode and not collection_only: + library_operations(config, library, items=items) logger.removeHandler(library_handler) except Exception as e: @@ -310,164 +274,215 @@ def update_libraries(config): if library.optimize: library.query(library.PlexServer.library.optimize) -def mass_metadata(config, library, items=None): +def library_operations(config, library, items=None): logger.info("") - util.separator(f"Mass Editing {library.type} Library: {library.name}") + util.separator(f"{library.name} Library Operations") logger.info("") - if items is None: - items = library.get_all() + if library.split_duplicates: items = library.search(**{"duplicate": True}) for item in items: item.split() logger.info(util.adjust_space(f"{item.title[:25]:<25} | Splitting")) - radarr_adds = [] - sonarr_adds = [] - trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else [] - for i, item in enumerate(items, 1): - try: - library.reload(item) - except Failed as e: - logger.error(e) - continue - util.print_return(f"Processing: {i}/{len(items)} {item.title}") - tmdb_id = None - tvdb_id = None - imdb_id = None - if config.Cache: - t_id, i_id, guid_media_type, _ = config.Cache.query_guid_map(item.guid) - if t_id: - if "movie" in guid_media_type: - tmdb_id = t_id[0] - else: - tvdb_id = t_id[0] - if i_id: - imdb_id = i_id[0] - if not tmdb_id and not tvdb_id: - tmdb_id = library.get_tmdb_from_map(item) - if not tmdb_id and not tvdb_id and library.is_show: - tvdb_id = library.get_tvdb_from_map(item) - - if library.mass_trakt_rating_update: + if library.assets_for_all or library.mass_genre_update or library.mass_audience_rating_update or \ + library.mass_critic_rating_update or library.mass_trakt_rating_update or library.radarr_add_all or library.sonarr_add_all: + if items is None: + items = library.get_all() + radarr_adds = [] + sonarr_adds = [] + trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else [] + + for i, item in enumerate(items, 1): try: - if library.is_movie and tmdb_id in trakt_ratings: - new_rating = trakt_ratings[tmdb_id] - elif library.is_show and tvdb_id in trakt_ratings: - new_rating = trakt_ratings[tvdb_id] - else: - raise Failed - if str(item.userRating) != str(new_rating): - library.edit_query(item, {"userRating.value": new_rating, "userRating.locked": 1}) - logger.info(util.adjust_space(f"{item.title[:25]:<25} | User Rating | {new_rating}")) - except Failed: - pass - - if library.Radarr and library.radarr_add_all and tmdb_id: - radarr_adds.append(tmdb_id) - if library.Sonarr and library.sonarr_add_all and tvdb_id: - sonarr_adds.append(tvdb_id) - - tmdb_item = None - if library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" or library.mass_critic_rating_update == "tmdb": - if tvdb_id and not tmdb_id: - tmdb_id = config.Convert.tvdb_to_tmdb(tvdb_id) - if tmdb_id: + library.reload(item) + except Failed as e: + logger.error(e) + continue + util.print_return(f"Processing: {i}/{len(items)} {item.title}") + tmdb_id = None + tvdb_id = None + imdb_id = None + if config.Cache: + t_id, i_id, guid_media_type, _ = config.Cache.query_guid_map(item.guid) + if t_id: + if "movie" in guid_media_type: + tmdb_id = t_id[0] + else: + tvdb_id = t_id[0] + if i_id: + imdb_id = i_id[0] + if not tmdb_id and not tvdb_id: + tmdb_id = library.get_tmdb_from_map(item) + if not tmdb_id and not tvdb_id and library.is_show: + tvdb_id = library.get_tvdb_from_map(item) + + if library.mass_trakt_rating_update: try: - tmdb_item = config.TMDb.get_movie(tmdb_id) if library.is_movie else config.TMDb.get_show(tmdb_id) - except Failed as e: - logger.error(util.adjust_space(str(e))) - else: - logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TMDb ID for Guid: {item.guid}")) - - omdb_item = None - if library.mass_genre_update in ["omdb", "imdb"] or library.mass_audience_rating_update in ["omdb", "imdb"] or library.mass_critic_rating_update in ["omdb", "imdb"]: - if config.OMDb.limit is False: - if tmdb_id and not imdb_id: - imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) - elif tvdb_id and not imdb_id: - imdb_id = config.Convert.tvdb_to_imdb(tvdb_id) - if imdb_id: + if library.is_movie and tmdb_id in trakt_ratings: + new_rating = trakt_ratings[tmdb_id] + elif library.is_show and tvdb_id in trakt_ratings: + new_rating = trakt_ratings[tvdb_id] + else: + raise Failed + if str(item.userRating) != str(new_rating): + library.edit_query(item, {"userRating.value": new_rating, "userRating.locked": 1}) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | User Rating | {new_rating}")) + except Failed: + pass + + if library.Radarr and library.radarr_add_all and tmdb_id: + radarr_adds.append(tmdb_id) + if library.Sonarr and library.sonarr_add_all and tvdb_id: + sonarr_adds.append(tvdb_id) + + tmdb_item = None + if library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" or library.mass_critic_rating_update == "tmdb": + if tvdb_id and not tmdb_id: + tmdb_id = config.Convert.tvdb_to_tmdb(tvdb_id) + if tmdb_id: try: - omdb_item = config.OMDb.get_omdb(imdb_id) + tmdb_item = config.TMDb.get_movie(tmdb_id) if library.is_movie else config.TMDb.get_show(tmdb_id) except Failed as e: logger.error(util.adjust_space(str(e))) - except Exception: - logger.error(f"IMDb ID: {imdb_id}") - raise else: - logger.info(util.adjust_space(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}")) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TMDb ID for Guid: {item.guid}")) + + omdb_item = None + if library.mass_genre_update in ["omdb", "imdb"] or library.mass_audience_rating_update in ["omdb", "imdb"] or library.mass_critic_rating_update in ["omdb", "imdb"]: + if config.OMDb.limit is False: + if tmdb_id and not imdb_id: + imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) + elif tvdb_id and not imdb_id: + imdb_id = config.Convert.tvdb_to_imdb(tvdb_id) + if imdb_id: + try: + omdb_item = config.OMDb.get_omdb(imdb_id) + except Failed as e: + logger.error(util.adjust_space(str(e))) + except Exception: + logger.error(f"IMDb ID: {imdb_id}") + raise + else: + logger.info(util.adjust_space(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}")) + + tvdb_item = None + if library.mass_genre_update == "tvdb": + if tvdb_id: + try: + tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie) + except Failed as e: + logger.error(util.adjust_space(str(e))) + else: + logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}")) - tvdb_item = None - if library.mass_genre_update == "tvdb": - if tvdb_id: + if not tmdb_item and not omdb_item and not tvdb_item: + continue + + if library.mass_genre_update: try: - tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie) - except Failed as e: - logger.error(util.adjust_space(str(e))) - else: - logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}")) + if tmdb_item and library.mass_genre_update == "tmdb": + new_genres = [genre.name for genre in tmdb_item.genres] + elif omdb_item and library.mass_genre_update in ["omdb", "imdb"]: + new_genres = omdb_item.genres + elif tvdb_item and library.mass_genre_update == "tvdb": + new_genres = tvdb_item.genres + else: + raise Failed + library.edit_tags("genre", item, sync_tags=new_genres) + except Failed: + pass + if library.mass_audience_rating_update: + try: + if tmdb_item and library.mass_audience_rating_update == "tmdb": + new_rating = tmdb_item.vote_average + elif omdb_item and library.mass_audience_rating_update in ["omdb", "imdb"]: + new_rating = omdb_item.imdb_rating + else: + raise Failed + if new_rating is None: + logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) + else: + if library.mass_audience_rating_update and str(item.audienceRating) != str(new_rating): + library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1}) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | Audience Rating | {new_rating}")) + except Failed: + pass + if library.mass_critic_rating_update: + try: + if tmdb_item and library.mass_critic_rating_update == "tmdb": + new_rating = tmdb_item.vote_average + elif omdb_item and library.mass_critic_rating_update in ["omdb", "imdb"]: + new_rating = omdb_item.imdb_rating + else: + raise Failed + if new_rating is None: + logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) + else: + if library.mass_critic_rating_update and str(item.rating) != str(new_rating): + library.edit_query(item, {"rating.value": new_rating, "rating.locked": 1}) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")) + except Failed: + pass - if not tmdb_item and not omdb_item and not tvdb_item: - continue + if library.assets_for_all: + library.update_item_from_assets(item, create=library.create_asset_folders) - if library.mass_genre_update: + if library.Radarr and library.radarr_add_all: try: - if tmdb_item and library.mass_genre_update == "tmdb": - new_genres = [genre.name for genre in tmdb_item.genres] - elif omdb_item and library.mass_genre_update in ["omdb", "imdb"]: - new_genres = omdb_item.genres - elif tvdb_item and library.mass_genre_update == "tvdb": - new_genres = tvdb_item.genres - else: - raise Failed - library.edit_tags("genre", item, sync_tags=new_genres) - except Failed: - pass - if library.mass_audience_rating_update: - try: - if tmdb_item and library.mass_audience_rating_update == "tmdb": - new_rating = tmdb_item.vote_average - elif omdb_item and library.mass_audience_rating_update in ["omdb", "imdb"]: - new_rating = omdb_item.imdb_rating - else: - raise Failed - if new_rating is None: - logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) - else: - if library.mass_audience_rating_update and str(item.audienceRating) != str(new_rating): - library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1}) - logger.info(util.adjust_space(f"{item.title[:25]:<25} | Audience Rating | {new_rating}")) - except Failed: - pass - if library.mass_critic_rating_update: + library.Radarr.add_tmdb(radarr_adds) + except Failed as e: + logger.error(e) + + if library.Sonarr and library.sonarr_add_all: try: - if tmdb_item and library.mass_critic_rating_update == "tmdb": - new_rating = tmdb_item.vote_average - elif omdb_item and library.mass_critic_rating_update in ["omdb", "imdb"]: - new_rating = omdb_item.imdb_rating - else: - raise Failed - if new_rating is None: - logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) - else: - if library.mass_critic_rating_update and str(item.rating) != str(new_rating): - library.edit_query(item, {"rating.value": new_rating, "rating.locked": 1}) - logger.info(util.adjust_space(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")) - except Failed: - pass + library.Sonarr.add_tvdb(sonarr_adds) + except Failed as e: + logger.error(e) - if library.Radarr and library.radarr_add_all: - try: - library.Radarr.add_tmdb(radarr_adds) - except Failed as e: - logger.error(e) + if library.delete_collections_with_less is not None or library.delete_unmanaged_collections: + logger.info("") + suffix = "" + unmanaged = "" + if library.delete_collections_with_less is not None and library.delete_collections_with_less > 0: + suffix = f" with less then {library.delete_collections_with_less} item{'s' if library.delete_collections_with_less > 1 else ''}" + if library.delete_unmanaged_collections: + if library.delete_collections_with_less is None: + unmanaged = "Unmanaged Collections " + elif library.delete_collections_with_less > 0: + unmanaged = "Unmanaged Collections and " + util.separator(f"Deleting All {unmanaged}Collections{suffix}", space=False, border=False) + logger.info("") + unmanaged_collections = [] + for col in library.get_all_collections(): + if (library.delete_collections_with_less is not None + and (library.delete_collections_with_less == 0 or col.childCount < library.delete_collections_with_less)) \ + or (col.title not in library.collections and library.delete_unmanaged_collections): + library.query(col.delete) + logger.info(f"{col.title} Deleted") + elif col.title not in library.collections: + unmanaged_collections.append(col) + + if library.show_unmanaged and len(unmanaged_collections) > 0: + logger.info("") + util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + logger.info(col.title) + logger.info("") + logger.info(f"{len(unmanaged_collections)} Unmanaged Collection{'s' if len(unmanaged_collections) > 1 else ''}") + elif library.show_unmanaged: + logger.info("") + util.separator(f"No Unmanaged Collections in {library.name} Library", space=False, border=False) + logger.info("") - if library.Sonarr and library.sonarr_add_all: - try: - library.Sonarr.add_tvdb(sonarr_adds) - except Failed as e: - logger.error(e) + if library.assets_for_all and len(unmanaged_collections) > 0: + logger.info("") + util.separator(f"Unmanaged Collection Assets Check for {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + poster, background = library.find_collection_assets(col, create=library.create_asset_folders) + library.upload_images(col, poster=poster, background=background) def run_collection(config, library, metadata, requested_collections): global stats From 43206dbd5e1628b65272f2c63ecaf12feacff839 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 6 Nov 2021 17:28:39 -0400 Subject: [PATCH 44/57] moved webhooks from settings to its own section --- config/config.yml.template | 14 +++++++------- modules/config.py | 24 +++++++++++++----------- modules/webhooks.py | 6 +++--- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/config/config.yml.template b/config/config.yml.template index 457eca316..ca361ea49 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -18,7 +18,6 @@ settings: # Can be individually specified cache_expiration: 60 asset_directory: config/assets asset_folders: true - assets_for_all: false sync_mode: append show_unmanaged: true show_filtered: false @@ -30,13 +29,14 @@ settings: # Can be individually specified missing_only_released: false collection_minimum: 1 delete_below_minimum: true - error_webhooks: - run_start_webhooks: - run_end_webhooks: - collection_creation_webhooks: - collection_addition_webhooks: - collection_removing_webhooks: tvdb_language: eng +webhooks: # Can be individually specified per library as well + error: + run_start: + run_end: + collection_creation: + collection_addition: + collection_removing: plex: # Can be individually specified per library as well url: http://192.168.1.12:32400 token: #################### diff --git a/modules/config.py b/modules/config.py index 368e3b0b1..242e12168 100644 --- a/modules/config.py +++ b/modules/config.py @@ -192,14 +192,16 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False), "collection_minimum": check_for_attribute(self.data, "collection_minimum", parent="settings", var_type="int", default=1), "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), - "error_webhooks": check_for_attribute(self.data, "error_webhooks", parent="settings", var_type="list", default_is_none=True), - "run_start_webhooks": check_for_attribute(self.data, "run_start_webhooks", parent="settings", var_type="list", default_is_none=True), - "run_end_webhooks": check_for_attribute(self.data, "run_end_webhooks", parent="settings", var_type="list", default_is_none=True), - "collection_creation_webhooks": check_for_attribute(self.data, "collection_creation_webhooks", parent="settings", var_type="list", default_is_none=True), - "collection_addition_webhooks": check_for_attribute(self.data, "collection_addition_webhooks", parent="settings", var_type="list", default_is_none=True), - "collection_removing_webhooks": check_for_attribute(self.data, "collection_removing_webhooks", parent="settings", var_type="list", default_is_none=True), "tvdb_language": check_for_attribute(self.data, "tvdb_language", parent="settings", default="default") } + self.webhooks = { + "error": check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True), + "run_start": check_for_attribute(self.data, "run_start", parent="webhooks", var_type="list", default_is_none=True), + "run_end": check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True), + "collection_creation": check_for_attribute(self.data, "collection_creation", parent="webhooks", var_type="list", default_is_none=True), + "collection_addition": check_for_attribute(self.data, "collection_addition", parent="webhooks", var_type="list", default_is_none=True), + "collection_removing": check_for_attribute(self.data, "collection_removing", parent="webhooks", var_type="list", default_is_none=True), + } if self.general["cache"]: util.separator() self.Cache = Cache(self.config_path, self.general["cache_expiration"]) @@ -223,7 +225,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No else: logger.warning("notifiarr attribute not found") - self.Webhooks = Webhooks(self, self.general, notifiarr=self.NotifiarrFactory) + self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory) self.Webhooks.start_time_hooks(self.run_start_time) self.errors = [] @@ -394,10 +396,10 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) - params["error_webhooks"] = check_for_attribute(lib, "error_webhooks", parent="settings", var_type="list", default=self.general["error_webhooks"], do_print=False, save=False) - params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation_webhooks", parent="settings", var_type="list", default=self.general["collection_creation_webhooks"], do_print=False, save=False) - params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition_webhooks", parent="settings", var_type="list", default=self.general["collection_addition_webhooks"], do_print=False, save=False) - params["collection_removing_webhooks"] = check_for_attribute(lib, "collection_removing_webhooks", parent="settings", var_type="list", default=self.general["collection_removing_webhooks"], do_print=False, save=False) + params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False) + params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation", parent="webhooks", var_type="list", default=self.webhooks["collection_creation"], do_print=False, save=False) + params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition", parent="webhooks", var_type="list", default=self.webhooks["collection_addition"], do_print=False, save=False) + params["collection_removing_webhooks"] = check_for_attribute(lib, "collection_removing", parent="webhooks", var_type="list", default=self.webhooks["collection_removing"], do_print=False, save=False) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) diff --git a/modules/webhooks.py b/modules/webhooks.py index 44e20c317..98ed735ff 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -7,9 +7,9 @@ class Webhooks: def __init__(self, config, system_webhooks, library=None, notifiarr=None): self.config = config - self.error_webhooks = system_webhooks["error_webhooks"] if "error_webhooks" in system_webhooks else [] - self.run_start_webhooks = system_webhooks["run_start_webhooks"] if "run_start_webhooks" in system_webhooks else [] - self.run_end_webhooks = system_webhooks["run_end_webhooks"] if "run_end_webhooks" in system_webhooks else [] + self.error_webhooks = system_webhooks["error"] if "error" in system_webhooks else [] + self.run_start_webhooks = system_webhooks["run_start"] if "run_start" in system_webhooks else [] + self.run_end_webhooks = system_webhooks["run_end"] if "run_end" in system_webhooks else [] self.library = library self.notifiarr = notifiarr From 792e37dc7e4fcb171a788a2246bdfd56e9f19f5d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 12 Nov 2021 02:23:03 -0500 Subject: [PATCH 45/57] #424 adding to Radarr and Sonarr now respect the Exclusion Lists --- modules/radarr.py | 1 + modules/sonarr.py | 3 ++- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/radarr.py b/modules/radarr.py index 426eba0c6..c69139e59 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -17,6 +17,7 @@ def __init__(self, config, params): self.token = params["token"] try: self.api = RadarrAPI(self.url, self.token, session=self.config.session) + self.api.respect_list_exclusions_when_adding() except ArrException as e: raise Failed(e) self.add = params["add"] diff --git a/modules/sonarr.py b/modules/sonarr.py index 201544402..533ac127b 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -35,6 +35,7 @@ def __init__(self, config, params): self.token = params["token"] try: self.api = SonarrAPI(self.url, self.token, session=self.config.session) + self.api.respect_list_exclusions_when_adding() except ArrException as e: raise Failed(e) self.add = params["add"] @@ -59,7 +60,7 @@ def add_tvdb(self, tvdb_ids, **options): monitor = monitor_translation[options["monitor"] if "monitor" in options else self.monitor] quality_profile = options["quality"] if "quality" in options else self.quality_profile language_profile = options["language"] if "language" in options else self.language_profile - language_profile = language_profile if self.api.v3 else 1 + language_profile = language_profile if self.api._raw.v3 else 1 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 diff --git a/requirements.txt b/requirements.txt index 843f43854..605379466 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PlexAPI==4.7.2 tmdbv3api==1.7.6 -arrapi==1.1.7 +arrapi==1.2.3 lxml==4.6.4 requests==2.26.0 ruamel.yaml==0.17.17 From 22af973af336cc9696620aba2e6d06d4dc852275 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 12 Nov 2021 02:38:20 -0500 Subject: [PATCH 46/57] # 428 added season backgrounds to the assets directory --- modules/plex.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/plex.py b/modules/plex.py index 1e8153a0e..303e0b718 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -639,14 +639,23 @@ def update_item_from_assets(self, item, overlay=None, create=False): self.upload_images(item, poster=poster, background=background, overlay=overlay) if self.is_show: for season in self.query(item.seasons): + season_name = f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}" if item_dir: - season_filter = os.path.join(item_dir, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*") + season_poster_filter = os.path.join(item_dir, f"{season_name}.*") + season_background_filter = os.path.join(item_dir, f"{season_name}_background.*") else: - season_filter = os.path.join(ad, f"{name}_Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*") - matches = util.glob_filter(season_filter) + season_poster_filter = os.path.join(ad, f"{name}_{season_name}.*") + season_background_filter = os.path.join(ad, f"{name}_{season_name}_background.*") + matches = util.glob_filter(season_poster_filter) + season_poster = None + season_background = None if len(matches) > 0: season_poster = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Season {season.seasonNumber}'s ", is_url=False) - self.upload_images(season, poster=season_poster) + matches = util.glob_filter(season_background_filter) + if len(matches) > 0: + season_background = ImageData("asset_directory", os.path.abspath(matches[0]), prefix=f"{item.title} Season {season.seasonNumber}'s ", is_poster=False, is_url=False) + if season_poster or season_background: + self.upload_images(season, poster=season_poster, background=season_background) for episode in self.query(season.episodes): if item_dir: episode_filter = os.path.join(item_dir, f"{episode.seasonEpisode.upper()}.*") From 9277134538fb0e2661fc41bc31a0a6b503ed45c8 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 12 Nov 2021 11:49:04 -0500 Subject: [PATCH 47/57] final update cleanup --- README.md | 7 ++++--- config/config.yml.template | 18 +++++++++--------- modules/builder.py | 10 +++++----- modules/config.py | 11 ++++++----- modules/library.py | 2 +- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 986ea12d9..ad5753b57 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Plex Meta Manager [![GitHub release (latest by date)](https://img.shields.io/github/v/release/meisnate12/Plex-Meta-Manager?style=plastic)](https://github.com/meisnate12/Plex-Meta-Manager/releases) -[![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/meisnate12/plex-meta-manager/latest/develop?label=Number%20of%20Commits%20in%20Develop&style=plastic)](https://github.com/meisnate12/Plex-Meta-Manager/tree/develop) +[![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/meisnate12/plex-meta-manager/latest/develop?label=Commits%20in%20Develop&style=plastic)](https://github.com/meisnate12/Plex-Meta-Manager/tree/develop) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/meisnate12/plex-meta-manager?label=docker&sort=semver&style=plastic)](https://hub.docker.com/r/meisnate12/plex-meta-manager) [![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/meisnate12/plex-meta-manager?style=plastic)](https://hub.docker.com/r/meisnate12/plex-meta-manager) +[![Docker Pulls](https://img.shields.io/docker/pulls/meisnate12/plex-meta-manager?style=plastic)](https://hub.docker.com/r/meisnate12/plex-meta-manager) [![Discord](https://img.shields.io/discord/822460010649878528?label=Discord&style=plastic)](https://discord.gg/TsdpsFYqqm) [![Sponsor or Donate](https://img.shields.io/badge/-Sponsor_or_Donate-blueviolet?style=plastic)](https://github.com/sponsors/meisnate12) @@ -11,7 +12,7 @@ The original concept for Plex Meta Manager is [Plex Auto Collections](https://gi The script can update many metadata fields for movies, shows, collections, seasons, and episodes and can act as a backup if your plex DB goes down. It can even update metadata the plex UI can't like Season Names. If the time is put into the metadata configuration file you can have a way to recreate your library and all its metadata changes with the click of a button. -The script is designed to work with most Metadata agents including the new Plex Movie Agent, New Plex TV Agent, [Hama Anime Agent](https://github.com/ZeroQI/Hama.bundle), and [MyAnimeList Anime Agent](https://github.com/Fribb/MyAnimeList.bundle). +The script works with most Metadata agents including the new Plex Movie Agent, New Plex TV Agent, [Hama Anime Agent](https://github.com/ZeroQI/Hama.bundle), and [MyAnimeList Anime Agent](https://github.com/Fribb/MyAnimeList.bundle). ## Getting Started @@ -23,7 +24,7 @@ The script is designed to work with most Metadata agents including the new Plex ## Support -* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/TsdpsFYqqm). +* Before posting on GitHub about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/TsdpsFYqqm). * 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). diff --git a/config/config.yml.template b/config/config.yml.template index ca361ea49..543a0ea8f 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -36,7 +36,7 @@ webhooks: # Can be individually specified run_end: collection_creation: collection_addition: - collection_removing: + collection_removal: plex: # Can be individually specified per library as well url: http://192.168.1.12:32400 token: #################### @@ -50,6 +50,13 @@ tmdb: tautulli: # Can be individually specified per library as well url: http://192.168.1.12:8181 apikey: ################################ +omdb: + apikey: ######## +notifiarr: + apikey: #################################### +anidb: # Not required for AniDB builders unless you want mature content + username: ###### + password: ###### radarr: # Can be individually specified per library as well url: http://192.168.1.12:7878 token: ################################ @@ -73,10 +80,6 @@ sonarr: # Can be individually specified tag: search: false cutoff_search: false -omdb: - apikey: ######## -notifiarr: - apikey: #################################### trakt: client_id: ################################################################ client_secret: ################################################################ @@ -96,7 +99,4 @@ mal: access_token: token_type: expires_in: - refresh_token: -anidb: # Optional - username: ###### - password: ###### \ No newline at end of file + refresh_token: \ No newline at end of file diff --git a/modules/builder.py b/modules/builder.py index 4c58cfc0c..9995bd976 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -86,7 +86,7 @@ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name" ] -notification_details = ["collection_creation_webhooks", "collection_addition_webhooks", "collection_removing_webhooks"] +notification_details = ["collection_creation_webhooks", "collection_addition_webhooks", "collection_removal_webhooks"] details = ["collection_mode", "collection_order", "collection_level", "collection_minimum", "label"] + boolean_details + string_details + notification_details collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \ poster_details + background_details + summary_details + string_details @@ -179,7 +179,7 @@ def __init__(self, config, library, metadata, name, no_missing, data): "delete_below_minimum": self.library.delete_below_minimum, "collection_creation_webhooks": self.library.collection_creation_webhooks, "collection_addition_webhooks": self.library.collection_addition_webhooks, - "collection_removing_webhooks": self.library.collection_removing_webhooks, + "collection_removal_webhooks": self.library.collection_removal_webhooks, } self.item_details = {} self.radarr_details = {} @@ -1525,7 +1525,7 @@ def sync_collection(self): self.library.reload(item) logger.info(f"{self.name} Collection | - | {self.item_title(item)}") self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) - if self.details["collection_removing_webhooks"]: + if self.details["collection_removal_webhooks"]: if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: remove_id = self.library.movie_rating_key_map[item.ratingKey] elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: @@ -2045,11 +2045,11 @@ def send_notifications(self): if self.obj and ( (self.details["collection_creation_webhooks"] and self.created) or (self.details["collection_addition_webhooks"] and len(self.notification_additions) > 0) or - (self.details["collection_removing_webhooks"] and len(self.notification_removals) > 0) + (self.details["collection_removal_webhooks"] and len(self.notification_removals) > 0) ): self.obj.reload() self.library.Webhooks.collection_hooks( - self.details["collection_creation_webhooks"] + self.details["collection_addition_webhooks"] + self.details["collection_removing_webhooks"], + self.details["collection_creation_webhooks"] + self.details["collection_addition_webhooks"] + self.details["collection_removal_webhooks"], self.obj, created=self.created, additions=self.notification_additions, diff --git a/modules/config.py b/modules/config.py index 242e12168..2407501e2 100644 --- a/modules/config.py +++ b/modules/config.py @@ -85,16 +85,17 @@ def replace_attr(all_data, attr, par): replace_attr(new_config["libraries"][library], "save_missing", "plex") if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries") if "settings" in new_config: new_config["settings"] = new_config.pop("settings") + if "webhooks" in new_config: new_config["webhooks"] = new_config.pop("settings") if "plex" in new_config: new_config["plex"] = new_config.pop("plex") if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb") if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli") - if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") - if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr") + if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") + if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") + if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "mal" in new_config: new_config["mal"] = new_config.pop("mal") - if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=None, block_seq_indent=2) self.data = new_config except yaml.scanner.ScannerError as e: @@ -200,7 +201,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "run_end": check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True), "collection_creation": check_for_attribute(self.data, "collection_creation", parent="webhooks", var_type="list", default_is_none=True), "collection_addition": check_for_attribute(self.data, "collection_addition", parent="webhooks", var_type="list", default_is_none=True), - "collection_removing": check_for_attribute(self.data, "collection_removing", parent="webhooks", var_type="list", default_is_none=True), + "collection_removal": check_for_attribute(self.data, "collection_removal", parent="webhooks", var_type="list", default_is_none=True), } if self.general["cache"]: util.separator() @@ -399,7 +400,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False) params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation", parent="webhooks", var_type="list", default=self.webhooks["collection_creation"], do_print=False, save=False) params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition", parent="webhooks", var_type="list", default=self.webhooks["collection_addition"], do_print=False, save=False) - params["collection_removing_webhooks"] = check_for_attribute(lib, "collection_removing", parent="webhooks", var_type="list", default=self.webhooks["collection_removing"], do_print=False, save=False) + params["collection_removal_webhooks"] = check_for_attribute(lib, "collection_removal", parent="webhooks", var_type="list", default=self.webhooks["collection_removal"], do_print=False, save=False) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) diff --git a/modules/library.py b/modules/library.py index ff3b0ed51..8276d42ab 100644 --- a/modules/library.py +++ b/modules/library.py @@ -61,7 +61,7 @@ def __init__(self, config, params): self.error_webhooks = params["error_webhooks"] self.collection_creation_webhooks = params["collection_creation_webhooks"] self.collection_addition_webhooks = params["collection_addition_webhooks"] - self.collection_removing_webhooks = params["collection_removing_webhooks"] + self.collection_removal_webhooks = params["collection_removal_webhooks"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? From b9c53303e9bfd17c54352c3f08861c6cae5ac793 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 12 Nov 2021 13:48:38 -0500 Subject: [PATCH 48/57] config fixes --- VERSION | 2 +- modules/config.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 6adc0ea8f..1ab5ae484 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2-develop1026 \ No newline at end of file +1.12.2-develop1112 \ No newline at end of file diff --git a/modules/config.py b/modules/config.py index 2407501e2..09085a7a0 100644 --- a/modules/config.py +++ b/modules/config.py @@ -85,7 +85,7 @@ def replace_attr(all_data, attr, par): replace_attr(new_config["libraries"][library], "save_missing", "plex") if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries") if "settings" in new_config: new_config["settings"] = new_config.pop("settings") - if "webhooks" in new_config: new_config["webhooks"] = new_config.pop("settings") + if "webhooks" in new_config: new_config["webhooks"] = new_config.pop("webhooks") if "plex" in new_config: new_config["plex"] = new_config.pop("plex") if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb") if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli") @@ -388,6 +388,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No if params["asset_directory"] is None: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") + params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], 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) params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) @@ -397,10 +398,12 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) - params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False) - params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation", parent="webhooks", var_type="list", default=self.webhooks["collection_creation"], do_print=False, save=False) - params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition", parent="webhooks", var_type="list", default=self.webhooks["collection_addition"], do_print=False, save=False) - params["collection_removal_webhooks"] = check_for_attribute(lib, "collection_removal", parent="webhooks", var_type="list", default=self.webhooks["collection_removal"], do_print=False, save=False) + params["delete_unmanaged_collections"] = check_for_attribute(lib, "delete_unmanaged_collections", parent="settings", var_type="bool", default=False, do_print=False, save=False) + params["delete_collections_with_less"] = check_for_attribute(lib, "delete_collections_with_less", parent="settings", var_type="int", default_is_none=True, do_print=False, save=False) + params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True) + params["collection_creation_webhooks"] = check_for_attribute(lib, "collection_creation", parent="webhooks", var_type="list", default=self.webhooks["collection_creation"], do_print=False, save=False, default_is_none=True) + params["collection_addition_webhooks"] = check_for_attribute(lib, "collection_addition", parent="webhooks", var_type="list", default=self.webhooks["collection_addition"], do_print=False, save=False, default_is_none=True) + params["collection_removal_webhooks"] = check_for_attribute(lib, "collection_removal", parent="webhooks", var_type="list", default=self.webhooks["collection_removal"], do_print=False, save=False, default_is_none=True) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) From a6ec580431cee03653fea5082154157e3a439557 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 13 Nov 2021 18:51:12 -0500 Subject: [PATCH 49/57] #432 added trakt_boxoffice builder --- modules/builder.py | 13 +++++++++---- modules/trakt.py | 12 ++++++------ 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 9995bd976..03d64e954 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -69,7 +69,7 @@ movie_only_builders = [ "letterboxd_list", "letterboxd_list_details", "icheckmovies_list", "icheckmovies_list_details", "stevenlu_popular", "tmdb_collection", "tmdb_collection_details", "tmdb_movie", "tmdb_movie_details", "tmdb_now_playing", - "tvdb_movie", "tvdb_movie_details" + "tvdb_movie", "tvdb_movie_details", "trakt_boxoffice" ] summary_details = [ "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", @@ -147,7 +147,7 @@ "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated", "tmdb_trending_daily", "tmdb_trending_weekly", "tmdb_discover", "tvdb_list", "imdb_list", "stevenlu_popular", "anidb_popular", - "trakt_list", "trakt_trending", "trakt_popular", + "trakt_list", "trakt_trending", "trakt_popular", "trakt_boxoffice", "trakt_collected_daily", "trakt_collected_weekly", "trakt_collected_monthly", "trakt_collected_yearly", "trakt_collected_all", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", @@ -1033,11 +1033,16 @@ def _trakt(self, method_name, method_data): self.builders.append(("trakt_list", trakt_list)) if method_name.endswith("_details"): self.summaries[method_name] = self.config.Trakt.list_description(trakt_lists[0]) - elif method_name.startswith(("trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected")): - self.builders.append((method_name, util.parse(method_name, method_data, datatype="int", default=10))) elif method_name in ["trakt_watchlist", "trakt_collection"]: for trakt_list in self.config.Trakt.validate_trakt(method_data, self.library.is_movie, trakt_type=method_name[6:]): self.builders.append((method_name, trakt_list)) + elif method_name == "trakt_boxoffice": + if util.parse(method_name, method_data, datatype="bool", default=False): + self.builders.append((method_name, 10)) + else: + raise Failed(f"Collection Error: {method_name} must be set to true") + elif method_name in trakt.builders: + self.builders.append((method_name, util.parse(method_name, method_data, datatype="int", default=10))) def _tvdb(self, method_name, method_data): values = util.get_list(method_data) diff --git a/modules/trakt.py b/modules/trakt.py index d0bc2d047..b56449081 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -12,7 +12,7 @@ "trakt_collected_daily", "trakt_collected_weekly", "trakt_collected_monthly", "trakt_collected_yearly", "trakt_collected_all", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", - "trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", "trakt_trending", "trakt_watchlist" + "trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", "trakt_trending", "trakt_watchlist", "trakt_boxoffice" ] sorts = [ "rank", "added", "title", "released", "runtime", "popularity", @@ -222,15 +222,15 @@ def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"): def get_trakt_ids(self, method, data, is_movie): pretty = method.replace("_", " ").title() media_type = "Movie" if is_movie else "Show" - if method.startswith(("trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected")): - logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") - terms = method.split("_") - return self._pagenation(f"{terms[1]}{f'/{terms[2]}' if len(terms) > 2 else ''}", data, is_movie) - elif method in ["trakt_collection", "trakt_watchlist"]: + if method in ["trakt_collection", "trakt_watchlist"]: logger.info(f"Processing {pretty} {media_type}s for {data}") return self._user_items(method[6:], data, is_movie) elif method == "trakt_list": logger.info(f"Processing {pretty}: {data}") return self._user_list(data) + elif method in builders: + logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") + terms = method.split("_") + return self._pagenation(f"{terms[1]}{f'/{terms[2]}' if len(terms) > 2 else ''}", data, is_movie) else: raise Failed(f"Trakt Error: Method {method} not supported") From f0a60af04af7bc7d77cfb1df051d783921400132 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 13 Nov 2021 23:33:42 -0500 Subject: [PATCH 50/57] TVDb Invalid predicate Fix --- modules/tvdb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tvdb.py b/modules/tvdb.py index 24f1f5094..d3cc2b3b5 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -70,8 +70,8 @@ def parse_page(xpath): def parse_title_summary(lang=None): place = "//div[@class='change_translation_text' and " - place += f"@data-language='{lang}'" if lang else "not(@style='display:none')" - return parse_page(f"{place}/@data-title"), parse_page(f"{place}]/p/text()[normalize-space()]") + place += f"@data-language='{lang}']" if lang else "not(@style='display:none')]" + return parse_page(f"{place}/@data-title"), parse_page(f"{place}/p/text()[normalize-space()]") self.title, self.summary = parse_title_summary(lang=self.language) if not self.title and self.language in language_translation: From fa0aedb24d82871c50d186edd7b8d6bcd9976411 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 14 Nov 2021 14:14:47 -0500 Subject: [PATCH 51/57] IMDb Tuple Fix --- modules/imdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/imdb.py b/modules/imdb.py index e0deb48d6..5acc74e03 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -25,7 +25,7 @@ def validate_imdb_lists(self, imdb_lists, language): imdb_dict = {"url": imdb_dict} dict_methods = {dm.lower(): dm for dm in imdb_dict} imdb_url = util.parse("url", imdb_dict, methods=dict_methods, parent="imdb_list").strip() - if not imdb_url.startswith((v for k, v in urls.items())): + if not imdb_url.startswith(tuple([v for k, v in urls.items()])): fails = "\n".join([f"{v} (For {k.replace('_', ' ').title()})" for k, v in urls.items()]) raise Failed(f"IMDb Error: {imdb_url} must begin with either:{fails}") self._total(imdb_url, language) From 6b24a7b1121c20dee4004eb5db2f097be7eb3593 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 15 Nov 2021 15:31:38 -0500 Subject: [PATCH 52/57] assets for all fix --- plex_meta_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 9d5804524..274f986d4 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -300,6 +300,8 @@ def library_operations(config, library, items=None): logger.error(e) continue util.print_return(f"Processing: {i}/{len(items)} {item.title}") + if library.assets_for_all: + library.update_item_from_assets(item, create=library.create_asset_folders) tmdb_id = None tvdb_id = None imdb_id = None @@ -425,8 +427,6 @@ def library_operations(config, library, items=None): except Failed: pass - if library.assets_for_all: - library.update_item_from_assets(item, create=library.create_asset_folders) if library.Radarr and library.radarr_add_all: try: From 68962de62cd0d47b45f15fe3ddf041240976ce57 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 15 Nov 2021 16:12:06 -0500 Subject: [PATCH 53/57] trakt_popular fix --- modules/trakt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/trakt.py b/modules/trakt.py index b56449081..a0b517d1e 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -154,7 +154,7 @@ def _parse(self, items, typeless=False, item_type=None): for item in items: if typeless: data = item - current_type = None + current_type = item_type elif item_type: data = item[item_type] current_type = item_type From 26eaa57212e4e0d13a872f9fc8286e73eaf166f4 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 15 Nov 2021 16:12:53 -0500 Subject: [PATCH 54/57] up version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 1ab5ae484..16ee7b3a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.2-develop1112 \ No newline at end of file +1.12.2-develop1115 \ No newline at end of file From 0e6f36b8f1e156e8234701f14f92dddd82042ed4 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 16 Nov 2021 10:07:09 -0500 Subject: [PATCH 55/57] adds show_missing_assets setting --- modules/config.py | 2 ++ modules/library.py | 1 + modules/plex.py | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/config.py b/modules/config.py index 09085a7a0..04bb63754 100644 --- a/modules/config.py +++ b/modules/config.py @@ -188,6 +188,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No "show_unmanaged": check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True), "show_filtered": check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False), "show_missing": check_for_attribute(self.data, "show_missing", parent="settings", var_type="bool", default=True), + "show_missing_assets": check_for_attribute(self.data, "show_missing_assets", parent="settings", var_type="bool", default=True), "save_missing": check_for_attribute(self.data, "save_missing", parent="settings", var_type="bool", default=True), "missing_only_released": check_for_attribute(self.data, "missing_only_released", parent="settings", var_type="bool", default=False), "create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False), @@ -393,6 +394,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) + params["show_missing_assets"] = check_for_attribute(lib, "show_missing_assets", parent="settings", var_type="bool", default=self.general["show_missing_assets"], do_print=False, save=False) params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False) params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) diff --git a/modules/library.py b/modules/library.py index 8276d42ab..0d1cceb94 100644 --- a/modules/library.py +++ b/modules/library.py @@ -44,6 +44,7 @@ def __init__(self, config, params): self.show_unmanaged = params["show_unmanaged"] self.show_filtered = params["show_filtered"] self.show_missing = params["show_missing"] + self.show_missing_assets = params["show_missing_assets"] self.save_missing = params["save_missing"] self.missing_only_released = params["missing_only_released"] self.create_asset_folders = params["create_asset_folders"] diff --git a/modules/plex.py b/modules/plex.py index 303e0b718..0fbc69bdb 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -608,7 +608,6 @@ def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None): def update_item_from_assets(self, item, overlay=None, create=False): name = os.path.basename(os.path.dirname(str(item.locations[0])) if self.is_movie else str(item.locations[0])) - logger.debug(name) found_folder = False poster = None background = None @@ -672,5 +671,5 @@ def update_item_from_assets(self, item, overlay=None, create=False): logger.info(f"Asset Directory Created: {os.path.join(self.asset_directory[0], name)}") elif not overlay and self.asset_folders and not found_folder: logger.error(f"Asset Warning: No asset folder found called '{name}'") - elif not poster and not background: + elif not poster and not background and self.show_missing_assets: logger.error(f"Asset Warning: No poster or background found in an assets folder for '{name}'") From 6ca4ccea379a918a2f28b5de252158065458617d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 16 Nov 2021 11:44:03 -0500 Subject: [PATCH 56/57] fix for multiple webhook notifications --- modules/webhooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/webhooks.py b/modules/webhooks.py index 98ed735ff..a615a5c49 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -17,7 +17,7 @@ def _request(self, webhooks, json): if self.config.trace_mode: logger.debug("") logger.debug(f"JSON: {json}") - for webhook in webhooks: + for webhook in list(set(webhooks)): if self.config.trace_mode: logger.debug(f"Webhook: {webhook}") if webhook == "notifiarr": From aae1ae22ac35421436d5c8298c63a5c70be3f167 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 17 Nov 2021 09:20:04 -0500 Subject: [PATCH 57/57] #435 PMM_WIDTH fix --- plex_meta_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 274f986d4..1934408c1 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -63,7 +63,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): resume = get_arg("PMM_RESUME", args.resume) times = get_arg("PMM_TIME", args.times) divider = get_arg("PMM_DIVIDER", args.divider) -screen_width = get_arg("PMM_WIDTH", args.width) +screen_width = get_arg("PMM_WIDTH", args.width, arg_int=True) config_file = get_arg("PMM_CONFIG", args.config) stats = {}