Plex-Meta-Manager/modules/library.py

367 lines
18 KiB
Python
Raw Normal View History

import os, time
2021-09-21 03:57:16 +00:00
from abc import ABC, abstractmethod
from modules import util, operations
2022-04-18 18:16:39 +00:00
from modules.meta import MetadataFile, OverlayFile
2022-04-15 14:16:04 +00:00
from modules.operations import Operations
2022-07-14 13:46:33 +00:00
from modules.util import Failed, NotScheduled, YAML
from PIL import Image, ImageFilter
2021-09-21 03:57:16 +00:00
logger = util.logger
2021-09-21 03:57:16 +00:00
class Library(ABC):
def __init__(self, config, params):
self.Radarr = None
self.Sonarr = None
self.Tautulli = None
2021-11-03 14:36:11 +00:00
self.Webhooks = None
2022-04-15 14:16:04 +00:00
self.Operations = Operations(config, self)
2022-04-18 18:16:39 +00:00
self.Overlays = None
2021-10-04 17:51:32 +00:00
self.Notifiarr = None
2021-09-21 03:57:16 +00:00
self.collections = []
self.metadatas = []
2022-11-02 03:10:43 +00:00
self.queues = {}
self.queue_current = 0
2021-09-21 03:57:16 +00:00
self.metadata_files = []
2022-04-18 18:16:39 +00:00
self.overlay_files = []
2021-09-21 03:57:16 +00:00
self.movie_map = {}
self.show_map = {}
self.imdb_map = {}
self.anidb_map = {}
self.mal_map = {}
self.movie_rating_key_map = {}
self.show_rating_key_map = {}
2023-01-27 15:16:00 +00:00
self.imdb_rating_key_map = {}
2022-04-20 21:30:31 +00:00
self.cached_items = {}
2021-09-21 03:57:16 +00:00
self.run_again = []
self.type = ""
self.config = config
self.name = params["name"]
self.original_mapping_name = params["mapping_name"]
self.metadata_path = params["metadata_path"]
2022-04-18 18:16:39 +00:00
self.overlay_path = params["overlay_path"]
2021-12-18 03:02:24 +00:00
self.skip_library = params["skip_library"]
2021-12-13 04:57:38 +00:00
self.asset_depth = params["asset_depth"]
2021-12-03 22:05:24 +00:00
self.asset_directory = params["asset_directory"] if params["asset_directory"] else []
2021-09-21 03:57:16 +00:00
self.default_dir = params["default_dir"]
self.mapping_name, output = util.validate_filename(self.original_mapping_name)
self.image_table_name = self.config.Cache.get_image_table_name(self.original_mapping_name) if self.config.Cache else None
2022-04-18 18:16:39 +00:00
self.overlay_folder = os.path.join(self.config.default_dir, "overlays")
self.overlay_backup = os.path.join(self.overlay_folder, f"{self.mapping_name} Original Posters")
self.report_path = params["report_path"] if params["report_path"] else os.path.join(self.default_dir, f"{self.mapping_name}_report.yml")
self.report_data = {}
2021-09-21 03:57:16 +00:00
self.asset_folders = params["asset_folders"]
self.create_asset_folders = params["create_asset_folders"]
self.dimensional_asset_rename = params["dimensional_asset_rename"]
self.prioritize_assets = params["prioritize_assets"]
self.download_url_assets = params["download_url_assets"]
self.show_missing_season_assets = params["show_missing_season_assets"]
self.show_missing_episode_assets = params["show_missing_episode_assets"]
self.show_asset_not_needed = params["show_asset_not_needed"]
2021-09-21 03:57:16 +00:00
self.sync_mode = params["sync_mode"]
self.default_collection_order = params["default_collection_order"]
self.minimum_items = params["minimum_items"]
2022-01-11 19:37:48 +00:00
self.item_refresh_delay = params["item_refresh_delay"]
self.delete_below_minimum = params["delete_below_minimum"]
self.delete_not_scheduled = params["delete_not_scheduled"]
self.missing_only_released = params["missing_only_released"]
2021-09-21 03:57:16 +00:00
self.show_unmanaged = params["show_unmanaged"]
self.show_unconfigured = params["show_unconfigured"]
2021-09-21 03:57:16 +00:00
self.show_filtered = params["show_filtered"]
2021-12-17 00:16:08 +00:00
self.show_options = params["show_options"]
2021-09-21 03:57:16 +00:00
self.show_missing = params["show_missing"]
2021-11-16 15:07:09 +00:00
self.show_missing_assets = params["show_missing_assets"]
self.save_report = params["save_report"]
self.only_filter_missing = params["only_filter_missing"]
self.ignore_ids = params["ignore_ids"]
self.ignore_imdb_ids = params["ignore_imdb_ids"]
self.assets_for_all = params["assets_for_all"]
self.delete_collections = params["delete_collections"]
2023-01-25 07:43:25 +00:00
self.mass_studio_update = params["mass_studio_update"]
2021-09-21 03:57:16 +00:00
self.mass_genre_update = params["mass_genre_update"]
self.mass_audience_rating_update = params["mass_audience_rating_update"]
self.mass_critic_rating_update = params["mass_critic_rating_update"]
2022-06-09 14:20:43 +00:00
self.mass_user_rating_update = params["mass_user_rating_update"]
self.mass_episode_audience_rating_update = params["mass_episode_audience_rating_update"]
self.mass_episode_critic_rating_update = params["mass_episode_critic_rating_update"]
self.mass_episode_user_rating_update = params["mass_episode_user_rating_update"]
2022-02-07 22:46:24 +00:00
self.mass_content_rating_update = params["mass_content_rating_update"]
self.mass_original_title_update = params["mass_original_title_update"]
self.mass_originally_available_update = params["mass_originally_available_update"]
self.mass_imdb_parental_labels = params["mass_imdb_parental_labels"]
self.mass_poster_update = params["mass_poster_update"]
self.mass_background_update = params["mass_background_update"]
2021-12-30 18:59:54 +00:00
self.radarr_add_all_existing = params["radarr_add_all_existing"]
self.radarr_remove_by_tag = params["radarr_remove_by_tag"]
2021-12-30 18:59:54 +00:00
self.sonarr_add_all_existing = params["sonarr_add_all_existing"]
self.sonarr_remove_by_tag = params["sonarr_remove_by_tag"]
2022-02-04 22:00:38 +00:00
self.update_blank_track_titles = params["update_blank_track_titles"]
self.remove_title_parentheses = params["remove_title_parentheses"]
2022-04-18 18:16:39 +00:00
self.remove_overlays = params["remove_overlays"]
2022-06-26 16:31:22 +00:00
self.reapply_overlays = params["reapply_overlays"]
2022-07-19 20:33:57 +00:00
self.reset_overlays = params["reset_overlays"]
self.mass_collection_mode = params["mass_collection_mode"]
self.metadata_backup = params["metadata_backup"]
2021-12-07 07:10:07 +00:00
self.genre_mapper = params["genre_mapper"]
self.content_rating_mapper = params["content_rating_mapper"]
2021-12-26 15:48:52 +00:00
self.changes_webhooks = params["changes_webhooks"]
2021-09-21 03:57:16 +00:00
self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex?
self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex?
self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex?
self.optimize = params["plex"]["optimize"] # TODO: Here or just in Plex?
self.stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0, "names": []}
2021-12-30 21:29:16 +00:00
self.status = {}
2021-12-30 18:59:54 +00:00
2022-06-09 14:20:43 +00:00
self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.remove_title_parentheses \
or self.mass_audience_rating_update or self.mass_critic_rating_update or self.mass_user_rating_update \
or self.mass_episode_audience_rating_update or self.mass_episode_critic_rating_update or self.mass_episode_user_rating_update \
2022-09-23 15:01:38 +00:00
or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_original_title_update\
2023-01-25 07:43:25 +00:00
or self.mass_imdb_parental_labels or self.genre_mapper or self.content_rating_mapper or self.mass_studio_update\
or self.radarr_add_all_existing or self.sonarr_add_all_existing or self.mass_poster_update or self.mass_background_update else False
self.library_operation = True if self.items_library_operation or self.delete_collections or self.mass_collection_mode \
or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.show_unmanaged or self.show_unconfigured \
or self.metadata_backup or self.update_blank_track_titles else False
2022-09-23 01:59:02 +00:00
self.meta_operations = [i for i in [getattr(self, o) for o in operations.meta_operations] if i]
2022-01-28 18:36:21 +00:00
if self.asset_directory:
logger.info("")
for ad in self.asset_directory:
logger.info(f"Using Asset Directory: {ad}")
if output:
logger.info("")
logger.info(output)
2022-04-26 19:56:46 +00:00
def scan_files(self, operations_only, overlays_only, collection_only):
if not operations_only and not overlays_only:
for file_type, metadata_file, temp_vars, asset_directory in self.metadata_path:
try:
meta_obj = MetadataFile(self.config, self, file_type, metadata_file, temp_vars, asset_directory)
if meta_obj.collections:
self.collections.extend([c for c in meta_obj.collections])
if meta_obj.metadata:
2022-05-09 20:40:39 +00:00
self.metadatas.extend([m for m in meta_obj.metadata])
2022-04-26 19:56:46 +00:00
self.metadata_files.append(meta_obj)
except Failed as e:
logger.error(e)
2022-10-28 20:48:15 +00:00
logger.info(f"Metadata File Failed To Load")
2022-07-14 13:46:33 +00:00
except NotScheduled as e:
logger.info("")
logger.separator(f"Skipping {e} Metadata File")
2022-04-26 19:56:46 +00:00
if not operations_only and not collection_only:
for file_type, overlay_file, temp_vars, asset_directory in self.overlay_path:
try:
2022-11-02 03:10:43 +00:00
overlay_obj = OverlayFile(self.config, self, file_type, overlay_file, temp_vars, asset_directory, self.queue_current)
2022-06-03 15:46:25 +00:00
self.overlay_files.append(overlay_obj)
2022-11-02 03:10:43 +00:00
for qk, qv in overlay_obj.queues.items():
self.queues[self.queue_current] = qv
self.queue_current += 1
2022-04-26 19:56:46 +00:00
except Failed as e:
logger.error(e)
2022-10-28 20:48:15 +00:00
logger.info(f"Overlay File Failed To Load")
2021-09-21 03:57:16 +00:00
2022-06-13 20:02:40 +00:00
def upload_images(self, item, poster=None, background=None, overlay=False):
2021-09-21 03:57:16 +00:00
poster_uploaded = False
if poster is not None:
try:
2022-06-13 20:02:40 +00:00
image_compare = None
if self.config.Cache:
_, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, self.image_table_name)
if not image_compare or str(poster.compare) != str(image_compare):
2023-02-06 20:34:40 +00:00
if overlay:
self.reload(item, force=True)
if overlay and "Overlay" in [la.tag for la in self.item_labels(item)]:
2022-06-17 15:00:07 +00:00
item.removeLabel("Overlay")
2021-09-21 03:57:16 +00:00
self._upload_image(item, poster)
poster_uploaded = True
logger.info(f"Detail: {poster.attribute} updated {poster.message}")
elif self.show_asset_not_needed:
2021-09-21 03:57:16 +00:00
logger.info(f"Detail: {poster.prefix}poster update not needed")
except Failed:
logger.stacktrace()
2021-09-21 03:57:16 +00:00
logger.error(f"Detail: {poster.attribute} failed to update {poster.message}")
background_uploaded = False
if background is not None:
try:
2022-06-13 20:02:40 +00:00
image_compare = None
2021-09-21 03:57:16 +00:00
if self.config.Cache:
2022-06-13 20:02:40 +00:00
_, image_compare, _ = self.config.Cache.query_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds")
if not image_compare or str(background.compare) != str(image_compare):
2021-09-21 03:57:16 +00:00
self._upload_image(item, background)
background_uploaded = True
logger.info(f"Detail: {background.attribute} updated {background.message}")
elif self.show_asset_not_needed:
2021-09-21 03:57:16 +00:00
logger.info(f"Detail: {background.prefix}background update not needed")
except Failed:
logger.stacktrace()
2021-09-21 03:57:16 +00:00
logger.error(f"Detail: {background.attribute} failed to update {background.message}")
if self.config.Cache:
if poster_uploaded:
2022-06-13 20:02:40 +00:00
self.config.Cache.update_image_map(item.ratingKey, self.image_table_name, "", poster.compare if poster else "")
2021-09-21 03:57:16 +00:00
if background_uploaded:
2022-06-13 20:02:40 +00:00
self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", "", background.compare)
2021-09-21 03:57:16 +00:00
2022-05-04 16:27:17 +00:00
return poster_uploaded, background_uploaded
def get_id_from_maps(self, key):
key = str(key)
if key in self.movie_rating_key_map:
return self.movie_rating_key_map[key]
elif key in self.show_rating_key_map:
return self.show_rating_key_map[key]
2021-12-17 14:24:46 +00:00
@abstractmethod
2021-10-04 17:51:32 +00:00
def notify(self, text, collection=None, critical=True):
2021-12-17 14:24:46 +00:00
pass
2021-10-04 17:51:32 +00:00
2022-12-28 18:58:12 +00:00
@abstractmethod
def notify_delete(self, message):
pass
2021-09-21 03:57:16 +00:00
@abstractmethod
def _upload_image(self, item, image):
pass
@abstractmethod
2022-04-18 18:16:39 +00:00
def upload_poster(self, item, image, url=False):
2021-09-21 03:57:16 +00:00
pass
2023-01-27 16:28:53 +00:00
def poster_update(self, item, image, tmdb=None, title=None):
return self.image_update(item, image, tmdb=tmdb, title=title)
def background_update(self, item, image, tmdb=None, title=None):
return self.image_update(item, image, tmdb=tmdb, title=title, poster=False)
@abstractmethod
def image_update(self, item, image, tmdb=None, title=None, poster=True):
pass
2021-09-21 03:57:16 +00:00
@abstractmethod
def reload(self, item, force=False):
2021-09-21 03:57:16 +00:00
pass
@abstractmethod
def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True, locked=True, is_locked=None):
2021-09-21 03:57:16 +00:00
pass
2022-06-13 20:02:40 +00:00
@abstractmethod
def item_labels(self, item):
pass
@abstractmethod
def find_poster_url(self, item):
pass
def check_image_for_overlay(self, image_url, image_path, remove=False):
image_response = self.config.get(image_url)
if image_response.status_code >= 400:
raise Failed("Image Download Failed")
if image_response.headers["Content-Type"] not in ["image/png", "image/jpeg", "image/webp"]:
raise Failed("Image Not PNG, JPG, or WEBP")
if image_response.headers["Content-Type"] == "image/jpeg":
image_path += ".jpg"
elif image_response.headers["Content-Type"] == "image/webp":
image_path += ".webp"
else:
image_path += ".png"
with open(image_path, "wb") as handler:
handler.write(image_response.content)
while util.is_locked(image_path):
time.sleep(1)
with Image.open(image_path) as image:
exif_tags = image.getexif()
if 0x04bc in exif_tags and exif_tags[0x04bc] == "overlay":
os.remove(image_path)
raise Failed("Poster already has an Overlay")
if remove:
os.remove(image_path)
else:
return image_path
@abstractmethod
2023-02-05 19:10:08 +00:00
def item_posters(self, item, providers=None):
pass
2021-09-21 03:57:16 +00:00
@abstractmethod
2022-07-26 19:58:53 +00:00
def get_all(self, builder_level=None, load=False):
2021-09-21 03:57:16 +00:00
pass
def add_additions(self, collection, items, is_movie):
self._add_to_file("Added", collection, items, is_movie)
2021-09-21 03:57:16 +00:00
def add_missing(self, collection, items, is_movie):
self._add_to_file("Missing", collection, items, is_movie)
def add_removed(self, collection, items, is_movie):
self._add_to_file("Removed", collection, items, is_movie)
def add_filtered(self, collection, items, is_movie):
self._add_to_file("Filtered", collection, items, is_movie)
def _add_to_file(self, file_type, collection, items, is_movie):
if collection not in self.report_data:
self.report_data[collection] = {}
parts = isinstance(items[0], str)
if parts:
other = f"Parts {file_type}"
section = other
elif is_movie:
other = f"Movies {file_type}"
section = f"{other} (TMDb IDs)"
else:
other = f"Shows {file_type}"
section = f"{other} (TVDb IDs)"
if section not in self.report_data[collection]:
self.report_data[collection][section] = [] if parts else {}
if parts:
self.report_data[collection][section].extend(items)
else:
for title, item_id in items:
if item_id:
self.report_data[collection][section][int(item_id)] = title
else:
if other not in self.report_data[collection]:
self.report_data[collection][other] = []
self.report_data[collection][other].append(title)
2023-01-09 21:22:39 +00:00
yaml = YAML(self.report_path, start_empty=True)
2022-05-12 19:10:03 +00:00
yaml.data = self.report_data
yaml.save()
2021-09-21 03:57:16 +00:00
2022-04-20 21:30:31 +00:00
def cache_items(self):
logger.info("")
logger.separator(f"Caching {self.name} Library Items", space=False, border=False)
logger.info("")
2021-09-21 03:57:16 +00:00
items = self.get_all()
2022-04-20 21:30:31 +00:00
for item in items:
2022-04-22 02:57:26 +00:00
self.cached_items[item.ratingKey] = (item, False)
2022-04-20 21:30:31 +00:00
return items
def map_guids(self, items):
2021-09-21 03:57:16 +00:00
for i, item in enumerate(items, 1):
2022-05-10 15:47:13 +00:00
if isinstance(item, tuple):
logger.ghost(f"Processing: {i}/{len(items)}")
key, guid = item
else:
logger.ghost(f"Processing: {i}/{len(items)} {item.title}")
key = item.ratingKey
guid = item.guid
if key not in self.movie_rating_key_map and key not in self.show_rating_key_map:
if isinstance(item, tuple):
item_type, check_id = self.config.Convert.scan_guid(guid)
id_type, main_id, imdb_id, _ = self.config.Convert.ids_from_cache(key, guid, item_type, check_id, self)
else:
id_type, main_id, imdb_id = self.config.Convert.get_id(item, self)
2021-09-21 03:57:16 +00:00
if main_id:
if id_type == "movie":
2022-05-10 15:47:13 +00:00
self.movie_rating_key_map[key] = main_id[0]
util.add_dict_list(main_id, key, self.movie_map)
2021-09-21 03:57:16 +00:00
elif id_type == "show":
2022-05-10 15:47:13 +00:00
self.show_rating_key_map[key] = main_id[0]
util.add_dict_list(main_id, key, self.show_map)
2021-09-21 03:57:16 +00:00
if imdb_id:
2023-01-27 15:16:00 +00:00
self.imdb_rating_key_map[key] = imdb_id[0]
2022-05-10 15:47:13 +00:00
util.add_dict_list(imdb_id, key, self.imdb_map)
2021-09-21 03:57:16 +00:00
logger.info("")
logger.info(f"Processed {len(items)} {self.type}s")