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
|
||||||
|
|
|
@ -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 | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ |
|
| `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
|
## 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 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `actor` | Uses the actor tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| `collection` | Uses the collection tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `content_rating` | Uses the content rating tags to match | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
@ -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 | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
| `country` | Uses the country tags to match | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||||
| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `director` | Uses the director tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
| `genre` | Uses the genre tags to match | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
|
||||||
| `label` | Uses the label tags to match | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
|
| `label` | Uses the label tags to match | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `producer` | Uses the actor tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
| `year` | Uses the year tag to match | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
|
||||||
| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `writer` | Uses the writer tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `resolution` | Uses the resolution tag to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `resolution` | Uses the resolution tag to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `audio_language` | Uses the audio language tags to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
|
| `subtitle_language` | Uses the subtitle language tags to match | ✅ | ✅<sup>1</sup> | ✅<sup>1</sup> | ✅ | ❌ | ❌ | ❌ |
|
||||||
| `tmdb_genre`<sup>1</sup> | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| `tmdb_genre`<sup>2</sup> | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
| `tmdb_keyword`<sup>1</sup> | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| `tmdb_keyword`<sup>2</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` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| `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
|
## 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 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| `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_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 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| `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
|
## Date Filters
|
||||||
|
|
||||||
|
|
|
@ -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. | ✅ |
|
| `name` | Name of the overlay. Each overlay name should be unique. | ✅ |
|
||||||
| `file` | Local location of the Overlay Image. | ❌ |
|
| `file` | Local location of the Overlay Image. | ❌ |
|
||||||
| `url` | URL of Overlay Image Online. | ❌ |
|
| `url` | URL of Overlay Image Online. | ❌ |
|
||||||
| `git` | Location in the [Configs Repo](https://github.com/meisnate12/Plex-Meta-Manager-Configs) of the Overlay Image. | ❌ |
|
| `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. | ❌ |
|
| `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`** | ❌ |
|
| `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. **`group` is required when using `weight`** | ❌ |
|
| `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 %. **`vertical_offset` is required when using `horizontal_offset`** | ❌ |
|
| `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. **Values:** `left`, `center`, `right` | ❌ |
|
| `horizontal_align` | Horizontal Alignment of the overlay.<br>**Values:** `left`, `center`, `right` | ❌ |
|
||||||
| `vertical_offset` | Vertical Offset of this overlay. Can be a %. **`horizontal_offset` is required when using `vertical_offset`** | ❌ |
|
| `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. **Values:** `top`, `center`, `bottom` | ❌ |
|
| `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 | ❌ |
|
| `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. **Value:** Integer greater than 0 | ❌ |
|
| `font_size` | Font Size for the Text Overlay.<br>**Value:** Integer greater than 0 | ❌ |
|
||||||
| `font_color` | Font Color for the Text Overlay. **Value:** Color Hex Code. ex `#00FF00` | ❌ |
|
| `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.
|
* 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:
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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("")
|
||||||
|
|
Loading…
Reference in a new issue