mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
add ability to run multiple times per day
This commit is contained in:
parent
00b128a967
commit
f28c09bf4f
5 changed files with 110 additions and 72 deletions
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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(",")]
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue