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: