From 1664a6002a4e1d4c5e5befe23cb11c19bd0215f0 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 17 May 2022 03:25:11 -0400 Subject: [PATCH] [103] overlay text backdrop and more filters --- VERSION | 2 +- docs/metadata/filters.md | 74 +++++++++++++++++++---------------- docs/metadata/overlay.md | 40 +++++++++++-------- modules/builder.py | 41 ++++++++++++-------- modules/mal.py | 14 ++++--- modules/overlays.py | 75 ++++++++++++++++++++++++------------ modules/plex.py | 2 +- modules/util.py | 83 +++++++++++++++++++++++++--------------- plex_meta_manager.py | 5 ++- 9 files changed, 203 insertions(+), 133 deletions(-) diff --git a/VERSION b/VERSION index e625af53..e564152d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.5-develop102 +1.16.5-develop103 diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md index 1c262064..03da904c 100644 --- a/docs/metadata/filters.md +++ b/docs/metadata/filters.md @@ -30,14 +30,17 @@ String filters can take multiple values **only as a list**. ### Attribute -| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:--------------------|:-----------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `title` | Uses the title attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `summary` | Uses the summary attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `studio` | Uses the studio attribute to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `record_label` | Uses the record label attribute to match | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | -| `filepath` | Uses the item's filepath to match | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | -| `audio_track_title` | Uses the audio track titles to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | +| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:--------------------|:-----------------------------------------|:--------:|:-------------------:|:-------------------:|:--------:|:-------------------:|:-------------------:|:--------:| +| `title` | Uses the title attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `summary` | Uses the summary attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `studio` | Uses the studio attribute to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `record_label` | Uses the record label attribute to match | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | +| `folder` | Uses the item's folder to match | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | +| `filepath` | Uses the item's filepath to match | ✅ | ✅1 | ✅1 | ✅ | ✅1 | ✅1 | ✅ | +| `audio_track_title` | Uses the audio track titles to match | ✅ | ✅1 | ✅1 | ✅ | ✅1 | ✅1 | ✅ | + +1 Filters using the special `episodes`/`tracks` filters with the default percent. ## Tag Filters @@ -59,27 +62,28 @@ Tag filters can take multiple values as a **list or a comma-separated string**. ### Attribute -| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | -| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | -| `label` | Uses the label tags to match | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | -| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `resolution` | Uses the resolution tag to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `tmdb_genre`1 | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_keyword`1 | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `origin_country`1 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | - -1 Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. +| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:-------------------:|:-------------------:|:--------:|:--------:|:--------:|:--------:| +| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `network` | Uses the network tags to match | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | +| `label` | Uses the label tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `resolution` | Uses the resolution tag to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `audio_language` | Uses the audio language tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `tmdb_genre`2 | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_keyword`2 | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `origin_country`2 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | + +1 Filters using the special `episodes` filter with the default percent. +2 Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. ## Boolean Filters @@ -87,11 +91,13 @@ Boolean Filters have no modifiers. ### Attribute -| Boolean Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:--------------------|:------------------------------------------------------------|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `has_collection` | Matches every item that has or does not have a collection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `has_dolby_vision` | Matches every item that has or does not have a dolby vision | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `has_overlay` | Matches every item that has or does not have an overlay | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Boolean Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:--------------------|:------------------------------------------------------------|:-------:|:-------------------:|:-------------------:|:--------:|:--------:|:--------:|:--------:| +| `has_collection` | Matches every item that has or does not have a collection | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `has_dolby_vision` | Matches every item that has or does not have a dolby vision | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `has_overlay` | Matches every item that has or does not have an overlay | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | + +1 Filters using the special `episodes` filter with the default percent. ## Date Filters diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md index a09bd121..38e94090 100644 --- a/docs/metadata/overlay.md +++ b/docs/metadata/overlay.md @@ -54,25 +54,31 @@ Each overlay definition needs to specify what overlay to use. This can happen in 3. Using a dictionary for more overlay location options. -| Attribute | Description | Required | -|:--------------------|:----------------------------------------------------------------------------------------------------------------|:--------:| -| `name` | Name of the overlay. Each overlay name should be unique. | ✅ | -| `file` | Local location of the Overlay Image. | ❌ | -| `url` | URL of Overlay Image Online. | ❌ | -| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | ❌ | -| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ | -| `group` | Name of the Grouping for this overlay. **`weight` is required when using `group`** | ❌ | -| `weight` | Weight of this overlay in its group. **`group` is required when using `weight`** | ❌ | -| `horizontal_offset` | Horizontal Offset of this overlay. Can be a %. **`vertical_offset` is required when using `horizontal_offset`** | ❌ | -| `horizontal_align` | Horizontal Alignment of the overlay. **Values:** `left`, `center`, `right` | ❌ | -| `vertical_offset` | Vertical Offset of this overlay. Can be a %. **`horizontal_offset` is required when using `vertical_offset`** | ❌ | -| `vertical_align` | Vertical Alignment of the overlay. **Values:** `top`, `center`, `bottom` | ❌ | -| `font` | System Font Filename or path to font file for the Text Overlay | ❌ | -| `font_size` | Font Size for the Text Overlay. **Value:** Integer greater than 0 | ❌ | -| `font_color` | Font Color for the Text Overlay. **Value:** Color Hex Code. ex `#00FF00` | ❌ | +| Attribute | Description | Required | +|:--------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:| +| `name` | Name of the overlay. Each overlay name should be unique. | ✅ | +| `file` | Local location of the Overlay Image. | ❌ | +| `url` | URL of Overlay Image Online. | ❌ | +| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | ❌ | +| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ | +| `group` | Name of the Grouping for this overlay. Only one overlay with the highest weight per group will be applied.
**`weight` is required when using `group`**
**Values:** group name | ❌ | +| `weight` | Weight of this overlay in its group.
**`group` is required when using `weight`**
**Values:** Integer | ❌ | +| `horizontal_offset` | Horizontal Offset of this overlay. Can be a %.
**`vertical_offset` is required when using `horizontal_offset`**
**Value:** Integer 0 or greater or 1%-100% | ❌ | +| `horizontal_align` | Horizontal Alignment of the overlay.
**Values:** `left`, `center`, `right` | ❌ | +| `vertical_offset` | Vertical Offset of this overlay. Can be a %.
**`horizontal_offset` is required when using `vertical_offset`**
**Value:** Integer 0 or greater or 1%-100% | ❌ | +| `vertical_align` | Vertical Alignment of the overlay.
**Values:** `top`, `center`, `bottom` | ❌ | +| `font` | System Font Filename or path to font file for the Text Overlay.
**Value:** System Font Filename or path to font file | ❌ | +| `font_size` | Font Size for the Text Overlay.
**Value:** Integer greater than 0 | ❌ | +| `font_color` | Font Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ | +| `back_color` | Backdrop Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ | +| `back_width` | Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text
**`back_height` is required when using `back_width`**
**Value:** Integer greater than 0 | ❌ | +| `back_height` | Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text
**`back_width` is required when using `back_height`**
**Value:** Integer greater than 0 | ❌ | +| `back_padding` | Backdrop Padding for the Text Overlay.
**Value:** Integer greater than 0 | ❌ | +| `back_radius` | Backdrop Radius for the Text Overlay.
**Value:** Integer greater than 0 | ❌ | +| `back_line_color` | Backdrop Line Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ | +| `back_line_width` | Backdrop Line Width for the Text Overlay.
**Value:** Integer greater than 0 | ❌ | * If `url`, `git`, and `repo` are all not defined then PMM will look in your `config/overlays` folder for a `.png` file named the same as the `name` attribute. -* Only one overlay with the highest weight per group will be applied. ```yaml overlays: diff --git a/modules/builder.py b/modules/builder.py index d271f2dc..a0c914f6 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -70,18 +70,18 @@ discover_status = { "Ended": "ended", "Canceled": "canceled", "Pilot": "pilot" } filters_by_type = { - "movie_show_season_episode_artist_album_track": ["title", "summary", "collection", "has_collection", "added", "last_played", "user_rating", "plays"], + "movie_show_season_episode_artist_album_track": ["title", "summary", "collection", "has_collection", "added", "last_played", "user_rating", "plays", "filepath", "label", "audio_track_title"], "movie_show_season_episode_album_track": ["year"], - "movie_show_episode_artist_track": ["filepath"], + "movie_show_season_episode_artist_album": ["has_overlay"], + "movie_show_season_episode": ["resolution", "audio_language", "subtitle_language", "has_dolby_vision"], "movie_show_episode_album": ["release", "critic_rating", "history"], "movie_show_episode_track": ["duration"], "movie_show_artist_album": ["genre"], "movie_show_episode": ["actor", "content_rating", "audience_rating"], - "movie_show_album": ["label"], - "movie_episode_track": ["audio_track_title"], - "movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"], - "movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"], + "movie_show": ["studio", "original_language", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"], + "movie_episode": ["director", "producer", "writer"], "movie_artist": ["country"], + "show_artist": ["folder"], "show_season": ["episodes"], "artist_album": ["tracks"], "show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"], @@ -101,7 +101,7 @@ tmdb_filters = [ "original_language", "origin_country", "tmdb_vote_count", "tmdb_year", "tmdb_keyword", "tmdb_genre", "first_episode_aired", "last_episode_aired", "tmdb_status", "tmdb_type", "tmdb_title" ] -string_filters = ["title", "summary", "studio", "record_label", "filepath", "audio_track_title", "tmdb_title"] +string_filters = ["title", "summary", "studio", "record_label", "folder", "filepath", "audio_track_title", "tmdb_title"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "origin_country", @@ -225,10 +225,12 @@ class CollectionBuilder: logger.debug(f"Value: {data[methods['allowed_library_types']]}") found_type = False for library_type in util.get_list(self.data[methods["allowed_library_types"]], lower=True): - if library_type not in plex.library_types: - raise Failed(f"{self.Type} Error: {library_type} is invalid. Options: {', '.join(plex.library_types)}") - elif library_type == self.library.Plex.type: + if library_type == "true" or library_type == self.library.Plex.type: found_type = True + elif library_type not in plex.library_types: + raise Failed(f"{self.Type} Error: {library_type} is invalid. Options: {', '.join(plex.library_types)}") + elif library_type == "false": + raise NotScheduled(f"Skipped because allowed_library_types is false") if not found_type: raise NotScheduled(f"Skipped because allowed_library_types {self.data[methods['allowed_library_types']]} doesn't match the library type: {self.library.Plex.type}") @@ -340,6 +342,7 @@ class CollectionBuilder: self.schedule = "" self.limit = 0 self.beginning_count = 0 + self.default_percent = 50 self.minimum = self.library.minimum_items self.tmdb_region = None self.ignore_ids = [i for i in self.library.ignore_ids] @@ -1452,10 +1455,14 @@ class CollectionBuilder: message = f"{self.Type} Error: {filter_final} is not a valid {self.collection_level} filter attribute" elif filter_final is None: message = f"{self.Type} Error: {filter_final} filter attribute is blank" - elif filter_attr in tmdb_filters: - self.tmdb_filters.append((filter_final, self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate))) else: - self.filters.append((filter_final, self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate))) + final_data = self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate) + if filter_attr in tmdb_filters: + self.tmdb_filters.append((filter_final, final_data)) + elif self.collection_level in ["show", "season", "artist", "album"] and filter_attr in ["filepath", "audio_track_title", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"]: + self.filters.append(("episodes" if self.collection_level in ["show", "season"] else "tracks", {filter_final: final_data, "percentage": self.default_percent})) + else: + self.filters.append((filter_final, final_data)) if message: if validate: raise Failed(message) @@ -1878,7 +1885,7 @@ class CollectionBuilder: return util.get_list(data, upper=True) elif attribute in ["original_language", "tmdb_keyword"]: return util.get_list(data, lower=True) - elif attribute in ["filepath", "tmdb_genre"]: + elif attribute in ["tmdb_genre"]: return util.get_list(data) elif attribute == "history": try: @@ -1965,14 +1972,14 @@ class CollectionBuilder: return util.parse(self.Type, attribute, data, datatype="bool") elif attribute in ["seasons", "episodes", "albums", "tracks"]: if isinstance(data, dict) and data: - percentage = 60 + percentage = self.default_percent if "percentage" in data: if data["percentage"] is None: - logger.warning(f"{self.Type} Warning: percentage filter attribute is blank using 60 as default") + logger.warning(f"{self.Type} Warning: percentage filter attribute is blank using {self.default_percent} as default") else: maybe = util.check_num(data["percentage"]) if maybe < 0 or maybe > 100: - logger.warning(f"{self.Type} Warning: percentage filter attribute must be a number 0-100 using 60 as default") + logger.warning(f"{self.Type} Warning: percentage filter attribute must be a number 0-100 using {self.default_percent} as default") else: percentage = maybe final_filters = {"percentage": percentage} diff --git a/modules/mal.py b/modules/mal.py index 19c0daf9..43d38e2f 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -1,4 +1,5 @@ import re, secrets, time, webbrowser +from json import JSONDecodeError from modules import util from modules.util import Failed, TimeoutExpired, YAML @@ -158,11 +159,14 @@ class MyAnimeList: token = authorization["access_token"] if authorization else self.authorization["access_token"] if self.config.trace_mode: logger.debug(f"URL: {url}") - response = self.config.get_json(url, headers={"Authorization": f"Bearer {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 + try: + response = self.config.get_json(url, headers={"Authorization": f"Bearer {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 + except JSONDecodeError: + raise Failed(f"MyAnimeList Error: Connection Failed") def _jiken_request(self, url, params=None): data = self.config.get_json(f"{jiken_base_url}{url}", params=params) diff --git a/modules/overlays.py b/modules/overlays.py index 1217435b..74184268 100644 --- a/modules/overlays.py +++ b/modules/overlays.py @@ -41,30 +41,34 @@ class Overlays: os.path.join(self.library.overlay_folder, old_overlay.title[:-8], f"{item.ratingKey}.png") ]) - if self.library.remove_overlays: - remove_overlays = self.get_overlay_items() - if self.library.is_show: - remove_overlays.extend(self.get_overlay_items(libtype="episode")) - remove_overlays.extend(self.get_overlay_items(libtype="season")) - elif self.library.is_music: - remove_overlays.extend(self.get_overlay_items(libtype="album")) + key_to_overlays = {} + properties = None + if not self.library.remove_overlays: + key_to_overlays, properties = self.compile_overlays() + ignore_list = [rk for rk in key_to_overlays] - logger.info("") - if remove_overlays: - logger.separator(f"Removing Overlays for the {self.library.name} Library") - for i, item in enumerate(remove_overlays, 1): - item_title = self.get_item_sort_title(item, atr="title") - logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item_title}") - self.remove_overlay(item, item_title, "Overlay", [ - os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png"), - os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") - ]) - logger.exorcise() - else: - logger.separator(f"No Overlays to Remove for the {self.library.name} Library") - logger.info("") + remove_overlays = self.get_overlay_items(ignore=ignore_list) + if self.library.is_show: + remove_overlays.extend(self.get_overlay_items(libtype="episode", ignore=ignore_list)) + remove_overlays.extend(self.get_overlay_items(libtype="season", ignore=ignore_list)) + elif self.library.is_music: + remove_overlays.extend(self.get_overlay_items(libtype="album", ignore=ignore_list)) + + logger.info("") + if remove_overlays: + logger.separator(f"Removing Overlays for the {self.library.name} Library") + for i, item in enumerate(remove_overlays, 1): + item_title = self.get_item_sort_title(item, atr="title") + logger.ghost(f"Restoring: {i}/{len(remove_overlays)} {item_title}") + self.remove_overlay(item, item_title, "Overlay", [ + os.path.join(self.library.overlay_backup, f"{item.ratingKey}.png"), + os.path.join(self.library.overlay_backup, f"{item.ratingKey}.jpg") + ]) + logger.exorcise() else: - key_to_overlays, properties = self.compile_overlays() + logger.separator(f"No Overlays to Remove for the {self.library.name} Library") + logger.info("") + if not self.library.remove_overlays: logger.info("") logger.separator(f"Applying Overlays for the {self.library.name} Library") logger.info("") @@ -164,7 +168,9 @@ class Overlays: image_height = 1080 if isinstance(item, Episode) else 1500 new_poster = Image.open(poster.location if poster else has_original) \ - .convert("RGBA").resize((image_width, image_height), Image.ANTIALIAS) + .convert("RGB").resize((image_width, image_height), Image.ANTIALIAS) + overlay_image = Image.new('RGBA', new_poster.size, (255, 255, 255, 0)) + drawing = ImageDraw.Draw(overlay_image) if blur_num > 0: new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num)) for over_name in normal_overlays: @@ -173,7 +179,6 @@ class Overlays: new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS) new_poster.paste(overlay.image, overlay.get_coordinates(image_width, image_height), overlay.image) if text_names: - drawing = ImageDraw.Draw(new_poster) for over_name in text_names: overlay = properties[over_name] text = over_name[5:-1] @@ -190,7 +195,27 @@ class Overlays: self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text) if per: text = f"{int(text * 10)}%" - drawing.text(overlay.get_coordinates(image_width, image_height, text=str(text)), str(text), font=overlay.font, fill=overlay.font_color) + x_cord, y_cord = overlay.get_coordinates(image_width, image_height, text=str(text)) + _, _, width, height = overlay.get_text_size(str(text)) + if overlay.back_color: + cords = ( + x_cord - overlay.back_padding, + y_cord - overlay.back_padding, + x_cord + (overlay.back_width if overlay.back_width else width) + overlay.back_padding, + y_cord + (overlay.back_height if overlay.back_height else height) + overlay.back_padding + ) + if overlay.back_width: + x_cord = int(x_cord + (overlay.back_width - width) / 2) + y_cord = int(y_cord + (overlay.back_height - height) / 2) + + if overlay.back_radius: + drawing.rounded_rectangle(cords, fill=overlay.back_color, outline=overlay.back_line_color, + width=overlay.back_line_width, radius=overlay.back_radius) + else: + drawing.rectangle(cords, fill=overlay.back_color, outline=overlay.back_line_color, + width=overlay.back_line_width) + drawing.text((x_cord, y_cord), str(text), font=overlay.font, fill=overlay.font_color, anchor='lt') + new_poster.paste(overlay_image, (0, 0), overlay_image) temp = os.path.join(self.library.overlay_folder, f"temp.png") new_poster.save(temp, "PNG") self.library.upload_poster(item, temp) diff --git a/modules/plex.py b/modules/plex.py index b7ebec62..817d36bd 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1169,7 +1169,7 @@ class Plex(Library): for media in item.media: for part in media.parts: values.extend([a.extendedDisplayTitle for a in part.audioStreams() if a.extendedDisplayTitle]) - elif filter_attr == "filepath": + elif filter_attr in ["filepath", "folder"]: values = [loc for loc in item.locations] else: values = [getattr(item, filter_actual)] diff --git a/modules/util.py b/modules/util.py index f2de8edc..e58484f3 100644 --- a/modules/util.py +++ b/modules/util.py @@ -840,8 +840,15 @@ class Overlay: self.path = None self.font = None self.font_name = None - self.font_size = 12 + self.font_size = 36 self.font_color = None + self.back_color = None + self.back_radius = None + self.back_line_width = None + self.back_line_color = None + self.back_padding = 0 + self.back_height = None + self.back_width = None logger.debug("") logger.debug("Validating Method: overlay") logger.debug(f"Value: {self.data}") @@ -855,16 +862,13 @@ class Overlay: if "group" in self.data and self.data["group"]: self.group = str(self.data["group"]) - if "weight" in self.data and self.data["weight"] is not None: - pri = check_num(self.data["weight"]) - if pri is None: - raise Failed(f"Overlay Error: overlay weight must be a number") - self.weight = pri + if "weight" in self.data: + self.weight = parse("Overlay", "weight", self.data["weight"], datatype="int", parent="overlay") if ("group" in self.data or "weight" in self.data) and (self.weight is None or not self.group): raise Failed(f"Overlay Error: overlay attribute's group and weight must be used together") - self.horizontal_align = parse("Overlay", "horizontal_align", self.data["horizontal_align"], options=["left", "center", "right"]) if "horizontal_align" in self.data else "left" - self.vertical_align = parse("Overlay", "vertical_align", self.data["vertical_align"], options=["top", "center", "bottom"]) if "vertical_align" in self.data else "top" + self.horizontal_align = parse("Overlay", "horizontal_align", self.data["horizontal_align"], parent="overlay", options=["left", "center", "right"]) if "horizontal_align" in self.data else "left" + self.vertical_align = parse("Overlay", "vertical_align", self.data["vertical_align"], parent="overlay", options=["top", "center", "bottom"]) if "vertical_align" in self.data else "top" self.horizontal_offset = None if "horizontal_offset" in self.data and self.data["horizontal_offset"] is not None: @@ -908,8 +912,8 @@ class Overlay: if self.vertical_offset is None and self.vertical_align == "center": self.vertical_offset = 0 - if (self.horizontal_offset is not None or self.vertical_offset is not None) and (self.horizontal_offset is None or self.vertical_offset is None): - raise Failed(f"Overlay Error: overlay horizontal_offset and overlay vertical_offset must be used together") + if (self.horizontal_offset is None and self.vertical_offset is not None) or (self.vertical_offset is None and self.horizontal_offset is not None): + raise Failed(f"Overlay Error: overlay attribute's horizontal_offset and vertical_offset must be used together") def get_and_save_image(image_url): response = self.config.get(image_url) @@ -958,12 +962,8 @@ class Overlay: self.name = f"text({match.group(1)})" if os.path.exists("fonts/Roboto-Medium.ttf"): self.font_name = "fonts/Roboto-Medium.ttf" - if "font_size" in self.data and self.data["font_size"] is not None: - font_size = check_num(self.data["font_size"]) - if font_size is None or font_size < 1: - logger.error(f"Overlay Error: overlay font_size: {self.data['font_size']} invalid must be a greater than 0") - else: - self.font_size = font_size + if "font_size" in self.data: + self.font_size = parse("Overlay", "font_size", self.data["font_size"], datatype="int", parent="overlay", default=self.font_size) if "font" in self.data and self.data["font"]: font = str(self.data["font"]) if not os.path.exists(font): @@ -972,13 +972,27 @@ class Overlay: raise Failed(f"Overlay Error: font: {font} not found. Options: {', '.join(fonts)}") self.font_name = font self.font = ImageFont.truetype(self.font_name, self.font_size) - if "font_color" in self.data and self.data["font_color"]: - try: - color_str = self.data["font_color"] - color_str = color_str if color_str.startswith("#") else f"#{color_str}" - self.font_color = ImageColor.getcolor(color_str, "RGB") - except ValueError: - logger.error(f"Overlay Error: overlay color: {self.data['color']} invalid") + def color(attr): + if attr in self.data and self.data[attr]: + try: + return ImageColor.getcolor(self.data[attr], "RGBA") + except ValueError: + raise Failed(f"Overlay Error: overlay {attr}: {self.data[attr]} invalid") + self.font_color = color("font_color") + self.back_color = color("back_color") + if "back_radius" in self.data: + self.back_radius = parse("Overlay", "back_radius", self.data["back_radius"], datatype="int", parent="overlay") + if "back_line_width" in self.data: + self.back_line_width = parse("Overlay", "back_line_width", self.data["back_line_width"], datatype="int", parent="overlay") + self.back_line_color = color("back_line_color") + if "back_padding" in self.data: + self.back_padding = parse("Overlay", "back_padding", self.data["back_padding"], datatype="int", parent="overlay", default=self.back_padding) + if "back_width" in self.data: + self.back_width = parse("Overlay", "back_width", self.data["back_width"], datatype="int", parent="overlay") + if "back_height" in self.data: + self.back_height = parse("Overlay", "back_height", self.data["back_height"], datatype="int", parent="overlay") + if (self.back_width and not self.back_height) or (self.back_height and not self.back_width): + raise Failed(f"Overlay Error: overlay attributes back_width and back_height must be used together") else: if "|" in self.name: raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'") @@ -1007,18 +1021,27 @@ class Overlay: output += f"{self.horizontal_align}{self.horizontal_offset}{self.vertical_offset}{self.vertical_align}" if self.font_name: output += f"{self.font_name}{self.font_size}" - if self.font_color: - output += str(self.font_color) + if self.back_width: + output += f"{self.back_width}{self.back_height}" + for value in [self.font_color, self.back_color, self.back_radius, self.back_padding, self.back_line_color, self.back_line_width]: + if value is not None: + output += f"{value}" return output def has_coordinates(self): return self.horizontal_offset is not None and self.vertical_offset is not None + def get_text_size(self, text): + return ImageDraw.Draw(Image.new("RGBA", (0, 0))).textbbox((0, 0), text, font=self.font, anchor='lt') + def get_coordinates(self, image_width, image_height, text=None): if not self.has_coordinates(): return 0, 0 - if text: - _, _, width, height = ImageDraw.Draw(Image.new("RGB", (0, 0))).textbbox((0, 0), text, font=self.font) + if self.back_width: + width = self.back_width + height = self.back_height + elif text: + _, _, width, height = self.get_text_size(text) else: width, height = self.image.size @@ -1031,7 +1054,5 @@ class Overlay: else: return value - x_cord = get_cord(self.horizontal_offset, image_width, width, self.horizontal_align) - y_cord = get_cord(self.vertical_offset, image_height, height, self.vertical_align) - - return x_cord, y_cord + return get_cord(self.horizontal_offset, image_width, width, self.horizontal_align), \ + get_cord(self.vertical_offset, image_height, height, self.vertical_align) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 51cb63df..a80ad61b 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -343,8 +343,9 @@ def run_config(config): logger.info("") logger.info(f"{'Title':<27} | Run Time |") logger.info(f"{logger.separating_character * 27} | {logger.separating_character * 8} |") - for text, value in library_status[library.name].items(): - logger.info(f"{text:<27} | {value:>8} |") + if library.name in library_status: + for text, value in library_status[library.name].items(): + logger.info(f"{text:<27} | {value:>8} |") logger.info("") print_status(library.status) if playlist_status: