[95] add alignment tools

This commit is contained in:
meisnate12 2022-05-15 16:29:54 -04:00
parent 65f8da3a21
commit 2b2b4e9a1f
10 changed files with 197 additions and 137 deletions

View file

@ -89,6 +89,6 @@ If you are unable to use the [Plex Meta Manager Discord Server](https://discord.
## 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")

BIN
Salma.otf Normal file

Binary file not shown.

View file

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

View file

@ -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. | ❌ |
| `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`** | ❌ |
| `x_coordinate` | Top Left X Coordinate of this overlay. **`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_coordinate` | X Coordinate of this overlay. Can be a %. **`y_coordinate` is required when using `x_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_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` | ❌ |
@ -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)`
Default font `Salma.otf` provided by [Alifinart Studio](https://www.behance.net/alifinart)
```yaml
overlays:
audience_rating:
@ -113,7 +117,7 @@ overlays:
name: text(audience_rating)
x_coordinate: 15
y_coordinate: 15
font: arial.ttf
font: Salma.otf
font_size: 200
plex_all: true
```

View file

@ -715,8 +715,8 @@ class ConfigFile:
logger.error("Config Error: operations must be a dictionary")
def error_check(attr, service):
logger.error(f"Config Error: Operation {attr} cannot be {params[attr]} without a successful {service} Connection")
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"]:
if params[mass_key] == "omdb" and self.OMDb is None:

View file

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

View file

@ -160,15 +160,17 @@ class Overlays:
logger.error(f"{item_title[:60]:<60} | Overlay Error: No poster found")
elif changed_image or overlay_change:
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) \
.convert("RGBA") \
.resize((1920, 1080) if isinstance(item, Episode) else (1000, 1500), Image.ANTIALIAS)
.convert("RGBA").resize((image_width, image_height), Image.ANTIALIAS)
if blur_num > 0:
new_poster = new_poster.filter(ImageFilter.GaussianBlur(blur_num))
for over_name in normal_overlays:
overlay = properties[over_name]
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:
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, (0, 0), overlay.image)
@ -176,7 +178,6 @@ class Overlays:
drawing = ImageDraw.Draw(new_poster)
for over_name in text_names:
overlay = properties[over_name]
font = ImageFont.truetype(overlay.font, overlay.font_size) if overlay.font else None
text = over_name[5:-1]
if text in ["audience_rating", "critic_rating", "user_rating"]:
rating_type = text
@ -187,7 +188,7 @@ class Overlays:
text = getattr(item, actual)
if self.config.Cache:
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")
new_poster.save(temp, "PNG")
self.library.upload_poster(item, temp)
@ -277,19 +278,6 @@ class Overlays:
for suppress_name in over_obj.suppress:
if suppress_name in properties and rk in properties[suppress_name].keys:
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 over_key in over_obj.keys:

View file

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

View file

@ -4,7 +4,7 @@ from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.audio import Album, Track
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.video import Season, Episode, Movie
from PIL import ImageColor
from PIL import Image, ImageColor, ImageDraw, ImageFont
try:
import msvcrt
@ -840,126 +840,195 @@ class Overlay:
self.path = None
self.coordinates = None
self.font = None
self.font_name = None
self.font_size = 12
self.font_color = None
logger.debug("")
logger.debug("Validating Method: overlay")
logger.debug(f"Value: {self.data}")
if isinstance(self.data, dict):
if "name" not in self.data or not self.data["name"]:
raise Failed(f"Overlay Error: overlay must have the name attribute")
self.name = str(self.data["name"])
if "group" in self.data and self.data["group"]:
self.group = str(self.data["group"])
if "weight" in self.data and self.data["weight"] is not None:
pri = check_num(self.data["weight"])
if pri is None:
raise Failed(f"Overlay Error: overlay weight must be a number")
self.weight = pri
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")
x_cord = None
y_cord = None
if "x_coordinate" in self.data and self.data["x_coordinate"] is not None:
x_cord = check_num(self.data["x_coordinate"])
if x_cord is None or x_cord < 0:
raise Failed(f"Overlay Error: overlay x_coordinate: {self.data['x_coordinate']} must be a number 0 or greater")
if "y_coordinate" in self.data and self.data["y_coordinate"] is not None:
y_cord = check_num(self.data["y_coordinate"])
if y_cord is None or y_cord < 0:
raise Failed(f"Overlay Error: overlay y_coordinate: {self.data['y_coordinate']} must be a number 0 or greater")
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")
if x_cord is not None or y_cord is not None:
self.coordinates = (x_cord, y_cord)
def get_and_save_image(image_url):
response = self.config.get(image_url)
if response.status_code >= 400:
raise Failed(f"Overlay Error: Overlay Image not found at: {image_url}")
if "Content-Type" not in response.headers or response.headers["Content-Type"] != "image/png":
raise Failed(f"Overlay Error: Overlay Image not a png: {image_url}")
if not os.path.exists(library.overlay_folder) or not os.path.isdir(library.overlay_folder):
os.makedirs(library.overlay_folder, exist_ok=False)
logger.info(f"Creating Overlay Folder found at: {library.overlay_folder}")
clean_image_name, _ = validate_filename(self.name)
image_path = os.path.join(library.overlay_folder, f"{clean_image_name}.png")
if os.path.exists(image_path):
os.remove(image_path)
with open(image_path, "wb") as handler:
handler.write(response.content)
while is_locked(image_path):
time.sleep(1)
return image_path
if not self.name.startswith(("blur", "text")):
if "file" in self.data and self.data["file"]:
self.path = self.data["file"]
elif "git" in self.data and self.data["git"]:
self.path = get_and_save_image(f"{github_base}{self.data['git']}.png")
elif "repo" in self.data and self.data["repo"]:
self.path = get_and_save_image(f"{self.config.custom_repo}{self.data['repo']}.png")
elif "url" in self.data and self.data["url"]:
self.path = get_and_save_image(self.data["url"])
if self.name.startswith("blur"):
try:
match = re.search("\\(([^)]+)\\)", self.name)
if not match or 0 >= int(match.group(1)) > 100:
raise ValueError
self.name = f"blur({match.group(1)})"
except ValueError:
logger.error(f"Overlay Error: failed to parse overlay blur name: {self.name} defaulting to blur(50)")
self.name = "blur(50)"
elif self.name.startswith("text"):
if not self.coordinates:
raise Failed(f"Overlay Error: overlay attribute's x_coordinate and y_coordinate are required when using text")
match = re.search("\\(([^)]+)\\)", self.name)
if not match:
raise Failed(f"Overlay Error: failed to parse overlay text name: {self.name}")
self.name = f"text({match.group(1)})"
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 = font
if "font_size" in self.data and self.data["font_size"] is not None:
font_size = check_num(self.data["font_size"])
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")
else:
self.font_size = font_size
if "font_color" in self.data and self.data["font_color"]:
try:
color_str = self.data["font_color"]
color_str = color_str if color_str.startswith("#") else f"#{color_str}"
self.font_color = ImageColor.getcolor(color_str, "RGB")
except ValueError:
logger.error(f"Overlay Error: overlay color: {self.data['color']} invalid")
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")
if not os.path.exists(self.path):
raise Failed(f"Overlay Error: Overlay Image not found at: {self.path}")
else:
self.name = str(self.data)
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"]:
raise Failed(f"Overlay Error: overlay must have the name attribute")
self.name = str(self.data["name"])
if "group" in self.data and self.data["group"]:
self.group = str(self.data["group"])
if "weight" in self.data and self.data["weight"] is not None:
pri = check_num(self.data["weight"])
if pri is None:
raise Failed(f"Overlay Error: overlay weight must be a number")
self.weight = pri
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")
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
if "x_coordinate" in self.data and self.data["x_coordinate"] is not None:
x_cord = self.data["x_coordinate"]
per = False
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:
y_cord = self.data["y_coordinate"]
per = False
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):
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:
self.coordinates = (x_cord, y_cord)
def get_and_save_image(image_url):
response = self.config.get(image_url)
if response.status_code >= 400:
raise Failed(f"Overlay Error: Overlay Image not found at: {image_url}")
if "Content-Type" not in response.headers or response.headers["Content-Type"] != "image/png":
raise Failed(f"Overlay Error: Overlay Image not a png: {image_url}")
if not os.path.exists(library.overlay_folder) or not os.path.isdir(library.overlay_folder):
os.makedirs(library.overlay_folder, exist_ok=False)
logger.info(f"Creating Overlay Folder found at: {library.overlay_folder}")
clean_image_name, _ = validate_filename(self.name)
image_path = os.path.join(library.overlay_folder, f"{clean_image_name}.png")
if os.path.exists(image_path):
os.remove(image_path)
with open(image_path, "wb") as handler:
handler.write(response.content)
while is_locked(image_path):
time.sleep(1)
return image_path
if not self.name.startswith(("blur", "text")):
if "file" in self.data and self.data["file"]:
self.path = self.data["file"]
elif "git" in self.data and self.data["git"]:
self.path = get_and_save_image(f"{github_base}{self.data['git']}.png")
elif "repo" in self.data and self.data["repo"]:
self.path = get_and_save_image(f"{self.config.custom_repo}{self.data['repo']}.png")
elif "url" in self.data and self.data["url"]:
self.path = get_and_save_image(self.data["url"])
if self.name.startswith("blur"):
try:
match = re.search("\\(([^)]+)\\)", self.name)
if not match or 0 >= int(match.group(1)) > 100:
raise ValueError
self.name = f"blur({match.group(1)})"
except ValueError:
logger.error(f"Overlay Error: failed to parse overlay blur name: {self.name} defaulting to blur(50)")
self.name = "blur(50)"
elif self.name.startswith("text"):
if not self.coordinates:
raise Failed(f"Overlay Error: overlay attribute's x_coordinate and y_coordinate are required when using text")
match = re.search("\\(([^)]+)\\)", self.name)
if not match:
raise Failed(f"Overlay Error: failed to parse overlay text name: {self.name}")
self.name = f"text({match.group(1)})"
if os.path.exists("Salma.otf"):
self.font_name = "Salma.otf"
if "font_size" in self.data and self.data["font_size"] is not None:
font_size = check_num(self.data["font_size"])
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")
else:
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"]:
try:
color_str = self.data["font_color"]
color_str = color_str if color_str.startswith("#") else f"#{color_str}"
self.font_color = ImageColor.getcolor(color_str, "RGB")
except ValueError:
logger.error(f"Overlay Error: overlay color: {self.data['color']} invalid")
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")
if not os.path.exists(self.path):
raise Failed(f"Overlay Error: Overlay Image not found at: {self.path}")
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")
def get_overlay_compare(self):
output = self.name
if self.group:
output += f"{self.group}{self.weight}"
if self.coordinates:
output += str(self.coordinates)
if self.font:
output += f"{self.font}{self.font_size}"
output += f"{self.coordinates}{self.x_align}{self.y_align}"
if self.font_name:
output += f"{self.font_name}{self.font_size}"
if self.font_color:
output += str(self.font_color)
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

View file

@ -444,7 +444,7 @@ def run_libraries(config):
config.Cache.delete_list_ids(list_key)
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])
if not library.is_other and not library.is_music:
if not library.is_music:
logger.info("")
logger.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")