From f374dda928b9c54054dd26b47413adc20a9b1b33 Mon Sep 17 00:00:00 2001 From: Brian Lindner <8153326+BrianLindner@users.noreply.github.com> Date: Fri, 1 Jan 2021 11:12:22 -0500 Subject: [PATCH] moving code over --- logging.conf | 36 ++++ plexutil.py | 104 ++++++++++++ requirements.txt | 4 + schedule_preroll.py | 394 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 538 insertions(+) create mode 100644 logging.conf create mode 100644 plexutil.py create mode 100644 requirements.txt create mode 100644 schedule_preroll.py diff --git a/logging.conf b/logging.conf new file mode 100644 index 0000000..0ba1af9 --- /dev/null +++ b/logging.conf @@ -0,0 +1,36 @@ +[loggers] +keys=root,main + +[handlers] +keys=consoleHandler,fileHandler + +[formatters] +keys=consoleFormatter,fileFormatter + +[logger_root] +level=WARNING +handlers=consoleHandler,fileHandler + +[logger_main] +level=WARNING +handlers=consoleHandler,fileHandler +qualname=__main__ +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=ERROR +formatter=consoleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=logging.handlers.RotatingFileHandler +level=WARNING +formatter=fileFormatter +args=('log/schedule_preroll.log', 'a', 20000, 5) + +[formatter_consoleFormatter] +format=%(levelname)-8s - %(name)-12s - %(message)s + +[formatter_fileFormatter] +format=[%(asctime)s] - %(levelname)-8s - %(name)-12s - %(message)s diff --git a/plexutil.py b/plexutil.py new file mode 100644 index 0000000..5404137 --- /dev/null +++ b/plexutil.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +"""Plex config parsing utilities + +Raises: + FileNotFoundError: [description] + KeyError: [description] + KeyError: [description] + ConfigError: [description] +""" +import os +import sys +import logging +from configparser import ConfigParser +from plexapi.server import PlexServer, CONFIG + +logger = logging.getLogger(__name__) + +filename = os.path.basename(sys.argv[0]) +SCRIPT_NAME = os.path.splitext(filename)[0] + +def getPlexConfig(config_file=None): + """Return Plex Config paramaters for connection info {PLEX_URL, PLEX_TOKEN}\n + Attempts to use either a local config.ini (primary) as well as a PlexAPI system config.ini (secondary) + + Raises: + KeyError: Config Params not found in config file(s) + FileNotFoundError: If cannot find a config file + + Returns: + dict: Dict of config params {PLEX_URL, PLEX_TOKEN} + """ + cfg = {} + plex_url = '' + plex_token = '' + use_local_config = False + use_plexapi_config = False + + # Look for a local Config.ini file, use settings if present + local_config = ConfigParser() + + if config_file != None: + if os.path.exists(config_file): + filename = config_file + else: + raise FileNotFoundError('Config file -c "{}" not found'.format(config_file)) + else: + filename = 'config.ini' + + #try reading a local file + local_config.read(filename) + + if len(local_config.sections()) > 0: #len(found_config) > 0: + # if local config.ini file found, try to use local first + if local_config.has_section('auth'): + try: + server = local_config['auth'] + plex_url = server['server_baseurl'] + plex_token = server['server_token'] + + if len(plex_url) > 1 and len(plex_token) > 1: + use_local_config = True + except KeyError as e: + logger.error('Key Value not found {}', exc_info=e) + raise e + else: + msg = '[auth] section not found in LOCAL config.ini file' + logger.error(msg) + raise KeyError(msg) + + if not use_local_config and len(CONFIG.sections()) > 0: + # use PlexAPI Default ~/.config/plexapi/config.ini OR from PLEXAPI_CONFIG_PATH + # IF not manually set locally in local Config.ini above + # See https://python-plexapi.readthedocs.io/en/latest/configuration.html + if CONFIG.has_section('auth'): + try: + server = CONFIG.data['auth'] + plex_url = server.get('server_baseurl') + plex_token = server.get('server_token') + + if len(plex_url) > 1 and len(plex_token) > 1: + use_plexapi_config = True + except KeyError as e: + logger.error('Key Value not found', exc_info=e) + raise e + else: + msg = "[auth] section not found in PlexAPI MAIN config.ini file" + logger.error(msg) + raise KeyError(msg) + + if not use_local_config and not use_plexapi_config: + msg = 'No Plex config information found {server_baseurl, server_token}' + logger.error(msg) + raise ConfigError(msg) + + cfg['PLEX_URL'] = plex_url + cfg['PLEX_TOKEN']= plex_token + + return cfg + +if __name__ == '__main__': + msg = 'Script not meant to be run directly, please import into other scripts.\n\n' + \ + 'usage:\nimport {}'.format(SCRIPT_NAME) + '\n' + \ + 'cfg = {}.getPlexConfig()'.format(SCRIPT_NAME) + '\n' + logger.error(msg) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..260b6c6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +plexapi==4.2.* +configparser==5.0.* +requests==2.25.* +pyyaml==5.3.* \ No newline at end of file diff --git a/schedule_preroll.py b/schedule_preroll.py new file mode 100644 index 0000000..161359c --- /dev/null +++ b/schedule_preroll.py @@ -0,0 +1,394 @@ +#!/usr/bin/python +"""Schedule Plex server related Pre-roll Intro videos +A helper script to automate management of Plex pre-rolls. +Define when you want different pre-rolls to play throughout the year. + +Ideas include: +- Holiday pre-roll rotations +- Special occasions +- Summer/Winter rotations +- Breaking up the monoteny +- Keeping your family/friends on their toes! + +Set it and forget it! + +Optional Arguments: + -h, --help show this help message and exit + -c [], --config-path [] path/to/config.ini file to use + (Optional: uses local/plexapi default path if omitted) + -s [],--schedule-path [] path/to/preroll_schedules.yaml file to use + (Optional: uses local path if omitted) + -l [], --logconfig-path [] path/to/logging.conf logging configuration file + (Optional: uses local path if omitted) + +Requirements: +- See Requirements.txt for Python modules + +Scheduling: + Add to system scheduler such as: + > crontab -e + > 0 0 * * * python path/to/schedule_preroll.py >/dev/null 2>&1 + +Raises: + FileNotFoundError: [description] + KeyError: [description] + ConfigError: [description] + FileNotFoundError: [description] +""" +import os +import sys +import logging +import logging.config +import requests +import datetime +import yaml +from argparse import ArgumentParser +from configparser import ConfigParser +from configparser import Error as ConfigError +from plexapi.server import PlexServer, CONFIG + +# import local modules +import plexutil + +logger = logging.getLogger(__name__) + +filename = os.path.basename(sys.argv[0]) +SCRIPT_NAME = os.path.splitext(filename)[0] + +def getArguments(): + """Return command line arguments + See https://docs.python.org/3/howto/argparse.html + + Returns: + argparse.Namespace: Namespace object + """ + description = 'Automate scheduling of pre-roll intros for Plex' + version = '0.7.0' + + log_default = './logging.conf' + config_default = './config.ini' + schedule_default = './preroll_schedules.yaml' + parser = ArgumentParser(description='{}'.format(description)) + parser.add_argument('-v', '--version', action='version', version='%(prog)s {}'.format(version), help='show the version number and exit') + parser.add_argument('-l', '--logconfig-path', dest='log_config_file', default=log_default, action='store', help='Path to logging config file. Default: {}'.format(log_default)) + parser.add_argument('-c', '--config-path', dest='config_file', action='store', help='Path to Config.ini to use for Plex Server info. Default: {}'.format(config_default)) + parser.add_argument('-s', '--schedule-path', dest='schedule_file', action='store', help='Path to pre-roll schedule file (YAML) to be use. Default: {}'.format(schedule_default)) + args = parser.parse_args() + + return args + +def getYAMLSchema(): + """Return the main schema layout of the preroll_schedules.yaml file + + Returns: + dict: Dict of main schema items + """ + schema = {'default': None, 'monthly': None, 'weekly': None, 'date_range': None, 'misc': None} + return schema + +def getWeekRange(year, weeknum): + """Return the starting/ending date range of a given year/week + + Args: + year (int): Year to calc range for + weeknum (int): Month of the year (1-12) + + Returns: + Date: Start date of the Year/Month + Date: End date of the Year/Month + """ + start = datetime.datetime.strptime('{}-W{}-0'.format(year, int(weeknum)-1), "%Y-W%W-%w").date() + end = start + datetime.timedelta(days=6) + + return start, end + +def getMonthRange(year, monthnum): + """Return the starting/ending date range of a given year/month + + Args: + year (int): Year to calc range for + monthnum (int): Month of the year (1-12) + + Returns: + Date: Start date of the Year/Month + Date: End date of the Year/Month + """ + start = datetime.date(year, monthnum, 1) + next_month = start.replace(day=28) + datetime.timedelta(days=4) + end = next_month - datetime.timedelta(days=next_month.day) + + return start, end + +def getPrerollSchedule(filename=None): + """Return a listing of defined preroll schedules for searching/use + + Raises: + FileNotFoundError: If no schedule config file exists + + Returns: + list: list of schedules (Dict: {Type, StartDate, EndDate, Path}) + """ + default_files = ['preroll_schedules.yaml', 'preroll_schedules.yml'] + + schedule_file = None + if filename != None: + if os.path.exists(filename): + schedule_file = filename + else: + raise FileNotFoundError('Preroll Schedule file -s "{}" not found'.format(filename)) + else: + for f in default_files: + if os.path.exists(f): + schedule_file = f + break + + # if we still cant find a schedule file, we hae to abort + if not schedule_file: + msg = 'No {} Found'.format(' / '.join(default_files)) + logger.critical(msg) + raise FileNotFoundError(msg) + + with open(schedule_file, 'r') as file: + #contents = yaml.load(file, Loader=yaml.SafeLoader) + contents = yaml.load(file, Loader=yaml.FullLoader) + + today = datetime.date.today() + schedule = [] + for schedule_type in getYAMLSchema(): + if schedule_type == 'weekly': + try: + use = contents[schedule_type]['enabled'] + + if use: + for i in range(1,53): + try: + path = contents[schedule_type][i] + + if path: + entry = {} + start, end = getWeekRange(today.year, i) + entry['Type'] = schedule_type + entry['StartDate'] = start + entry['EndDate'] = end + entry['Path'] = path + + schedule.append(entry) + except KeyError as e: + # skip KeyError for missing Weeks + logger.debug('Key Value not found: "{}"->"{}", skipping week'.format(schedule_type, i)) + continue + except KeyError as e: + logger.error('Key Value not found in "{}" section'.format(schedule_type), exc_info=e) + raise e + elif schedule_type == 'monthly': + try: + use = contents[schedule_type]['enabled'] + + if use: + for i in range(1,13): + month_abrev = datetime.date(today.year, i, 1).strftime('%b').lower() + try: + path = contents[schedule_type][month_abrev] + + if path: + entry = {} + start, end = getMonthRange(today.year, i) + entry['Type'] = schedule_type + entry['StartDate'] = start + entry['EndDate'] = end + entry['Path'] = path + + schedule.append(entry) + except KeyError as e: + # skip KeyError for missing Months + logger.warning('Key Value not found: "{}"->"{}", skipping month'.format(schedule_type, month_abrev)) + continue + except KeyError as e: + logger.error('Key Value not found in "{}" section'.format(schedule_type), exc_info=e) + raise e + elif schedule_type == 'date_range': + try: + use = contents[schedule_type]['enabled'] + if use: + for r in contents[schedule_type]['ranges']: + try: + path = r['path'] + + if path: + entry = {} + entry['Type'] = schedule_type + entry['StartDate'] = r['start_date'] + entry['EndDate'] = r['end_date'] + entry['Path'] = path + + schedule.append(entry) + except KeyError as e: + #logger.error('Key Value not found: "{}"'.format(schedule_type), exc_info=e) + raise e + except KeyError as e: + logger.error('Key Value not found in "{}" section'.format(schedule_type), exc_info=e) + raise e + elif schedule_type == 'misc': + try: + use = contents[schedule_type]['enabled'] + if use: + try: + path = contents[schedule_type]['always_use'] + + if path: + entry = {} + entry['Type'] = schedule_type + entry['StartDate'] = datetime.date(today.year, today.month, today.day) + entry['EndDate'] = datetime.date(today.year, today.month, today.day) + entry['Path'] = path + + schedule.append(entry) + except KeyError as e: + #logger.error('Key Value not found: "{}"'.format(schedule_type), exc_info=e) + raise e + except KeyError as e: + logger.error('Key Value not found in "{}" section'.format(schedule_type), exc_info=e) + raise e + elif schedule_type == 'default': + try: + use = contents[schedule_type]['enabled'] + if use: + try: + path = contents[schedule_type]['path'] + + if path: + entry = {} + entry['Type'] = schedule_type + entry['StartDate'] = datetime.date(today.year, today.month, today.day) + entry['EndDate'] = datetime.date(today.year, today.month, today.day) + entry['Path'] = path + + schedule.append(entry) + except KeyError as e: + #logger.error('Key Value not found: "{}"'.format(schedule_type), exc_info=e) + raise e + except KeyError as e: + logger.error('Key Value not found in "{}" section'.format(schedule_type), exc_info=e) + raise e + + else: + continue + + # Sort list so most recent Ranges appear first + schedule.sort(reverse=True, key=lambda x:x['StartDate']) + + return schedule + +def buildListingString(items, play_all=False): + """Build the Plex formatted string of preroll paths + + Args: + items (list): List of preroll video paths to place into a string listing + play_all (bool, optional): Play all videos. Defaults to False (Random choice) + + Returns: + string: CSV Listing (, or ;) based on play_all param of preroll video paths + """ + if play_all: + # use , to play all entries + listing = ','.join(items) + else: + pass + #use ; to play random selection + listing = ';'.join(items) + + return listing + +def getPrerollListingString(schedule_file=None): + """Return listing of preroll videos to be used by Plex + + Returns: + string: listing of preroll video paths to be used for Extras + """ + listing = '' + entries = dict(getYAMLSchema()) + today = datetime.date.today() + + for e in getYAMLSchema(): + entries[e] = [] + + # the the time series schedule of pre-rolls + schedule = getPrerollSchedule(schedule_file) + + for entry in schedule: + if entry['StartDate'] <= today <= entry['EndDate']: + try: + entry_type = entry['Type'] + except KeyError: + continue + + path = entry['Path'] + + if path: + entries[entry_type].append(path) + + # Build the merged output based or order of Priority + merged_list = [] + if entries['misc']: + merged_list.extend(entries['misc']) + if entries['date_range']: + merged_list.extend(entries['date_range']) + if entries['weekly'] and not entries['date_range']: + merged_list.extend(entries['weekly']) + if entries['monthly'] \ + and not entries['weekly'] and not entries['date_range']: + merged_list.extend(entries['monthly']) + if entries['default'] \ + and not entries['monthly'] and not entries['weekly'] and not entries['date_range']: + merged_list.extend(entries['default']) + + listing = buildListingString(merged_list) + + return listing + +def setPrerollList(plex, preroll_listing): + """Save Plex Preroll info to PlexServer settings + + Args: + plex (PlexServer): Plex server to update + preroll_listing (string, list): csv listing or List of preroll paths to save + """ + # if happend to send in an Iterable List, merge to a string + if type(preroll_listing) is list: + preroll_listing = buildListingString(preroll_listing) + + plex.settings.get('cinemaTrailersPrerollID').set(preroll_listing) + plex.settings.save() + +if __name__ == '__main__': + args = getArguments() + + log_config = args.log_config_file + if os.path.exists(log_config): + logging.config.fileConfig(log_config, disable_existing_loggers=False) + else: + logger.warning('Logging Config file "{}" not available') + + cfg = plexutil.getPlexConfig(args.config_file) + + # Initialize Session information + sess = requests.Session() + # Ignore verifying the SSL certificate + sess.verify = False # '/path/to/certfile' + # If verify is set to a path of a directory (not a cert file), + # the directory needs to be processed with the c_rehash utility + # from OpenSSL. + if sess.verify is False: + # Disable the warning that the request is insecure, we know that... + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + try: + plex = PlexServer(cfg['PLEX_URL'], cfg['PLEX_TOKEN'], session=sess) + except Exception as e: + logger.error('Error Connecting to Plex', exc_info=e) + raise e + + prerolls = getPrerollListingString(args.schedule_file) + logger.info('Saving Preroll List: "{}"'.format(prerolls)) + setPrerollList(plex, prerolls) \ No newline at end of file