[95] add alignment tools

pull/877/head
meisnate12 3 years ago
parent 65f8da3a21
commit 2b2b4e9a1f

@ -89,6 +89,6 @@ If you are unable to use the [Plex Meta Manager Discord Server](https://discord.
## IBRACORP Video Walkthrough ## IBRACORP Video Walkthrough
[IBRACORP](https://ibracorp.io/) made a video walkthough for installing Plex Meta Manager on unRAID. While you might not be using unRAID the video goes over many key aspects of Plex Meta Manager and can be a great place to start learning how to use the script. [IBRACORP](https://ibracorp.io/) made a video walkthrough for installing Plex Meta Manager on unRAID. While you might not be using unRAID the video goes over many key aspects of Plex Meta Manager and can be a great place to start learning how to use the script.
[![Plex Meta Manager](https://img.youtube.com/vi/dF69MNoot3w/0.jpg)](https://www.youtube.com/watch?v=dF69MNoot3w "Plex Meta Manager") [![Plex Meta Manager](https://img.youtube.com/vi/dF69MNoot3w/0.jpg)](https://www.youtube.com/watch?v=dF69MNoot3w "Plex Meta Manager")

Binary file not shown.

@ -1 +1 @@
1.16.5-develop94 1.16.5-develop95

@ -63,8 +63,10 @@ Each overlay definition needs to specify what overlay to use. This can happen in
| `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ | | `repo` | Location in the [Custom Repo](../config/settings.md#custom-repo) of the Overlay Image. | ❌ |
| `group` | Name of the Grouping for this overlay. **`weight` is required when using `group`** | ❌ | | `group` | Name of the Grouping for this overlay. **`weight` is required when using `group`** | ❌ |
| `weight` | Weight of this overlay in its group. **`group` is required when using `weight`** | ❌ | | `weight` | Weight of this overlay in its group. **`group` is required when using `weight`** | ❌ |
| `x_coordinate` | Top Left X Coordinate of this overlay. **`y_coordinate` is required when using `x_coordinate`** | ❌ | | `x_coordinate` | X Coordinate of this overlay. Can be a %. **`y_coordinate` is required when using `x_coordinate`** | ❌ |
| `y_coordinate` | Top Left Y Coordinate of this overlay. **`x_coordinate` is required when using `y_coordinate`** | ❌ | | `x_align` | Where the `x_coordinate` is calculated from. **Values:** `left`, `center`, `right` | ❌ |
| `y_coordinate` | Y Coordinate of this overlay. Can be a %. **`x_coordinate` is required when using `y_coordinate`** | ❌ |
| `y_align` | Where the `y_coordinate` is calculated from. **Values:** `top`, `center`, `bottom` | ❌ |
| `font` | System Font Filename or path to font file for the Text Overlay | ❌ | | `font` | System Font Filename or path to font file for the Text Overlay | ❌ |
| `font_size` | Font Size for the Text Overlay. **Value:** Integer greater than 0 | ❌ | | `font_size` | Font Size for the Text Overlay. **Value:** Integer greater than 0 | ❌ |
| `font_color` | Font Color for the Text Overlay. **Value:** Color Hex Code. ex `#00FF00` | ❌ | | `font_color` | Font Color for the Text Overlay. **Value:** Color Hex Code. ex `#00FF00` | ❌ |
@ -106,6 +108,8 @@ The `x_coordinate` and `y_coordinate` overlay attributes are required when using
You can add an items rating to the image by using `text(audience_rating)`, `text(critic_rating)`, or `text(user_rating)` You can add an items rating to the image by using `text(audience_rating)`, `text(critic_rating)`, or `text(user_rating)`
Default font `Salma.otf` provided by [Alifinart Studio](https://www.behance.net/alifinart)
```yaml ```yaml
overlays: overlays:
audience_rating: audience_rating:
@ -113,7 +117,7 @@ overlays:
name: text(audience_rating) name: text(audience_rating)
x_coordinate: 15 x_coordinate: 15
y_coordinate: 15 y_coordinate: 15
font: arial.ttf font: Salma.otf
font_size: 200 font_size: 200
plex_all: true plex_all: true
``` ```

@ -715,8 +715,8 @@ class ConfigFile:
logger.error("Config Error: operations must be a dictionary") logger.error("Config Error: operations must be a dictionary")
def error_check(attr, service): def error_check(attr, service):
logger.error(f"Config Error: Operation {attr} cannot be {params[attr]} without a successful {service} Connection")
params[attr] = None params[attr] = None
logger.error(f"Config Error: {attr} cannot be {params[attr]} without a successful {service} Connection")
for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]: for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]:
if params[mass_key] == "omdb" and self.OMDb is None: if params[mass_key] == "omdb" and self.OMDb is None:

@ -314,7 +314,6 @@ class MetadataFile(DataFile):
auto_list = {} auto_list = {}
all_keys = [] all_keys = []
dynamic_data = None dynamic_data = None
logger.debug(exclude)
def _check_dict(check_dict): def _check_dict(check_dict):
for ck, cv in check_dict.items(): for ck, cv in check_dict.items():
all_keys.append(ck) all_keys.append(ck)
@ -527,7 +526,7 @@ class MetadataFile(DataFile):
for template_name in template_names: for template_name in template_names:
if template_name not in self.templates: if template_name not in self.templates:
raise Failed(f"Config Error: {map_name} template: {template_name} not found") raise Failed(f"Config Error: {map_name} template: {template_name} not found")
if "<<value>>" in str(self.templates[template_name][0]) or f"<<{auto_type}>>" in str(self.templates[template_name][0]): if any([a in str(self.templates[template_name][0]) for a in ["<<value>>", "<<key>>", f"<<{auto_type}>>"]]):
has_var = True has_var = True
if not has_var: if not has_var:
raise Failed(f"Config Error: One {map_name} template: {template_names} is required to have the template variable <<value>>") raise Failed(f"Config Error: One {map_name} template: {template_names} is required to have the template variable <<value>>")

@ -160,15 +160,17 @@ class Overlays:
logger.error(f"{item_title[:60]:<60} | Overlay Error: No poster found") logger.error(f"{item_title[:60]:<60} | Overlay Error: No poster found")
elif changed_image or overlay_change: elif changed_image or overlay_change:
try: try:
image_width = 1920 if isinstance(item, Episode) else 1000
image_height = 1080 if isinstance(item, Episode) else 1500
new_poster = Image.open(poster.location if poster else has_original) \ new_poster = Image.open(poster.location if poster else has_original) \
.convert("RGBA") \ .convert("RGBA").resize((image_width, image_height), Image.ANTIALIAS)
.resize((1920, 1080) if isinstance(item, Episode) else (1000, 1500), Image.ANTIALIAS)
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))
for over_name in normal_overlays: for over_name in normal_overlays:
overlay = properties[over_name] overlay = properties[over_name]
if overlay.coordinates: if overlay.coordinates:
new_poster.paste(overlay.image, overlay.coordinates, overlay.image) new_poster.paste(overlay.image, overlay.get_coordinates(image_width, image_height), overlay.image)
else: else:
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS) new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, (0, 0), overlay.image) new_poster.paste(overlay.image, (0, 0), overlay.image)
@ -176,7 +178,6 @@ class Overlays:
drawing = ImageDraw.Draw(new_poster) drawing = ImageDraw.Draw(new_poster)
for over_name in text_names: for over_name in text_names:
overlay = properties[over_name] overlay = properties[over_name]
font = ImageFont.truetype(overlay.font, overlay.font_size) if overlay.font else None
text = over_name[5:-1] text = over_name[5:-1]
if text in ["audience_rating", "critic_rating", "user_rating"]: if text in ["audience_rating", "critic_rating", "user_rating"]:
rating_type = text rating_type = text
@ -187,7 +188,7 @@ class Overlays:
text = getattr(item, actual) text = getattr(item, actual)
if self.config.Cache: if self.config.Cache:
self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text) self.config.Cache.update_overlay_ratings(item.ratingKey, rating_type, text)
drawing.text(overlay.coordinates, str(text), font=font, fill=overlay.font_color) drawing.text(overlay.get_coordinates(image_width, image_height, text=str(text)), str(text), font=overlay.font, fill=overlay.font_color)
temp = os.path.join(self.library.overlay_folder, f"temp.png") temp = os.path.join(self.library.overlay_folder, f"temp.png")
new_poster.save(temp, "PNG") new_poster.save(temp, "PNG")
self.library.upload_poster(item, temp) self.library.upload_poster(item, temp)
@ -277,19 +278,6 @@ class Overlays:
for suppress_name in over_obj.suppress: for suppress_name in over_obj.suppress:
if suppress_name in properties and rk in properties[suppress_name].keys: if suppress_name in properties and rk in properties[suppress_name].keys:
properties[suppress_name].keys.remove(rk) properties[suppress_name].keys.remove(rk)
if not overlay_name.startswith(("blur", "text")):
image_compare = None
if self.config.Cache:
_, image_compare, _ = self.config.Cache.query_image_map(overlay_name, f"{self.library.image_table_name}_overlays")
overlay_size = os.stat(over_obj.path).st_size
over_obj.updated = not image_compare or str(overlay_size) != str(image_compare)
try:
over_obj.image = Image.open(over_obj.path).convert("RGBA")
if self.config.Cache:
self.config.Cache.update_image_map(overlay_name, f"{self.library.image_table_name}_overlays", overlay_name, overlay_size)
except OSError:
logger.error(f"Overlay Error: overlay image {over_obj.path} failed to load")
properties.pop(overlay_name)
for overlay_name, over_obj in properties.items(): for overlay_name, over_obj in properties.items():
for over_key in over_obj.keys: for over_key in over_obj.keys:

