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"):