You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Plex-Meta-Manager/plex_meta_manager.py

1205 lines
63 KiB

import argparse, logging, os, sys, time
from datetime import datetime
from logging.handlers import RotatingFileHandler
try:
import plexapi, schedule
from modules import util
from modules.builder import CollectionBuilder
from modules.config import ConfigFile
from modules.meta import MetadataFile
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)
if sys.version_info[0] != 3 or sys.version_info[1] < 6:
print("Version Error: Version: %s.%s.%s incompatible please use Python 3.6+" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))
sys.exit(0)
parser = argparse.ArgumentParser()
parser.add_argument("-db", "--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False)
parser.add_argument("-tr", "--trace", dest="trace", help=argparse.SUPPRESS, action="store_true", default=False)
parser.add_argument("-c", "--config", dest="config", help="Run with desired *.yml file", type=str)
parser.add_argument("-t", "--time", "--times", dest="times", help="Times to update each day use format HH:MM (Default: 03:00) (comma-separated list)", default="03:00", type=str)
parser.add_argument("-re", "--resume", dest="resume", help="Resume collection run from a specific collection", type=str)
parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False)
parser.add_argument("-is", "--ignore-schedules", dest="ignore_schedules", help="Run ignoring collection schedules", action="store_true", default=False)
parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False)
parser.add_argument("-co", "--collection-only", "--collections-only", dest="collection_only", help="Run only collection operations", action="store_true", default=False)
parser.add_argument("-lo", "--library-only", "--libraries-only", dest="library_only", help="Run only library operations", action="store_true", default=False)
parser.add_argument("-lf", "--library-first", "--libraries-first", dest="library_first", help="Run library operations before collections", action="store_true", default=False)
parser.add_argument("-rc", "-cl", "--collection", "--collections", "--run-collection", "--run-collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str)
parser.add_argument("-rl", "-l", "--library", "--libraries", "--run-library", "--run-libraries", dest="libraries", help="Process only specified libraries (comma-separated list)", type=str)
parser.add_argument("-rm", "-m", "--metadata", "--metadata-files", "--run-metadata-files", dest="metadata", help="Process only specified Metadata files (comma-separated list)", type=str)
parser.add_argument("-dc", "--delete", "--delete-collections", dest="delete", help="Deletes all Collections in the Plex Library before running", action="store_true", default=False)
parser.add_argument("-nc", "--no-countdown", dest="no_countdown", help="Run without displaying the countdown", action="store_true", default=False)
parser.add_argument("-nm", "--no-missing", dest="no_missing", help="Run without running the missing section", action="store_true", default=False)
parser.add_argument("-ro", "--read-only-config", dest="read_only_config", help="Run without writing to the config", action="store_true", default=False)
parser.add_argument("-d", "--divider", dest="divider", help="Character that divides the sections (Default: '=')", default="=", type=str)
parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default: 100)", default=100, type=int)
args = parser.parse_args()
def get_arg(env_str, default, arg_bool=False, arg_int=False):
env_var = os.environ.get(env_str)
if env_var:
if arg_bool:
if env_var is True or env_var is False:
return env_var
elif env_var.lower() in ["t", "true"]:
return True
else:
return False
elif arg_int:
return int(env_var)
else:
return str(env_var)
else:
return default
config_file = get_arg("PMM_CONFIG", args.config)
times = get_arg("PMM_TIME", args.times)
run = get_arg("PMM_RUN", args.run, arg_bool=True)
test = get_arg("PMM_TEST", args.test, arg_bool=True)
ignore_schedules = get_arg("PMM_IGNORE_SCHEDULES", args.ignore_schedules, arg_bool=True)
collection_only = get_arg("PMM_COLLECTIONS_ONLY", args.collection_only, arg_bool=True)
library_only = get_arg("PMM_LIBRARIES_ONLY", args.library_only, arg_bool=True)
library_first = get_arg("PMM_LIBRARIES_FIRST", args.library_first, arg_bool=True)
collections = get_arg("PMM_COLLECTIONS", args.collections)
libraries = get_arg("PMM_LIBRARIES", args.libraries)
metadata_files = get_arg("PMM_METADATA_FILES", args.metadata)
delete = get_arg("PMM_DELETE_COLLECTIONS", args.delete, arg_bool=True)
resume = get_arg("PMM_RESUME", args.resume)
no_countdown = get_arg("PMM_NO_COUNTDOWN", args.no_countdown, arg_bool=True)
no_missing = get_arg("PMM_NO_MISSING", args.no_missing, arg_bool=True)
read_only_config = get_arg("PMM_READ_ONLY_CONFIG", args.read_only_config, arg_bool=True)
divider = get_arg("PMM_DIVIDER", args.divider)
screen_width = get_arg("PMM_WIDTH", args.width, arg_int=True)
debug = get_arg("PMM_DEBUG", args.debug, arg_bool=True)
trace = get_arg("PMM_TRACE", args.trace, arg_bool=True)
util.separating_character = divider[0]
if screen_width < 90 or screen_width > 300:
print(f"Argument Error: width argument invalid: {screen_width} must be an integer between 90 and 300 using the default 100")
screen_width = 100
util.screen_width = screen_width
default_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config")
if config_file and os.path.exists(config_file):
default_dir = os.path.join(os.path.dirname(os.path.abspath(config_file)))
elif config_file and not os.path.exists(config_file):
print(f"Config Error: config not found at {os.path.abspath(config_file)}")
sys.exit(0)
elif not os.path.exists(os.path.join(default_dir, "config.yml")):
print(f"Config Error: config not found at {os.path.abspath(default_dir)}")
sys.exit(0)
os.makedirs(os.path.join(default_dir, "logs"), exist_ok=True)
logger = logging.getLogger("Plex Meta Manager")
logger.setLevel(logging.DEBUG)
def fmt_filter(record):
record.levelname = f"[{record.levelname}]"
record.filename = f"[{record.filename}:{record.lineno}]"
return True
cmd_handler = logging.StreamHandler()
cmd_handler.setLevel(logging.DEBUG if test or debug or trace else logging.INFO)
logger.addHandler(cmd_handler)
sys.excepthook = util.my_except_hook
version = "Unknown"
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) as handle:
for line in handle.readlines():
line = line.strip()
if len(line) > 0:
version = line
break
plexapi.BASE_HEADERS['X-Plex-Client-Identifier'] = "Plex-Meta-Manager"
def start(attrs):
file_logger = os.path.join(default_dir, "logs", "meta.log")
should_roll_over = os.path.isfile(file_logger)
file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8")
util.apply_formatter(file_handler)
file_handler.addFilter(fmt_filter)
if should_roll_over:
file_handler.doRollover()
logger.addHandler(file_handler)
util.separator()
logger.info("")
logger.info(util.centered(" ____ _ __ __ _ __ __ "))
logger.info(util.centered("| _ \\| | _____ __ | \\/ | ___| |_ __ _ | \\/ | __ _ _ __ __ _ __ _ ___ _ __ "))
logger.info(util.centered("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_ \\ / _` |/ _` |/ _ \\ '__|"))
logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | "))
logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.centered(" |___/ "))
logger.info(f" Version: {version}")
if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} "
elif "test" in attrs and attrs["test"]: start_type = "Test "
elif "collections" in attrs and attrs["collections"]: start_type = "Collections "
elif "libraries" in attrs and attrs["libraries"]: start_type = "Libraries "
else: start_type = ""
start_time = datetime.now()
if "time" not in attrs:
attrs["time"] = start_time.strftime("%H:%M")
attrs["time_obj"] = start_time
util.separator(debug=True)
logger.debug(f"--config (PMM_CONFIG): {config_file}")
logger.debug(f"--time (PMM_TIME): {times}")
logger.debug(f"--run (PMM_RUN): {run}")
logger.debug(f"--run-tests (PMM_TEST): {test}")
logger.debug(f"--collections-only (PMM_COLLECTIONS_ONLY): {collection_only}")
logger.debug(f"--libraries-only (PMM_LIBRARIES_ONLY): {library_only}")
logger.debug(f"--libraries-first (PMM_LIBRARIES_FIRST): {library_first}")
logger.debug(f"--run-collections (PMM_COLLECTIONS): {collections}")
logger.debug(f"--run-libraries (PMM_LIBRARIES): {libraries}")
logger.debug(f"--run-metadata-files (PMM_METADATA_FILES): {metadata_files}")
logger.debug(f"--ignore-schedules (PMM_IGNORE_SCHEDULES): {ignore_schedules}")
logger.debug(f"--delete-collections (PMM_DELETE_COLLECTIONS): {delete}")
logger.debug(f"--resume (PMM_RESUME): {resume}")
logger.debug(f"--no-countdown (PMM_NO_COUNTDOWN): {no_countdown}")
logger.debug(f"--no-missing (PMM_NO_MISSING): {no_missing}")
logger.debug(f"--read-only-config (PMM_READ_ONLY_CONFIG): {read_only_config}")
logger.debug(f"--divider (PMM_DIVIDER): {divider}")
logger.debug(f"--width (PMM_WIDTH): {screen_width}")
logger.debug(f"--debug (PMM_DEBUG): {debug}")
logger.debug(f"--trace (PMM_TRACE): {trace}")
logger.debug("")
util.separator(f"Starting {start_type}Run")
config = None
stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0}
try:
config = ConfigFile(default_dir, attrs, read_only_config)
except Exception as e:
util.print_stacktrace()
util.print_multiline(e, critical=True)
else:
try:
stats = update_libraries(config)
except Exception as e:
config.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
logger.info("")
end_time = datetime.now()
run_time = str(end_time - start_time).split('.')[0]
if config:
try:
config.Webhooks.end_time_hooks(start_time, end_time, run_time, stats)
except Failed as e:
util.print_stacktrace()
logger.error(f"Webhooks Error: {e}")
util.separator(f"Finished {start_type}Run\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}")
logger.removeHandler(file_handler)
def update_libraries(config):
for library in config.libraries:
if library.skip_library:
logger.info("")
util.separator(f"Skipping {library.name} Library")
continue
try:
os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True)
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log")
should_roll_over = os.path.isfile(col_file_logger)
library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
if should_roll_over:
library_handler.doRollover()
logger.addHandler(library_handler)
plexapi.server.TIMEOUT = library.timeout
logger.info("")
util.separator(f"{library.name} Library")
if config.library_first and library.library_operation and not config.test_mode and not collection_only:
library_operations(config, library)
logger.debug("")
logger.debug(f"Mapping Name: {library.original_mapping_name}")
logger.debug(f"Folder Name: {library.mapping_name}")
logger.debug(f"Missing Path: {library.missing_path}")
for ad in library.asset_directory:
logger.debug(f"Asset Directory: {ad}")
logger.debug(f"Asset Folders: {library.asset_folders}")
logger.debug(f"Create Asset Folders: {library.create_asset_folders}")
logger.debug(f"Download URL Assets: {library.download_url_assets}")
logger.debug(f"Sync Mode: {library.sync_mode}")
logger.debug(f"Collection Minimum: {library.minimum_items}")
logger.debug(f"Delete Below Minimum: {library.delete_below_minimum}")
logger.debug(f"Delete Not Scheduled: {library.delete_not_scheduled}")
logger.debug(f"Default Collection Order: {library.default_collection_order}")
logger.debug(f"Missing Only Released: {library.missing_only_released}")
logger.debug(f"Only Filter Missing: {library.only_filter_missing}")
logger.debug(f"Show Unmanaged: {library.show_unmanaged}")
logger.debug(f"Show Filtered: {library.show_filtered}")
logger.debug(f"Show Missing: {library.show_missing}")
logger.debug(f"Show Missing Assets: {library.show_missing_assets}")
logger.debug(f"Save Missing: {library.save_missing}")
logger.debug(f"Clean Bundles: {library.clean_bundles}")
logger.debug(f"Empty Trash: {library.empty_trash}")
logger.debug(f"Optimize: {library.optimize}")
logger.debug(f"Timeout: {library.timeout}")
if config.delete_collections:
logger.info("")
util.separator(f"Deleting all Collections from the {library.name} Library", space=False, border=False)
logger.info("")
for collection in library.get_all_collections():
logger.info(f"Collection {collection.title} Deleted")
library.query(collection.delete)
if not library.is_other and not library.is_music and (library.metadata_files or library.original_mapping_name in config.library_map) and not library_only:
logger.info("")
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
library.map_guids()
for metadata in library.metadata_files:
metadata_name = metadata.get_file_name()
if config.requested_metadata_files and metadata_name not in config.requested_metadata_files:
continue
logger.info("")
util.separator(f"Running {metadata_name} Metadata File\n{metadata.path}")
if not config.test_mode and not config.resume_from and not collection_only:
try:
metadata.update_metadata()
except Failed as e:
library.notify(e)
logger.error(e)
collections_to_run = metadata.get_collections(config.requested_collections)
if config.resume_from and config.resume_from not in collections_to_run:
logger.info("")
logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}")
continue
if collections_to_run and not library_only:
logger.info("")
util.separator(f"{'Test ' if config.test_mode else ''}Collections")
logger.removeHandler(library_handler)
run_collection(config, library, metadata, collections_to_run)
logger.addHandler(library_handler)
if library.run_sort:
logger.info("")
util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False)
logger.info("")
for builder in library.run_sort:
logger.info("")
util.separator(f"Sorting {builder.name} Collection", space=False, border=False)
logger.info("")
builder.sort_collection()
if not config.library_first and library.library_operation and not config.test_mode and not collection_only:
library_operations(config, library)
logger.removeHandler(library_handler)
except Exception as e:
library.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
playlist_status = {}
playlist_stats = {}
if config.playlist_files:
os.makedirs(os.path.join(default_dir, "logs", "playlists"), exist_ok=True)
pf_file_logger = os.path.join(default_dir, "logs", "playlists", "playlists.log")
should_roll_over = os.path.isfile(pf_file_logger)
playlists_handler = RotatingFileHandler(pf_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(playlists_handler)
if should_roll_over:
playlists_handler.doRollover()
logger.addHandler(playlists_handler)
playlist_status, playlist_stats = run_playlists(config)
logger.removeHandler(playlists_handler)
has_run_again = False
for library in config.libraries:
if library.run_again:
has_run_again = True
break
amount_added = 0
if has_run_again and not library_only:
logger.info("")
util.separator("Run Again")
logger.info("")
for x in range(1, config.general["run_again_delay"] + 1):
util.print_return(f"Waiting to run again in {config.general['run_again_delay'] - x + 1} minutes")
for y in range(60):
time.sleep(1)
util.print_end()
for library in config.libraries:
if library.run_again:
try:
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log")
library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
logger.addHandler(library_handler)
library_handler.addFilter(fmt_filter)
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"{library.name} Library Run Again")
logger.info("")
library.map_guids()
for builder in library.run_again:
logger.info("")
util.separator(f"{builder.name} Collection in {library.name}")
logger.info("")
try:
amount_added += builder.run_collections_again()
except Failed as e:
library.notify(e, collection=builder.name, critical=False)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.removeHandler(library_handler)
except Exception as e:
library.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
used_url = []
for library in config.libraries:
if library.url not in used_url:
used_url.append(library.url)
if library.empty_trash:
library.query(library.PlexServer.library.emptyTrash)
if library.clean_bundles:
library.query(library.PlexServer.library.cleanBundles)
if library.optimize:
library.query(library.PlexServer.library.optimize)
longest = 20
for library in config.libraries:
for title in library.status:
if len(title) > longest:
longest = len(title)
if playlist_status:
for title in playlist_status:
if len(title) > longest:
longest = len(title)
def print_status(section, status):
logger.info("")
util.separator(f"{section} Summary", space=False, border=False)
logger.info("")
logger.info(f"{'Title':^{longest}} | + | = | - | {'Status':^13}")
breaker = f"{util.separating_character * longest}|{util.separating_character * 5}|{util.separating_character * 5}|{util.separating_character * 5}|"
util.separator(breaker, space=False, border=False, side_space=False, left=True)
for name, data in status.items():
logger.info(f"{name:<{longest}} | {data['added']:^3} | {data['unchanged']:^3} | {data['removed']:^3} | {data['status']}")
if data["errors"]:
for error in data["errors"]:
util.print_multiline(error, info=True)
logger.info("")
util.separator("Summary")
for library in config.libraries:
print_status(library.name, library.status)
if playlist_status:
print_status("Playlists", playlist_status)
stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0}
stats["added"] += amount_added
for library in config.libraries:
stats["created"] += library.stats["created"]
stats["modified"] += library.stats["modified"]
stats["deleted"] += library.stats["deleted"]
stats["added"] += library.stats["added"]
stats["unchanged"] += library.stats["unchanged"]
stats["removed"] += library.stats["removed"]
stats["radarr"] += library.stats["radarr"]
stats["sonarr"] += library.stats["sonarr"]
if playlist_stats:
stats["created"] += playlist_stats["created"]
stats["modified"] += playlist_stats["modified"]
stats["deleted"] += playlist_stats["deleted"]
stats["added"] += playlist_stats["added"]
stats["unchanged"] += playlist_stats["unchanged"]
stats["removed"] += playlist_stats["removed"]
stats["radarr"] += playlist_stats["radarr"]
stats["sonarr"] += playlist_stats["sonarr"]
return stats
def library_operations(config, library):
logger.info("")
util.separator(f"{library.name} Library Operations")
logger.info("")
logger.debug(f"Assets For All: {library.assets_for_all}")
logger.debug(f"Delete Collections With Less: {library.delete_collections_with_less}")
logger.debug(f"Delete Unmanaged Collections: {library.delete_unmanaged_collections}")
logger.debug(f"Mass Genre Update: {library.mass_genre_update}")
logger.debug(f"Mass Audience Rating Update: {library.mass_audience_rating_update}")
logger.debug(f"Mass Critic Rating Update: {library.mass_critic_rating_update}")
logger.debug(f"Mass Content Rating Update: {library.mass_content_rating_update}")
logger.debug(f"Mass Trakt Rating Update: {library.mass_trakt_rating_update}")
logger.debug(f"Mass Collection Mode Update: {library.mass_collection_mode}")
logger.debug(f"Split Duplicates: {library.split_duplicates}")
logger.debug(f"Radarr Add All Existing: {library.radarr_add_all_existing}")
logger.debug(f"Radarr Remove by Tag: {library.radarr_remove_by_tag}")
logger.debug(f"Sonarr Add All Existing: {library.sonarr_add_all_existing}")
logger.debug(f"Sonarr Remove by Tag: {library.sonarr_remove_by_tag}")
logger.debug(f"Update Blank Track Titles: {library.update_blank_track_titles}")
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"Metadata Backup: {library.metadata_backup}")
logger.debug(f"Item Operation: {library.items_library_operation}")
logger.debug("")
if library.split_duplicates:
items = library.search(**{"duplicate": True})
for item in items:
item.split()
logger.info(util.adjust_space(f"{item.title[:25]:<25} | Splitting"))
if library.update_blank_track_titles:
tracks = library.get_all(collection_level="track")
for i, item in enumerate(tracks, 1):
util.print_return(f"Processing Track: {i}/{len(tracks)} {item.title}")
if not item.title and item.sortTitle:
library.edit_query(item, {"title.locked": 1, "title.value": item.sortTitle})
logger.info(f"Track: {item.sortTitle} was updated with sort title")
util.print_end()
tmdb_collections = {}
if library.items_library_operation:
items = library.get_all(load=True)
radarr_adds = []
sonarr_adds = []
trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else []
for i, item in enumerate(items, 1):
try:
library.reload(item)
except Failed as e:
logger.error(e)
continue
util.print_return(f"Processing: {i}/{len(items)} {item.title}")
if library.assets_for_all:
library.find_assets(item)
tmdb_id, tvdb_id, imdb_id = library.get_ids(item)
if library.mass_trakt_rating_update:
try:
if library.is_movie and tmdb_id in trakt_ratings:
new_rating = trakt_ratings[tmdb_id]
elif library.is_show and tvdb_id in trakt_ratings:
new_rating = trakt_ratings[tvdb_id]
else:
raise Failed
if str(item.userRating) != str(new_rating):
library.edit_query(item, {"userRating.value": new_rating, "userRating.locked": 1})
logger.info(util.adjust_space(f"{item.title[:25]:<25} | User Rating | {new_rating}"))
except Failed:
pass
path = os.path.dirname(str(item.locations[0])) if library.is_movie else str(item.locations[0])
if library.Radarr and library.radarr_add_all_existing and tmdb_id:
path = path.replace(library.Radarr.plex_path, library.Radarr.radarr_path)
path = path[:-1] if path.endswith(('/', '\\')) else path
radarr_adds.append((tmdb_id, path))
if library.Sonarr and library.sonarr_add_all_existing and tvdb_id:
path = path.replace(library.Sonarr.plex_path, library.Sonarr.sonarr_path)
path = path[:-1] if path.endswith(('/', '\\')) else path
sonarr_adds.append((tvdb_id, path))
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":
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 == "omdb" or library.mass_audience_rating_update == "omdb" \
or library.mass_critic_rating_update == "omdb" or library.mass_content_rating_update == "omdb":
if config.OMDb.limit is False:
if tmdb_id and not imdb_id:
imdb_id = config.Convert.tmdb_to_imdb(tmdb_id)
elif tvdb_id and not imdb_id:
imdb_id = config.Convert.tvdb_to_imdb(tvdb_id)
if imdb_id:
try:
omdb_item = config.OMDb.get_omdb(imdb_id)
except Failed as e:
logger.error(util.adjust_space(str(e)))
except Exception:
logger.error(f"IMDb ID: {imdb_id}")
raise
else:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}"))
tvdb_item = None
if library.mass_genre_update == "tvdb":
if tvdb_id:
try:
tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie)
except Failed as e:
logger.error(util.adjust_space(str(e)))
else:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}"))
mdb_item = None
if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \
or library.mass_content_rating_update in ["mdb", "mdb_commonsense"]:
if config.Mdblist.limit is False:
if tmdb_id and not imdb_id:
imdb_id = config.Convert.tmdb_to_imdb(tmdb_id)
elif tvdb_id and not imdb_id:
imdb_id = config.Convert.tvdb_to_imdb(tvdb_id)
if imdb_id:
try:
mdb_item = config.Mdblist.get_imdb(imdb_id)
except Failed as e:
logger.error(util.adjust_space(str(e)))
except Exception:
logger.error(f"IMDb ID: {imdb_id}")
raise
else:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}"))
if library.tmdb_collections and tmdb_item and tmdb_item.collection:
tmdb_collections[tmdb_item.collection.id] = tmdb_item.collection.name
def get_rating(attribute):
if tmdb_item and attribute == "tmdb":
return tmdb_item.vote_average
elif omdb_item and attribute == "omdb":
return omdb_item.imdb_rating
elif mdb_item and attribute == "mdb":
return mdb_item.score / 10 if mdb_item.score else None
elif mdb_item and attribute == "mdb_imdb":
return mdb_item.imdb_rating if mdb_item.imdb_rating else None
elif mdb_item and attribute == "mdb_metacritic":
return mdb_item.metacritic_rating / 10 if mdb_item.metacritic_rating else None
elif mdb_item and attribute == "mdb_metacriticuser":
return mdb_item.metacriticuser_rating if mdb_item.metacriticuser_rating else None
elif mdb_item and attribute == "mdb_trakt":
return mdb_item.trakt_rating / 10 if mdb_item.trakt_rating else None
elif mdb_item and attribute == "mdb_tomatoes":
return mdb_item.tmdb_rating / 10 if mdb_item.tomatoes_rating else None
elif mdb_item and attribute == "mdb_tomatoesaudience":
return mdb_item.tomatoesaudience_rating / 10 if mdb_item.tomatoesaudience_rating else None
elif mdb_item and attribute == "mdb_tmdb":
return mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None
elif mdb_item and attribute == "mdb_letterboxd":
return mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None
else:
raise Failed
if library.mass_genre_update:
try:
if tmdb_item and library.mass_genre_update == "tmdb":
new_genres = [genre.name for genre in tmdb_item.genres]
elif omdb_item and library.mass_genre_update == "omdb":
new_genres = omdb_item.genres
elif tvdb_item and library.mass_genre_update == "tvdb":
new_genres = tvdb_item.genres
else:
raise Failed
library.edit_tags("genre", item, sync_tags=new_genres)
except Failed:
pass
if library.mass_audience_rating_update:
try:
new_rating = get_rating(library.mass_audience_rating_update)
if new_rating is None:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found"))
elif str(item.audienceRating) != str(new_rating):
library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1})
logger.info(util.adjust_space(f"{item.title[:25]:<25} | Audience Rating | {new_rating}"))
except Failed:
pass
if library.mass_critic_rating_update:
try:
new_rating = get_rating(library.mass_critic_rating_update)
if new_rating is None:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found"))
elif str(item.rating) != str(new_rating):
library.edit_query(item, {"rating.value": new_rating, "rating.locked": 1})
logger.info(util.adjust_space(f"{item.title[:25]:<25} | Critic Rating | {new_rating}"))
except Failed:
pass
if library.mass_content_rating_update:
try:
if omdb_item and library.mass_content_rating_update == "omdb":
new_rating = omdb_item.content_rating
elif mdb_item and library.mass_content_rating_update == "mdb":
new_rating = mdb_item.certification if mdb_item.certification else None
elif mdb_item and library.mass_content_rating_update == "mdb_commonsense":
new_rating = mdb_item.commonsense if mdb_item.commonsense else None
else:
raise Failed
if new_rating is None:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Content Rating Found"))
elif str(item.rating) != str(new_rating):
library.edit_query(item, {"contentRating.value": new_rating, "contentRating.locked": 1})
logger.info(util.adjust_space(f"{item.title[:25]:<25} | Content Rating | {new_rating}"))
except Failed:
pass
if library.genre_mapper:
try:
adds = []
deletes = []
library.reload(item)
for genre in item.genres:
if genre.tag in library.genre_mapper:
deletes.append(genre.tag)
if library.genre_mapper[genre.tag]:
adds.append(library.genre_mapper[genre.tag])
library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes)
except Failed:
pass
if library.Radarr and library.radarr_add_all_existing:
try:
library.Radarr.add_tmdb(radarr_adds)
except Failed as e:
logger.error(e)
if library.Sonarr and library.sonarr_add_all_existing:
try:
library.Sonarr.add_tvdb(sonarr_adds)
except Failed as e:
logger.error(e)
if tmdb_collections or library.genre_collections:
logger.info("")
util.separator(f"Starting Automated Collections")
logger.info("")
new_collections = {}
templates = {}
if tmdb_collections:
templates["TMDb Collection"] = library.tmdb_collections["template"]
for _i, _n in tmdb_collections.items():
if int(_i) not in library.tmdb_collections["exclude_ids"]:
template = {"name": "TMDb Collection", "collection_id": _i}
for k, v in library.tmdb_collections["dictionary_variables"].items():
if int(_i) in v:
template[k] = v[int(_i)]
for suffix in library.tmdb_collections["remove_suffix"]:
if _n.endswith(suffix):
_n = _n[:-len(suffix)]
new_collections[_n.strip()] = {"template": template}
if library.genre_collections:
templates["Genre Collection"] = library.genre_collections["template"]
for genre in library.get_tags("genre"):
if genre.title not in library.genre_collections["exclude_genres"]:
template = {"name": "Genre Collection", "genre": genre.title}
for k, v in library.genre_collections["dictionary_variables"].items():
if genre.title in v:
template[k] = v[genre.title]
title = library.genre_collections["title_format"]
title = title.replace("<<genre>>", genre.title)
if "<<library_type>>" in title:
title = title.replace("<<library_type>>", library.type)
new_collections[title] = {"template": template}
metadata = MetadataFile(config, library, "Data", {"collections": new_collections, "templates": templates})
if metadata.collections:
library.collections.extend([c for c in metadata.collections])
run_collection(config, library, metadata, metadata.get_collections(None))
if library.radarr_remove_by_tag:
library.Radarr.remove_all_with_tags(library.radarr_remove_by_tag)
if library.sonarr_remove_by_tag:
library.Sonarr.remove_all_with_tags(library.sonarr_remove_by_tag)
if library.delete_collections_with_less is not None or library.delete_unmanaged_collections:
logger.info("")
print_suffix = ""
unmanaged = ""
if library.delete_collections_with_less is not None and library.delete_collections_with_less > 0:
print_suffix = f" with less then {library.delete_collections_with_less} item{'s' if library.delete_collections_with_less > 1 else ''}"
if library.delete_unmanaged_collections:
if library.delete_collections_with_less is None:
unmanaged = "Unmanaged Collections "
elif library.delete_collections_with_less > 0:
unmanaged = "Unmanaged Collections and "
util.separator(f"Deleting All {unmanaged}Collections{print_suffix}", space=False, border=False)
logger.info("")
unmanaged_collections = []
for col in library.get_all_collections():
if (library.delete_collections_with_less is not None
and (library.delete_collections_with_less == 0 or col.childCount < library.delete_collections_with_less)) \
or (col.title not in library.collections and library.delete_unmanaged_collections):
library.query(col.delete)
logger.info(f"{col.title} Deleted")
elif col.title not in library.collections:
unmanaged_collections.append(col)
if library.mass_collection_mode:
library.collection_mode_query(col, library.mass_collection_mode)
if library.show_unmanaged and len(unmanaged_collections) > 0:
logger.info("")
util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
logger.info(col.title)
logger.info("")
logger.info(f"{len(unmanaged_collections)} Unmanaged Collection{'s' if len(unmanaged_collections) > 1 else ''}")
elif library.show_unmanaged:
logger.info("")
util.separator(f"No Unmanaged Collections in {library.name} Library", space=False, border=False)
logger.info("")
if library.assets_for_all and len(unmanaged_collections) > 0:
logger.info("")
util.separator(f"Unmanaged Collection Assets Check for {library.name} Library", space=False, border=False)
logger.info("")
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("")
try:
meta, _, _ = yaml.util.load_yaml_guess_indent(open(library.metadata_backup["path"]))
except yaml.scanner.ScannerError as e:
meta = {}
util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True)
filename, file_extension = os.path.splitext(library.metadata_backup["path"])
i = 1
while os.path.exists(f"{filename}{i}{file_extension}"):
i += 1
os.rename(library.metadata_backup["path"], f"{filename}{i}{file_extension}")
logger.error(f"Backup failed to load saving copy to {filename}{i}{file_extension}")
if "metadata" not in meta:
meta["metadata"] = {}
items = library.get_all(load=True)
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"]:
def run_dict(save_dict, the_dict):
for kk, vv in the_dict.items():
if isinstance(vv, dict):
run_dict(save_dict[kk], vv)
else:
save_dict[kk] = vv
run_dict(meta["metadata"][map_key], attrs)
util.print_end()
try:
yaml.round_trip_dump(meta, open(library.metadata_backup["path"], "w", encoding="utf-8"), block_seq_indent=2)
logger.info(f"{len(meta['metadata'])} {library.type.capitalize()}{'s' if len(meta['metadata']) > 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():
collection_start = datetime.now()
if config.test_mode and ("test" not in collection_attrs or collection_attrs["test"] is not True):
no_template_test = True
if "template" in collection_attrs and collection_attrs["template"]:
for data_template in util.get_list(collection_attrs["template"], split=False):
if "name" in data_template \
and data_template["name"] \
and metadata.templates \
and data_template["name"] in metadata.templates \
and metadata.templates[data_template["name"]] \
and "test" in metadata.templates[data_template["name"]] \
and metadata.templates[data_template["name"]]["test"] is True:
no_template_test = False
if no_template_test:
continue
if config.resume_from and config.resume_from != mapping_name:
continue
elif config.resume_from == mapping_name:
config.resume_from = None
logger.info("")
util.separator(f"Resuming Collections")
if "name_mapping" in collection_attrs and collection_attrs["name_mapping"]:
collection_log_name, output_str = util.validate_filename(collection_attrs["name_mapping"])
else:
collection_log_name, output_str = util.validate_filename(mapping_name)
collection_log_folder = os.path.join(default_dir, "logs", library.mapping_name, "collections", collection_log_name)
os.makedirs(collection_log_folder, exist_ok=True)
col_file_logger = os.path.join(collection_log_folder, "collection.log")
should_roll_over = os.path.isfile(col_file_logger)
collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(collection_handler)
if should_roll_over:
collection_handler.doRollover()
logger.addHandler(collection_handler)
library.status[mapping_name] = {"status": "", "errors": [], "created": False, "modified": False, "deleted": False, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0}
try:
util.separator(f"{mapping_name} Collection in {library.name}")
logger.info("")
if output_str:
logger.info(output_str)
logger.info("")
util.separator(f"Validating {mapping_name} Attributes", space=False, border=False)
builder = CollectionBuilder(config, metadata, mapping_name, no_missing, collection_attrs, library=library)
logger.info("")
util.separator(f"Running {mapping_name} Collection", space=False, border=False)
if len(builder.schedule) > 0:
util.print_multiline(builder.schedule, info=True)
if len(builder.smart_filter_details) > 0:
logger.info("")
util.print_multiline(builder.smart_filter_details, info=True)
items_added = 0
items_removed = 0
valid = True
if not builder.smart_url and builder.builders and not builder.blank_collection:
logger.info("")
logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}")
if builder.filters or builder.tmdb_filters:
logger.info("")
for filter_key, filter_value in builder.filters:
logger.info(f"Collection Filter {filter_key}: {filter_value}")
for filter_key, filter_value in builder.tmdb_filters:
logger.info(f"Collection Filter {filter_key}: {filter_value}")
for method, value in builder.builders:
logger.debug("")
logger.debug(f"Builder: {method}: {value}")
logger.info("")
builder.filter_and_save_items(builder.gather_ids(method, value))
if len(builder.added_items) > 0 and len(builder.added_items) + builder.beginning_count >= builder.minimum and builder.build_collection:
items_added, items_unchanged = builder.add_to_collection()
library.stats["added"] += items_added
library.status[mapping_name]["added"] = items_added
library.stats["unchanged"] += items_unchanged
library.status[mapping_name]["unchanged"] = items_unchanged
items_removed = 0
if builder.sync:
items_removed = builder.sync_collection()
library.stats["removed"] += items_removed
library.status[mapping_name]["removed"] = items_removed
elif len(builder.added_items) + builder.beginning_count < builder.minimum and builder.build_collection:
logger.info("")
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
valid = False
if builder.details["delete_below_minimum"] and builder.obj:
logger.info("")
util.print_multiline(builder.delete(), info=True)
builder.deleted = True
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
radarr_add, sonarr_add = builder.run_missing()
library.stats["radarr"] += radarr_add
library.status[mapping_name]["radarr"] += radarr_add
library.stats["sonarr"] += sonarr_add
library.status[mapping_name]["sonarr"] += sonarr_add
run_item_details = True
if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection):
try:
builder.load_collection()
if builder.created:
library.stats["created"] += 1
library.status[mapping_name]["created"] = True
elif items_added > 0 or items_removed > 0:
library.stats["modified"] += 1
library.status[mapping_name]["modified"] = True
except Failed:
util.print_stacktrace()
run_item_details = False
logger.info("")
util.separator("No Collection to Update", space=False, border=False)
else:
builder.update_details()
if builder.deleted:
library.stats["deleted"] += 1
library.status[mapping_name]["deleted"] = True
if builder.server_preroll is not None:
library.set_server_preroll(builder.server_preroll)
logger.info("")
logger.info(f"Plex Server Movie pre-roll video updated to {builder.server_preroll}")
if (builder.item_details or builder.custom_sort) and run_item_details and builder.builders:
try:
builder.load_collection_items()
except Failed:
logger.info("")
util.separator("No Items Found", space=False, border=False)
else:
if builder.item_details:
builder.update_item_details()
if builder.custom_sort:
library.run_sort.append(builder)
# builder.sort_collection()
builder.send_notifications()
if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0):
library.run_again.append(builder)
if library.status[mapping_name]["created"]:
library.status[mapping_name]["status"] = "Created"
elif library.status[mapping_name]["deleted"]:
library.status[mapping_name]["status"] = "Deleted"
elif library.status[mapping_name]["modified"]:
library.status[mapping_name]["status"] = "Modified"
else:
library.status[mapping_name]["status"] = "Unchanged"
except NotScheduled as e:
util.print_multiline(e, info=True)
library.status[mapping_name]["status"] = "Not Scheduled"
except Failed as e:
library.notify(e, collection=mapping_name)
util.print_stacktrace()
util.print_multiline(e, error=True)
library.status[mapping_name]["status"] = "PMM Failure"
library.status[mapping_name]["errors"].append(e)
except Exception as e:
library.notify(f"Unknown Error: {e}", collection=mapping_name)
util.print_stacktrace()
logger.error(f"Unknown Error: {e}")
library.status[mapping_name]["status"] = "Unknown Error"
library.status[mapping_name]["errors"].append(e)
logger.info("")
util.separator(f"Finished {mapping_name} Collection\nCollection Run Time: {str(datetime.now() - collection_start).split('.')[0]}")
logger.removeHandler(collection_handler)
def run_playlists(config):
stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0}
status = {}
logger.info("")
util.separator("Playlists")
logger.info("")
for playlist_file in config.playlist_files:
for mapping_name, playlist_attrs in playlist_file.playlists.items():
playlist_start = datetime.now()
if config.test_mode and ("test" not in playlist_attrs or playlist_attrs["test"] is not True):
no_template_test = True
if "template" in playlist_attrs and playlist_attrs["template"]:
for data_template in util.get_list(playlist_attrs["template"], split=False):
if "name" in data_template \
and data_template["name"] \
and playlist_file.templates \
and data_template["name"] in playlist_file.templates \
and playlist_file.templates[data_template["name"]] \
and "test" in playlist_file.templates[data_template["name"]] \
and playlist_file.templates[data_template["name"]]["test"] is True:
no_template_test = False
if no_template_test:
continue
if "name_mapping" in playlist_attrs and playlist_attrs["name_mapping"]:
playlist_log_name, output_str = util.validate_filename(playlist_attrs["name_mapping"])
else:
playlist_log_name, output_str = util.validate_filename(mapping_name)
playlist_log_folder = os.path.join(default_dir, "logs", "playlists", playlist_log_name)
os.makedirs(playlist_log_folder, exist_ok=True)
ply_file_logger = os.path.join(playlist_log_folder, "playlist.log")
should_roll_over = os.path.isfile(ply_file_logger)
playlist_handler = RotatingFileHandler(ply_file_logger, delay=True, mode="w", backupCount=3,
encoding="utf-8")
util.apply_formatter(playlist_handler)
if should_roll_over:
playlist_handler.doRollover()
logger.addHandler(playlist_handler)
status[mapping_name] = {"status": "", "errors": [], "created": False, "modified": False, "deleted": False, "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0}
server_name = None
library_names = None
try:
util.separator(f"{mapping_name} Playlist")
logger.info("")
if output_str:
logger.info(output_str)
logger.info("")
util.separator(f"Validating {mapping_name} Attributes", space=False, border=False)
builder = CollectionBuilder(config, playlist_file, mapping_name, no_missing, playlist_attrs)
logger.info("")
util.separator(f"Running {mapping_name} Playlist", space=False, border=False)
if len(builder.schedule) > 0:
util.print_multiline(builder.schedule, info=True)
items_added = 0
items_removed = 0
valid = True
logger.info("")
logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}")
if builder.filters or builder.tmdb_filters:
logger.info("")
for filter_key, filter_value in builder.filters:
logger.info(f"Playlist Filter {filter_key}: {filter_value}")
for filter_key, filter_value in builder.tmdb_filters:
logger.info(f"Playlist Filter {filter_key}: {filter_value}")
method, value = builder.builders[0]
logger.debug("")
logger.debug(f"Builder: {method}: {value}")
logger.info("")
if "plex" in method:
ids = []
for pl_library in builder.libraries:
ids.extend(pl_library.get_rating_keys(method, value))
elif "tautulli" in method:
ids = []
for pl_library in builder.libraries:
ids.extend(pl_library.Tautulli.get_rating_keys(pl_library, value, True))
else:
ids = builder.gather_ids(method, value)
builder.filter_and_save_items(ids)
if len(builder.added_items) >= builder.minimum:
items_added, items_unchanged = builder.add_to_collection()
stats["added"] += items_added
status[mapping_name]["added"] += items_added
stats["unchanged"] += items_unchanged
status[mapping_name]["unchanged"] += items_unchanged
items_removed = 0
if builder.sync:
items_removed = builder.sync_collection()
stats["removed"] += items_removed
status[mapping_name]["removed"] += items_removed
elif len(builder.added_items) < builder.minimum:
logger.info("")
logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist")
valid = False
if builder.details["delete_below_minimum"] and builder.obj:
logger.info("")
util.print_multiline(builder.delete(), info=True)
builder.deleted = True
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
radarr_add, sonarr_add = builder.run_missing()
stats["radarr"] += radarr_add
status[mapping_name]["radarr"] += radarr_add
stats["sonarr"] += sonarr_add
status[mapping_name]["sonarr"] += sonarr_add
run_item_details = True
try:
builder.load_collection()
if builder.created:
stats["created"] += 1
status[mapping_name]["created"] = True
elif items_added > 0 or items_removed > 0:
stats["modified"] += 1
status[mapping_name]["modified"] = True
except Failed:
util.print_stacktrace()
run_item_details = False
logger.info("")
util.separator("No Playlist to Update", space=False, border=False)
else:
builder.update_details()
if builder.deleted:
stats["deleted"] += 1
status[mapping_name]["deleted"] = True
if valid and run_item_details and builder.builders and (builder.item_details or builder.custom_sort):
try:
builder.load_collection_items()
except Failed:
logger.info("")
util.separator("No Items Found", space=False, border=False)
else:
if builder.item_details:
builder.update_item_details()
if builder.custom_sort:
builder.sort_collection()
if valid:
builder.sync_playlist()
builder.send_notifications(playlist=True)
except NotScheduled as e:
util.print_multiline(e, info=True)
status[mapping_name]["status"] = "Not Scheduled"
except Failed as e:
config.notify(e, server=server_name, library=library_names, playlist=mapping_name)
util.print_stacktrace()
util.print_multiline(e, error=True)
status[mapping_name]["status"] = "PMM Failure"
status[mapping_name]["errors"].append(e)
except Exception as e:
config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name)
util.print_stacktrace()
logger.error(f"Unknown Error: {e}")
status[mapping_name]["status"] = "Unknown Error"
status[mapping_name]["errors"].append(e)
logger.info("")
util.separator(f"Finished {mapping_name} Playlist\nPlaylist Run Time: {str(datetime.now() - playlist_start).split('.')[0]}")
logger.removeHandler(playlist_handler)
return status, stats
try:
if run or test or collections or libraries or metadata_files or resume:
start({
"config_file": config_file,
"test": test,
"delete": delete,
"ignore_schedules": ignore_schedules,
"collections": collections,
"libraries": libraries,
"metadata_files": metadata_files,
"library_first": library_first,
"resume": resume,
"trace": trace
})
else:
times_to_run = util.get_list(times)
valid_times = []
for time_to_run in times_to_run:
try:
valid_times.append(datetime.strftime(datetime.strptime(time_to_run, "%H:%M"), "%H:%M"))
except ValueError:
if time_to_run:
raise Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format")
else:
raise Failed(f"Argument Error: blank time argument")
for time_to_run in valid_times:
schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run, "delete": delete, "library_first": library_first, "trace": trace})
while True:
schedule.run_pending()
if not no_countdown:
current_time = datetime.now().strftime("%H:%M")
seconds = None
og_time_str = ""
for time_to_run in valid_times:
new_seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current_time, "%H:%M")).total_seconds()
if new_seconds < 0:
new_seconds += 86400
if (seconds is None or new_seconds < seconds) and new_seconds > 0:
seconds = new_seconds
og_time_str = time_to_run
if seconds is not None:
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
time_str = f"{hours} Hour{'s' if hours > 1 else ''} and " if hours > 0 else ""
time_str += f"{minutes} Minute{'s' if minutes > 1 else ''}"
util.print_return(f"Current Time: {current_time} | {time_str} until the next run at {og_time_str} | Runs: {', '.join(times_to_run)}")
else:
logger.error(f"Time Error: {valid_times}")
time.sleep(60)
except KeyboardInterrupt:
util.separator("Exiting Plex Meta Manager")