From ff14c2d80fcfd28223808caed94c6bc7abea9a32 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Tue, 26 Jul 2022 14:30:40 -0400 Subject: [PATCH] [9] Add Special Text Overlays --- VERSION | 2 +- docs/config/myanimelist.md | 5 +- docs/metadata/metadata/show.md | 4 +- docs/metadata/overlay.md | 66 +++++++++++++++++++----- modules/builder.py | 52 +++++++++---------- modules/cache.py | 17 ++++--- modules/meta.py | 6 ++- modules/overlay.py | 63 +++++++++++++++++++++-- modules/overlays.py | 91 ++++++++++++++++++++++++---------- modules/plex.py | 10 +++- modules/util.py | 23 +++------ requirements.txt | 3 +- 12 files changed, 241 insertions(+), 101 deletions(-) diff --git a/VERSION b/VERSION index 60fb67db..817a99f7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.2-develop8 +1.17.2-develop9 diff --git a/docs/config/myanimelist.md b/docs/config/myanimelist.md index 7501f785..c61fa18b 100644 --- a/docs/config/myanimelist.md +++ b/docs/config/myanimelist.md @@ -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" diff --git a/docs/metadata/metadata/show.md b/docs/metadata/metadata/show.md index da6b9a28..35374ced 100644 --- a/docs/metadata/metadata/show.md +++ b/docs/metadata/metadata/show.md @@ -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
**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 | ✅ | ❌ | ✅ | diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md index aeb2797a..44320454 100644 --- a/docs/metadata/overlay.md +++ b/docs/metadata/overlay.md @@ -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.
**Value:** Integer greater than 0 | ❌ | | `back_line_color` | Backdrop Line Color for the Text Overlay.
**Value:** Color Hex Code in format `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`. | ❌ | | `back_line_width` | Backdrop Line Width for the Text Overlay.
**Value:** Integer greater than 0 | ❌ | +| `text_format` | Text Format for Special Text Overlays.
**`text_format` Only works with text overlays**
**Value:** Integer 0 or greater | ❌ | | `addon_offset` | Text Addon Image Offset from the text.
**`addon_offset` Only works with text overlays**
**Value:** Integer 0 or greater | ❌ | | `addon_position` | Text Addon Image Alignment in relation to the text.
**`addon_position` Only works with text overlays**
**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 | `<>` -> ratings (`8.7`, `9.0`)
`<>` -> rating out of 100 (`87`, `90`)
`<>` -> rating removing `.0` as needed (`8.7`, `9`) | +| text(critic_rating) | Doesnt work with Seasons | `<>` -> ratings (`8.7`, `9.0`)
`<>` -> rating out of 100 (`87`, `90`)
`<>` -> rating removing `.0` as needed (`8.7`, `9`) | +| text(user_rating) | | `<>` -> ratings (`8.7`, `9.0`)
`<>` -> rating out of 100 (`87`, `90`)
`<>` -> rating removing `.0` as needed (`8.7`, `9`) | +| text(title) | ✅ | `<>` -> Title of the Item | +| text(show_title) | Doesnt work with Movies and Shows | `<>` -> Title of the Item's Show | +| text(season_title) | Only works with Episodes | `<>` -> Title of the Item's Season | +| text(original_title) | Only works with Movies and Shows | `<>` -> Original Title of the Item | +| text(episode_count) | Only works with Shows and Seasons | `<>` -> Number of Episodes in the Show or Season | +| text(content_rating) | Doesnt work with Seasons | `<>` -> Content Rating of the Item | +| text(season_episode) | Only works with Seasons and Episodes | `<>` -> Season Number
`<` -> Season Number With 10s Padding
`<>` -> Season Number With 100s Padding
`<>` -> Episode Number
`<` -> Episode Number With 10s Padding
`<>` -> Episode Number With 100s Padding | +| text(runtime) | Doesnt work with Shows and Seasons | `<>` -> Runtime of the Item in minutes
`<>` -> Hours in runtime of the Item
`<>` -> Minutes remaining in the hour in the runtime of the Item | +| text(originally_available) | Doesnt work with Seasons | `<>` -> Original Available Date of the Item
`<>` -> 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: <>% 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 ``` -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 +#### Text Addon Images + +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 overlays: diff --git a/modules/builder.py b/modules/builder.py index c1678d7b..afbc040c 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -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 diff --git a/modules/cache.py b/modules/cache.py index aa6880f6..80f5db38 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -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)) diff --git a/modules/meta.py b/modules/meta.py index f2c85208..07d78ab6 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -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") diff --git a/modules/overlay.py b/modules/overlay.py index b5a074c3..7da4699f 100644 --- a/modules/overlay.py +++ b/modules/overlay.py @@ -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 = "<>" if self.name[-2] == "#" else f"<>{'' 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"<>" 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"<>" 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"<>" 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("<>", self.data["text_format"]) + if not match and "<>" 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 "<>" 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<>" if self.level == "season" else "S<>E<>" + else: + self.text_overlay_format = "<>" + 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 diff --git a/modules/overlays.py b/modules/overlays.py index fc6e9fc2..629cbfd8 100644 --- a/modules/overlays.py +++ b/modules/overlays.py @@ -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 "<>" in full_text: + full_text = full_text.replace("<>", actual_value) + if text_overlay.name in overlay.rating_special_text: + if "<>" in full_text: + full_text = full_text.replace("<>", f"{int(actual_value * 10)}%") + if "<>" in full_text: + full_text = full_text.replace("<>", f"{int(actual_value * 10)}") + if "<>" in full_text: + full_text = full_text.replace("<>", str(actual_value)[:-2] if str(actual_value).endswith(".0") else actual_value) + elif text_overlay.name == "text(originally_available)": + if "<>" in full_text: + full_text = full_text.replace("<>", actual_value.strftime("%Y-%m-%d")) + match = re.search("<>", full_text) + if match: + full_text = re.sub("<>", str(actual_value.strftime(match.group(1))), full_text) + elif text_overlay.name == "text(runtime)": + if "<>" in full_text: + full_text = full_text.replace("<>", actual_value / 60000) + if "<>" in full_text: + full_text = full_text.replace("<>", (actual_value / 60000) // 60) + if "<>" in full_text: + full_text = full_text.replace("<>", (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() diff --git a/modules/plex.py b/modules/plex.py index ddc65f86..cbb844d1 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -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", diff --git a/modules/util.py b/modules/util.py index 3ef30cd9..7f2b3bf3 100644 --- a/modules/util.py +++ b/modules/util.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 88a4dc92..8a40458c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ ruamel.yaml==0.17.21 schedule==1.1.0 retrying==1.3.3 pathvalidate==2.5.0 -pillow==9.2.0 \ No newline at end of file +pillow==9.2.0 +num2words==0.5.10 \ No newline at end of file