mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
[103] overlay text backdrop and more filters
This commit is contained in:
parent
66c4fbc2a6
commit
1664a6002a
9 changed files with 203 additions and 133 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.16.5-develop102
|
||||
1.16.5-develop103
|
||||
|
|
|
@ -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 | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ |
|
||||
| `audio_track_title` | Uses the audio track titles to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ |
|
||||
|
||||
<sup>1</sup> 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`<sup>1</sup> | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `tmdb_keyword`<sup>1</sup> | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `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` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| 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 | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||
| `audio_language` | Uses the audio language tags to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||
| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||
| `tmdb_genre`<sup>2</sup> | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `tmdb_keyword`<sup>2</sup> | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `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` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
<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
|
||||
|
||||
|
@ -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 | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||
| `has_overlay` | Matches every item that has or does not have an overlay | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
<sup>1</sup> Filters using the special `episodes` filter with the default percent.
|
||||
|
||||
## Date Filters
|
||||
|
||||
|
|
|
@ -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.<br>**`weight` is required when using `group`**<br>**Values:** group name | ❌ |
|
||||
| `weight` | Weight of this overlay in its group.<br>**`group` is required when using `weight`**<br>**Values:** Integer | ❌ |
|
||||
| `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% | ❌ |
|
||||
| `horizontal_align` | Horizontal Alignment of the overlay.<br>**Values:** `left`, `center`, `right` | ❌ |
|
||||
| `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% | ❌ |
|
||||
| `vertical_align` | Vertical Alignment of the overlay.<br>**Values:** `top`, `center`, `bottom` | ❌ |
|
||||
| `font` | System Font Filename or path to font file for the Text Overlay.<br>**Value:** System Font Filename or path to font file | ❌ |
|
||||
| `font_size` | Font Size for the Text Overlay.<br>**Value:** Integer greater than 0 | ❌ |
|
||||
| `font_color` | Font Color for the Text Overlay.<br>**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
|
||||
| `back_color` | Backdrop Color for the Text Overlay.<br>**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<br>**`back_height` is required when using `back_width`**<br>**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<br>**`back_width` is required when using `back_height`**<br>**Value:** Integer greater than 0 | ❌ |
|
||||
| `back_padding` | Backdrop Padding for the Text Overlay.<br>**Value:** Integer greater than 0 | ❌ |
|
||||
| `back_radius` | Backdrop Radius for the Text Overlay.<br>**Value:** Integer greater than 0 | ❌ |
|
||||
| `back_line_color` | Backdrop Line Color for the Text Overlay.<br>**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ |
|
||||
| `back_line_width` | Backdrop Line Width for the Text Overlay.<br>**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:
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||
|
||||
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("")
|
||||
else:
|
||||
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]
|
||||
|
||||
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:
|
||||
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)
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue