[20] add overlay queues

pull/915/head
meisnate12 2 years ago
parent f8518e7b46
commit 81239286b3

@ -1 +1 @@
1.17.0-develop19 1.17.0-develop20

@ -18,6 +18,7 @@ class Library(ABC):
self.Notifiarr = None self.Notifiarr = None
self.collections = [] self.collections = []
self.metadatas = [] self.metadatas = []
self.queue_names = []
self.metadata_files = [] self.metadata_files = []
self.overlay_files = [] self.overlay_files = []
self.movie_map = {} self.movie_map = {}
@ -132,7 +133,9 @@ class Library(ABC):
if not operations_only and not collection_only: if not operations_only and not collection_only:
for file_type, overlay_file, temp_vars, asset_directory in self.overlay_path: for file_type, overlay_file, temp_vars, asset_directory in self.overlay_path:
try: try:
self.overlay_files.append(OverlayFile(self.config, self, file_type, overlay_file, temp_vars, asset_directory)) overlay_obj = OverlayFile(self.config, self, file_type, overlay_file, temp_vars, asset_directory)
self.overlay_files.append(overlay_obj)
self.queue_names.extend([q for q in overlay_obj.queues])
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)

@ -88,10 +88,13 @@ class DataFile:
if response.status_code >= 400: if response.status_code >= 400:
raise Failed(f"URL Error: No file found at {content_path}") raise Failed(f"URL Error: No file found at {content_path}")
yaml = YAML(input_data=response.content, check_empty=True) yaml = YAML(input_data=response.content, check_empty=True)
elif os.path.exists(os.path.abspath(file_path)):
yaml = YAML(path=os.path.abspath(file_path), check_empty=True)
else: else:
raise Failed(f"File Error: File does not exist {os.path.abspath(file_path)}") if not file_path.endswith(".yml"):
file_path = f"{file_path}.yml"
if os.path.exists(os.path.abspath(file_path)):
yaml = YAML(path=os.path.abspath(file_path), check_empty=True)
else:
raise Failed(f"File Error: File does not exist {os.path.abspath(file_path)}")
return yaml.data return yaml.data
def apply_template(self, name, data, template_call): def apply_template(self, name, data, template_call):
@ -1190,6 +1193,7 @@ class OverlayFile(DataFile):
data = self.load_file(self.type, self.path) data = self.load_file(self.type, self.path)
self.overlays = get_dict("overlays", data) self.overlays = get_dict("overlays", data)
self.templates = get_dict("templates", data) self.templates = get_dict("templates", data)
self.queues = get_dict("queues", data, library.queue_names)
self.external_templates(data) self.external_templates(data)
if not self.overlays: if not self.overlays:
raise Failed("YAML Error: overlays attribute is required") raise Failed("YAML Error: overlays attribute is required")

