#468 added metadata_backup library operation

This commit is contained in:
meisnate12 2022-01-27 10:22:58 -05:00
parent ce7a432424
commit e2de5ae19e
8 changed files with 203 additions and 101 deletions

View file

@ -558,6 +558,7 @@ class ConfigFile:
"radarr_remove_by_tag": None,
"sonarr_remove_by_tag": None,
"mass_collection_mode": None,
"metadata_backup": None,
"genre_collections": None
}
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
@ -641,6 +642,18 @@ class ConfigFile:
params["mass_collection_mode"] = util.check_collection_mode(lib["operations"]["mass_collection_mode"])
except Failed as e:
logger.error(e)
if "metadata_backup" in lib["operations"]:
params["metadata_backup"] = {
"path": os.path.join(default_dir, f"{str(library_name)}_Metadata_Backup.yml"),
"exclude": [],
"sync_tags": False,
"add_blank_entries": True
}
if lib["operations"]["metadata_backup"] and isinstance(lib["operations"]["metadata_backup"], dict):
params["metadata_backup"]["path"] = check_for_attribute(lib["operations"]["metadata_backup"], "path", var_type="path", default=params["metadata_backup"]["path"], save=False)
params["metadata_backup"]["exclude"] = check_for_attribute(lib["operations"]["metadata_backup"], "exclude", var_type="comma_list", default_is_none=True, save=False)
params["metadata_backup"]["sync_tags"] = check_for_attribute(lib["operations"]["metadata_backup"], "sync_tags", var_type="bool", default=False, save=False)
params["metadata_backup"]["add_blank_entries"] = check_for_attribute(lib["operations"]["metadata_backup"], "add_blank_entries", var_type="bool", default=True, save=False)
if "tmdb_collections" in lib["operations"]:
params["tmdb_collections"] = {
"exclude_ids": [],

View file

@ -76,6 +76,7 @@ class Library(ABC):
self.sonarr_add_all_existing = params["sonarr_add_all_existing"]
self.sonarr_remove_by_tag = params["sonarr_remove_by_tag"]
self.mass_collection_mode = params["mass_collection_mode"]
self.metadata_backup = params["metadata_backup"]
self.tmdb_collections = params["tmdb_collections"]
self.genre_collections = params["genre_collections"]
self.genre_mapper = params["genre_mapper"]
@ -89,8 +90,9 @@ class Library(ABC):
self.status = {}
self.items_library_operation = self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \
or self.mass_critic_rating_update or self.mass_trakt_rating_update or self.genre_mapper \
or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing
or self.mass_critic_rating_update or self.mass_trakt_rating_update or self.genre_mapper \
or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing \
or self.metadata_backup
self.library_operation = self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \
or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \
or self.genre_collections or self.show_unmanaged

View file

@ -9,19 +9,6 @@ logger = logging.getLogger("Plex Meta Manager")
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
advance_tags_to_edit = {
"Movie": ["metadata_language", "use_original_title"],
"Show": ["episode_sorting", "keep_episodes", "delete_episodes", "season_display", "episode_ordering",
"metadata_language", "use_original_title"],
"Artist": ["album_sorting"]
}
tags_to_edit = {
"Movie": ["genre", "label", "collection", "country", "director", "producer", "writer"],
"Show": ["genre", "label", "collection"],
"Artist": ["genre", "style", "mood", "country", "collection", "similar_artist"]
}
def get_dict(attribute, attr_data, check_list=None):
if check_list is None:
check_list = []
@ -33,9 +20,9 @@ def get_dict(attribute, attr_data, check_list=None):
if _name in check_list:
logger.warning(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}")
elif _data is None:
logger.error(f"Config Error: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data")
logger.warning(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data")
elif not isinstance(_data, dict):
logger.error(f"Config Error: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary")
logger.warning(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary")
else:
new_dict[str(_name)] = _data
return new_dict
@ -435,11 +422,11 @@ class MetadataFile(DataFile):
edits = {}
add_edit("title", item, meta, methods, value=title)
add_edit("sort_title", item, meta, methods, key="titleSort")
add_edit("user_rating", item, meta, methods, key="userRating", var_type="float")
if not self.library.is_music:
add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
add_edit("critic_rating", item, meta, methods, value=rating, key="rating", var_type="float")
add_edit("audience_rating", item, meta, methods, key="audienceRating", var_type="float")
add_edit("user_rating", item, meta, methods, key="userRating", var_type="float")
add_edit("content_rating", item, meta, methods, key="contentRating")
add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title)
add_edit("studio", item, meta, methods, value=studio)
@ -450,12 +437,12 @@ class MetadataFile(DataFile):
advance_edits = {}
prefs = [p.id for p in item.preferences()]
for advance_edit in advance_tags_to_edit[self.library.type]:
key, options = plex.item_advance_keys[f"item_{advance_edit}"]
for advance_edit in util.advance_tags_to_edit[self.library.type]:
if advance_edit in methods:
if advance_edit in ["metadata_language", "use_original_title"] and self.library.agent not in plex.new_plex_agents:
logger.error(f"Metadata Error: {advance_edit} attribute only works for with the New Plex Movie Agent and New Plex TV Agent")
elif meta[methods[advance_edit]]:
key, options = plex.item_advance_keys[f"item_{advance_edit}"]
method_data = str(meta[methods[advance_edit]]).lower()
if method_data not in options:
logger.error(f"Metadata Error: {meta[methods[advance_edit]]} {advance_edit} attribute invalid")
@ -467,7 +454,7 @@ class MetadataFile(DataFile):
if self.library.edit_item(item, mapping_name, self.library.type, advance_edits, advanced=True):
updated = True
for tag_edit in tags_to_edit[self.library.type]:
for tag_edit in util.tags_to_edit[self.library.type]:
if self.edit_tags(tag_edit, item, meta, methods, extra=genres if tag_edit == "genre" else None):
updated = True
@ -495,24 +482,10 @@ class MetadataFile(DataFile):
logger.error(f"Metadata Error: Season: {season_id} not found")
continue
season_methods = {sm.lower(): sm for sm in season_dict}
if "title" in season_methods and season_dict[season_methods["title"]]:
title = season_dict[season_methods["title"]]
else:
title = season.title
if "sub" in season_methods:
if season_dict[season_methods["sub"]] is None:
logger.error("Metadata Error: sub attribute is blank")
elif season_dict[season_methods["sub"]] is True and "(SUB)" not in title:
title = f"{title} (SUB)"
elif season_dict[season_methods["sub"]] is False and title.endswith(" (SUB)"):
title = title[:-6]
else:
logger.error("Metadata Error: sub attribute must be True or False")
edits = {}
add_edit("title", season, season_dict, season_methods, value=title)
add_edit("title", season, season_dict, season_methods)
add_edit("summary", season, season_dict, season_methods)
add_edit("user_rating", season, season_dict, season_methods, key="userRating", var_type="float")
if self.library.edit_item(season, season_id, "Season", edits):
updated = True
self.set_images(season, season_dict, season_methods)
@ -538,24 +511,12 @@ class MetadataFile(DataFile):
logger.error(f"Metadata Error: Episode {episode_str} in Season {season_id} not found")
continue
episode_methods = {em.lower(): em for em in episode_dict}
if "title" in episode_methods and episode_dict[episode_methods["title"]]:
title = episode_dict[episode_methods["title"]]
else:
title = episode.title
if "sub" in episode_dict:
if episode_dict[episode_methods["sub"]] is None:
logger.error("Metadata Error: sub attribute is blank")
elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
title = f"{title} (SUB)"
elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
title = title[:-6]
else:
logger.error("Metadata Error: sub attribute must be True or False")
edits = {}
add_edit("title", episode, episode_dict, episode_methods, value=title)
add_edit("title", episode, episode_dict, episode_methods)
add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort")
add_edit("rating", episode, episode_dict, episode_methods, var_type="float")
add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float")
add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float")
add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float")
add_edit("originally_available", episode, episode_dict, episode_methods, key="originallyAvailableAt", var_type="date")
add_edit("summary", episode, episode_dict, episode_methods)
if self.library.edit_item(episode, f"{episode_str} in Season: {season_id}", "Episode", edits):
@ -589,24 +550,12 @@ class MetadataFile(DataFile):
logger.error(f"Metadata Error: episode {episode_id} of season {season_id} not found")
continue
episode_methods = {em.lower(): em for em in episode_dict}
if "title" in episode_methods and episode_dict[episode_methods["title"]]:
title = episode_dict[episode_methods["title"]]
else:
title = episode.title
if "sub" in episode_dict:
if episode_dict[episode_methods["sub"]] is None:
logger.error("Metadata Error: sub attribute is blank")
elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
title = f"{title} (SUB)"
elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
title = title[:-6]
else:
logger.error("Metadata Error: sub attribute must be True or False")
edits = {}
add_edit("title", episode, episode_dict, episode_methods, value=title)
add_edit("title", episode, episode_dict, episode_methods)
add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort")
add_edit("rating", episode, episode_dict, episode_methods, var_type="float")
add_edit("critic_rating", episode, episode_dict, episode_methods, key="rating", var_type="float")
add_edit("audience_rating", episode, episode_dict, episode_methods, key="audienceRating", var_type="float")
add_edit("user_rating", episode, episode_dict, episode_methods, key="userRating", var_type="float")
add_edit("originally_available", episode, episode_dict, episode_methods, key="originallyAvailableAt", var_type="date")
add_edit("summary", episode, episode_dict, episode_methods)
if self.library.edit_item(episode, f"{season_id} Episode: {episode_id}", "Season", edits):
@ -643,7 +592,8 @@ class MetadataFile(DataFile):
edits = {}
add_edit("title", album, album_dict, album_methods, value=title)
add_edit("sort_title", album, album_dict, album_methods, key="titleSort")
add_edit("rating", album, album_dict, album_methods, var_type="float")
add_edit("critic_rating", album, album_dict, album_methods, key="rating", var_type="float")
add_edit("user_rating", album, album_dict, album_methods, key="userRating", var_type="float")
add_edit("originally_available", album, album_dict, album_methods, key="originallyAvailableAt", var_type="date")
add_edit("record_label", album, album_dict, album_methods, key="studio")
add_edit("summary", album, album_dict, album_methods)
@ -684,7 +634,7 @@ class MetadataFile(DataFile):
title = track.title
edits = {}
add_edit("title", track, track_dict, track_methods, value=title)
add_edit("rating", track, track_dict, track_methods, var_type="float")
add_edit("user_rating", track, track_dict, track_methods, key="userRating", var_type="float")
add_edit("track", track, track_dict, track_methods, key="index", var_type="int")
add_edit("disc", track, track_dict, track_methods, key="parentIndex", var_type="int")
add_edit("original_artist", track, track_dict, track_methods, key="originalTitle")

View file

@ -1,15 +1,16 @@
import logging, os, plexapi, requests
from datetime import datetime
from modules import builder, util
from modules.library import Library
from modules.util import Failed, ImageData
from PIL import Image
from plexapi import utils
from plexapi.audio import Artist
from plexapi.audio import Artist, Track, Album
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.collection import Collection
from plexapi.playlist import Playlist
from plexapi.server import PlexServer
from plexapi.video import Movie, Show
from plexapi.video import Movie, Show, Season, Episode
from retrying import retry
from urllib import parse
from xml.etree.ElementTree import ParseError
@ -111,8 +112,8 @@ modifier_translation = {
"": "", ".not": "!", ".is": "%3D", ".isnot": "!%3D", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C",
".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E"
}
episode_sorting_options = {"default": "-1", "oldest": "0", "newest": "1"}
album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2}
episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1}
keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30}
delete_episodes_options = {"never": 0, "day": 1, "week": 7, "refresh": 100}
season_display_options = {"default": -1, "show": 0, "hide": 1}
@ -987,3 +988,108 @@ class Plex(Library):
elif isinstance(item, (Movie, Show)) and not poster and not background and self.show_missing_assets:
logger.warning(f"Asset Warning: No poster or background found in an assets folder for '{name}'")
return None, None, found_folder
def get_ids(self, item):
tmdb_id = None
tvdb_id = None
imdb_id = None
if self.config.Cache:
t_id, i_id, guid_media_type, _ = self.config.Cache.query_guid_map(item.guid)
if t_id:
if "movie" in guid_media_type:
tmdb_id = t_id[0]
else:
tvdb_id = t_id[0]
if i_id:
imdb_id = i_id[0]
if not tmdb_id and not tvdb_id:
tmdb_id = self.get_tmdb_from_map(item)
if not tmdb_id and not tvdb_id and self.is_show:
tvdb_id = self.get_tvdb_from_map(item)
return tmdb_id, tvdb_id, imdb_id
def get_locked_attributes(self, item, titles=None):
attrs = {}
fields = {f.name: f for f in item.fields if f.locked}
if isinstance(item, (Movie, Show)) and titles and titles.count(item.title) > 1:
map_key = f"{item.title} ({item.year})"
attrs["title"] = item.title
attrs["year"] = item.year
elif isinstance(item, (Season, Episode, Track)) and item.index:
map_key = int(item.index)
else:
map_key = item.title
if "title" in fields:
if isinstance(item, (Movie, Show)):
tmdb_id, tvdb_id, imdb_id = self.get_ids(item)
tmdb_item = self.config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=isinstance(item, Movie))
if tmdb_item:
attrs["alt_title"] = tmdb_item.title
elif isinstance(item, (Season, Episode, Track)):
attrs["title"] = item.title
def check_field(plex_key, pmm_key, var_key=None):
if plex_key in fields and pmm_key not in self.metadata_backup["exclude"]:
if not var_key:
var_key = plex_key
if hasattr(item, var_key):
plex_value = getattr(item, var_key)
if isinstance(plex_value, list):
plex_tags = [t.tag for t in plex_value]
if len(plex_tags) > 0 or self.metadata_backup["sync_tags"]:
attrs[f"{pmm_key}.sync" if self.metadata_backup["sync_tags"] else pmm_key] = None if not plex_tags else plex_tags[0] if len(plex_tags) == 1 else plex_tags
elif isinstance(plex_value, datetime):
attrs[pmm_key] = datetime.strftime(plex_value, "%Y-%m-%d")
else:
attrs[pmm_key] = plex_value
check_field("titleSort", "sort_title")
check_field("originalTitle", "original_artist" if self.is_music else "original_title")
check_field("originallyAvailableAt", "originally_available")
check_field("contentRating", "content_rating")
check_field("userRating", "user_rating")
check_field("audienceRating", "audience_rating")
check_field("rating", "critic_rating")
check_field("studio", "record_label" if self.is_music else "studio")
check_field("tagline", "tagline")
check_field("summary", "summary")
check_field("index", "track")
check_field("parentIndex", "disc")
check_field("director", "director", var_key="directors")
check_field("country", "country", var_key="countries")
check_field("genre", "genre", var_key="genres")
check_field("writer", "writer", var_key="writers")
check_field("producer", "producer", var_key="producers")
check_field("collection", "collection", var_key="collections")
check_field("label", "label", var_key="labels")
check_field("mood", "mood", var_key="moods")
check_field("style", "style", var_key="styles")
check_field("similar", "similar_artist")
for advance_edit in util.advance_tags_to_edit[self.type]:
key, options = item_advance_keys[f"item_{advance_edit}"]
if advance_edit in self.metadata_backup["exclude"] or not hasattr(item, key):
continue
keys = {v: k for k, v in options.items()}
if keys[getattr(item, key)] not in ["default", "all", "never"]:
attrs[advance_edit] = keys[getattr(item, key)]
def _recur(sub):
sub_items = {}
for sub_item in getattr(item, sub)():
sub_item_key, sub_item_attrs = self.get_locked_attributes(sub_item)
if sub_item_attrs:
sub_items[sub_item_key] = sub_item_attrs
if sub_items:
attrs[sub] = sub_items
if isinstance(item, Show):
_recur("seasons")
elif isinstance(item, Season):
_recur("episodes")
elif isinstance(item, Artist):
_recur("albums")
elif isinstance(item, Album):
_recur("tracks")
return map_key, attrs if attrs else None

