mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
[9] Add Special Text Overlays
This commit is contained in:
parent
3f5dd3bcfa
commit
ff14c2d80f
12 changed files with 241 additions and 101 deletions
2
VERSION
2
VERSION
|
@ -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.
|
||||
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.
|
||||
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
|
||||
![MAL Details](mal.png)
|
||||
Click "Allow"
|
||||
|
|
|
@ -115,9 +115,9 @@ The available attributes for editing shows, seasons, and episodes are as follows
|
|||
|:-----------------------|:--------------------------------------------------------------|:--------:|:--------:|:--------:|
|
||||
| `title` | Text to change 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 | ✅ | ❌ | ✅ |
|
||||
| `content_rating` | Text to change Content Rating | ✅ | ❌ | ❌ |
|
||||
| `content_rating` | Text to change Content Rating | ✅ | ❌ | ✅ |
|
||||
| `user_rating` | Number to change User Rating | ✅ | ✅ | ✅ |
|
||||
| `audience_rating` | Number to change Audience Rating | ✅ | ❌ | ✅ |
|
||||
| `critic_rating` | Number to change Critic Rating | ✅ | ❌ | ✅ |
|
||||
|
|
|
@ -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 | ❌ |
|
||||
| `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 | ❌ |
|
||||
| `text_format` | Text Format for Special Text Overlays.<br>**`text_format` Only works with text overlays**<br>**Value:** Integer 0 or greater | ❌ |
|
||||
| `addon_offset` | Text Addon Image Offset from the text.<br>**`addon_offset` Only works with text overlays**<br>**Value:** Integer 0 or greater | ❌ |
|
||||
| `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` | ❌ |
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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)`
|
||||
|
||||
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#)`
|
||||
|
||||
You can add an items rating percentage (`87%`, `90%`) to the image by using `text(audience_rating%)`, `text(critic_rating%)`, or `text(user_rating%)`
|
||||
|
||||
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 `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.
|
||||
|
||||
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`
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### Special Text Overlays
|
||||
|
||||
You can use the item's metadata to determine the text.
|
||||
|
||||
The final text can be formatted using the `text_format` attribute and the format variables.
|
||||
|
||||
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) | ✅ | `<<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
|
||||
overlays:
|
||||
audience_rating:
|
||||
overlay:
|
||||
name: text(audience_rating)
|
||||
text_format: <<value%>>%
|
||||
horizontal_offset: 225
|
||||
horizontal_align: center
|
||||
vertical_offset: 15
|
||||
|
@ -176,12 +211,17 @@ overlays:
|
|||
font_color: "#FFFFFF"
|
||||
back_color: "#00000099"
|
||||
back_radius: 30
|
||||
back_width: 150
|
||||
back_width: 300
|
||||
back_height: 105
|
||||
```
|
||||
|
||||
#### Text Addon Images
|
||||
|
||||
You can add an image to accompany the text by specifying the image location using `file`, `url`, `git`, or `repo`.
|
||||
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
|
||||
|
||||
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
|
||||
overlays:
|
||||
|
|
|
@ -245,6 +245,29 @@ class CollectionBuilder:
|
|||
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}")
|
||||
|
||||
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 "overlay" in methods:
|
||||
overlay_data = data[methods["overlay"]]
|
||||
|
@ -260,7 +283,7 @@ class CollectionBuilder:
|
|||
suppress = util.get_list(data[methods["suppress_overlays"]])
|
||||
else:
|
||||
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.valid_users = []
|
||||
|
@ -466,29 +489,6 @@ class CollectionBuilder:
|
|||
else:
|
||||
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:
|
||||
logger.debug("")
|
||||
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 \
|
||||
or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]:
|
||||
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")
|
||||
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"])
|
||||
logger.info(f"Collection Mode | {self.details['collection_mode']}")
|
||||
advance_update = True
|
||||
|
|
|
@ -27,6 +27,7 @@ class Cache:
|
|||
cursor.execute("DROP TABLE IF EXISTS omdb_data2")
|
||||
cursor.execute("DROP TABLE IF EXISTS tvdb_data")
|
||||
cursor.execute("DROP TABLE IF EXISTS tvdb_data2")
|
||||
cursor.execute("DROP TABLE IF EXISTS overlay_ratings")
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS guids_map (
|
||||
key INTEGER PRIMARY KEY,
|
||||
|
@ -246,11 +247,11 @@ class Cache:
|
|||
expiration_date TEXT)"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""CREATE TABLE IF NOT EXISTS overlay_ratings (
|
||||
"""CREATE TABLE IF NOT EXISTS overlay_special_text (
|
||||
key INTEGER PRIMARY KEY,
|
||||
rating_key INTEGER,
|
||||
type TEXT,
|
||||
rating REAL)"""
|
||||
text TEXT)"""
|
||||
)
|
||||
cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'")
|
||||
if cursor.fetchone()[0] > 0:
|
||||
|
@ -866,20 +867,20 @@ class Cache:
|
|||
[(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])
|
||||
|
||||
def query_overlay_ratings(self, rating_key, rating_type):
|
||||
def query_overlay_special_text(self, rating_key, data_type):
|
||||
rating = None
|
||||
with sqlite3.connect(self.cache_path) as connection:
|
||||
connection.row_factory = sqlite3.Row
|
||||
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()
|
||||
if row:
|
||||
rating = row["rating"]
|
||||
rating = row["text"]
|
||||
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:
|
||||
connection.row_factory = sqlite3.Row
|
||||
with closing(connection.cursor()) as cursor:
|
||||
cursor.execute("INSERT OR IGNORE INTO overlay_ratings(rating_key, type) VALUES(?, ?)", (rating_key, rating_type))
|
||||
cursor.execute("UPDATE overlay_ratings SET rating = ? WHERE rating_key = ? AND type = ?", (rating, 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_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))
|
||||
else:
|
||||
return og_txt
|
||||
for i in range(6):
|
||||
if i == 2 or i == 4:
|
||||
for i_check in range(6):
|
||||
if i_check == 2 or i_check == 4:
|
||||
for dm, dd in default.items():
|
||||
_data = scan_text(_data, dm, dd)
|
||||
else:
|
||||
|
@ -1103,6 +1103,7 @@ class MetadataFile(DataFile):
|
|||
episode.batchEdits()
|
||||
add_edit("title", episode, episode_dict, episode_methods)
|
||||
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("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")
|
||||
|
@ -1143,6 +1144,7 @@ class MetadataFile(DataFile):
|
|||
episode.batchEdits()
|
||||
add_edit("title", episode, episode_dict, episode_methods)
|
||||
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("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")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os, re, time
|
||||
from datetime import datetime
|
||||
from PIL import Image, ImageColor, ImageDraw, ImageFont
|
||||
from modules import util
|
||||
from modules.util import Failed
|
||||
|
@ -7,8 +8,12 @@ logger = util.logger
|
|||
|
||||
portrait_dim = (1000, 1500)
|
||||
landscape_dim = (1920, 1080)
|
||||
rating_mods = ["0", "%", "#"]
|
||||
special_text_overlays = [f"text({a}{s})" for a in ["audience_rating", "critic_rating", "user_rating"] for s in [""] + rating_mods]
|
||||
rating_special_text = [f"text({a})" for a in ["audience_rating", "critic_rating", "user_rating"]]
|
||||
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):
|
||||
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:
|
||||
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.library = library
|
||||
self.original_mapping_name = original_mapping_name
|
||||
self.data = overlay_data
|
||||
self.suppress = suppress
|
||||
self.level = level
|
||||
self.keys = []
|
||||
self.updated = False
|
||||
self.image = None
|
||||
|
@ -89,6 +95,7 @@ class Overlay:
|
|||
self.font_color = None
|
||||
self.addon_offset = 0
|
||||
self.addon_position = None
|
||||
self.text_overlay_format = None
|
||||
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: overlay")
|
||||
|
@ -242,7 +249,53 @@ class Overlay:
|
|||
self.font_color = ImageColor.getcolor(self.data["font_color"], "RGBA")
|
||||
except ValueError:
|
||||
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
|
||||
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])
|
||||
|
@ -377,7 +430,7 @@ class Overlay:
|
|||
output += f"{self.back_box[0]}{self.back_box[1]}{self.back_align}"
|
||||
if self.addon_position is not None:
|
||||
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:
|
||||
output += f"{value}"
|
||||
return output
|
||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
|||
from modules import plex, util, overlay
|
||||
from modules.builder import CollectionBuilder
|
||||
from modules.util import Failed, NonExisting, NotScheduled
|
||||
from num2words import num2words
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.video import Movie, Show, Season, Episode
|
||||
from PIL import Image, ImageFilter
|
||||
|
@ -120,14 +121,14 @@ class Overlays:
|
|||
for over_name in over_names:
|
||||
current_overlay = properties[over_name]
|
||||
if current_overlay.name in overlay.special_text_overlays:
|
||||
rating_type = current_overlay.name[5:-1]
|
||||
if rating_type.endswith(tuple(overlay.rating_mods)):
|
||||
rating_type = rating_type[:-1]
|
||||
cache_rating = self.config.Cache.query_overlay_ratings(item.ratingKey, rating_type)
|
||||
actual = plex.attribute_translation[rating_type]
|
||||
if not hasattr(item, actual) or getattr(item, actual) is None:
|
||||
data_type = current_overlay.name[5:-1]
|
||||
actual = plex.attribute_translation[data_type] if data_type in plex.attribute_translation[data_type] else data_type
|
||||
cache_value = self.config.Cache.query_overlay_special_text(item.ratingKey, data_type)
|
||||
if cache_value is None or not hasattr(item, actual) or getattr(item, actual) is None:
|
||||
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
|
||||
|
||||
try:
|
||||
|
@ -196,22 +197,61 @@ class Overlays:
|
|||
if blur_num > 0:
|
||||
new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num))
|
||||
|
||||
def get_text(text):
|
||||
text = text[5:-1]
|
||||
if f"text({text})" in overlay.special_text_overlays:
|
||||
rating_code = text[-1:]
|
||||
text_rating_type = text[:-1] if rating_code in overlay.rating_mods else text
|
||||
text_actual = plex.attribute_translation[text_rating_type]
|
||||
if not hasattr(item, text_actual) or getattr(item, text_actual) is None:
|
||||
raise Failed(f"Overlay Warning: No {text_rating_type} found")
|
||||
text = getattr(item, text_actual)
|
||||
def get_text(text_overlay):
|
||||
full_text = text_overlay.name[5:-1]
|
||||
if text_overlay.name in overlay.special_text_overlays:
|
||||
if full_text == "season_episode" and text_overlay.level == "season":
|
||||
actual_attr = "seasonNumber"
|
||||
elif full_text == "show_title":
|
||||
actual_attr = "parentTitle" if text_overlay.level == "season" else "grandparentTitle"
|
||||
elif full_text in plex.attribute_translation[full_text]:
|
||||
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:
|
||||
self.config.Cache.update_overlay_ratings(item.ratingKey, text_rating_type, text)
|
||||
if rating_code in ["%", "0"]:
|
||||
text = f"{int(text * 10)}{'%' if rating_code == '%' else ''}"
|
||||
if rating_code == "#" and str(text).endswith(".0"):
|
||||
text = str(text)[:-2]
|
||||
return str(text)
|
||||
self.config.Cache.update_overlay_special_text(item.ratingKey, full_text, actual_value)
|
||||
full_text = str(text_overlay.text_overlay_format)
|
||||
if text_overlay.name in overlay.value_overlays + overlay.rating_special_text + ["text(originally_available)"] and "<<value>>" in full_text:
|
||||
full_text = full_text.replace("<<value>>", actual_value)
|
||||
if text_overlay.name in overlay.rating_special_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:
|
||||
current_overlay = properties[over_name]
|
||||
|
@ -219,7 +259,7 @@ class Overlays:
|
|||
if current_overlay.name in overlay.special_text_overlays:
|
||||
image_box = current_overlay.image.size if current_overlay.image else None
|
||||
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:
|
||||
logger.warning(e)
|
||||
continue
|
||||
|
@ -254,7 +294,7 @@ class Overlays:
|
|||
if current_overlay.name.startswith("text"):
|
||||
image_box = current_overlay.image.size if current_overlay.image else None
|
||||
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:
|
||||
logger.warning(e)
|
||||
continue
|
||||
|
@ -282,8 +322,7 @@ class Overlays:
|
|||
logger.info(f"{item_title[:60]:<60} | Overlay Update Not Needed")
|
||||
|
||||
if self.config.Cache and poster_compare:
|
||||
self.config.Cache.update_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays",
|
||||
item.thumb, poster_compare, overlay='|'.join(compare_names))
|
||||
self.config.Cache.update_image_map(item.ratingKey, f"{self.library.image_table_name}_overlays", item.thumb, poster_compare, overlay='|'.join(compare_names))
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
logger.exorcise()
|
||||
|
|
|
@ -140,13 +140,21 @@ attribute_translation = {
|
|||
"label": "labels",
|
||||
"producer": "producers",
|
||||
"release": "originallyAvailableAt",
|
||||
"originally_available": "originallyAvailableAt",
|
||||
"added": "addedAt",
|
||||
"last_played": "lastViewedAt",
|
||||
"plays": "viewCount",
|
||||
"user_rating": "userRating",
|
||||
"writer": "writers",
|
||||
"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 = {
|
||||
"actors": "actor", "role": "actor", "roles": "actor",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import glob, logging, os, re, requests, ruamel.yaml, signal, sys, time
|
||||
from datetime import datetime, timedelta
|
||||
from num2words import num2words
|
||||
from pathvalidate import is_valid_filename, sanitize_filename
|
||||
from plexapi.audio import Album, Track
|
||||
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
|
||||
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):
|
||||
if nightly:
|
||||
return get_version("nightly")
|
||||
|
@ -329,7 +324,7 @@ def item_title(item):
|
|||
else:
|
||||
return f"{item.parentTitle} Season {item.index}: {item.title}"
|
||||
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:
|
||||
return f"{text}: {item.title}"
|
||||
else:
|
||||
|
@ -567,7 +562,7 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
|
|||
if run_time.startswith("hour"):
|
||||
try:
|
||||
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):
|
||||
all_check += 1
|
||||
else:
|
||||
|
@ -585,9 +580,8 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
|
|||
elif run_time.startswith("month"):
|
||||
try:
|
||||
if 1 <= int(param) <= 31:
|
||||
schedule_str += f"\nScheduled monthly on the {make_ordinal(int(param))}"
|
||||
if current_time.day == int(param) or (
|
||||
current_time.day == last_day.day and int(param) > last_day.day):
|
||||
schedule_str += f"\nScheduled monthly on the {num2words(param, to='ordinal_num')}"
|
||||
if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day):
|
||||
all_check += 1
|
||||
else:
|
||||
raise ValueError
|
||||
|
@ -599,9 +593,8 @@ def schedule_check(attribute, data, current_time, run_hour, is_all=False):
|
|||
opt = param.split("/")
|
||||
month = int(opt[0])
|
||||
day = int(opt[1])
|
||||
schedule_str += f"\nScheduled yearly on {pretty_months[month]} {make_ordinal(day)}"
|
||||
if current_time.month == month and (current_time.day == day or (
|
||||
current_time.day == last_day.day and day > last_day.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 (current_time.day == last_day.day and day > last_day.day)):
|
||||
all_check += 1
|
||||
else:
|
||||
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")
|
||||
end = datetime.strptime(f"{month_end}/{day_end}", "%m/%d")
|
||||
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):
|
||||
all_check += 1
|
||||
else:
|
||||
|
|
|
@ -8,3 +8,4 @@ schedule==1.1.0
|
|||
retrying==1.3.3
|
||||
pathvalidate==2.5.0
|
||||
pillow==9.2.0
|
||||
num2words==0.5.10
|
Loading…
Reference in a new issue