add ability to run multiple times per day

This commit is contained in:
meisnate12 2021-05-25 18:22:59 -04:00
parent 00b128a967
commit f28c09bf4f
5 changed files with 110 additions and 72 deletions

View file

@ -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

View file

@ -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():
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)):
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:
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:
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:
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:

View file

@ -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 = {}

View file

@ -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(",")]

View file

@ -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")