[103] overlay text backdrop and more filters

This commit is contained in:
meisnate12 2022-05-17 03:25:11 -04:00
parent 66c4fbc2a6
commit 1664a6002a
9 changed files with 203 additions and 133 deletions

View file

@ -1 +1 @@
1.16.5-develop102 1.16.5-develop103

View file

@ -31,13 +31,16 @@ String filters can take multiple values **only as a list**.
### Attribute ### Attribute
| String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | String Filter | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track |
|:--------------------|:-----------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| |:--------------------|:-----------------------------------------|:--------:|:-------------------:|:-------------------:|:--------:|:-------------------:|:-------------------:|:--------:|
| `title` | Uses the title attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `title` | Uses the title attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `summary` | Uses the summary attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `summary` | Uses the summary attribute to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `studio` | Uses the studio attribute to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `studio` | Uses the studio attribute to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| `record_label` | Uses the record label attribute to match | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | | `record_label` | Uses the record label attribute to match | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
| `filepath` | Uses the item's filepath to match | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | | `folder` | Uses the item's folder to match | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| `audio_track_title` | Uses the audio track titles to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#9989; | | `filepath` | Uses the item's filepath to match | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; |
| `audio_track_title` | Uses the audio track titles to match | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; |
<sup>1</sup> Filters using the special `episodes`/`tracks` filters with the default percent.
## Tag Filters ## Tag Filters
@ -60,7 +63,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**.
### Attribute ### Attribute
| Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | Tag Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track |
|:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| |:-----------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:-------------------:|:-------------------:|:--------:|:--------:|:--------:|:--------:|
| `actor` | Uses the actor tags to match | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `actor` | Uses the actor tags to match | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `collection` | Uses the collection tags to match | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | | `collection` | Uses the collection tags to match | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; |
| `content_rating` | Uses the content rating tags to match | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `content_rating` | Uses the content rating tags to match | &#9989; | &#9989; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
@ -68,18 +71,19 @@ Tag filters can take multiple values as a **list or a comma-separated string**.
| `country` | Uses the country tags to match | &#9989; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | | `country` | Uses the country tags to match | &#9989; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; |
| `director` | Uses the director tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `director` | Uses the director tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `genre` | Uses the genre tags to match | &#9989; | &#9989; | &#10060; | &#10060; | &#9989; | &#9989; | &#10060; | | `genre` | Uses the genre tags to match | &#9989; | &#9989; | &#10060; | &#10060; | &#9989; | &#9989; | &#10060; |
| `label` | Uses the label tags to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#9989; | &#10060; | | `label` | Uses the label tags to match | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; |
| `producer` | Uses the actor tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `producer` | Uses the actor tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `year` | Uses the year tag to match | &#9989; | &#9989; | &#9989; | &#9989; | &#10060; | &#9989; | &#9989; | | `year` | Uses the year tag to match | &#9989; | &#9989; | &#9989; | &#9989; | &#10060; | &#9989; | &#9989; |
| `writer` | Uses the writer tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `writer` | Uses the writer tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; |
| `resolution` | Uses the resolution tag to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `resolution` | Uses the resolution tag to match | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#10060; | &#10060; | &#10060; |
| `audio_language` | Uses the audio language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `audio_language` | Uses the audio language tags to match | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#10060; | &#10060; | &#10060; |
| `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `subtitle_language` | Uses the subtitle language tags to match | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#10060; | &#10060; | &#10060; |
| `tmdb_genre`<sup>1</sup> | Uses the genre from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `tmdb_genre`<sup>2</sup> | Uses the genre from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `tmdb_keyword`<sup>1</sup> | Uses the keyword from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `tmdb_keyword`<sup>2</sup> | Uses the keyword from TMDb to match | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
| `origin_country`<sup>1</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `origin_country`<sup>2</sup> | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match<br>Example: `origin_country: us` | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; |
<sup>1</sup> Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers. <sup>1</sup> Filters using the special `episodes` filter with the default percent.
<sup>2</sup> Also filters out missing movies/shows from being added to Radarr/Sonarr. These Values also cannot use the `count` modifiers.
## Boolean Filters ## Boolean Filters
@ -88,10 +92,12 @@ Boolean Filters have no modifiers.
### Attribute ### Attribute
| Boolean Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | | Boolean Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track |
|:--------------------|:------------------------------------------------------------|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| |:--------------------|:------------------------------------------------------------|:-------:|:-------------------:|:-------------------:|:--------:|:--------:|:--------:|:--------:|
| `has_collection` | Matches every item that has or does not have a collection | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | | `has_collection` | Matches every item that has or does not have a collection | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; |
| `has_dolby_vision` | Matches every item that has or does not have a dolby vision | &#9989; | &#10060; | &#10060; | &#9989; | &#10060; | &#10060; | &#10060; | | `has_dolby_vision` | Matches every item that has or does not have a dolby vision | &#9989; | &#9989;<sup>1</sup> | &#9989;<sup>1</sup> | &#9989; | &#10060; | &#10060; | &#10060; |
| `has_overlay` | Matches every item that has or does not have an overlay | &#9989; | &#9989; | &#10060; | &#10060; | &#10060; | &#10060; | &#10060; | | `has_overlay` | Matches every item that has or does not have an overlay | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#9989; | &#10060; |
<sup>1</sup> Filters using the special `episodes` filter with the default percent.
## Date Filters ## Date Filters

View file

@ -55,24 +55,30 @@ Each overlay definition needs to specify what overlay to use. This can happen in
3. Using a dictionary for more overlay location options. 3. Using a dictionary for more overlay location options.
| Attribute | Description | Required | | Attribute | Description | Required |
|:--------------------|:----------------------------------------------------------------------------------------------------------------|:--------:| |:--------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|
| `name` | Name of the overlay. Each overlay name should be unique. | &#9989; | | `name` | Name of the overlay. Each overlay name should be unique. | &#9989; |
| `file` | Local location of the Overlay Image. | &#10060; | | `file` | Local location of the Overlay Image. | &#10060; |
| `url` | URL of Overlay Image Online. | &#10060; | | `url` | URL of Overlay Image Online. | &#10060; |
| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | &#10060; | | `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | &#10060; |
| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | &#10060; | | `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | &#10060; |
| `group` | Name of the Grouping for this overlay. **`weight` is required when using `group`** | &#10060; | | `group` | Name of the Grouping for this overlay. Only one overlay with the highest weight per group will be applied.<br>**`weight` is required when using `group`**<br>**Values:** group name | &#10060; |
| `weight` | Weight of this overlay in its group. **`group` is required when using `weight`** | &#10060; | | `weight` | Weight of this overlay in its group.<br>**`group` is required when using `weight`**<br>**Values:** Integer | &#10060; |
| `horizontal_offset` | Horizontal Offset of this overlay. Can be a %. **`vertical_offset` is required when using `horizontal_offset`** | &#10060; | | `horizontal_offset` | Horizontal Offset of this overlay. Can be a %.<br>**`vertical_offset` is required when using `horizontal_offset`**<br>**Value:** Integer 0 or greater or 1%-100% | &#10060; |
| `horizontal_align` | Horizontal Alignment of the overlay. **Values:** `left`, `center`, `right` | &#10060; | | `horizontal_align` | Horizontal Alignment of the overlay.<br>**Values:** `left`, `center`, `right` | &#10060; |
| `vertical_offset` | Vertical Offset of this overlay. Can be a %. **`horizontal_offset` is required when using `vertical_offset`** | &#10060; | | `vertical_offset` | Vertical Offset of this overlay. Can be a %.<br>**`horizontal_offset` is required when using `vertical_offset`**<br>**Value:** Integer 0 or greater or 1%-100% | &#10060; |
| `vertical_align` | Vertical Alignment of the overlay. **Values:** `top`, `center`, `bottom` | &#10060; | | `vertical_align` | Vertical Alignment of the overlay.<br>**Values:** `top`, `center`, `bottom` | &#10060; |
| `font` | System Font Filename or path to font file for the Text Overlay | &#10060; | | `font` | System Font Filename or path to font file for the Text Overlay.<br>**Value:** System Font Filename or path to font file | &#10060; |
| `font_size` | Font Size for the Text Overlay. **Value:** Integer greater than 0 | &#10060; | | `font_size` | Font Size for the Text Overlay.<br>**Value:** Integer greater than 0 | &#10060; |
| `font_color` | Font Color for the Text Overlay. **Value:** Color Hex Code. ex `#00FF00` | &#10060; | | `font_color` | Font Color for the Text Overlay.<br>**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | &#10060; |
| `back_color` | Backdrop Color for the Text Overlay.<br>**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | &#10060; |
| `back_width` | Backdrop Width for the Text Overlay. If `back_width` is not specified the Backdrop Sizes to the text<br>**`back_height` is required when using `back_width`**<br>**Value:** Integer greater than 0 | &#10060; |
| `back_height` | Backdrop Height for the Text Overlay. If `back_height` is not specified the Backdrop Sizes to the text<br>**`back_width` is required when using `back_height`**<br>**Value:** Integer greater than 0 | &#10060; |
| `back_padding` | Backdrop Padding for the Text Overlay.<br>**Value:** Integer greater than 0 | &#10060; |
| `back_radius` | Backdrop Radius for the Text Overlay.<br>**Value:** Integer greater than 0 | &#10060; |
| `back_line_color` | Backdrop Line Color for the Text Overlay.<br>**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | &#10060; |
| `back_line_width` | Backdrop Line Width for the Text Overlay.<br>**Value:** Integer greater than 0 | &#10060; |
* 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. * 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 ```yaml
overlays: overlays:

View file

@ -70,18 +70,18 @@ discover_status = {
"Ended": "ended", "Canceled": "canceled", "Pilot": "pilot" "Ended": "ended", "Canceled": "canceled", "Pilot": "pilot"
} }
filters_by_type = { 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_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_album": ["release", "critic_rating", "history"],
"movie_show_episode_track": ["duration"], "movie_show_episode_track": ["duration"],
"movie_show_artist_album": ["genre"], "movie_show_artist_album": ["genre"],
"movie_show_episode": ["actor", "content_rating", "audience_rating"], "movie_show_episode": ["actor", "content_rating", "audience_rating"],
"movie_show_album": ["label"], "movie_show": ["studio", "original_language", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"],
"movie_episode_track": ["audio_track_title"], "movie_episode": ["director", "producer", "writer"],
"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_artist": ["country"], "movie_artist": ["country"],
"show_artist": ["folder"],
"show_season": ["episodes"], "show_season": ["episodes"],
"artist_album": ["tracks"], "artist_album": ["tracks"],
"show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"], "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", "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" "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"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"]
tag_filters = [ tag_filters = [
"actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "origin_country", "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']]}") logger.debug(f"Value: {data[methods['allowed_library_types']]}")
found_type = False found_type = False
for library_type in util.get_list(self.data[methods["allowed_library_types"]], lower=True): for library_type in util.get_list(self.data[methods["allowed_library_types"]], lower=True):
if library_type not in plex.library_types: if library_type == "true" or library_type == self.library.Plex.type:
raise Failed(f"{self.Type} Error: {library_type} is invalid. Options: {', '.join(plex.library_types)}")
elif library_type == self.library.Plex.type:
found_type = True 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: 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}") 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.schedule = ""
self.limit = 0 self.limit = 0
self.beginning_count = 0 self.beginning_count = 0
self.default_percent = 50
self.minimum = self.library.minimum_items self.minimum = self.library.minimum_items
self.tmdb_region = None self.tmdb_region = None
self.ignore_ids = [i for i in self.library.ignore_ids] 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" message = f"{self.Type} Error: {filter_final} is not a valid {self.collection_level} filter attribute"
elif filter_final is None: elif filter_final is None:
message = f"{self.Type} Error: {filter_final} filter attribute is blank" 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: 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 message:
if validate: if validate:
raise Failed(message) raise Failed(message)
@ -1878,7 +1885,7 @@ class CollectionBuilder:
return util.get_list(data, upper=True) return util.get_list(data, upper=True)
elif attribute in ["original_language", "tmdb_keyword"]: elif attribute in ["original_language", "tmdb_keyword"]:
return util.get_list(data, lower=True) return util.get_list(data, lower=True)
elif attribute in ["filepath", "tmdb_genre"]: elif attribute in ["tmdb_genre"]:
return util.get_list(data) return util.get_list(data)
elif attribute == "history": elif attribute == "history":
try: try:
@ -1965,14 +1972,14 @@ class CollectionBuilder:
return util.parse(self.Type, attribute, data, datatype="bool") return util.parse(self.Type, attribute, data, datatype="bool")
elif attribute in ["seasons", "episodes", "albums", "tracks"]: elif attribute in ["seasons", "episodes", "albums", "tracks"]:
if isinstance(data, dict) and data: if isinstance(data, dict) and data:
percentage = 60 percentage = self.default_percent
if "percentage" in data: if "percentage" in data:
if data["percentage"] is None: 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: else:
maybe = util.check_num(data["percentage"]) maybe = util.check_num(data["percentage"])
if maybe < 0 or maybe > 100: 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: else:
percentage = maybe percentage = maybe
final_filters = {"percentage": percentage} final_filters = {"percentage": percentage}

View file

@ -1,4 +1,5 @@
import re, secrets, time, webbrowser import re, secrets, time, webbrowser
from json import JSONDecodeError
from modules import util from modules import util
from modules.util import Failed, TimeoutExpired, YAML from modules.util import Failed, TimeoutExpired, YAML
@ -158,11 +159,14 @@ class MyAnimeList:
token = authorization["access_token"] if authorization else self.authorization["access_token"] token = authorization["access_token"] if authorization else self.authorization["access_token"]
if self.config.trace_mode: if self.config.trace_mode:
logger.debug(f"URL: {url}") logger.debug(f"URL: {url}")
try:
response = self.config.get_json(url, headers={"Authorization": f"Bearer {token}"}) response = self.config.get_json(url, headers={"Authorization": f"Bearer {token}"})
if self.config.trace_mode: if self.config.trace_mode:
logger.debug(f"Response: {response}") logger.debug(f"Response: {response}")
if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}") if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}")
else: return response else: return response
except JSONDecodeError:
raise Failed(f"MyAnimeList Error: Connection Failed")
def _jiken_request(self, url, params=None): def _jiken_request(self, url, params=None):
data = self.config.get_json(f"{jiken_base_url}{url}", params=params) data = self.config.get_json(f"{jiken_base_url}{url}", params=params)

View file

@ -41,13 +41,18 @@ class Overlays:
os.path.join(self.library.overlay_folder, old_overlay.title[:-8], f"{item.ratingKey}.png") os.path.join(self.library.overlay_folder, old_overlay.title[:-8], f"{item.ratingKey}.png")
]) ])
if self.library.remove_overlays: key_to_overlays = {}
remove_overlays = self.get_overlay_items() properties = None
if not self.library.remove_overlays:
key_to_overlays, properties = self.compile_overlays()
ignore_list = [rk for rk in key_to_overlays]
remove_overlays = self.get_overlay_items(ignore=ignore_list)
if self.library.is_show: if self.library.is_show:
remove_overlays.extend(self.get_overlay_items(libtype="episode")) remove_overlays.extend(self.get_overlay_items(libtype="episode", ignore=ignore_list))
remove_overlays.extend(self.get_overlay_items(libtype="season")) remove_overlays.extend(self.get_overlay_items(libtype="season", ignore=ignore_list))
elif self.library.is_music: elif self.library.is_music:
remove_overlays.extend(self.get_overlay_items(libtype="album")) remove_overlays.extend(self.get_overlay_items(libtype="album", ignore=ignore_list))
logger.info("") logger.info("")
if remove_overlays: if remove_overlays:
@ -63,8 +68,7 @@ class Overlays:
else: else:
logger.separator(f"No Overlays to Remove for the {self.library.name} Library") logger.separator(f"No Overlays to Remove for the {self.library.name} Library")
logger.info("") logger.info("")
else: if not self.library.remove_overlays:
key_to_overlays, properties = self.compile_overlays()
logger.info("") logger.info("")
logger.separator(f"Applying Overlays for the {self.library.name} Library") logger.separator(f"Applying Overlays for the {self.library.name} Library")
logger.info("") logger.info("")
@ -164,7 +168,9 @@ class Overlays:
image_height = 1080 if isinstance(item, Episode) else 1500 image_height = 1080 if isinstance(item, Episode) else 1500
new_poster = Image.open(poster.location if poster else has_original) \ 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: if blur_num > 0:
new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num)) new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num))
for over_name in normal_overlays: for over_name in normal_overlays:
@ -173,7 +179,6 @@ class Overlays:
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS) new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, overlay.get_coordinates(image_width, image_height), overlay.image) new_poster.paste(overlay.image, overlay.get_coordinates(image_width, image_height), overlay.image)
if text_names: if text_names:
drawing = ImageDraw.Draw(new_poster)
for over_name in text_names: for over_name in text_names:
overlay = properties[over_name] overlay = properties[over_name]
text = over_name[5:-1] text = over_name[5:-1]
@ -190,7 +195,27 @@ class Overlays:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text) self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text)
if per: if per:
text = f"{int(text * 10)}%" 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") temp = os.path.join(self.library.overlay_folder, f"temp.png")
new_poster.save(temp, "PNG") new_poster.save(temp, "PNG")
self.library.upload_poster(item, temp) self.library.upload_poster(item, temp)

View file

@ -1169,7 +1169,7 @@ class Plex(Library):
for media in item.media: for media in item.media:
for part in media.parts: for part in media.parts:
values.extend([a.extendedDisplayTitle for a in part.audioStreams() if a.extendedDisplayTitle]) 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] values = [loc for loc in item.locations]
else: else:
values = [getattr(item, filter_actual)] values = [getattr(item, filter_actual)]

View file

@ -840,8 +840,15 @@ class Overlay:
self.path = None self.path = None
self.font = None self.font = None
self.font_name = None self.font_name = None
self.font_size = 12 self.font_size = 36
self.font_color = None 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("")
logger.debug("Validating Method: overlay") logger.debug("Validating Method: overlay")
logger.debug(f"Value: {self.data}") logger.debug(f"Value: {self.data}")
@ -855,16 +862,13 @@ class Overlay:
if "group" in self.data and self.data["group"]: if "group" in self.data and self.data["group"]:
self.group = str(self.data["group"]) self.group = str(self.data["group"])
if "weight" in self.data and self.data["weight"] is not None: if "weight" in self.data:
pri = check_num(self.data["weight"]) self.weight = parse("Overlay", "weight", self.data["weight"], datatype="int", parent="overlay")
if pri is None:
raise Failed(f"Overlay Error: overlay weight must be a number")
self.weight = pri
if ("group" in self.data or "weight" in self.data) and (self.weight is None or not self.group): 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") 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.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"], options=["top", "center", "bottom"]) if "vertical_align" in self.data else "top" 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 self.horizontal_offset = None
if "horizontal_offset" in self.data and self.data["horizontal_offset"] is not 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": if self.vertical_offset is None and self.vertical_align == "center":
self.vertical_offset = 0 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): 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 horizontal_offset and overlay vertical_offset must be used together") raise Failed(f"Overlay Error: overlay attribute's horizontal_offset and vertical_offset must be used together")
def get_and_save_image(image_url): def get_and_save_image(image_url):
response = self.config.get(image_url) response = self.config.get(image_url)
@ -958,12 +962,8 @@ class Overlay:
self.name = f"text({match.group(1)})" self.name = f"text({match.group(1)})"
if os.path.exists("fonts/Roboto-Medium.ttf"): if os.path.exists("fonts/Roboto-Medium.ttf"):
self.font_name = "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: if "font_size" in self.data:
font_size = check_num(self.data["font_size"]) self.font_size = parse("Overlay", "font_size", self.data["font_size"], datatype="int", parent="overlay", default=self.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" in self.data and self.data["font"]: if "font" in self.data and self.data["font"]:
font = str(self.data["font"]) font = str(self.data["font"])
if not os.path.exists(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)}") raise Failed(f"Overlay Error: font: {font} not found. Options: {', '.join(fonts)}")
self.font_name = font self.font_name = font
self.font = ImageFont.truetype(self.font_name, self.font_size) self.font = ImageFont.truetype(self.font_name, self.font_size)
if "font_color" in self.data and self.data["font_color"]: def color(attr):
if attr in self.data and self.data[attr]:
try: try:
color_str = self.data["font_color"] return ImageColor.getcolor(self.data[attr], "RGBA")
color_str = color_str if color_str.startswith("#") else f"#{color_str}"
self.font_color = ImageColor.getcolor(color_str, "RGB")
except ValueError: except ValueError:
logger.error(f"Overlay Error: overlay color: {self.data['color']} invalid") 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: else:
if "|" in self.name: if "|" in self.name:
raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'") 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}" output += f"{self.horizontal_align}{self.horizontal_offset}{self.vertical_offset}{self.vertical_align}"
if self.font_name: if self.font_name:
output += f"{self.font_name}{self.font_size}" output += f"{self.font_name}{self.font_size}"
if self.font_color: if self.back_width:
output += str(self.font_color) 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 return output
def has_coordinates(self): def has_coordinates(self):
return self.horizontal_offset is not None and self.vertical_offset is not None 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): def get_coordinates(self, image_width, image_height, text=None):
if not self.has_coordinates(): if not self.has_coordinates():
return 0, 0 return 0, 0
if text: if self.back_width:
_, _, width, height = ImageDraw.Draw(Image.new("RGB", (0, 0))).textbbox((0, 0), text, font=self.font) width = self.back_width
height = self.back_height
elif text:
_, _, width, height = self.get_text_size(text)
else: else:
width, height = self.image.size width, height = self.image.size
@ -1031,7 +1054,5 @@ class Overlay:
else: else:
return value return value
x_cord = get_cord(self.horizontal_offset, image_width, width, self.horizontal_align) return get_cord(self.horizontal_offset, image_width, width, self.horizontal_align), \
y_cord = get_cord(self.vertical_offset, image_height, height, self.vertical_align) get_cord(self.vertical_offset, image_height, height, self.vertical_align)
return x_cord, y_cord

View file

@ -343,6 +343,7 @@ def run_config(config):
logger.info("") logger.info("")
logger.info(f"{'Title':<27} | Run Time |") logger.info(f"{'Title':<27} | Run Time |")
logger.info(f"{logger.separating_character * 27} | {logger.separating_character * 8} |") logger.info(f"{logger.separating_character * 27} | {logger.separating_character * 8} |")
if library.name in library_status:
for text, value in library_status[library.name].items(): for text, value in library_status[library.name].items():
logger.info(f"{text:<27} | {value:>8} |") logger.info(f"{text:<27} | {value:>8} |")
logger.info("") logger.info("")