View file

@ -238,3 +238,20 @@ class TMDb:
if len(ids) > 0:
logger.info(f"Processing {pretty}: ({tmdb_id}) {tmdb_name} ({len(ids)} Item{'' if len(ids) == 1 else 's'})")
return ids
def get_item(self, item, tmdb_id, tvdb_id, imdb_id, is_movie=True):
tmdb_item = None
if tvdb_id and not tmdb_id:
tmdb_id = self.config.Convert.tvdb_to_tmdb(tvdb_id)
if imdb_id and not tmdb_id:
_id, _type = self.config.Convert.imdb_to_tmdb(imdb_id)
if _id and ((_type == "movie" and is_movie) or (_type == "show" and not is_movie)):
tmdb_id = _id
if tmdb_id:
try:
tmdb_item = self.get_movie(tmdb_id) if is_movie else self.get_show(tmdb_id)
except Failed as e:
logger.error(util.adjust_space(str(e)))
else:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TMDb ID for Guid: {item.guid}"))
return tmdb_item

View file

@ -75,6 +75,18 @@ collection_mode_options = {
"hide_items": "hideItems", "hideitems": "hideItems",
"show_items": "showItems", "showitems": "showItems"
}
advance_tags_to_edit = {
"Movie": ["metadata_language", "use_original_title"],
"Show": ["episode_sorting", "keep_episodes", "delete_episodes", "season_display", "episode_ordering",
"metadata_language", "use_original_title"],
"Artist": ["album_sorting"]
}
tags_to_edit = {
"Movie": ["genre", "label", "collection", "country", "director", "producer", "writer"],
"Show": ["genre", "label", "collection"],
"Artist": ["genre", "style", "mood", "country", "collection", "similar_artist"]
}
def tab_new_lines(data):
return str(data).replace("\n", "\n ") if "\n" in str(data) else str(data)

