diff --git a/VERSION b/VERSION index 29d3b9ee..2c5222ee 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.0-develop44 +1.17.0-develop45 diff --git a/docs/metadata/overlay.md b/docs/metadata/overlay.md index a00e87ff..75633a19 100644 --- a/docs/metadata/overlay.md +++ b/docs/metadata/overlay.md @@ -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.
**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 | ❌ | +| `addon_offset` | Text Addon Image Offset from the text.
**`addon_offset` Only works with text overlays**
**Value:** Integer 0 or greater | ❌ | +| `addon_align` | Text Addon Image Alignment in relation to the text.
**`addon_align` Only works with text overlays**
**Values:** `left`, `right`, `top`, `bottom` | ❌ | * 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 ``` +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 are defined by the name given to the `group` attribute. Only one overlay with the highest weight per group will be applied. diff --git a/modules/overlays.py b/modules/overlays.py index 2b3cab2b..d1f300e1 100644 --- a/modules/overlays.py +++ b/modules/overlays.py @@ -9,8 +9,6 @@ from PIL import Image, ImageFilter logger = util.logger -special_text_overlays = [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%", "#"]] - class Overlays: def __init__(self, config, library): self.config = config @@ -120,7 +118,7 @@ class Overlays: if self.config.Cache: 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] if rating_type.endswith(("%", "#")): rating_type = rating_type[:-1] @@ -182,30 +180,40 @@ class Overlays: if blur_num > 0: 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: overlay = properties[over_name] if over_name.startswith("text"): - text = over_name[5:-1] - if text in special_text_overlays: - per = text.endswith("%") - flat = text.endswith("#") - rating_type = text[:-1] if per or flat else text - actual = plex.attribute_translation[rating_type] - if not hasattr(item, actual) or getattr(item, actual) is None: - logger.warning(f"Overlay Warning: No {rating_type} found") + if over_name[5:-1] in util.special_text_overlays: + image_box = overlay.image.size if overlay.image else None + try: + overlay_image, addon_box = overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(over_name)) + except Failed as e: + logger.warning(e) 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_poster.paste(overlay_image, (0, 0), overlay_image) + if overlay.image: + new_poster.paste(overlay.image, addon_box, overlay.image) else: 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: if overlay.has_coordinates(): if overlay.portrait is not None: @@ -229,24 +237,15 @@ class Overlays: over_name = sorted_weights[o][1] overlay = properties[over_name] if over_name.startswith("text"): - text = over_name[5:-1] - if text in special_text_overlays: - per = text.endswith("%") - flat = text.endswith("#") - rating_type = text[:-1] if per or flat else text - actual = plex.attribute_translation[rating_type] - 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) + image_box = overlay.image.size if overlay.image else None + try: + overlay_image, addon_box = overlay.get_backdrop((canvas_width, canvas_height), box=image_box, text=get_text(over_name), new_cords=cord) + except Failed as e: + logger.warning(e) + continue new_poster.paste(overlay_image, (0, 0), overlay_image) + if overlay.image: + new_poster.paste(overlay.image, addon_box, overlay.image) else: if overlay.has_back: overlay_image, overlay_box = overlay.get_backdrop((canvas_width, canvas_height), box=overlay.image.size, new_cords=cord) diff --git a/modules/util.py b/modules/util.py index 363bbeb1..ccafdefd 100644 --- a/modules/util.py +++ b/modules/util.py @@ -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/" previous_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): 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_size = 36 self.font_color = None + self.addon_offset = None + self.addon_align = None logger.debug("") logger.debug("Validating Method: overlay") logger.debug(f"Value: {self.data}") @@ -990,7 +993,7 @@ class Overlay: time.sleep(1) 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"]: self.path = self.data["file"] elif "git" in self.data and self.data["git"]: @@ -1000,7 +1003,9 @@ class Overlay: elif "url" in self.data and 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: match = re.search("\\(([^)]+)\\)", self.name) if not match or 0 >= int(match.group(1)) > 100: @@ -1012,6 +1017,22 @@ class Overlay: elif self.name.startswith("text"): 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") + 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) if not match: raise Failed(f"Overlay Error: failed to parse overlay text name: {self.name}") @@ -1043,12 +1064,11 @@ class Overlay: except ValueError: raise Failed(f"Overlay Error: overlay font_color: {self.data['font_color']} invalid") text = self.name[5:-1] - if text not in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]: - self.portrait, _ = self.get_backdrop(portrait_dim, text=text) - self.landscape, _ = self.get_backdrop(landscape_dim, text=text) + if text not in special_text_overlays: + box = self.image.size if self.image else None + 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: - if "|" in self.name: - raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'") if not self.path: clean_name, _ = validate_filename(self.name) 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): overlay_image = None + width = None + height = None + box_width = None + box_height = None if text is not None: _, _, 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) if text is not None or self.has_back: overlay_image = Image.new("RGBA", canvas_box, (255, 255, 255, 0)) @@ -1094,8 +1125,40 @@ class Overlay: else: 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: 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) def get_overlay_compare(self): diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 39845269..67681530 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -825,6 +825,7 @@ def run_playlists(config): except Deleted as e: logger.info(e) + status[mapping_name]["status"] = "Deleted" except NotScheduled as e: logger.info(e) if str(e).endswith("and was deleted"):