[9] Add Special Text Overlays

pull/994/head
meisnate12 2 years ago
parent 3f5dd3bcfa
commit ff14c2d80f

@ -1 +1 @@
1.17.2-develop8 1.17.2-develop9

@ -39,7 +39,10 @@ mal:
11. You should see `Successfully registered.` followed by a link that says `Return to list` click this link. 11. You should see `Successfully registered.` followed by a link that says `Return to list` click this link.
12. On this page Click the `Edit` button next to the application you just created. 12. On this page Click the `Edit` button next to the application you just created.
13. Record the `Client ID` and `Client Secret` found on the application page. 13. Record the `Client ID` and `Client Secret` found on the application page.
14. Go to this URL but replace `CLIENT_ID` with your Client ID `https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=CLIENT_ID&code_challenge=k_UHwN_eHAPQVXiceC-rYGkozKqrJmKxPUIUOBIKo1noq_4XGRVCViP_dGcwB-fkPql8f56mmWj5aWCa2HDeugf6sRvnc9Rjhbb1vKGYLY0IwWsDNXRqXdksaVGJthux` 14. Go to this URL but replace `CLIENT_ID` with your Client ID
```
https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=CLIENT_ID&code_challenge=k_UHwN_eHAPQVXiceC-rYGkozKqrJmKxPUIUOBIKo1noq_4XGRVCViP_dGcwB-fkPql8f56mmWj5aWCa2HDeugf6sRvnc9Rjhbb1vKGYLY0IwWsDNXRqXdksaVGJthux
```
15. You should see a page that looks like this 15. You should see a page that looks like this
![MAL Details](mal.png) ![MAL Details](mal.png)
Click "Allow" Click "Allow"

@ -115,9 +115,9 @@ The available attributes for editing shows, seasons, and episodes are as follows
|:-----------------------|:--------------------------------------------------------------|:--------:|:--------:|:--------:| |:-----------------------|:--------------------------------------------------------------|:--------:|:--------:|:--------:|
| `title` | Text to change Title | ❌ | ✅ | ✅ | | `title` | Text to change Title | ❌ | ✅ | ✅ |
| `sort_title` | Text to change Sort Title | ✅ | ❌ | ✅ | | `sort_title` | Text to change Sort Title | ✅ | ❌ | ✅ |
| `original_title` | Text to change Original Title | ✅ | ❌ | ✅ | | `original_title` | Text to change Original Title | ✅ | ❌ | ❌ |
| `originally_available` | Date to change Originally Available<br>**Format:** YYYY-MM-DD | &#9989; | &#10060; | &#9989; | | `originally_available` | Date to change Originally Available<br>**Format:** YYYY-MM-DD | &#9989; | &#10060; | &#9989; |
| `content_rating` | Text to change Content Rating | &#9989; | &#10060; | &#10060; | | `content_rating` | Text to change Content Rating | &#9989; | &#10060; | &#9989; |
| `user_rating` | Number to change User Rating | &#9989; | &#9989; | &#9989; | | `user_rating` | Number to change User Rating | &#9989; | &#9989; | &#9989; |
| `audience_rating` | Number to change Audience Rating | &#9989; | &#10060; | &#9989; | | `audience_rating` | Number to change Audience Rating | &#9989; | &#10060; | &#9989; |
| `critic_rating` | Number to change Critic Rating | &#9989; | &#10060; | &#9989; | | `critic_rating` | Number to change Critic Rating | &#9989; | &#10060; | &#9989; |

@ -93,6 +93,7 @@ There are many attributes available when using overlays to edit how they work.
| `back_radius` | Backdrop Radius 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_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; | | `back_line_width` | Backdrop Line Width for the Text Overlay.<br>**Value:** Integer greater than 0 | &#10060; |
| `text_format` | Text Format for Special Text Overlays.<br>**`text_format` Only works with text overlays**<br>**Value:** Integer 0 or greater | &#10060; |
| `addon_offset` | Text Addon Image Offset from the text.<br>**`addon_offset` Only works with text overlays**<br>**Value:** Integer 0 or greater | &#10060; | | `addon_offset` | Text Addon Image Offset from the text.<br>**`addon_offset` Only works with text overlays**<br>**Value:** Integer 0 or greater | &#10060; |
| `addon_position` | Text Addon Image Alignment in relation to the text.<br>**`addon_position` Only works with text overlays**<br>**Values:** `left`, `right`, `top`, `bottom` | &#10060; | | `addon_position` | Text Addon Image Alignment in relation to the text.<br>**`addon_position` Only works with text overlays**<br>**Values:** `left`, `right`, `top`, `bottom` | &#10060; |
@ -150,23 +151,57 @@ You can control the backdrop of the text using the various `back_*` attributes.
The `horizontal_offset` and `vertical_offset` overlay attributes are required when using Text Overlays. The `horizontal_offset` and `vertical_offset` overlay attributes are required when using Text Overlays.
You can add an items rating number (`8.7`, `9.0`) to the image by using `text(audience_rating)`, `text(critic_rating)`, or `text(user_rating)` PMM includes multiple fonts in the [`fonts` folder](https://github.com/meisnate12/Plex-Meta-Manager/tree/master/fonts) which can be called using `fonts/fontname.ttf`
You can add an items rating number removing `.0` as needed (`8.7`, `9`) to the image by using `text(audience_rating#)`, `text(critic_rating#)`, or `text(user_rating#)` ```yaml
overlays:
audience_rating:
overlay:
name: text(Direct Play)
horizontal_offset: 0
horizontal_align: center
vertical_offset: 150
vertical_align: bottom
font: fonts/Inter-Medium.ttf
font_size: 63
font_color: "#FFFFFF"
back_color: "#00000099"
back_radius: 30
```
You can add an items rating percentage (`87%`, `90%`) to the image by using `text(audience_rating%)`, `text(critic_rating%)`, or `text(user_rating%)` #### Special Text Overlays
You can add an items rating out of 100 (`87`, `90`) to the image by using `text(audience_rating0)`, `text(critic_rating0)`, or `text(user_rating0)` You can use the item's metadata to determine the text.
You can use the `mass_audience_rating_update` or `mass_critic_rating_update` [Library Operation](../config/operations) to update your plex ratings to various services like `tmdb`, `imdb`, `mdb`, `metacritic`, `letterboxd` and many more. The final text can be formatted using the `text_format` attribute and the format variables.
PMM includes multiple fonts in the [`fonts` folder](https://github.com/meisnate12/Plex-Meta-Manager/tree/master/fonts) which can be called using `fonts/fontname.ttf` The available options are:
| Attribute | Requirements | Format Variables |
|:---------------------------|:-------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| text(audience_rating) | Doesnt work with Seasons | `<<value>>` -> ratings (`8.7`, `9.0`)<br>`<<value%>>` -> rating out of 100 (`87`, `90`)<br>`<<value#>>` -> rating removing `.0` as needed (`8.7`, `9`) |
| text(critic_rating) | Doesnt work with Seasons | `<<value>>` -> ratings (`8.7`, `9.0`)<br>`<<value%>>` -> rating out of 100 (`87`, `90`)<br>`<<value#>>` -> rating removing `.0` as needed (`8.7`, `9`) |
| text(user_rating) | | `<<value>>` -> ratings (`8.7`, `9.0`)<br>`<<value%>>` -> rating out of 100 (`87`, `90`)<br>`<<value#>>` -> rating removing `.0` as needed (`8.7`, `9`) |
| text(title) | &#9989; | `<<value>>` -> Title of the Item |
| text(show_title) | Doesnt work with Movies and Shows | `<<value>>` -> Title of the Item's Show |
| text(season_title) | Only works with Episodes | `<<value>>` -> Title of the Item's Season |
| text(original_title) | Only works with Movies and Shows | `<<value>>` -> Original Title of the Item |
| text(episode_count) | Only works with Shows and Seasons | `<<value>>` -> Number of Episodes in the Show or Season |
| text(content_rating) | Doesnt work with Seasons | `<<value>>` -> Content Rating of the Item |
| text(season_episode) | Only works with Seasons and Episodes | `<<season>>` -> Season Number<br>`<<season0>` -> Season Number With 10s Padding<br>`<<season00>>` -> Season Number With 100s Padding<br>`<<episode>>` -> Episode Number<br>`<<episode0>` -> Episode Number With 10s Padding<br>`<<episode00>>` -> Episode Number With 100s Padding |
| text(runtime) | Doesnt work with Shows and Seasons | `<<value>>` -> Runtime of the Item in minutes<br>`<<valueH>>` -> Hours in runtime of the Item<br>`<<valueM>>` -> Minutes remaining in the hour in the runtime of the Item |
| text(originally_available) | Doesnt work with Seasons | `<<value>>` -> Original Available Date of the Item<br>`<<value[DATE_FORMAT_STRING]>>` -> Original Available Date of the Item in the given format. [Format Options](https://strftime.org/) |
Note: You can use the `mass_audience_rating_update` or `mass_critic_rating_update` [Library Operation](../config/operations) to update your plex ratings to various services like `tmdb`, `imdb`, `mdb`, `metacritic`, `letterboxd` and many more.
##### Example
I want to have the audience_rating display with a `%` out of 100 vs 0.0-10.0.
```yaml ```yaml
overlays: overlays:
audience_rating: audience_rating:
overlay: overlay:
name: text(audience_rating) name: text(audience_rating)
text_format: <<value%>>%
horizontal_offset: 225 horizontal_offset: 225
horizontal_align: center horizontal_align: center
vertical_offset: 15 vertical_offset: 15
@ -176,12 +211,17 @@ overlays:
font_color: "#FFFFFF" font_color: "#FFFFFF"
back_color: "#00000099" back_color: "#00000099"
back_radius: 30 back_radius: 30
back_width: 150 back_width: 300
back_height: 105 back_height: 105
``` ```
You can add an image to accompany the text by specifying the image location using `file`, `url`, `git`, or `repo`. #### Text Addon Images
Then you can use `addon_offset` to control the space between the text and the image and `addon_position` to control which side of the text the image will be
You can add an image to accompany the text by specifying the image location using `file`, `url`, `git`, or `repo`.
Use `addon_offset` to control the space between the text and the image.
Use `addon_position` to control which side of the text the image will be located on.
```yaml ```yaml
overlays: overlays:

@ -245,6 +245,29 @@ class CollectionBuilder:
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}")
if self.playlist: self.collection_level = "item"
elif self.library.is_show: self.collection_level = "show"
elif self.library.is_music: self.collection_level = "artist"
else: self.collection_level = "movie"
if "collection_level" in methods and not self.library.is_movie and not self.playlist:
logger.debug("")
logger.debug("Validating Method: collection_level")
level = self.data[methods["collection_level"]]
if level is None:
logger.error(f"{self.Type} Error: collection_level attribute is blank")
else:
logger.debug(f"Value: {level}")
level = level.lower()
if (self.library.is_show and level in plex.collection_level_show_options) or (self.library.is_music and level in plex.collection_level_music_options):
self.collection_level = level
elif (self.library.is_show and level != "show") or (self.library.is_music and level != "artist"):
if self.library.is_show:
options = "\n\tseason (Collection at the Season Level)\n\tepisode (Collection at the Episode Level)"
else:
options = "\n\talbum (Collection at the Album Level)\n\ttrack (Collection at the Track Level)"
raise Failed(f"{self.Type} Error: {self.data[methods['collection_level']]} collection_level invalid{options}")
self.parts_collection = self.collection_level in plex.collection_level_options
if self.overlay: if self.overlay:
if "overlay" in methods: if "overlay" in methods:
overlay_data = data[methods["overlay"]] overlay_data = data[methods["overlay"]]
@ -260,7 +283,7 @@ class CollectionBuilder:
suppress = util.get_list(data[methods["suppress_overlays"]]) suppress = util.get_list(data[methods["suppress_overlays"]])
else: else:
logger.error(f"Overlay Error: suppress_overlays attribute is blank") logger.error(f"Overlay Error: suppress_overlays attribute is blank")
self.overlay = Overlay(config, library, str(self.mapping_name), overlay_data, suppress) self.overlay = Overlay(config, library, str(self.mapping_name), overlay_data, suppress, self.collection_level)
self.sync_to_users = None self.sync_to_users = None
self.valid_users = [] self.valid_users = []
@ -466,29 +489,6 @@ class CollectionBuilder:
else: else:
self.sync = self.data[methods["sync_mode"]].lower() == "sync" self.sync = self.data[methods["sync_mode"]].lower() == "sync"
if self.playlist: self.collection_level = "item"
elif self.library.is_show: self.collection_level = "show"
elif self.library.is_music: self.collection_level = "artist"
else: self.collection_level = "movie"
if "collection_level" in methods and not self.library.is_movie and not self.playlist:
logger.debug("")
logger.debug("Validating Method: collection_level")
level = self.data[methods["collection_level"]]
if level is None:
logger.error(f"{self.Type} Error: collection_level attribute is blank")
else:
logger.debug(f"Value: {level}")
level = level.lower()
if (self.library.is_show and level in plex.collection_level_show_options) or (self.library.is_music and level in plex.collection_level_music_options):
self.collection_level = level
elif (self.library.is_show and level != "show") or (self.library.is_music and level != "artist"):
if self.library.is_show:
options = "\n\tseason (Collection at the Season Level)\n\tepisode (Collection at the Episode Level)"
else:
options = "\n\talbum (Collection at the Album Level)\n\ttrack (Collection at the Track Level)"
raise Failed(f"{self.Type} Error: {self.data[methods['collection_level']]} collection_level invalid{options}")
self.parts_collection = self.collection_level in plex.collection_level_options
if "tmdb_person" in methods: if "tmdb_person" in methods:
logger.debug("") logger.debug("")
logger.debug("Validating Method: tmdb_person") logger.debug("Validating Method: tmdb_person")
@ -2498,10 +2498,10 @@ class CollectionBuilder:
if (self.blank_collection and self.created) or int(self.obj.collectionMode) not in plex.collection_mode_keys \ if (self.blank_collection and self.created) or int(self.obj.collectionMode) not in plex.collection_mode_keys \
or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]: or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]:
if self.blank_collection and self.created: if self.blank_collection and self.created:
self.library.collection_mode_query(self.obj, "default")
logger.info(f"Collection Mode | default")
self.library.collection_mode_query(self.obj, "hide") self.library.collection_mode_query(self.obj, "hide")
logger.info(f"Collection Mode | hide") logger.info(f"Collection Mode | hide")
self.library.collection_mode_query(self.obj, "default")
logger.info(f"Collection Mode | default")
self.library.collection_mode_query(self.obj, self.details["collection_mode"]) self.library.collection_mode_query(self.obj, self.details["collection_mode"])
logger.info(f"Collection Mode | {self.details['collection_mode']}") logger.info(f"Collection Mode | {self.details['collection_mode']}")
advance_update = True advance_update = True