@ -439,7 +439,7 @@ class Plex(Library):
if label: if label:
label_id = next((c.key for c in self.get_tags("label") if c.title == label), None) label_id = next((c.key for c in self.get_tags("label") if c.title == label), None)
if label_id: if label_id:
args = f"{args}&{label_id}" args = f"{args}&label={label_id}"
else: else:
return [] return []
return self.get_filter_items(args) return self.get_filter_items(args)

@ -4,7 +4,7 @@ from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.audio import Album, Track from plexapi.audio import Album, Track
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.video import Season, Episode, Movie from plexapi.video import Season, Episode, Movie
from PIL import ImageColor from PIL import Image, ImageColor, ImageDraw, ImageFont
try: try:
import msvcrt import msvcrt
@ -840,12 +840,16 @@ class Overlay:
self.path = None self.path = None
self.coordinates = None self.coordinates = None
self.font = None self.font = None
self.font_name = None
self.font_size = 12 self.font_size = 12
self.font_color = None self.font_color = 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}")
if isinstance(self.data, dict): if not isinstance(self.data, dict):
self.data = {"name": str(self.data)}
logger.warning(f"Overlay Warning: No overlay attribute using mapping name {self.data} as the overlay name")
if "name" not in self.data or not self.data["name"]: if "name" not in self.data or not self.data["name"]:
raise Failed(f"Overlay Error: overlay must have the name attribute") raise Failed(f"Overlay Error: overlay must have the name attribute")
self.name = str(self.data["name"]) self.name = str(self.data["name"])
@ -860,18 +864,52 @@ class Overlay:
if ("group" in self.data or "weight" in self.data) and (self.weight is None or not self.group): if ("group" in self.data or "weight" in self.data) and (self.weight is None or not self.group):
raise Failed(f"Overlay Error: overlay attribute's group and weight must be used together") raise Failed(f"Overlay Error: overlay attribute's group and weight must be used together")
self.x_align = parse("Overlay", "x_align", self.data["x_align"], options=["left", "center", "right"]) if "x_align" in self.data else "left"
self.y_align = parse("Overlay", "y_align", self.data["y_align"], options=["top", "center", "bottom"]) if "y_align" in self.data else "top"
x_cord = None x_cord = None
y_cord = None
if "x_coordinate" in self.data and self.data["x_coordinate"] is not None: if "x_coordinate" in self.data and self.data["x_coordinate"] is not None:
x_cord = check_num(self.data["x_coordinate"]) x_cord = self.data["x_coordinate"]
if x_cord is None or x_cord < 0: per = False
raise Failed(f"Overlay Error: overlay x_coordinate: {self.data['x_coordinate']} must be a number 0 or greater") if str(x_cord).endswith("%"):
x_cord = x_cord[:-1]
per = True
x_cord = check_num(x_cord)
error = f"Overlay Error: overlay x_coordinate: {self.data['x_coordinate']} must be a number"
if x_cord is None:
raise Failed(error)
if self.x_align != "center" and not per and x_cord < 0:
raise Failed(f"{error} 0 or greater")
elif self.x_align != "center" and per and x_cord > 100:
raise Failed(f"{error} between 0% and 100%")
elif self.x_align == "center" and per and (x_cord > 50 or x_cord < -50):
raise Failed(f"{error} between -50% and 50%")
if per:
x_cord = f"{x_cord}%"
y_cord = None
if "y_coordinate" in self.data and self.data["y_coordinate"] is not None: if "y_coordinate" in self.data and self.data["y_coordinate"] is not None:
y_cord = check_num(self.data["y_coordinate"]) y_cord = self.data["y_coordinate"]
if y_cord is None or y_cord < 0: per = False
raise Failed(f"Overlay Error: overlay y_coordinate: {self.data['y_coordinate']} must be a number 0 or greater") if str(y_cord).endswith("%"):
y_cord = y_cord[:-1]
per = True
y_cord = check_num(y_cord)
error = f"Overlay Error: overlay y_coordinate: {self.data['y_coordinate']} must be a number"
if y_cord is None:
raise Failed(error)
if self.y_align != "center" and not per and y_cord < 0:
raise Failed(f"{error} 0 or greater")
elif self.y_align != "center" and per and y_cord > 100:
raise Failed(f"{error} between 0% and 100%")
elif self.y_align == "center" and per and (y_cord > 50 or y_cord < -50):
raise Failed(f"{error} between -50% and 50%")
if per:
y_cord = f"{y_cord}%"
if ("x_coordinate" in self.data or "y_coordinate" in self.data) and (x_cord is None or y_cord is None): if ("x_coordinate" in self.data or "y_coordinate" in self.data) and (x_cord is None or y_cord is None):
raise Failed(f"Overlay Error: overlay x_coordinate and overlay y_coordinate must be used together") raise Failed(f"Overlay Error: overlay x_coordinate and overlay y_coordinate must be used together")
if x_cord is not None or y_cord is not None: if x_cord is not None or y_cord is not None:
self.coordinates = (x_cord, y_cord) self.coordinates = (x_cord, y_cord)
@ -920,19 +958,22 @@ class Overlay:
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}")
self.name = f"text({match.group(1)})" self.name = f"text({match.group(1)})"
if "font" in self.data and self.data["font"]: if os.path.exists("Salma.otf"):
font = str(self.data["font"]) self.font_name = "Salma.otf"
if not os.path.exists(font):
fonts = get_system_fonts()
if font not in fonts:
raise Failed(f"Overlay Error: font: {font} not found. Options: {', '.join(fonts)}")
self.font = font
if "font_size" in self.data and self.data["font_size"] is not None: if "font_size" in self.data and self.data["font_size"] is not None:
font_size = check_num(self.data["font_size"]) font_size = check_num(self.data["font_size"])
if font_size is None or font_size < 1: if font_size is None or font_size < 1:
logger.error(f"Overlay Error: overlay font_size: {self.data['font_size']} invalid must be a greater than 0") logger.error(f"Overlay Error: overlay font_size: {self.data['font_size']} invalid must be a greater than 0")
else: else:
self.font_size = font_size self.font_size = font_size
if "font" in self.data and self.data["font"]:
font = str(self.data["font"])
if not os.path.exists(font):
fonts = get_system_fonts()
if font not in fonts:
raise Failed(f"Overlay Error: font: {font} not found. Options: {', '.join(fonts)}")
self.font_name = font
self.font = ImageFont.truetype(self.font_name, self.font_size)
if "font_color" in self.data and self.data["font_color"]: if "font_color" in self.data and self.data["font_color"]:
try: try:
color_str = self.data["font_color"] color_str = self.data["font_color"]
@ -948,18 +989,46 @@ class Overlay:
self.path = os.path.join(library.overlay_folder, f"{clean_name}.png") self.path = os.path.join(library.overlay_folder, f"{clean_name}.png")
if not os.path.exists(self.path): if not os.path.exists(self.path):
raise Failed(f"Overlay Error: Overlay Image not found at: {self.path}") raise Failed(f"Overlay Error: Overlay Image not found at: {self.path}")
else: image_compare = None
self.name = str(self.data) if self.config.Cache:
logger.warning(f"Overlay Warning: No overlay attribute using mapping name {self.data} as the overlay name") _, 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")
def get_overlay_compare(self): def get_overlay_compare(self):
output = self.name output = self.name
if self.group: if self.group:
output += f"{self.group}{self.weight}" output += f"{self.group}{self.weight}"
if self.coordinates: if self.coordinates:
output += str(self.coordinates) output += f"{self.coordinates}{self.x_align}{self.y_align}"
if self.font: if self.font_name:
output += f"{self.font}{self.font_size}" output += f"{self.font_name}{self.font_size}"
if self.font_color: if self.font_color:
output += str(self.font_color) output += str(self.font_color)
return output return output
def get_coordinates(self, image_width, image_length, text=None):
if text:
_, _, width, height = ImageDraw.Draw(Image.new("RGB", (0, 0))).textbbox((0, 0), text, font=self.font)
else:
width, height = self.image.size
x_cord, y_cord = self.coordinates
if str(x_cord).endswith("%"):
x_cord = image_width * 0.01 * int(x_cord[:-1])
if str(y_cord).endswith("%"):
y_cord = image_length * 0.01 * int(y_cord[:-1])
if self.x_align == "right":
x_cord = image_width - width - x_cord
elif self.x_align == "center":
x_cord = (image_width / 2) - (width / 2) + x_cord
if self.x_align == "bottom":
y_cord = image_length - height - y_cord
elif self.x_align == "center":
y_cord = (image_length / 2) - (height / 2) + y_cord
return x_cord, y_cord

@ -444,7 +444,7 @@ def run_libraries(config):
config.Cache.delete_list_ids(list_key) config.Cache.delete_list_ids(list_key)
list_key = config.Cache.update_list_cache("library", library.mapping_name, expired, 1) list_key = config.Cache.update_list_cache("library", library.mapping_name, expired, 1)
config.Cache.update_list_ids(list_key, [(i.ratingKey, i.guid) for i in temp_items]) config.Cache.update_list_ids(list_key, [(i.ratingKey, i.guid) for i in temp_items])
if not library.is_other and not library.is_music: if not library.is_music:
logger.info("") logger.info("")
logger.separator(f"Mapping {library.name} Library", space=False, border=False) logger.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("") logger.info("")

Loading…
Cancel
Save