mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
[45] addon images for text overlays
This commit is contained in:
parent
e41e3e7280
commit
25b32453ea
5 changed files with 136 additions and 47 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.17.0-develop44
|
||||
1.17.0-develop45
|
||||
|
|
|
@ -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 | ❌ |
|
||||
| `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 | ❌ |
|
||||
| `addon_offset` | Text Addon Image Offset from the text.<br>**`addon_offset` Only works with text overlays**<br>**Value:** Integer 0 or greater | ❌ |
|
||||
| `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` | ❌ |
|
||||
|
||||
* 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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"):
|
||||
|
|
Loading…
Reference in a new issue