@ -27,6 +27,7 @@ class Cache:
cursor.execute("DROP TABLE IF EXISTS omdb_data2") cursor.execute("DROP TABLE IF EXISTS omdb_data2")
cursor.execute("DROP TABLE IF EXISTS tvdb_data") cursor.execute("DROP TABLE IF EXISTS tvdb_data")
cursor.execute("DROP TABLE IF EXISTS tvdb_data2") cursor.execute("DROP TABLE IF EXISTS tvdb_data2")
cursor.execute("DROP TABLE IF EXISTS overlay_ratings")
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids_map ( """CREATE TABLE IF NOT EXISTS guids_map (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
@ -246,11 +247,11 @@ class Cache:
expiration_date TEXT)""" expiration_date TEXT)"""
) )
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS overlay_ratings ( """CREATE TABLE IF NOT EXISTS overlay_special_text (
key INTEGER PRIMARY KEY, key INTEGER PRIMARY KEY,
rating_key INTEGER, rating_key INTEGER,
type TEXT, type TEXT,
rating REAL)""" text TEXT)"""
) )
cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'") cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'")
if cursor.fetchone()[0] > 0: if cursor.fetchone()[0] > 0:
@ -866,20 +867,20 @@ class Cache:
[(r.name, r.date.strftime("%Y-%m-%d") if r.date else None, [(r.name, r.date.strftime("%Y-%m-%d") if r.date else None,
expiration_date.strftime("%Y-%m-%d"), r.season, r.round) for r in races]) expiration_date.strftime("%Y-%m-%d"), r.season, r.round) for r in races])
def query_overlay_ratings(self, rating_key, rating_type): def query_overlay_special_text(self, rating_key, data_type):
rating = None rating = None
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM overlay_ratings WHERE rating_key = ? AND type = ?", (rating_key, rating_type)) cursor.execute("SELECT * FROM overlay_special_text WHERE rating_key = ? AND type = ?", (rating_key, data_type))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
rating = row["rating"] rating = row["text"]
return rating return rating
def update_overlay_ratings(self, rating_key, rating_type, rating): def update_overlay_special_text(self, rating_key, data_type, text):
with sqlite3.connect(self.cache_path) as connection: with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("INSERT OR IGNORE INTO overlay_ratings(rating_key, type) VALUES(?, ?)", (rating_key, rating_type)) cursor.execute("INSERT OR IGNORE INTO overlay_special_text(rating_key, type) VALUES(?, ?)", (rating_key, data_type))
cursor.execute("UPDATE overlay_ratings SET rating = ? WHERE rating_key = ? AND type = ?", (rating, rating_key, rating_type)) cursor.execute("UPDATE overlay_special_text SET text = ? WHERE rating_key = ? AND type = ?", (text, rating_key, data_type))

@ -318,8 +318,8 @@ class DataFile:
return str(og_txt).replace(f"<<{var}>>", str(actual_value)) return str(og_txt).replace(f"<<{var}>>", str(actual_value))
else: else:
return og_txt return og_txt
for i in range(6): for i_check in range(6):
if i == 2 or i == 4: if i_check == 2 or i_check == 4:
for dm, dd in default.items(): for dm, dd in default.items():
_data = scan_text(_data, dm, dd) _data = scan_text(_data, dm, dd)
else: else:
@ -1103,6 +1103,7 @@ class MetadataFile(DataFile):
episode.batchEdits() episode.batchEdits()
add_edit("title", episode, episode_dict, episode_methods) add_edit("title", episode, episode_dict, episode_methods)
add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort") add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort")
add_edit("content_rating", episode, episode_dict, episode_methods, key="contentRating")
add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float") add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float")
add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float") add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float")
add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float") add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float")
@ -1143,6 +1144,7 @@ class MetadataFile(DataFile):
episode.batchEdits() episode.batchEdits()
add_edit("title", episode, episode_dict, episode_methods) add_edit("title", episode, episode_dict, episode_methods)
add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort") add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort")
add_edit("content_rating", episode, episode_dict, episode_methods, key="contentRating")
add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float") add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float")
add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float") add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float")
add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float") add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float")