@ -10,6 +10,8 @@ from PIL import Image, ImageFilter
logger = util.logger logger = util.logger
special_text_overlays = [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%", "#"]]
class Overlays: class Overlays:
def __init__(self, config, library): def __init__(self, config, library):
self.config = config self.config = config
@ -42,9 +44,10 @@ class Overlays:
]) ])
key_to_overlays = {} key_to_overlays = {}
queues = {}
properties = None properties = None
if not self.library.remove_overlays: if not self.library.remove_overlays:
key_to_overlays, properties = self.compile_overlays() key_to_overlays, properties, queues = self.compile_overlays()
ignore_list = [rk for rk in key_to_overlays] ignore_list = [rk for rk in key_to_overlays]
remove_overlays = self.get_overlay_items(ignore=ignore_list) remove_overlays = self.get_overlay_items(ignore=ignore_list)
@ -87,17 +90,26 @@ class Overlays:
compare_names = {properties[ov].get_overlay_compare(): ov for ov in over_names} compare_names = {properties[ov].get_overlay_compare(): ov for ov in over_names}
blur_num = 0 blur_num = 0
text_names = [] applied_names = []
normal_overlays = [] queue_overlays = {}
for over_name in over_names: for over_name in over_names:
if over_name.startswith("blur"): if over_name.startswith("blur"):
blur_test = int(re.search("\\(([^)]+)\\)", over_name).group(1)) blur_test = int(re.search("\\(([^)]+)\\)", over_name).group(1))
if blur_test > blur_num: if blur_test > blur_num:
blur_num = blur_test blur_num = blur_test
elif over_name.startswith("text"): elif properties[over_name].queue:
text_names.append(over_name) if properties[over_name].queue not in queue_overlays:
queue_overlays[properties[over_name].queue] = {}
queue_overlays[properties[over_name].queue][properties[over_name].weight] = properties[over_name]
else: else:
normal_overlays.append(over_name) applied_names.append(over_name)
for over_name in applied_names:
overlay = properties[over_name]
if overlay.queue:
if overlay.queue not in queue_overlays:
queue_overlays[overlay.queue] = {}
queue_overlays[overlay.queue][overlay.weight] = over_name
overlay_change = False if has_overlay else True overlay_change = False if has_overlay else True
if not overlay_change: if not overlay_change:
@ -110,10 +122,12 @@ class Overlays:
if compare_name not in overlay_compare or properties[original_name].updated: if compare_name not in overlay_compare or properties[original_name].updated:
overlay_change = True overlay_change = True
if text_names and self.config.Cache: if self.config.Cache:
for over_name in text_names: for over_name in over_names:
rating_type = over_name[5:-1] if over_name in special_text_overlays:
if rating_type in ["audience_rating", "critic_rating", "user_rating"]: rating_type = over_name[5:-1]
if rating_type.endswith(("%", "#")):
rating_type = rating_type[:-1]
cache_rating = self.config.Cache.query_overlay_ratings(item.ratingKey, rating_type) cache_rating = self.config.Cache.query_overlay_ratings(item.ratingKey, rating_type)
actual = plex.attribute_translation[rating_type] actual = plex.attribute_translation[rating_type]
if not hasattr(item, actual) or getattr(item, actual) is None: if not hasattr(item, actual) or getattr(item, actual) is None:
@ -171,39 +185,79 @@ class Overlays:
.convert("RGB").resize((canvas_width, canvas_height), Image.ANTIALIAS) .convert("RGB").resize((canvas_width, canvas_height), 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 applied_names:
overlay = properties[over_name] overlay = properties[over_name]
if overlay.has_coordinates(): if over_name.startswith("text"):
if overlay.portrait is not None: 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))
else:
overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.portrait 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)
overlay_box = overlay.landscape_box if isinstance(item, Episode) else overlay.portrait_box
new_poster.paste(overlay.image, overlay_box, overlay.image)
else:
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, (0, 0), overlay.image)
for over_name in text_names:
overlay = properties[over_name]
text = over_name[5:-1]
if text in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%", "#"]]:
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))
else: else:
overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.portrait if overlay.has_coordinates():
new_poster.paste(overlay_image, (0, 0), overlay_image) if overlay.portrait is not None:
overlay_image = overlay.landscape if isinstance(item, Episode) else overlay.portrait
new_poster.paste(overlay_image, (0, 0), overlay_image)
overlay_box = overlay.landscape_box if isinstance(item, Episode) else overlay.portrait_box
new_poster.paste(overlay.image, overlay_box, overlay.image)
else:
new_poster = new_poster.resize(overlay.image.size, Image.ANTIALIAS)
new_poster.paste(overlay.image, (0, 0), overlay.image)
for queue, weights in queue_overlays.items():
if queue not in queues:
logger.error(f"Overlay Error: no queue {queue} found")
continue
cords = queues[queue]
sorted_weights = sorted(weights.items(), reverse=True)
for o, cord in enumerate(cords):
if len(sorted_weights) <= o:
break
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)
new_poster.paste(overlay_image, (0, 0), 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)
new_poster.paste(overlay_image, (0, 0), overlay_image)
else:
overlay_box = (cord[1], cord[3])
new_poster.paste(overlay.image, overlay_box, overlay.image)
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)
@ -243,8 +297,16 @@ class Overlays:
properties = {} properties = {}
overlay_groups = {} overlay_groups = {}
key_to_overlays = {} key_to_overlays = {}
queues = {}
for overlay_file in self.library.overlay_files: for overlay_file in self.library.overlay_files:
for k, v in overlay_file.queues.items():
if not isinstance(v, list):
raise Failed(f"Overlay Error: Queue: {k} must be a list")
try:
queues[k] = [util.parse_cords(q, f"{k} queue", required=True) for q in v]
except Failed as e:
logger.error(e)
for k, v in overlay_file.overlays.items(): for k, v in overlay_file.overlays.items():
try: try:
builder = CollectionBuilder(self.config, overlay_file, k, v, library=self.library, overlay=True) builder = CollectionBuilder(self.config, overlay_file, k, v, library=self.library, overlay=True)
@ -317,7 +379,7 @@ class Overlays:
for v in gv: for v in gv:
if final != v: if final != v:
key_to_overlays[over_key][1].remove(v) key_to_overlays[over_key][1].remove(v)
return key_to_overlays, properties return key_to_overlays, properties, queues
def find_poster_url(self, item): def find_poster_url(self, item):
try: try:

@ -842,6 +842,61 @@ class YAML:
portrait_dim = (1000, 1500) portrait_dim = (1000, 1500)
landscape_dim = (1920, 1080) landscape_dim = (1920, 1080)
def parse_cords(data, parent, required=False):
horizontal_align = parse("Overlay", "horizontal_align", data["horizontal_align"], parent=parent,
options=["left", "center", "right"]) if "horizontal_align" in data else "left"
vertical_align = parse("Overlay", "vertical_align", data["vertical_align"], parent=parent,
options=["top", "center", "bottom"]) if "vertical_align" in data else "top"
horizontal_offset = None
if "horizontal_offset" in data and data["horizontal_offset"] is not None:
x_off = data["horizontal_offset"]
per = False
if str(x_off).endswith("%"):
x_off = x_off[:-1]
per = True
x_off = check_num(x_off)
error = f"Overlay Error: {parent} horizontal_offset: {data['horizontal_offset']} must be a number"
if x_off is None:
raise Failed(error)
if horizontal_align != "center" and not per and x_off < 0:
raise Failed(f"{error} 0 or greater")
elif horizontal_align != "center" and per and (x_off > 100 or x_off < 0):
raise Failed(f"{error} between 0% and 100%")
elif horizontal_align == "center" and per and (x_off > 50 or x_off < -50):
raise Failed(f"{error} between -50% and 50%")
horizontal_offset = f"{x_off}%" if per else x_off
if horizontal_offset is None and horizontal_align == "center":
horizontal_offset = 0
if required and horizontal_offset is None:
raise Failed(f"Overlay Error: {parent} horizontal_offset is required")
vertical_offset = None
if "vertical_offset" in data and data["vertical_offset"] is not None:
y_off = data["vertical_offset"]
per = False
if str(y_off).endswith("%"):
y_off = y_off[:-1]
per = True
y_off = check_num(y_off)
error = f"Overlay Error: {parent} vertical_offset: {data['vertical_offset']} must be a number"
if y_off is None:
raise Failed(error)
if vertical_align != "center" and not per and y_off < 0:
raise Failed(f"{error} 0 or greater")
elif vertical_align != "center" and per and (y_off > 100 or y_off < 0):
raise Failed(f"{error} between 0% and 100%")
elif vertical_align == "center" and per and (y_off > 50 or y_off < -50):
raise Failed(f"{error} between -50% and 50%")
vertical_offset = f"{y_off}%" if per else y_off
if vertical_offset is None and vertical_align == "center":
vertical_offset = 0
if required and vertical_offset is None:
raise Failed(f"Overlay Error: {parent} vertical_offset is required")
return horizontal_align, horizontal_offset, vertical_align, vertical_offset
class Overlay: class Overlay:
def __init__(self, config, library, overlay_data, suppress): def __init__(self, config, library, overlay_data, suppress):
self.config = config self.config = config
@ -856,6 +911,7 @@ class Overlay:
self.portrait = None self.portrait = None
self.portrait_box = None self.portrait_box = None
self.group = None self.group = None
self.queue = None
self.weight = None self.weight = None
self.path = None self.path = None
self.font = None self.font = None
@ -875,55 +931,17 @@ class Overlay:
if "group" in self.data and self.data["group"]: if "group" in self.data and self.data["group"]:
self.group = str(self.data["group"]) self.group = str(self.data["group"])
if "queue" in self.data and self.data["queue"]:
self.queue = str(self.data["queue"])
if "weight" in self.data: if "weight" in self.data:
self.weight = parse("Overlay", "weight", self.data["weight"], datatype="int", parent="overlay") self.weight = parse("Overlay", "weight", self.data["weight"], datatype="int", parent="overlay", minimum=0)
if ("group" in self.data or "weight" in self.data) and (self.weight is None or not self.group): if "group" 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 requires the weight attribute")
elif "queue" in self.data and (self.weight is None or not self.queue):
self.horizontal_align = parse("Overlay", "horizontal_align", self.data["horizontal_align"], parent="overlay", options=["left", "center", "right"]) if "horizontal_align" in self.data else "left" raise Failed(f"Overlay Error: overlay attribute's queue requires the weight attribute")
self.vertical_align = parse("Overlay", "vertical_align", self.data["vertical_align"], parent="overlay", options=["top", "center", "bottom"]) if "vertical_align" in self.data else "top" elif self.group and self.queue:
raise Failed(f"Overlay Error: overlay attribute's group and queue cannot be used together")
self.horizontal_offset = None self.horizontal_align, self.horizontal_offset, self.vertical_align, self.vertical_offset = parse_cords(self.data, "overlay")
if "horizontal_offset" in self.data and self.data["horizontal_offset"] is not None:
x_off = self.data["horizontal_offset"]
per = False
if str(x_off).endswith("%"):
x_off = x_off[:-1]
per = True
x_off = check_num(x_off)
error = f"Overlay Error: overlay horizontal_offset: {self.data['horizontal_offset']} must be a number"
if x_off is None:
raise Failed(error)
if self.horizontal_align != "center" and not per and x_off < 0:
raise Failed(f"{error} 0 or greater")
elif self.horizontal_align != "center" and per and (x_off > 100 or x_off < 0):
raise Failed(f"{error} between 0% and 100%")
elif self.horizontal_align == "center" and per and (x_off > 50 or x_off < -50):
raise Failed(f"{error} between -50% and 50%")
self.horizontal_offset = f"{x_off}%" if per else x_off
if self.horizontal_offset is None and self.horizontal_align == "center":
self.horizontal_offset = 0
self.vertical_offset = None
if "vertical_offset" in self.data and self.data["vertical_offset"] is not None:
y_off = self.data["vertical_offset"]
per = False
if str(y_off).endswith("%"):
y_off = y_off[:-1]
per = True
y_off = check_num(y_off)
error = f"Overlay Error: overlay vertical_offset: {self.data['vertical_offset']} must be a number"
if y_off is None:
raise Failed(error)
if self.vertical_align != "center" and not per and y_off < 0:
raise Failed(f"{error} 0 or greater")
elif self.vertical_align != "center" and per and (y_off > 100 or y_off < 0):
raise Failed(f"{error} between 0% and 100%")
elif self.vertical_align == "center" and per and (y_off > 50 or y_off < -50):
raise Failed(f"{error} between -50% and 50%")
self.vertical_offset = f"{y_off}%" if per else y_off
if self.vertical_offset is None and self.vertical_align == "center":
self.vertical_offset = 0
if (self.horizontal_offset is None and self.vertical_offset is not None) or (self.vertical_offset is None and self.horizontal_offset is not None): if (self.horizontal_offset is None and self.vertical_offset is not None) or (self.vertical_offset is None and self.horizontal_offset is not None):
raise Failed(f"Overlay Error: overlay attribute's must be used together") raise Failed(f"Overlay Error: overlay attribute's must be used together")
@ -946,7 +964,8 @@ class Overlay:
raise Failed(f"Overlay Error: overlay attributes back_width and back_height must be used together") raise Failed(f"Overlay Error: overlay attributes back_width and back_height must be used together")
elif back_width >= 0 and back_height >= 0: elif back_width >= 0 and back_height >= 0:
self.back_box = (back_width, back_height) self.back_box = (back_width, back_height)
if (self.back_color or self.back_line_color) and not self.has_coordinates(): self.has_back = True if self.back_color or self.back_line_color else False
if self.has_back and not self.has_coordinates():
raise Failed(f"Overlay Error: horizontal_offset and vertical_offset are required when using a backdrop") raise Failed(f"Overlay Error: horizontal_offset and vertical_offset are required when using a backdrop")
def get_and_save_image(image_url): def get_and_save_image(image_url):
@ -1013,8 +1032,8 @@ class Overlay:
raise Failed(f"Overlay Error: overlay font_color: {self.data['font_color']} invalid") raise Failed(f"Overlay Error: overlay font_color: {self.data['font_color']} invalid")
text = self.name[5:-1] text = self.name[5:-1]
if text not in [f"{a}{s}" for a in ["audience_rating", "critic_rating", "user_rating"] for s in ["", "%"]]: 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.portrait, _ = self.get_backdrop(portrait_dim, text=text)
self.landscape = self.get_backdrop(landscape_dim, text=text) self.landscape, _ = self.get_backdrop(landscape_dim, text=text)
else: else:
if "|" in self.name: if "|" in self.name:
raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'") raise Failed(f"Overlay Error: Overlay Name: {self.name} cannot contain '|'")
@ -1038,16 +1057,16 @@ class Overlay:
except OSError: except OSError:
raise Failed(f"Overlay Error: overlay image {self.path} failed to load") raise Failed(f"Overlay Error: overlay image {self.path} failed to load")
def get_backdrop(self, canvas_box, box=None, text=None): def get_backdrop(self, canvas_box, box=None, text=None, new_cords=None):
overlay_image = None overlay_image = None
if text is not None: if text is not None:
_, _, width, height = self.get_text_size(text) _, _, width, height = self.get_text_size(text)
box = (width, height) box = (width, height)
x_cord, y_cord = self.get_coordinates(canvas_box, box) x_cord, y_cord = self.get_coordinates(canvas_box, box, new_cords=new_cords)
if text is not None or self.back_color or self.back_line_color: if text is not None or self.has_back:
overlay_image = Image.new("RGBA", canvas_box, (255, 255, 255, 0)) overlay_image = Image.new("RGBA", canvas_box, (255, 255, 255, 0))
drawing = ImageDraw.Draw(overlay_image) drawing = ImageDraw.Draw(overlay_image)
if self.back_color or self.back_line_color: if self.has_back:
cords = ( cords = (
x_cord - self.back_padding, x_cord - self.back_padding,
y_cord - self.back_padding, y_cord - self.back_padding,
@ -1088,8 +1107,8 @@ class Overlay:
def get_text_size(self, text): def get_text_size(self, text):
return ImageDraw.Draw(Image.new("RGBA", (0, 0))).textbbox((0, 0), text, font=self.font, anchor='lt') return ImageDraw.Draw(Image.new("RGBA", (0, 0))).textbbox((0, 0), text, font=self.font, anchor='lt')
def get_coordinates(self, canvas_box, box): def get_coordinates(self, canvas_box, box, new_cords=None):
if not self.has_coordinates(): if new_cords is None and not self.has_coordinates():
return 0, 0 return 0, 0
if self.back_box: if self.back_box:
box = self.back_box box = self.back_box
@ -1103,5 +1122,11 @@ class Overlay:
else: else:
return value return value
return get_cord(self.horizontal_offset, canvas_box[0], box[0], self.horizontal_align), \ if new_cords is None:
get_cord(self.vertical_offset, canvas_box[1], box[1], self.vertical_align) ho = self.horizontal_offset
ha = self.horizontal_align
vo = self.vertical_offset
va = self.vertical_align
else:
ha, ho, va, vo = new_cords
return get_cord(ho, canvas_box[0], box[0], ha), get_cord(vo, canvas_box[1], box[1], va)

Loading…
Cancel
Save