[45] addon images for text overlays

This commit is contained in:
meisnate12 2022-06-21 02:12:47 -04:00
parent e41e3e7280
commit 25b32453ea
5 changed files with 136 additions and 47 deletions

View file

@ -1 +1 @@
1.17.0-develop44 1.17.0-develop45

View file

@ -92,6 +92,8 @@ 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; |
| `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_align` | Text Addon Image Alignment in relation to the text.<br>**`addon_align` Only works with text overlays**<br>**Values:** `left`, `right`, `top`, `bottom` | &#10060; |
* If `url`, `git`, and `repo` are all not defined then PMM will look in your `config/overlays` folder for a `.png` file named the same as the `name` attribute. * If `url`, `git`, and `repo` are all not defined then PMM will look in your `config/overlays` folder for a `.png` file named the same as the `name` attribute.
@ -175,6 +177,30 @@ overlays:
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`.
Then you can use `addon_offset` to control the space between the text and the image and `addon_align` to control which side of the text the image will be
```yaml
overlays:
audience_rating:
overlay:
name: text(audience_rating)
horizontal_offset: 225
horizontal_align: center
vertical_offset: 15
vertical_align: top
font: fonts/Inter-Medium.ttf
font_size: 63
font_color: "#FFFFFF"
back_color: "#00000099"
back_radius: 30
back_width: 300
back_height: 105
git: PMM/overlay/images/raw/IMDB_Rating
addon_align: left
addon_offset: 25
```
### Overlay Groups ### Overlay Groups
Overlay groups are defined by the name given to the `group` attribute. Only one overlay with the highest weight per group will be applied. Overlay groups are defined by the name given to the `group` attribute. Only one overlay with the highest weight per group will be applied.

View file

@ -9,8 +9,6 @@ from PIL import Image, ImageFilter
logger = util.logger logger = util.logger
special_text_overlays = [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%", "#"]]
class Overlays: class Overlays:
def __init__(self, config, library): def __init__(self, config, library):
self.config = config self.config = config
@ -120,7 +118,7 @@ class Overlays:
if self.config.Cache: if self.config.Cache:
for over_name in over_names: for over_name in over_names:
if over_name in special_text_overlays: if over_name in util.special_text_overlays:
rating_type = over_name[5:-1] rating_type = over_name[5:-1]
if rating_type.endswith(("%", "#")): if rating_type.endswith(("%", "#")):
rating_type = rating_type[:-1] rating_type = rating_type[:-1]
@ -182,30 +180,40 @@ 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):
text = text[5:-1]
if text in util.special_text_overlays:
per = text.endswith("%")
flat = text.endswith("#")
text_rating_type = text[:-1] if per or flat 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)
if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, text_rating_type, text)
if per:
text = f"{int(text * 10)}%"
if flat and str(text).endswith(".0"):
text = str(text)[:-2]
return str(text)
for over_name in applied_names: for over_name in applied_names:
overlay = properties[over_name] overlay = properties[over_name]
if over_name.startswith("text"): if over_name.startswith("text"):
text = over_name[5:-1] if over_name[5:-1] in util.special_text_overlays:
if text in special_text_overlays: image_box = overlay.image.size if overlay.image else None
per = text.endswith("%") try:
flat = text.endswith("#") overlay_image, addon_box = overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(over_name))
rating_type = text[:-1] if per or flat else text except Failed as e:
actual = plex.attribute_translation[rating_type] logger.warning(e)
if not hasattr(item, actual) or getattr(item, actual) is None:
logger.warning(f"Overlay Warning: No {rating_type} found")
continue continue
text = getattr(item, actual) new_poster.paste(overlay_image, (0, 0), overlay_image)
if self.config.Cache: if overlay.image:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text) new_poster.paste(overlay.image, addon_box, overlay.image)
if per:
text = f"{int(text * 10)}%"
if flat and str(text).endswith(".0"):
text = str(text)[:-2]
overlay_image, _ = overlay.get_backdrop((canvas_width, canvas_height), text=str(text))
else: else:
overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.portrait overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.portrait
new_poster.paste(overlay_image, (0, 0), overlay_image) new_poster.paste(overlay_image, (0, 0), overlay_image)
else: else:
if overlay.has_coordinates(): if overlay.has_coordinates():
if overlay.portrait is not None: if overlay.portrait is not None:
@ -229,24 +237,15 @@ class Overlays:
over_name = sorted_weights[o][1] over_name = sorted_weights[o][1]
overlay = properties[over_name] overlay = properties[over_name]
if over_name.startswith("text"): if over_name.startswith("text"):
text = over_name[5:-1] image_box = overlay.image.size if overlay.image else None
if text in special_text_overlays: try:
per = text.endswith("%") overlay_image, addon_box = overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(over_name), new_cords=cord)
flat = text.endswith("#") except Failed as e:
rating_type = text[:-1] if per or flat else text logger.warning(e)
actual = plex.attribute_translation[rating_type] continue
if not hasattr(item, actual) or getattr(item, actual) is None:
logger.warning(f"Overlay Warning: No {rating_type} found")
continue
text = getattr(item, actual)
if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text)
if per:
text = f"{int(text * 10)}%"
if flat and str(text).endswith(".0"):
text = str(text)[:-2]
overlay_image, _ = overlay.get_backdrop((canvas_width, canvas_height), text=str(text), new_cords=cord)
new_poster.paste(overlay_image, (0, 0), overlay_image) new_poster.paste(overlay_image, (0, 0), overlay_image)
if overlay.image:
new_poster.paste(overlay.image, addon_box, overlay.image)
else: else:
if overlay.has_back: if overlay.has_back:
overlay_image, overlay_box = overlay.get_backdrop((canvas_width, canvas_height), box=overlay.image.size, new_cords=cord) overlay_image, overlay_box = overlay.get_backdrop((canvas_width, canvas_height), box=overlay.image.size, new_cords=cord)

View file

@ -93,6 +93,7 @@ parental_labels = [f"{t.capitalize()}:{v}" for t in parental_types for v in pare
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/" github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
previous_time = None previous_time = None
start_time = None start_time = None
special_text_overlays = [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%", "#"]]
def make_ordinal(n): def make_ordinal(n):
return f"{n}{'th' if 11 <= (n % 100) <= 13 else ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]}" return f"{n}{'th' if 11 <= (n % 100) <= 13 else ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]}"
@ -921,6 +922,8 @@ class Overlay:
self.font_name = None self.font_name = None
self.font_size = 36 self.font_size = 36
self.font_color = None self.font_color = None
self.addon_offset = None
self.addon_align = None
logger.debug("") logger.debug("")
logger.debug("Validating Method: overlay") logger.debug("Validating Method: overlay")
logger.debug(f"Value: {self.data}") logger.debug(f"Value: {self.data}")
@ -990,7 +993,7 @@ class Overlay:
time.sleep(1) time.sleep(1)
return image_path return image_path
if not self.name.startswith(("blur", "text")): if not self.name.startswith("blur"):
if "file" in self.data and self.data["file"]: if "file" in self.data and self.data["file"]:
self.path = self.data["file"] self.path = self.data["file"]
elif "git" in self.data and self.data["git"]: elif "git" in self.data and self.data["git"]:
@ -1000,7 +1003,9 @@ class Overlay:
elif "url" in self.data and self.data["url"]: elif "url" in self.data and self.data["url"]:
self.path = get_and_save_image(self.data["url"]) self.path = get_and_save_image(self.data["url"])
if self.name.startswith("blur"): if "|" in self.name:
raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'")
elif self.name.startswith("blur"):
try: try:
match = re.search("\\(([^)]+)\\)", self.name) match = re.search("\\(([^)]+)\\)", self.name)
if not match or 0 >= int(match.group(1)) > 100: if not match or 0 >= int(match.group(1)) > 100:
@ -1012,6 +1017,22 @@ class Overlay:
elif self.name.startswith("text"): elif self.name.startswith("text"):
if not self.has_coordinates() and not self.queue: if not self.has_coordinates() and not self.queue:
raise Failed(f"Overlay Error: overlay attribute's horizontal_offset and vertical_offset are required when using text") raise Failed(f"Overlay Error: overlay attribute's horizontal_offset and vertical_offset are required when using text")
if self.path:
if not os.path.exists(self.path):
raise Failed(f"Overlay Error: Text Overlay Addon Image not found at: {self.path}")
self.addon_offset = parse("Overlay", "addon_offset", self.data["addon_offset"], datatype="int", parent="overlay") if "addon_offset" in self.data else 0
self.addon_align = parse("Overlay", "addon_align", self.data["addon_align"], parent="overlay", options=["left", "right", "top", "bottom"]) if "addon_align" in self.data else "left"
image_compare = None
if self.config.Cache:
_, image_compare, _ = self.config.Cache.query_image_map(self.name, f"{self.library.image_table_name}_overlays")
overlay_size = os.stat(self.path).st_size
self.updated = not image_compare or str(overlay_size) != str(image_compare)
try:
self.image = Image.open(self.path).convert("RGBA")
if self.config.Cache:
self.config.Cache.update_image_map(self.name, f"{self.library.image_table_name}_overlays", self.name, overlay_size)
except OSError:
raise Failed(f"Overlay Error: overlay image {self.path} failed to load")
match = re.search("\\(([^)]+)\\)", self.name) match = re.search("\\(([^)]+)\\)", self.name)
if not match: if not match:
raise Failed(f"Overlay Error: failed to parse overlay text name: {self.name}") raise Failed(f"Overlay Error: failed to parse overlay text name: {self.name}")
@ -1043,12 +1064,11 @@ class Overlay:
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")
text = self.name[5:-1] text = self.name[5:-1]
if text not in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]: if text not in special_text_overlays:
self.portrait, _ = self.get_backdrop(portrait_dim, text=text) box = self.image.size if self.image else None
self.landscape, _ = self.get_backdrop(landscape_dim, text=text) self.portrait, self.portrait_box = self.get_backdrop(portrait_dim, box=box, text=text)
self.landscape, self.landscape_box = self.get_backdrop(landscape_dim, box=box, text=text)
else: else:
if "|" in self.name:
raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'")
if not self.path: if not self.path:
clean_name, _ = validate_filename(self.name) clean_name, _ = validate_filename(self.name)
self.path = os.path.join(library.overlay_folder, f"{clean_name}.png") self.path = os.path.join(library.overlay_folder, f"{clean_name}.png")
@ -1071,9 +1091,20 @@ class Overlay:
def get_backdrop(self, canvas_box, box=None, text=None, new_cords=None): def get_backdrop(self, canvas_box, box=None, text=None, new_cords=None):
overlay_image = None overlay_image = None
width = None
height = None
box_width = None
box_height = None
if text is not None: if text is not None:
_, _, width, height = self.get_text_size(text) _, _, width, height = self.get_text_size(text)
box = (width, height) if box is not None:
box_width, box_height = box
if self.addon_align in ["left", "right"]:
box = (width + box_width + self.addon_offset, height if height > box_height else box_height)
else:
box = (width if width > box_width else box_width, height + box_height + self.addon_offset)
else:
box = (width, height)
x_cord, y_cord = self.get_coordinates(canvas_box, box, new_cords=new_cords) x_cord, y_cord = self.get_coordinates(canvas_box, box, new_cords=new_cords)
if text is not None or self.has_back: if text is not None or self.has_back:
overlay_image = Image.new("RGBA", canvas_box, (255, 255, 255, 0)) overlay_image = Image.new("RGBA", canvas_box, (255, 255, 255, 0))
@ -1094,8 +1125,40 @@ class Overlay:
else: else:
drawing.rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width) drawing.rectangle(cords, fill=self.back_color, outline=self.back_line_color, width=self.back_line_width)
a_x_cord = None
a_y_cord = None
if box_width:
if self.addon_align == "left":
a_x_cord = x_cord
x_cord = a_x_cord + box_width + self.addon_offset
elif self.addon_align == "right":
a_x_cord = x_cord + box_width + self.addon_offset
elif width == box_width:
a_x_cord = x_cord
elif width < box_width:
a_x_cord = x_cord
x_cord = x_cord + ((box_width - width) / 2)
else:
a_x_cord = x_cord + ((box_width - width) / 2)
if self.addon_align == "top":
a_y_cord = y_cord
y_cord = a_y_cord + box_height + self.addon_offset
elif self.addon_align == "bottom":
a_y_cord = y_cord + box_height + self.addon_offset
elif height == box_height:
a_y_cord = y_cord
elif height < box_height:
a_y_cord = y_cord
y_cord = y_cord + ((box_height - height) / 2)
else:
a_y_cord = y_cord + ((box_height - height) / 2)
if text is not None: if text is not None:
drawing.text((x_cord, y_cord), text, font=self.font, fill=self.font_color, anchor="lt") drawing.text((x_cord, y_cord), text, font=self.font, fill=self.font_color, anchor="lt")
if a_x_cord is not None:
x_cord = a_x_cord
y_cord = a_y_cord
return overlay_image, (x_cord, y_cord) return overlay_image, (x_cord, y_cord)
def get_overlay_compare(self): def get_overlay_compare(self):

View file

@ -825,6 +825,7 @@ def run_playlists(config):
except Deleted as e: except Deleted as e:
logger.info(e) logger.info(e)
status[mapping_name]["status"] = "Deleted"
except NotScheduled as e: except NotScheduled as e:
logger.info(e) logger.info(e)
if str(e).endswith("and was deleted"): if str(e).endswith("and was deleted"):