@ -1,4 +1,5 @@
import os, re, time import os, re, time
from datetime import datetime
from PIL import Image, ImageColor, ImageDraw, ImageFont from PIL import Image, ImageColor, ImageDraw, ImageFont
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
@ -7,8 +8,12 @@ logger = util.logger
portrait_dim = (1000, 1500) portrait_dim = (1000, 1500)
landscape_dim = (1920, 1080) landscape_dim = (1920, 1080)
rating_mods = ["0", "%", "#"] rating_special_text = [f"text({a})" for a in ["audience_rating", "critic_rating", "user_rating"]]
special_text_overlays = [f"text({a}{s})" for a in ["audience_rating", "critic_rating", "user_rating"] for s in [""] + rating_mods] value_overlays = ["title", "show_title", "season_title", "original_title", "episode_count", "content_rating"]
special_overlays = ["season_episode", "runtime", "originally_available"]
special_text_overlays = [f"text({a})" for a in value_overlays + special_overlays] + rating_special_text
old_special_text = [f"text({a}{s})" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["0", "%", "#"]]
all_special_text = special_text_overlays + old_special_text
def parse_cords(data, parent, required=False): def parse_cords(data, parent, required=False):
horizontal_align = util.parse("Overlay", "horizontal_align", data["horizontal_align"], parent=parent, horizontal_align = util.parse("Overlay", "horizontal_align", data["horizontal_align"], parent=parent,
@ -66,12 +71,13 @@ def parse_cords(data, parent, required=False):
class Overlay: class Overlay:
def __init__(self, config, library, original_mapping_name, overlay_data, suppress): def __init__(self, config, library, original_mapping_name, overlay_data, suppress, level):
self.config = config self.config = config
self.library = library self.library = library
self.original_mapping_name = original_mapping_name self.original_mapping_name = original_mapping_name
self.data = overlay_data self.data = overlay_data
self.suppress = suppress self.suppress = suppress
self.level = level
self.keys = [] self.keys = []
self.updated = False self.updated = False
self.image = None self.image = None
@ -89,6 +95,7 @@ class Overlay:
self.font_color = None self.font_color = None
self.addon_offset = 0 self.addon_offset = 0
self.addon_position = None self.addon_position = None
self.text_overlay_format = None
logger.debug("") logger.debug("")
logger.debug("Validating Method: overlay") logger.debug("Validating Method: overlay")
@ -242,7 +249,53 @@ class Overlay:
self.font_color = ImageColor.getcolor(self.data["font_color"], "RGBA") self.font_color = ImageColor.getcolor(self.data["font_color"], "RGBA")
except ValueError: except ValueError:
raise Failed(f"Overlay Error: overlay font_color: {self.data['font_color']} invalid") raise Failed(f"Overlay Error: overlay font_color: {self.data['font_color']} invalid")
if self.name not in special_text_overlays:
if self.name in all_special_text:
if self.name.startswith("text(critic") and self.level == "season":
raise Failed("Overlay Error: collection_level season doesn't have critic_ratings")
elif self.name.startswith("text(audience") and self.level == "season":
raise Failed("Overlay Error: collection_level season doesn't have audience_ratings")
elif self.name in ["text(season_episode)", "text(show_title)"] and self.level not in ["season", "episode"]:
raise Failed(f"Overlay Error: {self.name[5:-1]} only works with collection_level season and episode")
elif self.name == "text(runtime)" and self.level not in ["movie", "episode"]:
raise Failed("Overlay Error: runtime only works with movies and collection_level: episode")
elif self.name == "text(season_title)" and self.level != "episode":
raise Failed("Overlay Error: season_title only works with collection_level: episode")
elif self.name == "text(original_title)" and self.level not in ["movie", "show"]:
raise Failed("Overlay Error: original_title only works with movies and shows")
elif self.name == "text(episode_count)" and self.level not in ["show", "season"]:
raise Failed("Overlay Error: episode_count only works with shows and collection_level: season")
elif self.name == ["text(content_rating)", "text(originally_available)"] and self.level == "season":
raise Failed(f"Overlay Error: {self.name[5:-1]} only works with movies, shows, and collection_level: episode")
elif self.name in old_special_text:
self.text_overlay_format = "<<value#>>" if self.name[-2] == "#" else f"<<value%>>{'' if self.name[-2] == '0' else '%'}"
self.name = f"{self.name[:-2]})"
elif "text_format" in self.data and self.data["text_format"]:
if self.name in rating_special_text and not any((f"<<value{m}>>" in self.data["text_format"] for m in ["", "#", "%"])):
raise Failed("Overlay Error: text_format must have the value variable")
elif self.name == "text(season_episode)" and self.level == "season" and not any((f"<<season{m}>>" in self.data["text_format"] for m in ["", "W", "0", "00"])):
raise Failed("Overlay Error: text_format must have the season variable")
elif self.name == "text(season_episode)" and self.level == "episode" and not any((f"<<{a}{m}>>" in self.data["text_format"] for a in ["season", "episode"] for m in ["", "W", "0", "00"])):
raise Failed("Overlay Error: text_format must have the season or episode variable")
elif self.name == "text(runtime)" and not any((f"<<value{m}>>" in self.data["text_format"] for m in ["", "M", "H"])):
raise Failed("Overlay Error: text_format must have the value variable")
elif self.name == "text(originally_available)":
match = re.search("<<value\\[(.+)]>>", self.data["text_format"])
if not match and "<<value>>" not in self.data["text_format"]:
raise Failed("Overlay Error: text_format must have the value variable")
if match:
try:
datetime.now().strftime(match.group(1))
except ValueError:
raise Failed("Overlay Error: text_format date format not valid")
elif self.name[5:-1] in value_overlays and "<<value>>" not in self.data["text_format"]:
raise Failed("Overlay Error: text_format must have the value variable")
self.text_overlay_format = self.data["text_format"]
elif self.name == "text(season_episode)":
self.text_overlay_format = "S<<season0>>" if self.level == "season" else "S<<season0>>E<<episode0>>"
else:
self.text_overlay_format = "<<value>>"
else:
box = self.image.size if self.image else None box = self.image.size if self.image else None
self.portrait, self.portrait_box = self.get_backdrop(portrait_dim, box=box, text=self.name[5:-1]) self.portrait, self.portrait_box = self.get_backdrop(portrait_dim, box=box, text=self.name[5:-1])
self.landscape, self.landscape_box = self.get_backdrop(landscape_dim, box=box, text=self.name[5:-1]) self.landscape, self.landscape_box = self.get_backdrop(landscape_dim, box=box, text=self.name[5:-1])
@ -377,7 +430,7 @@ class Overlay:
output += f"{self.back_box[0]}{self.back_box[1]}{self.back_align}" output += f"{self.back_box[0]}{self.back_box[1]}{self.back_align}"
if self.addon_position is not None: if self.addon_position is not None:
output += f"{self.addon_position}{self.addon_offset}" output += f"{self.addon_position}{self.addon_offset}"
for value in [self.font_color, self.back_color, self.back_radius, self.back_padding, self.back_line_color, self.back_line_width]: for value in [self.font_color, self.back_color, self.back_radius, self.back_padding, self.back_line_color, self.back_line_width, self.text_overlay_format]:
if value is not None: if value is not None:
output += f"{value}" output += f"{value}"
return output return output

@ -3,6 +3,7 @@ from datetime import datetime
from modules import plex, util, overlay from modules import plex, util, overlay
from modules.builder import CollectionBuilder from modules.builder import CollectionBuilder
from modules.util import Failed, NonExisting, NotScheduled from modules.util import Failed, NonExisting, NotScheduled
from num2words import num2words
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.video import Movie, Show, Season, Episode from plexapi.video import Movie, Show, Season, Episode
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
@ -120,14 +121,14 @@ class Overlays:
for over_name in over_names: for over_name in over_names:
current_overlay = properties[over_name] current_overlay = properties[over_name]
if current_overlay.name in overlay.special_text_overlays: if current_overlay.name in overlay.special_text_overlays:
rating_type = current_overlay.name[5:-1] data_type = current_overlay.name[5:-1]
if rating_type.endswith(tuple(overlay.rating_mods)): actual = plex.attribute_translation[data_type] if data_type in plex.attribute_translation[data_type] else data_type
rating_type = rating_type[:-1] cache_value = self.config.Cache.query_overlay_special_text(item.ratingKey, data_type)
cache_rating = self.config.Cache.query_overlay_ratings(item.ratingKey, rating_type) if cache_value is None or not hasattr(item, actual) or getattr(item, actual) is None:
actual = plex.attribute_translation[rating_type]
if not hasattr(item, actual) or getattr(item, actual) is None:
continue continue
if getattr(item, actual) != cache_rating: if current_overlay.name in overlay.rating_special_text:
cache_value = float(cache_value)
if getattr(item, actual) != cache_value:
overlay_change = True overlay_change = True
try: try:
@ -196,22 +197,61 @@ class Overlays:
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))
def get_text(text): def get_text(text_overlay):
text = text[5:-1] full_text = text_overlay.name[5:-1]
if f"text({text})" in overlay.special_text_overlays: if text_overlay.name in overlay.special_text_overlays:
rating_code = text[-1:] if full_text == "season_episode" and text_overlay.level == "season":
text_rating_type = text[:-1] if rating_code in overlay.rating_mods else text actual_attr = "seasonNumber"
text_actual = plex.attribute_translation[text_rating_type] elif full_text == "show_title":
if not hasattr(item, text_actual) or getattr(item, text_actual) is None: actual_attr = "parentTitle" if text_overlay.level == "season" else "grandparentTitle"
raise Failed(f"Overlay Warning: No {text_rating_type} found") elif full_text in plex.attribute_translation[full_text]:
text = getattr(item, text_actual) actual_attr = plex.attribute_translation[full_text]
else:
actual_attr = full_text
if not hasattr(item, actual_attr) or getattr(item, actual_attr) is None:
raise Failed(f"Overlay Warning: No {full_text} found")
actual_value = getattr(item, actual_attr)
if self.config.Cache: if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, text_rating_type, text) self.config.Cache.update_overlay_special_text(item.ratingKey, full_text, actual_value)
if rating_code in ["%", "0"]: full_text = str(text_overlay.text_overlay_format)
text = f"{int(text * 10)}{'%' if rating_code == '%' else ''}" if text_overlay.name in overlay.value_overlays + overlay.rating_special_text + ["text(originally_available)"] and "<<value>>" in full_text:
if rating_code == "#" and str(text).endswith(".0"): full_text = full_text.replace("<<value>>", actual_value)
text = str(text)[:-2] if text_overlay.name in overlay.rating_special_text:
return str(text) if "<<value%>>" in full_text:
full_text = full_text.replace("<<value%>>", f"{int(actual_value * 10)}%")
if "<<value0>>" in full_text:
full_text = full_text.replace("<<value0>>", f"{int(actual_value * 10)}")
if "<<value#>>" in full_text:
full_text = full_text.replace("<<value#>>", str(actual_value)[:-2] if str(actual_value).endswith(".0") else actual_value)
elif text_overlay.name == "text(originally_available)":
if "<<value>>" in full_text:
full_text = full_text.replace("<<value>>", actual_value.strftime("%Y-%m-%d"))
match = re.search("<<value\\[(.+)]>>", full_text)
if match:
full_text = re.sub("<<value\\[(.+)]>>", str(actual_value.strftime(match.group(1))), full_text)
elif text_overlay.name == "text(runtime)":
if "<<value>>" in full_text:
full_text = full_text.replace("<<value>>", actual_value / 60000)
if "<<valueH>>" in full_text:
full_text = full_text.replace("<<valueH>>", (actual_value / 60000) // 60)
if "<<valueM>>" in full_text:
full_text = full_text.replace("<<valueM>>", (actual_value / 60000) % 60)
elif text_overlay.name == "text(season_episode)":
if text_overlay.level == "season":
season = actual_value
episode = None
else:
season, episode = actual_value[1:].split("E")
for attr, attr_val in [("season", season), ("episode", episode)]:
if attr_val and f"<<{attr}>>" in full_text:
full_text = full_text.replace(f"<<{attr}>>", attr_val)
if attr_val and f"<<{attr}W>>" in full_text:
full_text = full_text.replace(f"<<{attr}W>>", num2words(int(attr_val)))
if attr_val and f"<<{attr}0>>" in full_text:
full_text = full_text.replace(f"<<{attr}0>>", f"{int(attr_val):02}")
if attr_val and f"<<{attr}00>>" in full_text:
full_text = full_text.replace(f"<<{attr}00>>", f"{int(attr_val):03}")
return str(full_text)
for over_name in applied_names: for over_name in applied_names:
current_overlay = properties[over_name] current_overlay = properties[over_name]
@ -219,7 +259,7 @@ class Overlays:
if current_overlay.name in overlay.special_text_overlays: if current_overlay.name in overlay.special_text_overlays:
image_box = current_overlay.image.size if current_overlay.image else None image_box = current_overlay.image.size if current_overlay.image else None
try: try:
overlay_image, addon_box = current_overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(current_overlay.name)) overlay_image, addon_box = current_overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(current_overlay))
except Failed as e: except Failed as e:
logger.warning(e) logger.warning(e)
continue continue
@ -254,7 +294,7 @@ class Overlays:
if current_overlay.name.startswith("text"): if current_overlay.name.startswith("text"):
image_box = current_overlay.image.size if current_overlay.image else None image_box = current_overlay.image.size if current_overlay.image else None
try: try:
overlay_image, addon_box = current_overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(current_overlay.name), new_cords=cord) overlay_image, addon_box = current_overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(current_overlay), new_cords=cord)
except Failed as e: except Failed as e:
logger.warning(e) logger.warning(e)
continue continue
@ -282,8 +322,7 @@ class Overlays:
logger.info(f"{item_title[:60]:<60} | Overlay Update Not Needed") logger.info(f"{item_title[:60]:<60} | Overlay Update Not Needed")
if self.config.Cache and poster_compare: if self.config.Cache and poster_compare:
self.config.Cache.update_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays", self.config.Cache.update_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays", item.thumb, poster_compare, overlay='|'.join(compare_names))
item.thumb, poster_compare, overlay='|'.join(compare_names))
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
logger.exorcise() logger.exorcise()

@ -140,13 +140,21 @@ attribute_translation = {
"label": "labels", "label": "labels",
"producer": "producers", "producer": "producers",
"release": "originallyAvailableAt", "release": "originallyAvailableAt",
"originally_available": "originallyAvailableAt",
"added": "addedAt", "added": "addedAt",
"last_played": "lastViewedAt", "last_played": "lastViewedAt",
"plays": "viewCount", "plays": "viewCount",
"user_rating": "userRating", "user_rating": "userRating",
"writer": "writers", "writer": "writers",
"mood": "moods", "mood": "moods",
"style": "styles" "style": "styles",
"season_episode": "seasonEpisode",
"episode": "episodeNumber",
"season": "seasonNumber",
"original_title": "originalTitle",
"runtime": "duration",
"season_title": "parentTitle",
"episode_count": "leafCount"
} }
method_alias = { method_alias = {
"actors": "actor", "role": "actor", "roles": "actor", "actors": "actor", "role": "actor", "roles": "actor",

@ -1,5 +1,6 @@
import glob, logging, os, re, requests, ruamel.yaml, signal, sys, time import glob, logging, os, re, requests, ruamel.yaml, signal, sys, time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from num2words import num2words
from pathvalidate import is_valid_filename, sanitize_filename from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.audio import Album, Track from plexapi.audio import Album, Track
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
@ -95,12 +96,6 @@ parental_labels = [f"{t.capitalize()}:{v}" for t in parental_types for v in pare
previous_time = None previous_time = None
start_time = None start_time = None
def make_ordinal(n):
return f"{n}{'th' if 11 <= (n % 100) <= 13 else ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]}"
def add_zero(number):
return str(number) if len(str(number)) > 1 else f"0{number}"
def current_version(version, nightly=False): def current_version(version, nightly=False):
if nightly: if nightly:
return get_version("nightly") return get_version("nightly")
@ -329,7 +324,7 @@ def item_title(item):
else: else:
return f"{item.parentTitle} Season {item.index}: {item.title}" return f"{item.parentTitle} Season {item.index}: {item.title}"
elif isinstance(item, Episode): elif isinstance(item, Episode):
text = f"{item.grandparentTitle} S{add_zero(item.parentIndex)}E{add_zero(item.index)}" text = f"{item.grandparentTitle} S{item.parentIndex:02}E{item.index:02}"
if f"Season {item.parentIndex}" == item.parentTitle: if f"Season {item.parentIndex}" == item.parentTitle:
return f"{text}: {item.title}" return f"{text}: {item.title}"
else: else:
@ -567,7 +562,7 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
if run_time.startswith("hour"): if run_time.startswith("hour"):
try: try:
if 0 <= int(param) <= 23: if 0 <= int(param) <= 23:
schedule_str += f"\nScheduled to run on the {make_ordinal(int(param))} hour" schedule_str += f"\nScheduled to run on the {num2words(param, to='ordinal_num')} hour"
if run_hour == int(param): if run_hour == int(param):
all_check += 1 all_check += 1
else: else:
@ -585,9 +580,8 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
elif run_time.startswith("month"): elif run_time.startswith("month"):
try: try:
if 1 <= int(param) <= 31: if 1 <= int(param) <= 31:
schedule_str += f"\nScheduled monthly on the {make_ordinal(int(param))}" schedule_str += f"\nScheduled monthly on the {num2words(param, to='ordinal_num')}"
if current_time.day == int(param) or ( if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day):
current_time.day == last_day.day and int(param) > last_day.day):
all_check += 1 all_check += 1
else: else:
raise ValueError raise ValueError
@ -599,9 +593,8 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
opt = param.split("/") opt = param.split("/")
month = int(opt[0]) month = int(opt[0])
day = int(opt[1]) day = int(opt[1])
schedule_str += f"\nScheduled yearly on {pretty_months[month]} {make_ordinal(day)}" schedule_str += f"\nScheduled yearly on {pretty_months[month]} {num2words(day, to='ordinal_num')}"
if current_time.month == month and (current_time.day == day or ( if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)):
current_time.day == last_day.day and day > last_day.day)):
all_check += 1 all_check += 1
else: else:
raise ValueError raise ValueError
@ -619,7 +612,7 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
start = datetime.strptime(f"{month_start}/{day_start}", "%m/%d") start = datetime.strptime(f"{month_start}/{day_start}", "%m/%d")
end = datetime.strptime(f"{month_end}/{day_end}", "%m/%d") end = datetime.strptime(f"{month_end}/{day_end}", "%m/%d")
range_collection = True range_collection = True
schedule_str += f"\nScheduled between {pretty_months[month_start]} {make_ordinal(day_start)} and {pretty_months[month_end]} {make_ordinal(day_end)}" schedule_str += f"\nScheduled between {pretty_months[month_start]} {num2words(day_start, to='ordinal_num')} and {pretty_months[month_end]} {num2words(day_end, to='ordinal_num')}"
if start <= check <= end if start < end else (check <= end or check >= start): if start <= check <= end if start < end else (check <= end or check >= start):
all_check += 1 all_check += 1
else: else:

@ -7,4 +7,5 @@ ruamel.yaml==0.17.21
schedule==1.1.0 schedule==1.1.0
retrying==1.3.3 retrying==1.3.3
pathvalidate==2.5.0 pathvalidate==2.5.0
pillow==9.2.0 pillow==9.2.0
num2words==0.5.10
Loading…
Cancel
Save