diff --git a/.gitignore b/.gitignore index d07b982b..bde577dc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,10 @@ __pycache__/ /test.py logs/ config/* +!config/overlays/ !config/*.template +*.png +!overlay.png build/ develop-eggs/ dist/ diff --git a/config/overlays/4K-Dolby/overlay.png b/config/overlays/4K-Dolby/overlay.png new file mode 100644 index 00000000..35f1d825 Binary files /dev/null and b/config/overlays/4K-Dolby/overlay.png differ diff --git a/config/overlays/4K-HDR/overlay.png b/config/overlays/4K-HDR/overlay.png new file mode 100644 index 00000000..4a3be77f Binary files /dev/null and b/config/overlays/4K-HDR/overlay.png differ diff --git a/config/overlays/4K/overlay.png b/config/overlays/4K/overlay.png new file mode 100644 index 00000000..15a47c25 Binary files /dev/null and b/config/overlays/4K/overlay.png differ diff --git a/config/overlays/Dolby/overlay.png b/config/overlays/Dolby/overlay.png new file mode 100644 index 00000000..118f687f Binary files /dev/null and b/config/overlays/Dolby/overlay.png differ diff --git a/config/overlays/HDR/overlay.png b/config/overlays/HDR/overlay.png new file mode 100644 index 00000000..4ec1692e Binary files /dev/null and b/config/overlays/HDR/overlay.png differ diff --git a/modules/builder.py b/modules/builder.py index 1feedd56..cf978908 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -2,6 +2,7 @@ import logging, os, re from datetime import datetime, timedelta from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util from modules.util import Failed, Image +from PIL import Image from plexapi.exceptions import BadRequest, NotFound from plexapi.video import Movie, Show from urllib.parse import quote @@ -627,6 +628,14 @@ class CollectionBuilder: if "item_label.remove" in self.data and "item_label.sync" in self.data: raise Failed(f"Collection Error: Cannot use item_label.remove and item_label.sync together") self.item_details[method_final] = util.get_list(method_data) + elif method_name == "item_overlay": + overlay = os.path.join(config.default_dir, "overlays", method_data, "overlay.png") + if not os.path.exists(overlay): + raise Failed(f"Collection Error: {method_data} overlay image not found at {overlay}") + if method_data in self.library.overlays: + raise Failed(f"Each Overlay can only be used once per Library") + self.library.overlays.append(method_data) + self.item_details[method_name] = method_data elif method_name in plex.item_advance_keys: key, options = plex.item_advance_keys[method_name] if method_name in advance_new_agent and self.library.agent not in plex.new_plex_agents: @@ -1664,9 +1673,22 @@ class CollectionBuilder: except Failed as e: logger.error(e) + overlay = None + overlay_folder = None + rating_keys = [] + if "item_overlay" in self.item_details: + overlay_name = self.item_details["item_overlay"] + rating_keys = self.config.Cache.query_image_map_overlay(self.library.original_mapping_name, "poster", overlay_name) + overlay_folder = os.path.join(self.config.default_dir, "overlays", overlay_name) + overlay_image = Image.open(os.path.join(overlay_folder, "overlay.png")) + temp_image = os.path.join(overlay_folder, f"temp.png") + overlay = (overlay_name, overlay_folder, overlay_image, temp_image) + for item in items: + if int(item.ratingKey) in rating_keys: + rating_keys.remove(int(item.ratingKey)) poster, background = self.library.update_item_from_assets(item) - self.library.upload_images(item, poster=poster, background=background) + self.library.upload_images(item, poster=poster, background=background, overlay=overlay) self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags) advance_edits = {} for method_name, method_data in self.item_details.items(): @@ -1676,6 +1698,18 @@ class CollectionBuilder: advance_edits[key] = options[method_data] self.library.edit_item(item, item.title, "Movie" if self.library.is_movie else "Show", advance_edits, advanced=True) + for rating_key in rating_keys: + try: + item = self.fetch_item(rating_key) + except Failed as e: + logger.error(e) + continue + og_image = os.path.join(overlay_folder, f"{rating_key}.png") + if os.path.exists(og_image): + self.library._upload_file_poster(item, og_image) + os.remove(og_image) + self.config.Cache.update_image_map(item.ratingKey, self.library.original_mapping_name, "poster", "", "", "") + def update_details(self): if not self.obj and self.smart_url: self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url) @@ -1835,10 +1869,7 @@ class CollectionBuilder: if current in collection_items: logger.info(f"{name} Collection | = | {current_title}") else: - if self.smart_label_collection: - self.library.query_data(current.addLabel, name) - else: - self.library.query_data(current.addCollection, name) + self.library.query_data(current.addLabel if self.smart_label_collection else current.addCollection, name) logger.info(f"{name} Collection | + | {current_title}") logger.info(f"{len(rating_keys)} {'Movie' if self.library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed") diff --git a/modules/cache.py b/modules/cache.py index 37e17e93..95c004ce 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -233,6 +233,17 @@ class Cache: cursor.execute("INSERT OR IGNORE INTO anime_map(anidb) VALUES(?)", (anime_ids["anidb"],)) cursor.execute("UPDATE anime_map SET anilist = ?, myanimelist = ?, kitsu = ?, expiration_date = ? WHERE anidb = ?", (anime_ids["anidb"], anime_ids["myanimelist"], anime_ids["kitsu"], expiration_date.strftime("%Y-%m-%d"), anime_ids["anidb"])) + def query_image_map_overlay(self, library, image_type, overlay): + rks = [] + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute(f"SELECT * FROM image_map WHERE overlay = ? AND library = ? AND type = ?", (overlay, library, image_type)) + rows = cursor.fetchall() + for row in rows: + rks.append(int(row["rating_key"])) + return rks + def query_image_map(self, rating_key, library, image_type): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row @@ -240,12 +251,12 @@ class Cache: cursor.execute(f"SELECT * FROM image_map WHERE rating_key = ? AND library = ? AND type = ?", (rating_key, library, image_type)) row = cursor.fetchone() if row and row["location"]: - return row["location"], row["compare"] - return None, None + return row["location"], row["compare"], row["overlay"] + return None, None, None - def update_image_map(self, rating_key, library, image_type, location, compare): + def update_image_map(self, rating_key, library, image_type, location, compare, overlay): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: cursor.execute("INSERT OR IGNORE INTO image_map(rating_key, library, type) VALUES(?, ?, ?)", (rating_key, library, image_type)) - cursor.execute("UPDATE image_map SET location = ?, compare = ? WHERE rating_key = ? AND library = ? AND type = ?", (location, compare, rating_key, library, image_type)) + cursor.execute("UPDATE image_map SET location = ?, compare = ?, overlay = ? WHERE rating_key = ? AND library = ? AND type = ?", (location, compare, overlay, rating_key, library, image_type)) diff --git a/modules/plex.py b/modules/plex.py index b9a5aef1..68df7ed5 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1,12 +1,12 @@ -import glob, logging, os, requests +import glob, logging, os, plexapi, requests, shutil from modules import builder, util from modules.meta import Metadata from modules.util import Failed, Image -import plexapi from plexapi import utils from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.collection import Collection from plexapi.server import PlexServer +from PIL import Image from retrying import retry from ruamel import yaml from urllib import parse @@ -334,6 +334,7 @@ class Plex: self.movie_rating_key_map = {} self.show_rating_key_map = {} self.run_again = [] + self.overlays = [] def get_all_collections(self): return self.search(libtype="collection") @@ -426,13 +427,18 @@ class Plex: item.uploadArt(filepath=image.location) self.reload(item) - def upload_images(self, item, poster=None, background=None): + @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) + def _upload_file_poster(self, item, image): + item.uploadPoster(filepath=image) + self.reload(item) + + def upload_images(self, item, poster=None, background=None, overlay=None): poster_uploaded = False if poster is not None: try: image = None if self.config.Cache: - image, image_compare = self.config.Cache.query_image_map(item.ratingKey, self.original_mapping_name, "poster") + image, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, self.original_mapping_name, "poster") if str(poster.compare) != str(image_compare): image = None if image is None or image != item.thumb: @@ -445,6 +451,25 @@ class Plex: util.print_stacktrace() logger.error(f"Detail: {poster.attribute} failed to update {poster.message}") + overlay_name = "" + if overlay is not None: + overlay_name, overlay_folder, overlay_image, temp_image = overlay + image_overlay = None + if self.config.Cache: + image, _, image_overlay = self.config.Cache.query_image_map(item.ratingKey, self.original_mapping_name, "poster") + if poster_uploaded or not image_overlay or image_overlay != overlay_name: + og_image = requests.get(item.posterUrl).content + with open(temp_image, "wb") as handler: + handler.write(og_image) + shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.png")) + new_poster = Image.open(temp_image) + new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS) + new_poster.paste(overlay_image, (0, 0), overlay_image) + new_poster.save(temp_image) + self._upload_file_poster(item, temp_image) + poster_uploaded = True + logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}") + background_uploaded = False if background is not None: try: @@ -465,9 +490,9 @@ class Plex: if self.config.Cache: if poster_uploaded: - self.config.Cache.update_image_map(item.ratingKey, self.original_mapping_name, "poster", item.thumb, poster.compare) + self.config.Cache.update_image_map(item.ratingKey, self.original_mapping_name, "poster", item.thumb, poster.compare if poster else "", overlay_name) if background_uploaded: - self.config.Cache.update_image_map(item.ratingKey, self.original_mapping_name, "background", item.art, background.compare) + self.config.Cache.update_image_map(item.ratingKey, self.original_mapping_name, "background", item.art, background.compare, "") @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) def get_search_choices(self, search_name, title=True): diff --git a/overlays.psd b/overlays.psd new file mode 100644 index 00000000..35dc8550 Binary files /dev/null and b/overlays.psd differ diff --git a/requirements.txt b/requirements.txt index 7576a2b7..a18a3a5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ ruamel.yaml schedule retrying pathvalidate +pillow