View file

@ -12,6 +12,7 @@ try:
from modules.util import Failed, NotScheduled
from plexapi.exceptions import NotFound
from plexapi.video import Show, Season
from ruamel import yaml
except ModuleNotFoundError:
print("Requirements Error: Requirements are not installed")
sys.exit(0)
@ -445,7 +446,8 @@ def library_operations(config, library):
logger.debug(f"TMDb Collections: {library.tmdb_collections}")
logger.debug(f"Genre Collections: {library.genre_collections}")
logger.debug(f"Genre Mapper: {library.genre_mapper}")
logger.debug(f"TMDb Operation: {library.items_library_operation}")
logger.debug(f"Metadata Backup: {library.metadata_backup}")
logger.debug(f"Item Operation: {library.items_library_operation}")
if library.split_duplicates:
items = library.search(**{"duplicate": True})
@ -469,22 +471,7 @@ def library_operations(config, library):
util.print_return(f"Processing: {i}/{len(items)} {item.title}")
if library.assets_for_all:
library.find_assets(item)
tmdb_id = None
tvdb_id = None
imdb_id = None
if config.Cache:
t_id, i_id, guid_media_type, _ = config.Cache.query_guid_map(item.guid)
if t_id:
if "movie" in guid_media_type:
tmdb_id = t_id[0]
else:
tvdb_id = t_id[0]
if i_id:
imdb_id = i_id[0]
if not tmdb_id and not tvdb_id:
tmdb_id = library.get_tmdb_from_map(item)
if not tmdb_id and not tvdb_id and library.is_show:
tvdb_id = library.get_tvdb_from_map(item)
tmdb_id, tvdb_id, imdb_id = library.get_ids(item)
if library.mass_trakt_rating_update:
try:
@ -512,15 +499,7 @@ def library_operations(config, library):
tmdb_item = None
if library.tmdb_collections or library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" or library.mass_critic_rating_update == "tmdb":
if tvdb_id and not tmdb_id:
tmdb_id = config.Convert.tvdb_to_tmdb(tvdb_id)
if tmdb_id:
try:
tmdb_item = config.TMDb.get_movie(tmdb_id) if library.is_movie else config.TMDb.get_show(tmdb_id)
except Failed as e:
logger.error(util.adjust_space(str(e)))
else:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TMDb ID for Guid: {item.guid}"))
tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.is_movie)
omdb_item = None
if library.mass_genre_update in ["omdb", "imdb"] or library.mass_audience_rating_update in ["omdb", "imdb"] or library.mass_critic_rating_update in ["omdb", "imdb"]:
@ -713,6 +692,29 @@ def library_operations(config, library):
for col in unmanaged_collections:
library.find_assets(col)
if library.metadata_backup:
logger.info("")
util.separator(f"Metadata Backup for {library.name} Library", space=False, border=False)
logger.info("")
logger.info(f"Metadata Backup Path: {library.metadata_backup['path']}")
logger.info("")
meta = {}
items = library.get_all()
titles = [i.title for i in items]
for i, item in enumerate(items, 1):
util.print_return(f"Processing: {i}/{len(items)} {item.title}")
map_key, attrs = library.get_locked_attributes(item, titles)
if attrs or library.metadata_backup["add_blank_entries"]:
meta[map_key] = attrs
util.print_end()
with open(library.metadata_backup["path"], "w"):
pass
try:
yaml.round_trip_dump({"metadata": meta}, open(library.metadata_backup["path"], "w", encoding="utf-8"))
logger.info(f"{len(meta)} {library.type.capitalize()}{'s' if len(meta) > 1 else ''} Backed Up")
except yaml.scanner.ScannerError as e:
util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True)
def run_collection(config, library, metadata, requested_collections):
logger.info("")
for mapping_name, collection_attrs in requested_collections.items():

View file

@ -1,5 +1,5 @@
PlexAPI==4.9.1
tmdbapis==0.1.8
tmdbapis==0.1.9
arrapi==1.3.1
lxml==4.7.1
requests==2.27.1