diff --git a/VERSION b/VERSION index 03d22ea9..0676dea1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.5-develop3 +1.16.5-develop4 diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md index ecd6bbc0..ab65b861 100644 --- a/docs/metadata/overlay.md +++ b/docs/metadata/overlay.md @@ -6,7 +6,7 @@ Overlays and templates are defined within one or more Overlay files, which are l **To remove all overlays use the `remove_overlays` library operation.** -**To change a single overlay original Image either replace the image in the assets folder or remove the `Overlay` shared label and then PMM will overlay the new image** +**To change a single overlay original image either replace the image in the assets folder or remove the `Overlay` shared label and then PMM will overlay the new image** These are the attributes which can be used within the Overlay File: @@ -35,6 +35,26 @@ overlays: Each section must have the only required attribute, `overlay`. +### Overlay Name + +You can specify the Overlay Name in 3 ways. + +1. If there is no `overlay` attribute PMM will look in your `config/overlays` folder for a `.png` file named the same as the mapping name of the overlay definition. + ```yaml + overlays: + IMDb Top 250: + imdb_chart: top_movies + ``` + +2. If the `overlay` attribute is given a string PMM will look in your `config/overlays` folder for a `.png` file named the same as the string given. + ```yaml + overlays: + overlay: IMDb Top 250 + IMDb Top 250: + imdb_chart: top_movies + ``` + +3. Using a dictionary for more overlay location options. | Attribute | Description | Required | |:----------|:-------------------------------------------------------------------------------------------------------------|:--------:| @@ -53,7 +73,29 @@ overlays: imdb_chart: top_movies ``` -There are three types of attributes that can be utilized within an overlay: +### Remove Overlay + +You can add `remove_overlay` to an overlay definition and give it a list or comma separated string of overlay names you want removed from this item if this overlay is attached to the item. + +```yaml +overlays: + 4K: + plex_search: + all: + resolution: 4K + HDR: + plex_search: + all: + hdr: true + 4K-HDR: + remove_overlay: + - 4K + - HDR + plex_search: + all: + resolution: 4K + hdr: true +``` ### Builders @@ -93,7 +135,7 @@ These filter media items added to the collection by any of the Builders. overlays: 4K: overlay: - name: 4K # This will look for a local overlays/4K.png in your configs folder + name: 4K # This will look for a local overlays/4K.png in your config folder plex_search: all: resolution: 4K diff --git a/modules/builder.py b/modules/builder.py index c6caf1e5..3954e217 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -97,7 +97,7 @@ boolean_details = [ scheduled_boolean = ["visible_library", "visible_home", "visible_shared"] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ - "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", + "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "remove_overlay", "delete_not_scheduled", "tmdb_person", "build_collection", "collection_order", "collection_level", "overlay", "validate_builders", "libraries", "sync_to_users", "collection_name", "playlist_name", "name", "blank_collection" ] @@ -190,7 +190,7 @@ custom_sort_builders = [ "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" ] episode_parts_only = ["plex_pilots"] -overlay_only = ["overlay"] +overlay_only = ["overlay", "remove_overlay"] overlay_attributes = [ "filters", "limit", "show_missing", "save_missing", "missing_only_released", "minimum_items", "cache_builders", "tmdb_region" ] + all_builders + overlay_only @@ -211,7 +211,7 @@ music_attributes = [ ] + details + summary_details + poster_details + background_details class CollectionBuilder: - def __init__(self, config, metadata, name, data, library=None, overlay=None): + def __init__(self, config, metadata, name, data, library=None, overlay=None, extra=None): self.config = config self.metadata = metadata self.mapping_name = name @@ -229,6 +229,14 @@ class CollectionBuilder: self.type = "collection" self.Type = self.type.capitalize() + logger.separator(f"{self.mapping_name} {self.Type}{f' in {self.library.name}' if self.library else ''}") + logger.info("") + if extra: + logger.info(extra) + logger.info("") + + logger.separator(f"Validating {self.mapping_name} Attributes", space=False, border=False) + if "name" in methods: name = self.data[methods["name"]] elif f"{self.type}_name" in methods: @@ -256,6 +264,7 @@ class CollectionBuilder: self.data[attr] = new_attributes[attr] methods[attr.lower()] = attr + self.remove_overlays = [] if self.overlay: if "overlay" in methods: logger.debug("") @@ -294,10 +303,20 @@ class CollectionBuilder: self.overlay = data[methods["overlay"]] else: self.overlay = self.mapping_name + logger.warning(f"{self.Type} Warning: No overlay attribute using mapping name {self.mapping_name} as the overlay name") overlay_path = os.path.join(library.overlay_folder, f"{self.overlay}.png") if not os.path.exists(overlay_path): raise Failed(f"{self.Type} Error: Overlay Image not found at: {overlay_path}") + if "remove_overlay" in methods: + logger.debug("") + logger.debug("Validating Method: remove_overlay") + logger.debug(f"Value: {data[methods['remove_overlay']]}") + if data[methods["remove_overlay"]]: + self.remove_overlays = util.get_list(data[methods["remove_overlay"]]) + else: + logger.error(f"{self.Type} Error: remove_overlay attribute is blank") + if self.playlist: if "libraries" in methods: logger.debug("") @@ -388,8 +407,8 @@ class CollectionBuilder: s_attr = f"sync_to_user{'s' if 'sync_to_users' in methods else ''}" logger.debug("") logger.debug(f"Validating Method: {s_attr}") + logger.debug(f"Value: {self.data[methods[s_attr]]}") if self.data[methods[s_attr]]: - logger.debug(f"Value: {self.data[methods[s_attr]]}") self.sync_to_users = self.data[methods[s_attr]] else: logger.warning(f"Playlist Error: sync_to_users attribute is blank defaulting to playlist_sync_to_users: {self.sync_to_users}") diff --git a/modules/overlays.py b/modules/overlays.py index 08a9a803..f2952d94 100644 --- a/modules/overlays.py +++ b/modules/overlays.py @@ -30,7 +30,10 @@ class Overlays: builder = CollectionBuilder(self.config, overlay_file, k, v, library=self.library, overlay=True) logger.info("") - logger.separator(f"Running {k} Overlay", space=False, border=False) + logger.separator(f"Gathering Items for {k} Overlay", space=False, border=False) + + if builder.overlay not in overlay_rating_keys: + overlay_rating_keys[builder.overlay] = [] if builder.filters or builder.tmdb_filters: logger.info("") @@ -45,13 +48,17 @@ class Overlays: logger.info("") builder.filter_and_save_items(builder.gather_ids(method, value)) if builder.added_items: - if builder.overlay not in overlay_rating_keys: - overlay_rating_keys[builder.overlay] = [] for item in builder.added_items: item_keys[item.ratingKey] = item if item.ratingKey not in overlay_rating_keys[builder.overlay]: overlay_rating_keys[builder.overlay].append(item.ratingKey) + if builder.remove_overlays: + for rk in overlay_rating_keys[builder.overlay]: + for remove_overlay in builder.remove_overlays: + if remove_overlay in overlay_rating_keys and rk in overlay_rating_keys[remove_overlay]: + overlay_rating_keys[remove_overlay].remove(rk) + for overlay_name, over_keys in overlay_rating_keys.items(): clean_name, _ = util.validate_filename(overlay_name) image_compare = None @@ -68,6 +75,20 @@ class Overlays: if self.config.Cache: self.config.Cache.update_image_map(overlay_name, f"{self.library.image_table_name}_overlays", overlay_name, overlay_size) + def find_poster_url(plex_item): + if self.library.is_movie: + if plex_item.ratingKey in self.library.movie_rating_key_map: + return self.config.TMDb.get_movie(self.library.movie_rating_key_map[plex_item.ratingKey]).poster_url + elif self.library.is_show: + check_key = plex_item.ratingKey if isinstance(plex_item, Show) else plex_item.show().ratingKey + tmdb_id = self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[check_key]) + if isinstance(plex_item, Show) and plex_item.ratingKey in self.library.show_rating_key_map: + return self.config.TMDb.get_show(tmdb_id).poster_url + elif isinstance(plex_item, Season): + return self.config.TMDb.get_season(tmdb_id, plex_item.seasonNumber).poster_url + elif isinstance(plex_item, Episode): + return self.config.TMDb.get_episode(tmdb_id, plex_item.seasonNumber, plex_item.episodeNumber).still_url + def get_overlay_items(libtype=None): return [o for o in self.library.search(label="Overlay", libtype=libtype) if o.ratingKey not in item_overlays] @@ -78,6 +99,11 @@ class Overlays: elif self.library.is_music: remove_overlays.extend(get_overlay_items(libtype="album")) + if remove_overlays: + logger.info("") + logger.separator(f"Removing Overlays for the {self.library.name} Library") + logger.info("") + for i, item in enumerate(remove_overlays, 1): logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item.title}") clean_name, _ = util.validate_filename(item.title) @@ -86,29 +112,33 @@ class Overlays: folder_name=clean_name if self.library.asset_folders else None, prefix=f"{item.title}'s " ) - poster_location = None is_url = False + original = None if poster: poster_location = poster.location elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")): - poster_location = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png") + original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png") + poster_location = original elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")): - poster_location = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") + original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") + poster_location = original else: is_url = True - if self.library.is_movie: - if item.ratingKey in self.library.movie_rating_key_map: - poster_location = self.config.TMDb.get_movie(self.library.movie_rating_key_map[item.ratingKey]).poster_url - elif self.library.is_show: - if item.ratingKey in self.library.show_rating_key_map: - poster_location = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[item.ratingKey])).poster_url + poster_location = find_poster_url(item) if poster_location: self.library.upload_poster(item, poster_location, url=is_url) self.library.edit_tags("label", item, remove_tags=["Overlay"]) + if original: + os.remove(original) else: logger.error(f"No Poster found to restore for {item.title}") logger.exorcise() + if item_overlays: + logger.info("") + logger.separator(f"Applying Overlays for the {self.library.name} Library") + logger.info("") + for i, (over_key, over_names) in enumerate(item_overlays.items(), 1): try: item = item_keys[over_key] @@ -136,43 +166,38 @@ class Overlays: folder_name=clean_name if self.library.asset_folders else None, prefix=f"{item.title}'s " ) + has_original = False changed_image = False + new_backup = None if poster: if image_compare and str(poster.compare) != str(image_compare): changed_image = True - else: + elif has_overlay: if os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png")): has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png") elif os.path.exists(os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg")): has_original = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") else: - changed_image = True self.library.reload(item) - poster_url = item.posterUrl - if has_overlay: - if self.library.is_movie: - if item.ratingKey in self.library.movie_rating_key_map: - poster_url = self.config.TMDb.get_movie(self.library.movie_rating_key_map[item.ratingKey]).poster_url - elif self.library.is_show: - check_key = item.ratingKey if isinstance(item, Show) else item.show().ratingKey - tmdb_id = self.config.Convert.tvdb_to_tmdb(self.library.show_rating_key_map[check_key]) - if isinstance(item, Show) and item.ratingKey in self.library.show_rating_key_map: - poster_url = self.config.TMDb.get_show(tmdb_id).poster_url - elif isinstance(item, Season): - poster_url = self.config.TMDb.get_season(tmdb_id, item.seasonNumber).poster_url - elif isinstance(item, Episode): - poster_url = self.config.TMDb.get_episode(tmdb_id, item.seasonNumber, item.episodeNumber).still_url - response = self.config.get(poster_url) - if response.status_code >= 400: - raise Failed(f"Overlay Error: Poster Download Failed for {item.title}") - ext = "jpg" if response.headers["Content-Type"] == "image/jpeg" else "png" - backup_image = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.{ext}") - with open(backup_image, "wb") as handler: - handler.write(response.content) - while util.is_locked(backup_image): - time.sleep(1) - has_original = backup_image + new_backup = find_poster_url(item) + if new_backup is None: + new_backup = item.posterUrl + else: + self.library.reload(item) + new_backup = item.posterUrl + if new_backup: + changed_image = True + image_response = self.config.get(new_backup) + if image_response.status_code >= 400: + raise Failed(f"Overlay Error: Poster Download Failed for {item.title}") + i_ext = "jpg" if image_response.headers["Content-Type"] == "image/jpeg" else "png" + backup_image_path = os.path.join(self.library.overlay_backup, f"{item.ratingKey}.{i_ext}") + with open(backup_image_path, "wb") as handler: + handler.write(image_response.content) + while util.is_locked(backup_image_path): + time.sleep(1) + has_original = backup_image_path poster_uploaded = False if changed_image or overlay_change: diff --git a/plex_meta_manager.py b/plex_meta_manager.py index cb14f986..4dcd31b1 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -449,15 +449,7 @@ def run_collection(config, library, metadata, requested_collections): library.status[mapping_name] = {"status": "", "errors": [], "created": False, "modified": False, "deleted": False, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} try: - logger.separator(f"{mapping_name} Collection in {library.name}") - logger.info("") - if output_str: - logger.info(output_str) - logger.info("") - - logger.separator(f"Validating {mapping_name} Attributes", space=False, border=False) - - builder = CollectionBuilder(config, metadata, mapping_name, collection_attrs, library=library) + builder = CollectionBuilder(config, metadata, mapping_name, collection_attrs, library=library, extra=output_str) library.stats["names"].append(builder.name) logger.info("") @@ -625,15 +617,7 @@ def run_playlists(config): server_name = None library_names = None try: - logger.separator(f"{mapping_name} Playlist") - logger.info("") - if output_str: - logger.info(output_str) - logger.info("") - - logger.separator(f"Validating {mapping_name} Attributes", space=False, border=False) - - builder = CollectionBuilder(config, playlist_file, mapping_name, playlist_attrs) + builder = CollectionBuilder(config, playlist_file, mapping_name, playlist_attrs, extra=output_str) stats["names"].append(builder.name) logger.info("")