diff --git a/config/config.yml.template b/config/config.yml.template index 8f291cc1..9ee8c3de 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -33,6 +33,9 @@ plex: # Can be individually specified url: http://192.168.1.12:32400 token: #################### timeout: 60 + clean_bundles: false + empty_trash: false + optimize: false tmdb: apikey: ################################ language: en diff --git a/modules/builder.py b/modules/builder.py index 2ddf7135..67616d0a 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -345,42 +345,52 @@ class CollectionBuilder: last_day = next_month - timedelta(days=next_month.day) for schedule in schedule_list: run_time = str(schedule).lower() - if run_time.startswith("day") or run_time.startswith("daily"): + if run_time.startswith(("day", "daily")): skip_collection = False - elif run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): + elif run_time.startswith(("hour", "week", "month", "year")): match = re.search("\\(([^)]+)\\)", run_time) - if match: - param = match.group(1) - if run_time.startswith("week"): - if param.lower() in util.days_alias: - weekday = util.days_alias[param.lower()] - self.schedule += f"\nScheduled weekly on {util.pretty_days[weekday]}" - if weekday == current_time.weekday(): + if not match: + logger.error(f"Collection Error: failed to parse schedule: {schedule}") + continue + param = match.group(1) + if run_time.startswith("hour"): + try: + if 0 <= int(param) <= 23: + self.schedule += f"\nScheduled to run only on the {util.make_ordinal(param)} hour" + if config.run_hour == int(param): skip_collection = False else: - logger.error(f"Collection Error: weekly schedule attribute {schedule} invalid must be a day of the week i.e. weekly(Monday)") - elif run_time.startswith("month"): - try: - if 1 <= int(param) <= 31: - self.schedule += f"\nScheduled monthly on the {util.make_ordinal(param)}" - if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): - skip_collection = False - else: - logger.error(f"Collection Error: monthly schedule attribute {schedule} invalid must be between 1 and 31") - except ValueError: - logger.error(f"Collection Error: monthly schedule attribute {schedule} invalid must be an integer") - elif run_time.startswith("year"): - match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) - if match: - month = int(match.group(1)) - day = int(match.group(2)) - self.schedule += f"\nScheduled yearly on {util.pretty_months[month]} {util.make_ordinal(day)}" - if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): + raise ValueError + except ValueError: + logger.error(f"Collection Error: hourly schedule attribute {schedule} invalid must be an integer between 0 and 23") + elif run_time.startswith("week"): + if param.lower() not in util.days_alias: + logger.error(f"Collection Error: weekly schedule attribute {schedule} invalid must be a day of the week i.e. weekly(Monday)") + continue + weekday = util.days_alias[param.lower()] + self.schedule += f"\nScheduled weekly on {util.pretty_days[weekday]}" + if weekday == current_time.weekday(): + skip_collection = False + elif run_time.startswith("month"): + try: + if 1 <= int(param) <= 31: + self.schedule += f"\nScheduled monthly on the {util.make_ordinal(param)}" + if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): skip_collection = False else: - logger.error(f"Collection Error: yearly schedule attribute {schedule} invalid must be in the MM/DD format i.e. yearly(11/22)") - else: - logger.error(f"Collection Error: failed to parse schedule: {schedule}") + raise ValueError + except ValueError: + logger.error(f"Collection Error: monthly schedule attribute {schedule} invalid must be an integer between 1 and 31") + elif run_time.startswith("year"): + match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) + if not match: + logger.error(f"Collection Error: yearly schedule attribute {schedule} invalid must be in the MM/DD format i.e. yearly(11/22)") + continue + month = int(match.group(1)) + day = int(match.group(2)) + self.schedule += f"\nScheduled yearly on {util.pretty_months[month]} {util.make_ordinal(day)}" + if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): + skip_collection = False else: logger.error(f"Collection Error: schedule attribute {schedule} invalid") if len(self.schedule) == 0: diff --git a/modules/config.py b/modules/config.py index 994a2810..349d08bd 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,5 @@ import logging, os +from datetime import datetime from modules import util from modules.anidb import AniDBAPI from modules.anilist import AniListAPI @@ -48,7 +49,7 @@ mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata t library_types = {"movie": "For Movie Libraries", "show": "For Show Libraries"} class Config: - def __init__(self, default_dir, config_path=None, libraries_to_run=None): + def __init__(self, default_dir, config_path=None, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): logger.info("Locating config...") if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path) elif config_path and not os.path.exists(config_path): raise Failed(f"Config Error: config not found at {os.path.abspath(config_path)}") @@ -56,6 +57,13 @@ class Config: else: raise Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}") logger.info(f"Using {self.config_path} as config") + self.test_mode = is_test + self.run_start_time = time_scheduled + self.run_hour = datetime.strptime(time_scheduled, "%H:%M").hour + self.requested_collections = util.get_list(requested_collections) + self.requested_libraries = util.get_list(requested_libraries) + self.resume_from = resume_from + yaml.YAML().allow_duplicate_keys = True try: new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8")) @@ -312,9 +320,9 @@ class Config: self.libraries = [] try: libs = check_for_attribute(self.data, "libraries", throw=True) except Failed as e: raise Failed(e) - requested_libraries = util.get_list(libraries_to_run) if libraries_to_run else None + for library_name, lib in libs.items(): - if requested_libraries and library_name not in requested_libraries: + if self.requested_libraries and library_name not in self.requested_libraries: continue util.separator() params = {} diff --git a/modules/util.py b/modules/util.py index 62676d8d..bb3b425b 100644 --- a/modules/util.py +++ b/modules/util.py @@ -222,7 +222,8 @@ def compile_list(data): return data def get_list(data, lower=False, split=True, int_list=False): - if isinstance(data, list): return data + if data is None: return None + elif isinstance(data, list): return data elif isinstance(data, dict): return [data] elif split is False: return [str(data)] elif lower is True: return [d.strip().lower() for d in str(data).split(",")] diff --git a/plex_meta_manager.py b/plex_meta_manager.py index ba17638f..aecc392d 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -7,13 +7,17 @@ try: from modules.config import Config from modules.util import Failed except ModuleNotFoundError: - print("Error: Requirements are not installed") + 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("-c", "--config", dest="config", help="Run with desired *.yml file", type=str) -parser.add_argument("-t", "--time", dest="time", help="Time to update each day use format HH:MM (Default: 03:00)", default="03:00", type=str) +parser.add_argument("-t", "--time", dest="time", 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("-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) @@ -48,9 +52,10 @@ collections = os.environ.get("PMM_COLLECTIONS") if os.environ.get("PMM_COLLECTIO libraries = os.environ.get("PMM_LIBRARIES") if os.environ.get("PMM_LIBRARIES") else args.libraries resume = os.environ.get("PMM_RESUME") if os.environ.get("PMM_RESUME") else args.resume -time_to_run = os.environ.get("PMM_TIME") if os.environ.get("PMM_TIME") else args.time -if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", time_to_run): - raise util.Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format") +times_to_run = util.get_list(os.environ.get("PMM_TIME") if os.environ.get("PMM_TIME") else args.time) +for time_to_run in times_to_run: + if not re.match("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$", time_to_run): + raise util.Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format") util.separating_character = os.environ.get("PMM_DIVIDER")[0] if os.environ.get("PMM_DIVIDER") else args.divider[0] @@ -83,7 +88,7 @@ logger.addHandler(cmd_handler) sys.excepthook = util.my_except_hook -def start(config_path, is_test, daily, requested_collections, requested_libraries, resume_from): +def start(config_path, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): file_logger = os.path.join(default_dir, "logs", "meta.log") should_roll_over = os.path.isfile(file_logger) file_handler = logging.handlers.RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8") @@ -101,16 +106,20 @@ def start(config_path, is_test, daily, requested_collections, requested_librarie logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) logger.info(util.centered(" Version: 1.9.3-beta1 ")) - if daily: start_type = "Daily " + if time_scheduled: start_type = f"{time_scheduled} " elif is_test: start_type = "Test " elif requested_collections: start_type = "Collections " elif requested_libraries: start_type = "Libraries " else: start_type = "" start_time = datetime.now() + if time_scheduled is None: + time_scheduled = start_time.strftime("%H:%M") util.separator(f"Starting {start_type}Run") try: - config = Config(default_dir, config_path, requested_libraries) - update_libraries(config, is_test, requested_collections, resume_from) + config = Config(default_dir, config_path=config_path, is_test=is_test, + time_scheduled=time_scheduled, requested_collections=requested_collections, + requested_libraries=requested_libraries, resume_from=resume_from) + update_libraries(config) except Exception as e: util.print_stacktrace() logger.critical(e) @@ -118,7 +127,7 @@ def start(config_path, is_test, daily, requested_collections, requested_librarie util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}") logger.removeHandler(file_handler) -def update_libraries(config, is_test, requested_collections, resume_from): +def update_libraries(config): for library in config.libraries: 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") @@ -136,29 +145,29 @@ def update_libraries(config, is_test, requested_collections, resume_from): util.separator(f"Mapping {library.name} Library", space=False, border=False) logger.info("") movie_map, show_map = map_guids(config, library) - if not is_test and not resume_from and not collection_only and library.mass_update: + if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: mass_metadata(config, library, movie_map, show_map) for metadata in library.metadata_files: logger.info("") util.separator(f"Running Metadata File\n{metadata.path}") - if not is_test and not resume_from and not collection_only: + if not config.test_mode and not config.resume_from and not collection_only: try: - metadata.update_metadata(config.TMDb, is_test) + metadata.update_metadata(config.TMDb, config.test_mode) except Failed as e: logger.error(e) - collections_to_run = metadata.get_collections(requested_collections) - if resume_from and resume_from not in collections_to_run: + 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: {resume_from} not in Metadata File: {metadata.path}") + 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 is_test else ''}Collections") + util.separator(f"{'Test ' if config.test_mode else ''}Collections") logger.removeHandler(library_handler) - resume_from = run_collection(config, library, metadata, collections_to_run, is_test, resume_from, movie_map, show_map) + run_collection(config, library, metadata, movie_map, show_map) logger.addHandler(library_handler) - if not is_test and not requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): + if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): logger.info("") util.separator(f"Other {library.name} Library Operations") unmanaged_collections = [] @@ -240,9 +249,11 @@ def map_guids(config, library): movie_map = {} show_map = {} length = 0 - logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") + logger.info(f"Loading {'Movie' if library.is_movie else 'Show'} Library: {library.name}") logger.info("") items = library.Plex.all() + logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") + logger.info("") for i, item in enumerate(items, 1): length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") id_type, main_id = config.Convert.get_id(item, library, length) @@ -379,11 +390,11 @@ def mass_metadata(config, library, movie_map, show_map): except Failed as e: logger.error(e) -def run_collection(config, library, metadata, requested_collections, is_test, resume_from, movie_map, show_map): +def run_collection(config, library, metadata, movie_map, show_map): logger.info("") - for mapping_name, collection_attrs in requested_collections.items(): + for mapping_name, collection_attrs in config.requested_collections.items(): collection_start = datetime.now() - if is_test and ("test" not in collection_attrs or collection_attrs["test"] is not True): + 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): @@ -398,10 +409,10 @@ def run_collection(config, library, metadata, requested_collections, is_test, re if no_template_test: continue - if resume_from and resume_from != mapping_name: + if config.resume_from and config.resume_from != mapping_name: continue - elif resume_from == mapping_name: - resume_from = None + elif config.resume_from == mapping_name: + config.resume_from = None logger.info("") util.separator(f"Resuming Collections") @@ -481,27 +492,32 @@ def run_collection(config, library, metadata, requested_collections, is_test, re logger.info("") util.separator(f"Finished {mapping_name} Collection\nCollection Run Time: {str(datetime.now() - collection_start).split('.')[0]}") logger.removeHandler(collection_handler) - return resume_from try: if run or test or collections or libraries or resume: - start(config_file, test, False, collections, libraries, resume) + start(config_file, is_test=test, requested_collections=collections, requested_libraries=libraries, resume_from=resume) else: time_length = 0 - schedule.every().day.at(time_to_run).do(start, config_file, False, True, None, None, None) + for time_to_run in times_to_run: + schedule.every().day.at(time_to_run).do(start, config_file, time_scheduled=time_to_run) while True: schedule.run_pending() if not no_countdown: current = datetime.now().strftime("%H:%M") - seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current, "%H:%M")).total_seconds() + seconds = None + og_time_str = "" + for time_to_run in times_to_run: + new_seconds = (datetime.strptime(time_to_run, "%H:%M") - datetime.strptime(current, "%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 hours = int(seconds // 3600) - if hours < 0: - hours += 24 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 ''}" - - time_length = util.print_return(time_length, f"Current Time: {current} | {time_str} until the daily run at {time_to_run}") - time.sleep(1) + time_length = util.print_return(time_length, f"Current Time: {current} | {time_str} until the next run at {og_time_str} {times_to_run}") + time.sleep(60) except KeyboardInterrupt: util.separator("Exiting Plex Meta Manager")