mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
Added Image Overlay
This commit is contained in:
parent
d3db9bf162
commit
3e7cf35eaf
11 changed files with 86 additions and 15 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -12,7 +12,10 @@ __pycache__/
|
||||||
/test.py
|
/test.py
|
||||||
logs/
|
logs/
|
||||||
config/*
|
config/*
|
||||||
|
!config/overlays/
|
||||||
!config/*.template
|
!config/*.template
|
||||||
|
*.png
|
||||||
|
!overlay.png
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
|
|
BIN
config/overlays/4K-Dolby/overlay.png
Normal file
BIN
config/overlays/4K-Dolby/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
config/overlays/4K-HDR/overlay.png
Normal file
BIN
config/overlays/4K-HDR/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
config/overlays/4K/overlay.png
Normal file
BIN
config/overlays/4K/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
config/overlays/Dolby/overlay.png
Normal file
BIN
config/overlays/Dolby/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
BIN
config/overlays/HDR/overlay.png
Normal file
BIN
config/overlays/HDR/overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -2,6 +2,7 @@ import logging, os, re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util
|
from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util
|
||||||
from modules.util import Failed, Image
|
from modules.util import Failed, Image
|
||||||
|
from PIL import Image
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.video import Movie, Show
|
from plexapi.video import Movie, Show
|
||||||
from urllib.parse import quote
|
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:
|
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")
|
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)
|
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:
|
elif method_name in plex.item_advance_keys:
|
||||||
key, options = plex.item_advance_keys[method_name]
|
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:
|
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:
|
except Failed as e:
|
||||||
logger.error(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:
|
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)
|
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)
|
self.library.edit_tags("label", item, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags)
|
||||||
advance_edits = {}
|
advance_edits = {}
|
||||||
for method_name, method_data in self.item_details.items():
|
for method_name, method_data in self.item_details.items():
|
||||||
|
@ -1676,6 +1698,18 @@ class CollectionBuilder:
|
||||||
advance_edits[key] = options[method_data]
|
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)
|
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):
|
def update_details(self):
|
||||||
if not self.obj and self.smart_url:
|
if not self.obj and self.smart_url:
|
||||||
self.library.create_smart_collection(self.name, self.smart_type_key, 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:
|
if current in collection_items:
|
||||||
logger.info(f"{name} Collection | = | {current_title}")
|
logger.info(f"{name} Collection | = | {current_title}")
|
||||||
else:
|
else:
|
||||||
if self.smart_label_collection:
|
self.library.query_data(current.addLabel if self.smart_label_collection else current.addCollection, name)
|
||||||
self.library.query_data(current.addLabel, name)
|
|
||||||
else:
|
|
||||||
self.library.query_data(current.addCollection, name)
|
|
||||||
logger.info(f"{name} Collection | + | {current_title}")
|
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")
|
logger.info(f"{len(rating_keys)} {'Movie' if self.library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed")
|
||||||
|
|
||||||
|
|
|
@ -233,6 +233,17 @@ class Cache:
|
||||||
cursor.execute("INSERT OR IGNORE INTO anime_map(anidb) VALUES(?)", (anime_ids["anidb"],))
|
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"]))
|
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):
|
def query_image_map(self, rating_key, library, image_type):
|
||||||
with sqlite3.connect(self.cache_path) as connection:
|
with sqlite3.connect(self.cache_path) as connection:
|
||||||
connection.row_factory = sqlite3.Row
|
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))
|
cursor.execute(f"SELECT * FROM image_map WHERE rating_key = ? AND library = ? AND type = ?", (rating_key, library, image_type))
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row and row["location"]:
|
if row and row["location"]:
|
||||||
return row["location"], row["compare"]
|
return row["location"], row["compare"], row["overlay"]
|
||||||
return None, None
|
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:
|
with sqlite3.connect(self.cache_path) as connection:
|
||||||
connection.row_factory = sqlite3.Row
|
connection.row_factory = sqlite3.Row
|
||||||
with closing(connection.cursor()) as cursor:
|
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("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))
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import glob, logging, os, requests
|
import glob, logging, os, plexapi, requests, shutil
|
||||||
from modules import builder, util
|
from modules import builder, util
|
||||||
from modules.meta import Metadata
|
from modules.meta import Metadata
|
||||||
from modules.util import Failed, Image
|
from modules.util import Failed, Image
|
||||||
import plexapi
|
|
||||||
from plexapi import utils
|
from plexapi import utils
|
||||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||||
from plexapi.collection import Collection
|
from plexapi.collection import Collection
|
||||||
from plexapi.server import PlexServer
|
from plexapi.server import PlexServer
|
||||||
|
from PIL import Image
|
||||||
from retrying import retry
|
from retrying import retry
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
@ -334,6 +334,7 @@ class Plex:
|
||||||
self.movie_rating_key_map = {}
|
self.movie_rating_key_map = {}
|
||||||
self.show_rating_key_map = {}
|
self.show_rating_key_map = {}
|
||||||
self.run_again = []
|
self.run_again = []
|
||||||
|
self.overlays = []
|
||||||
|
|
||||||
def get_all_collections(self):
|
def get_all_collections(self):
|
||||||
return self.search(libtype="collection")
|
return self.search(libtype="collection")
|
||||||
|
@ -426,13 +427,18 @@ class Plex:
|
||||||
item.uploadArt(filepath=image.location)
|
item.uploadArt(filepath=image.location)
|
||||||
self.reload(item)
|
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
|
poster_uploaded = False
|
||||||
if poster is not None:
|
if poster is not None:
|
||||||
try:
|
try:
|
||||||
image = None
|
image = None
|
||||||
if self.config.Cache:
|
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):
|
if str(poster.compare) != str(image_compare):
|
||||||
image = None
|
image = None
|
||||||
if image is None or image != item.thumb:
|
if image is None or image != item.thumb:
|
||||||
|
@ -445,6 +451,25 @@ class Plex:
|
||||||
util.print_stacktrace()
|
util.print_stacktrace()
|
||||||
logger.error(f"Detail: {poster.attribute} failed to update {poster.message}")
|
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
|
background_uploaded = False
|
||||||
if background is not None:
|
if background is not None:
|
||||||
try:
|
try:
|
||||||
|
@ -465,9 +490,9 @@ class Plex:
|
||||||
|
|
||||||
if self.config.Cache:
|
if self.config.Cache:
|
||||||
if poster_uploaded:
|
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:
|
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)
|
@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):
|
def get_search_choices(self, search_name, title=True):
|
||||||
|
|
BIN
overlays.psd
Normal file
BIN
overlays.psd
Normal file
Binary file not shown.
|
@ -11,3 +11,4 @@ ruamel.yaml
|
||||||
schedule
|
schedule
|
||||||
retrying
|
retrying
|
||||||
pathvalidate
|
pathvalidate
|
||||||
|
pillow
|
||||||
|
|
Loading…
Reference in a new issue