From f55f90819d2c8bed5bfa3f77bc5eff0ee057da94 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Wed, 9 Aug 2017 19:07:55 -0700 Subject: [PATCH 01/34] Started adding export / export of playhead queue via json for easy transferring / restarting db. --- .gitignore | 1 + PseudoChannel.py | 58 ++++++++++++++++++++++++++++++++---- src/PseudoChannelDatabase.py | 8 +++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 8eaab71..5f26756 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ pseudo-channel.db env/ *.log *.pid +*.json diff --git a/PseudoChannel.py b/PseudoChannel.py index 6496977..6f2959b 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -24,6 +24,8 @@ import textwrap import os, sys from xml.dom import minidom import xml.etree.ElementTree as ET +import json +from pprint import pprint import schedule @@ -915,6 +917,39 @@ class PseudoChannel(): print str("+++++ {} {} {} {} {} {}".format(str(i + 1)+".", entry[8], entry[11], entry[6], " - ", entry[3])).encode(sys.stdout.encoding, errors='replace') + def write_json_to_file(self, data): + + fileName = "pseudo-schedule.json" + + writepath = './' + + if os.path.exists(writepath+fileName): + + os.remove(writepath+fileName) + + mode = 'a' if os.path.exists(writepath) else 'w' + + with open(writepath+fileName, mode) as f: + + f.write(data) + + def export_queue(self): + + shows_table = self.db.get_shows_table() + + json_string = json.dumps(shows_table) + + print "+++++ Exporting queue ", json_string + + self.write_json_to_file(json_string) + + def import_queue(self): + + with open('pseudo-schedule.json') as data_file: + data = json.load(data_file) + + pprint(data) + def exit_app(self): print " - Exiting Pseudo TV & cleaning up." @@ -1013,12 +1048,21 @@ if __name__ == '__main__': ''' * - * Make XML / HTML Schedule: "python PseudoChannel.py -i" + * Export queue: "python PseudoChannel.py -e" * ''' - parser.add_argument('-i', '--inject_commercials', + parser.add_argument('-e', '--export_queue', action='store_true', - help='Squeeze commercials in any media gaps if possible.') + help='Exports the current queue for episodes.') + + ''' + * + * Import queue: "python PseudoChannel.py -i" + * + ''' + parser.add_argument('-i', '--import_queue', + action='store_true', + help='Imports the current queue for episodes.') globals().update(vars(parser.parse_args())) @@ -1054,9 +1098,13 @@ if __name__ == '__main__': pseudo_channel.make_xml_schedule() - if args.inject_commercials: + if args.export_queue: - pseudo_channel.run_commercial_injection() + pseudo_channel.export_queue() + + if args.import_queue: + + pseudo_channel.import_queue() if args.run: diff --git a/src/PseudoChannelDatabase.py b/src/PseudoChannelDatabase.py index 74d2ef1..d2f76c5 100644 --- a/src/PseudoChannelDatabase.py +++ b/src/PseudoChannelDatabase.py @@ -282,6 +282,14 @@ class PseudoChannelDatabase(): Getters, etc. """ + def get_shows_table(self): + + sql = "SELECT lastEpisodetitle FROM shows" + + self.cursor.execute(sql) + + return self.cursor.fetchall() + def get_media(self, title, mediaType): media = mediaType From 1e7219cb7069623e05b7a0fd833d82c810341dca Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Wed, 9 Aug 2017 19:39:46 -0700 Subject: [PATCH 02/34] Importing the previously exported json queue seems to be working. Will overwrite all the shows table with the json data. --- PseudoChannel.py | 10 ++++++++++ src/PseudoChannelDatabase.py | 23 ++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 6f2959b..9f37417 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -945,11 +945,21 @@ class PseudoChannel(): def import_queue(self): + """Dropping previous shows table before adding the imported data""" + + self.db.clear_shows_table() + with open('pseudo-schedule.json') as data_file: data = json.load(data_file) pprint(data) + for row in data: + + print row + + self.db.import_shows_table_by_row(row[2], row[3], row[4], row[5], row[6], row[7]) + def exit_app(self): print " - Exiting Pseudo TV & cleaning up." diff --git a/src/PseudoChannelDatabase.py b/src/PseudoChannelDatabase.py index d2f76c5..ffd7aaa 100644 --- a/src/PseudoChannelDatabase.py +++ b/src/PseudoChannelDatabase.py @@ -121,6 +121,14 @@ class PseudoChannelDatabase(): self.conn.commit() + def clear_shows_table(self): + + sql = "DELETE FROM shows" + + self.cursor.execute(sql) + + self.conn.commit() + """Database functions. Setters, etc. @@ -278,13 +286,26 @@ class PseudoChannelDatabase(): media.plex_media_id ) + def import_shows_table_by_row(self, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID): + unix = int(time.time()) + try: + self.cursor.execute("REPLACE INTO shows " + "(unix, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID) VALUES (?, ?, ?, ?, ?, ?, ?)", + (unix, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID)) + self.conn.commit() + # Catch the exception + except Exception as e: + # Roll back any change if something goes wrong + self.conn.rollback() + raise e + """Database functions. Getters, etc. """ def get_shows_table(self): - sql = "SELECT lastEpisodetitle FROM shows" + sql = "SELECT * FROM shows" self.cursor.execute(sql) From ee863eb888e60d8959bf3b4d91531326f7c5a483 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 10 Aug 2017 10:02:07 -0700 Subject: [PATCH 03/34] Potential fix for the daily schedule update / interruption to currently playing media - issue. --- PseudoChannel.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++- pseudo_config.py | 11 ++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 9f37417..1e6a811 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -55,6 +55,8 @@ class PseudoChannel(): CONTROLLER_SERVER_PATH = config.controllerServerPath CONTROLLER_SERVER_PORT = config.controllerServerPort + USE_OVERRIDE_CACHE = config.useDailyOverlapCache + DEBUG = config.debug_mode def __init__(self): @@ -960,6 +962,52 @@ class PseudoChannel(): self.db.import_shows_table_by_row(row[2], row[3], row[4], row[5], row[6], row[7]) + def get_daily_schedule_cache_as_json(self): + + data = [] + + try: + with open('../.pseudo-cache/daily-schedule.json') as data_file: + data = json.load(data_file) + + #pprint(data) + + except IOError: + + print ("----- Having issues opening the pseudo-cache file.") + + return data + + def save_daily_schedule_as_json(self): + + daily_schedule_table = self.db.get_daily_schedule() + + json_string = json.dumps(daily_schedule_table) + + print "+++++ Saving Daily Schedule Cache " + + self.save_file(json_string, 'daily-schedule.json', '../.pseudo-cache/') + + def save_file(self, data, filename, path="./"): + + fileName = filename + + writepath = path + + if not os.path.exists(writepath): + + os.makedirs(writepath) + + if os.path.exists(writepath+fileName): + + os.remove(writepath+fileName) + + mode = 'a' if os.path.exists(writepath) else 'w' + + with open(writepath+fileName, mode) as f: + + f.write(data) + def exit_app(self): print " - Exiting Pseudo TV & cleaning up." @@ -1252,11 +1300,70 @@ if __name__ == '__main__': print "##### Generating Memory Schedule." + now = datetime.datetime.now() + + now = now.replace(year=1900, month=1, day=1) + + pseudo_cache = pseudo_channel.get_daily_schedule_cache_as_json() + + prev_end_time_to_watch_for = None + + if pseudo_channel.USE_OVERRIDE_CACHE: + + for cached_item in pseudo_cache: + + prev_start_time = datetime.datetime.strptime(cached_item[8], "%I:%M:%S %p") + + try: + + prev_end_time = datetime.datetime.strptime(cached_item[9], '%Y-%m-%d %H:%M:%S.%f') + + except ValueError: + + prev_end_time = datetime.datetime.strptime(cached_item[9], '%Y-%m-%d %H:%M:%S') + + """If update time is in between the prev media start / stop then there is overlap""" + if prev_start_time < now and prev_end_time > now: + + try: + + print "+++++ It looks like there is update schedule overlap", cached_item[3] + + except: + + pass + + prev_end_time_to_watch_for = prev_end_time + for item in schedulelist: trans_time = datetime.datetime.strptime(item[8], "%I:%M:%S %p").strftime("%H:%M") - schedule.every().day.at(trans_time).do(job_that_executes_once, item, schedulelist).tag('daily-tasks') + new_start_time = datetime.datetime.strptime(item[8], "%I:%M:%S %p") + + if prev_end_time_to_watch_for == None: + + schedule.every().day.at(trans_time).do(job_that_executes_once, item, schedulelist).tag('daily-tasks') + + else: + + """If prev end time is more then the start time of this media, skip it""" + if prev_end_time_to_watch_for > new_start_time: + + try: + + print "Skipping scheduling item do to cached overlap.", item[3] + + except: + + pass + + continue + + else: + + schedule.every().day.at(trans_time).do(job_that_executes_once, item, schedulelist).tag('daily-tasks') + print "+++++ Done." @@ -1281,6 +1388,9 @@ if __name__ == '__main__': def go_generate_daily_sched(): + """Saving current daily schedule as cached .json""" + pseudo_channel.save_daily_schedule_as_json() + schedule.clear('daily-tasks') pseudo_channel.generate_daily_schedule() generate_memory_schedule(pseudo_channel.db.get_daily_schedule()) diff --git a/pseudo_config.py b/pseudo_config.py index 699a3f4..7f625a3 100644 --- a/pseudo_config.py +++ b/pseudo_config.py @@ -59,8 +59,6 @@ plexLibraries = { "Commercials" : ["Commercials"], } -useGoogleCalendar = False - useCommercialInjection = True # How many seconds to pad commercials between each other / other media @@ -76,6 +74,15 @@ You can also leave the below controllerServerPath empty if you'd like to run you controllerServerPath = "http://192.168.1.28" controllerServerPort = "8000" +""" +When the schedule updates every 24 hours, it's possible that it will interrupt any shows / movies that were +playing from the previous day. To fix this, the app saves a "cached" schedule from the previous day to +override any media that is trying to play while the previous day is finishing. +""" +useDailyOverlapCache = True + dailyUpdateTime = "12:00 AM" debug_mode = True + +useGoogleCalendar = False \ No newline at end of file From 7f06384c2fb8dec0116c1a53c4a2b3766a6f30ed Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 10 Aug 2017 10:08:27 -0700 Subject: [PATCH 04/34] Added flag to only use the new cache update logic if app is being updated with new schedule. --- PseudoChannel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 1e6a811..b60872c 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -1296,7 +1296,7 @@ if __name__ == '__main__': return schedule.CancelJob - def generate_memory_schedule(schedulelist): + def generate_memory_schedule(schedulelist, isforupdate=False): print "##### Generating Memory Schedule." @@ -1308,8 +1308,8 @@ if __name__ == '__main__': prev_end_time_to_watch_for = None - if pseudo_channel.USE_OVERRIDE_CACHE: - + if pseudo_channel.USE_OVERRIDE_CACHE and isforupdate: + for cached_item in pseudo_cache: prev_start_time = datetime.datetime.strptime(cached_item[8], "%I:%M:%S %p") @@ -1393,7 +1393,7 @@ if __name__ == '__main__': schedule.clear('daily-tasks') pseudo_channel.generate_daily_schedule() - generate_memory_schedule(pseudo_channel.db.get_daily_schedule()) + generate_memory_schedule(pseudo_channel.db.get_daily_schedule(), True) schedule.every().day.at(daily_update_time).do( go_generate_daily_sched From 8056c4b9ad7e6a3529f26c70c3492de021415b8a Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Fri, 11 Aug 2017 14:09:44 -0700 Subject: [PATCH 05/34] Started adding test runners. --- .gitignore | 1 + pseudo_config.py | 8 ++++-- requirements.txt | 2 ++ src/Helpers.py | 43 +++++++++++++++++++++++++++++ startstop.sh | 3 +- tests/__init__.py | 0 tests/test_commercial_injection.py | 17 ++++++++++++ tests/test_update_daily_schedule.py | 25 +++++++++++++++++ 8 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/Helpers.py create mode 100644 tests/__init__.py create mode 100644 tests/test_commercial_injection.py create mode 100644 tests/test_update_daily_schedule.py diff --git a/.gitignore b/.gitignore index 5f26756..33d01ce 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ env/ *.log *.pid *.json +.cache/ diff --git a/pseudo_config.py b/pseudo_config.py index 7f625a3..c07c8f9 100644 --- a/pseudo_config.py +++ b/pseudo_config.py @@ -5,7 +5,7 @@ touch ../plex_token.py - 2) add this line to the newly created file: + 2) add these lines to the newly created file: baseurl = 'the url to your server' token = 'your plex token' @@ -20,7 +20,7 @@ "Movies" : ["Films"], - 6) For Google Calendar integration add your "gkey" to the "plex_token.py" file + 6) *Skip this feature for now* For Google Calendar integration add your "gkey" to the "plex_token.py" file ...(https://docs.simplecalendar.io/find-google-calendar-id/): gkey = "the key" @@ -61,7 +61,7 @@ plexLibraries = { useCommercialInjection = True -# How many seconds to pad commercials between each other / other media +"""How many seconds to pad commercials between each other / other media""" commercialPadding = 5 """ @@ -83,6 +83,8 @@ useDailyOverlapCache = True dailyUpdateTime = "12:00 AM" +"""Debug mode will give you more output in your terminal to help problem solve issues.s""" debug_mode = True +"""---""" useGoogleCalendar = False \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c9b7400..81dec23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,10 @@ idna==2.5 kombu==4.1.0 oauth2client==4.1.2 PlexAPI==2.0.2 +py==1.4.34 pyasn1==0.3.2 pyasn1-modules==0.0.11 +pytest==3.2.1 pytz==2017.2 requests==2.18.3 rsa==3.4.2 diff --git a/src/Helpers.py b/src/Helpers.py new file mode 100644 index 0000000..b75eea2 --- /dev/null +++ b/src/Helpers.py @@ -0,0 +1,43 @@ +import os + +import logging + +class Helpers(): + """Class for consolidating helper methods""" + + def __init__(self): + + pass + + def save_file(self, data, filename, path, overwrite=True): + + fileName = filename + + writepath = path + + if not os.path.exists(writepath): + + os.makedirs(writepath) + + if os.path.exists(writepath+fileName) and overwrite: + + os.remove(writepath+fileName) + + mode = 'a' if os.path.exists(writepath) else 'w' + + with open(writepath+fileName, mode) as f: + + f.write(data) + + def get_file(self, filename, path): + + if not os.path.exists(writepath): + + raise IOError("{}, doesn't exist").format(writepath) + + if not os.path.exists(writepath+fileName): + + raise IOError("{}, doesn't exist").format(fileName) + + return None + diff --git a/startstop.sh b/startstop.sh index 2e97c34..358be58 100755 --- a/startstop.sh +++ b/startstop.sh @@ -27,6 +27,7 @@ if [ ! -e $output_pid_path/$pid_file ]; then nohup $python_to_use ./PseudoChannel.py -m -r > /dev/null 2>&1 & echo $! > $output_pid_path/$pid_file echo "Started PseudoChannel.py @ Process: $!" + sleep .7 echo "Created $pid_file file in $output_pid_path dir" else @@ -39,7 +40,7 @@ else while [ -e /proc/$the_pid ] do echo "PseudoChannel.py @: $the_pid is still running" - sleep .6 + sleep .7 done echo "PseudoChannel.py @: $the_pid has finished" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_commercial_injection.py b/tests/test_commercial_injection.py new file mode 100644 index 0000000..42d93d2 --- /dev/null +++ b/tests/test_commercial_injection.py @@ -0,0 +1,17 @@ +import pytest +import datetime + +@pytest.mark.parametrize("commercial, expected", [ + (["1", "1501900754", "3", "001 - Kit_Kat_Commercial_-_Give_Me_A_Break_1988", "30890", "/library/metadata/3854"], 35890) +]) +def test_pad_the_commercial_dur(commercial, expected): + + commercial_as_list = list(commercial) + + commercial_as_list[4] = int(commercial_as_list[4]) + (5 * 1000) + + assert int(commercial_as_list[4]) == expected + +def test_inject_commercials(): + + pass \ No newline at end of file diff --git a/tests/test_update_daily_schedule.py b/tests/test_update_daily_schedule.py new file mode 100644 index 0000000..9788b85 --- /dev/null +++ b/tests/test_update_daily_schedule.py @@ -0,0 +1,25 @@ +import pytest +import datetime + +@pytest.mark.parametrize("prevstartime, prevendtime, nowtime, expected", [ + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:10:42.304000", True), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 16:56:42.304000", True), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 16:59:42.304000", True), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:02:42.304000", True), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:15:42.304000", True), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:23:43.304000", False), + ("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:25:00.304000", False), +]) +def test_prev_day_media_still_playing_on_update(prevendtime, prevstartime, nowtime, expected): + + prev_end_time_to_watch_for = None + + now = datetime.datetime.strptime(nowtime, '%Y-%m-%d %H:%M:%S.%f') + + prev_start_time = datetime.datetime.strptime(prevstartime, "%I:%M:%S %p") + + prev_end_time_format = '%Y-%m-%d %H:%M:%S.%f' if '.' in prevendtime else '%Y-%m-%d %H:%M:%S' + + prev_end_time = datetime.datetime.strptime(prevendtime, prev_end_time_format) + + assert (prev_start_time < now and prev_end_time > now) == expected \ No newline at end of file From c903a1c6d7b3388f88d7d26f497a1de6ff9b28fd Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Sun, 13 Aug 2017 11:48:29 -0700 Subject: [PATCH 06/34] Updates to scripts. --- PseudoChannel.py | 17 +++++++++++++++++ pseudo_config.py | 44 ++++++++++++++++++++++++++++---------------- startstop.sh | 48 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 80 insertions(+), 29 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index b60872c..e3e1ef4 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -59,6 +59,8 @@ class PseudoChannel(): DEBUG = config.debug_mode + ROTATE_LOG = config.rotateLog + def __init__(self): logging.basicConfig(filename="pseudo-channel.log", level=logging.INFO) @@ -1008,6 +1010,18 @@ class PseudoChannel(): f.write(data) + def rotate_log(self): + + try: + os.remove('../pseudo-channel.log') + except OSError: + pass + + try: + os.remove('./pseudo-channel.log') + except OSError: + pass + def exit_app(self): print " - Exiting Pseudo TV & cleaning up." @@ -1175,6 +1189,9 @@ if __name__ == '__main__': see if it's midnight (or 23.59), if so then generate a new daily_schedule """ + """Every rotate log""" + dayToRotateLog = pseudo_channel.ROTATE_LOG.lower() + schedule.every().friday.at("00:00").do(pseudo_channel.rotate_log) logging.info("+++++ Running PseudoChannel.py -r") diff --git a/pseudo_config.py b/pseudo_config.py index c07c8f9..4fc2be0 100644 --- a/pseudo_config.py +++ b/pseudo_config.py @@ -31,20 +31,6 @@ """ -import os, sys -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -# import ../plex_token.py - -try: - import plex_token as plex_token -except ImportError as e: - print "+++++ Cannot find plex_token file. Make sure you create a plex_token.py file with the appropriate data." - raise e - -baseurl = plex_token.baseurl -token = plex_token.token -gkey = plex_token.gkey - ''' * * List of plex clients to use (add multiple clients to control multiple TV's) @@ -83,8 +69,34 @@ useDailyOverlapCache = True dailyUpdateTime = "12:00 AM" -"""Debug mode will give you more output in your terminal to help problem solve issues.s""" +"""Debug mode will give you more output in your terminal to help problem solve issues.""" debug_mode = True """---""" -useGoogleCalendar = False \ No newline at end of file +useGoogleCalendar = False + +"""When to delete / remake the pseudo-channel.log - right at midnight, (i.e. 'friday') """ +rotateLog = "friday" + + +""" +##### Do not edit below this line. + +Below is logic to grab your Plex 'token' & Plex 'baseurl'. If you are following along and have created a 'plex_token.py' +file as instructed, you do not need to edit below this line. + +""" + +import os, sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# import ../plex_token.py + +try: + import plex_token as plex_token +except ImportError as e: + print "+++++ Cannot find plex_token file. Make sure you create a plex_token.py file with the appropriate data." + raise e + +baseurl = plex_token.baseurl +token = plex_token.token +gkey = plex_token.gkey \ No newline at end of file diff --git a/startstop.sh b/startstop.sh index 358be58..f125621 100755 --- a/startstop.sh +++ b/startstop.sh @@ -13,35 +13,57 @@ #----BEGIN EDITABLE VARS---- -pid_file=running.pid +SCRIPT_TO_EXECUTE_PLUS_ARGS='PseudoChannel.py -m -r' -output_pid_path=. +OUTPUT_PID_FILE=running.pid -python_to_use="$(which python)" +OUTPUT_PID_PATH=. + +PYTHON_TO_USE="$(which python)" + +# If using 'virtualenv' with python, specify the local virtualenv dir. +VIRTUAL_ENV_DIR="env" #----END EDITABLE VARS------- -if [ ! -e $output_pid_path/$pid_file ]; then +if [ -d "$VIRTUAL_ENV_DIR" ]; then + + PYTHON_TO_USE="$VIRTUAL_ENV_DIR/bin/python" + +fi + +if [ ! -e "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" ]; then # If the running.pid file doesn't exists, create it, start PseudoChannel.py and add the PID to it. - nohup $python_to_use ./PseudoChannel.py -m -r > /dev/null 2>&1 & echo $! > $output_pid_path/$pid_file + "$PYTHON_TO_USE" ./$SCRIPT_TO_EXECUTE_PLUS_ARGS > /dev/null 2>&1 & echo $! > "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" + + echo "Started $SCRIPT_TO_EXECUTE_PLUS_ARGS @ Process: $!" - echo "Started PseudoChannel.py @ Process: $!" sleep .7 - echo "Created $pid_file file in $output_pid_path dir" + + echo "Created $OUTPUT_PID_FILE file in $OUTPUT_PID_PATH dir" else # If the running.pid exists, read it & try to kill the process if it exists, then delete it. - the_pid=$(<$output_pid_path/$pid_file) - rm $output_pid_path/$pid_file - echo "Deleted $pid_file file in $output_pid_path dir" - kill $the_pid + the_pid=$(<$OUTPUT_PID_PATH/$OUTPUT_PID_FILE) + + rm "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" + + echo "Deleted $OUTPUT_PID_FILE file in $OUTPUT_PID_PATH dir" + + kill "$the_pid" + while [ -e /proc/$the_pid ] + do - echo "PseudoChannel.py @: $the_pid is still running" + + echo "$SCRIPT_TO_EXECUTE_PLUS_ARGS @: $the_pid is still running" + sleep .7 + done - echo "PseudoChannel.py @: $the_pid has finished" + + echo "$SCRIPT_TO_EXECUTE_PLUS_ARGS @: $the_pid has finished" fi \ No newline at end of file From 4dbf5b4c077d4b200b6518f8f5463d3984e8c1d1 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Tue, 15 Aug 2017 10:53:54 -0700 Subject: [PATCH 07/34] Experienced the duplicate entry error for todays daily_schedule error. Trying something different. --- PseudoChannel.py | 6 ++++-- startstop.sh | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index e3e1ef4..6c3e9ce 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -596,6 +596,10 @@ class PseudoChannel(): logging.info("##### Dropping previous daily_schedule database") + self.db.remove_all_daily_scheduled_items() + + sleep(1) + if self.USING_COMMERCIAL_INJECTION: self.commercials = PseudoChannelCommercial( self.db.get_commercials(), @@ -764,8 +768,6 @@ class PseudoChannel(): previous_episode = None - self.db.remove_all_daily_scheduled_items() - for entry in self.MEDIA: #print entry.natural_end_time diff --git a/startstop.sh b/startstop.sh index f125621..1b89f6b 100755 --- a/startstop.sh +++ b/startstop.sh @@ -3,7 +3,7 @@ # file: startstop.sh #---- -# Simple script to start / stop PseudoChannel.py +# Simple script to start / stop a python script in the background. #---- #---- @@ -26,12 +26,14 @@ VIRTUAL_ENV_DIR="env" #----END EDITABLE VARS------- +# If virtualenv specified & exists, using that version of python instead. if [ -d "$VIRTUAL_ENV_DIR" ]; then PYTHON_TO_USE="$VIRTUAL_ENV_DIR/bin/python" fi +# If the .pid file doesn't exist (let's assume no processes are running)... if [ ! -e "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" ]; then # If the running.pid file doesn't exists, create it, start PseudoChannel.py and add the PID to it. @@ -54,6 +56,8 @@ else kill "$the_pid" + COUNTER=1 + while [ -e /proc/$the_pid ] do @@ -62,8 +66,24 @@ else sleep .7 + COUNTER=$[$COUNTER +1] + + if [ $COUNTER -eq 20 ]; then + + kill -9 "$the_pid" + + fi + + if [ $COUNTER -eq 40 ]; then + + exit 1 + + fi + done echo "$SCRIPT_TO_EXECUTE_PLUS_ARGS @: $the_pid has finished" -fi \ No newline at end of file +fi + +exit 0 \ No newline at end of file From 691e0f4469512d9a7a6ede71da43cebc38857396 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Wed, 16 Aug 2017 16:25:33 -0700 Subject: [PATCH 08/34] Added support for xtra args in the xml to further specify random movie params. There are a few gotchas. The xml attribute should look like this: xtra='actor:tom cruise genre:action contentRating:r' and I the local db must be current. If it fails then it just dismisses all params and grabs random movie. --- PseudoChannel.py | 31 ++++++++++++++++++++++++++++++- pseudo_schedule.xml | 2 ++ src/PseudoChannelDatabase.py | 22 +++++++++++++++++----- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 6c3e9ce..1529290 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -26,6 +26,8 @@ from xml.dom import minidom import xml.etree.ElementTree as ET import json from pprint import pprint +import random +import re import schedule @@ -384,6 +386,8 @@ class PseudoChannel(): seriesOffset = time.attrib['series-offset'] if 'series-offset' in time.attrib else '' + xtra = time.attrib['xtra'] if 'xtra' in time.attrib else '' + start_time_unix = self.translate_time(time.text) print "Adding: ", time.tag, section, time.text, time.attrib['title'] @@ -400,6 +404,7 @@ class PseudoChannel(): strict_time, # strictTime time_shift, # timeShift overlap_max, # overlapMax + xtra, # xtra kargs (i.e. 'director=director') ) def drop_db(self): @@ -672,7 +677,31 @@ class PseudoChannel(): if str(entry[3]).lower() == "random": - the_movie = self.db.get_random_movie() + if(entry[13] != ''): + + movies = self.PLEX.library.section('Movies') + + movies_list = [] + + try: + + thestr = entry[13] + regex = re.compile(r"\b(\w+)\s*:\s*([^:]*)(?=\s+\w+\s*:|$)") + d = dict(regex.findall(thestr)) + for movie in movies.search(None, **d): + movies_list.append(movie) + + the_movie = self.db.get_movie(random.choice(movies_list).title) + + except: + + print("For some reason, I've failed getting movie with xtra args.") + + the_movie = self.db.get_random_movie() + + else: + + the_movie = self.db.get_random_movie() else: diff --git a/pseudo_schedule.xml b/pseudo_schedule.xml index 86f9fa3..cee144d 100644 --- a/pseudo_schedule.xml +++ b/pseudo_schedule.xml @@ -90,6 +90,8 @@ + + diff --git a/src/PseudoChannelDatabase.py b/src/PseudoChannelDatabase.py index ffd7aaa..53ae3e5 100644 --- a/src/PseudoChannelDatabase.py +++ b/src/PseudoChannelDatabase.py @@ -52,7 +52,7 @@ class PseudoChannelDatabase(): 'schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, ' 'mediaID INTEGER, title TEXT, duration INTEGER, startTime INTEGER, ' 'endTime INTEGER, dayOfWeek TEXT, startTimeUnix INTEGER, section TEXT, ' - 'strictTime TEXT, timeShift TEXT, overlapMax TEXT)') + 'strictTime TEXT, timeShift TEXT, overlapMax TEXT, xtra TEXT)') self.cursor.execute('CREATE TABLE IF NOT EXISTS ' 'daily_schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, ' @@ -202,13 +202,25 @@ class PseudoChannelDatabase(): self.conn.rollback() raise e - def add_schedule_to_db(self, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax): + def add_schedule_to_db(self, + mediaID, + title, + duration, + startTime, + endTime, + dayOfWeek, + startTimeUnix, + section, + strictTime, + timeShift, + overlapMax, + xtra): unix = int(time.time()) try: self.cursor.execute("REPLACE INTO schedule " - "(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax)) + "(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax, xtra) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax, xtra)) self.conn.commit() # Catch the exception except Exception as e: From bea4b4286b3e3e017c965aa3e0278a0ff4d881da Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 17 Aug 2017 06:42:03 -0700 Subject: [PATCH 09/34] Another try at fixing the occasional issue where the previous daily_schedule is not cleared when the new one is generated. --- PseudoChannel.py | 8 +++++++- pseudo_schedule.xml | 2 +- src/PseudoChannelDatabase.py | 18 ++++++++++++++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 1529290..4a863b5 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -601,7 +601,13 @@ class PseudoChannel(): logging.info("##### Dropping previous daily_schedule database") - self.db.remove_all_daily_scheduled_items() + + """A fix for the duplicate entries problem that comes up occasionally.""" + self.db.drop_daily_schedule_table() + + sleep(1) + + self.db.create_daily_schedule_table() sleep(1) diff --git a/pseudo_schedule.xml b/pseudo_schedule.xml index cee144d..5d413f0 100644 --- a/pseudo_schedule.xml +++ b/pseudo_schedule.xml @@ -90,7 +90,7 @@ - + diff --git a/src/PseudoChannelDatabase.py b/src/PseudoChannelDatabase.py index 53ae3e5..e03850c 100644 --- a/src/PseudoChannelDatabase.py +++ b/src/PseudoChannelDatabase.py @@ -101,9 +101,23 @@ class PseudoChannelDatabase(): pass - def drop_daily_schedule(self): + def drop_daily_schedule_table(self): - pass + sql = "DROP TABLE IF EXISTS daily_schedule" + + self.cursor.execute(sql) + + self.conn.commit() + + def create_daily_schedule_table(self): + + self.cursor.execute('CREATE TABLE IF NOT EXISTS ' + 'daily_schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, ' + 'mediaID INTEGER, title TEXT, episodeNumber INTEGER, seasonNumber INTEGER, ' + 'showTitle TEXT, duration INTEGER, startTime INTEGER, endTime INTEGER, ' + 'dayOfWeek TEXT, sectionType TEXT, plexMediaID TEXT)') + + self.conn.commit() def remove_all_scheduled_items(self): From 1fc3d1bf7ac7239c97fc7bfa2eca8c84a05eb459 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 17 Aug 2017 06:56:35 -0700 Subject: [PATCH 10/34] Added error handling for when a webserver is already running when starting or re-starting the app. --- src/PseudoDailyScheduleController.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PseudoDailyScheduleController.py b/src/PseudoDailyScheduleController.py index 0aeb1da..41542ed 100644 --- a/src/PseudoDailyScheduleController.py +++ b/src/PseudoDailyScheduleController.py @@ -9,6 +9,7 @@ import thread,SocketServer,SimpleHTTPServer from yattag import Doc from yattag import indent import os, sys +import socket import logging import logging.handlers @@ -112,6 +113,9 @@ class PseudoDailyScheduleController(): core.print_info("Exiting the SET web server...") httpd.socket.close() + except socket.error, exc: + print "Caught exception socket.error : %s" % exc + # handle the rest #except Exception: # print "[*] Exiting the SET web server...\n" From 69e92ef68e1f79bb000f3511d93f121e870e9e32 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 17 Aug 2017 12:12:59 -0700 Subject: [PATCH 11/34] Added documentation to XML / defaults to strict-time / time-shift. Also removed overlap-max for now until it's fully implemented. --- PseudoChannel.py | 4 +- pseudo_config.py | 1 - pseudo_schedule.xml | 217 +++++++++++++++++++++++++++++++++----------- 3 files changed, 164 insertions(+), 58 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 4a863b5..05757f7 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -378,9 +378,9 @@ class PseudoChannel(): day_of_week = child.tag - strict_time = time.attrib['strict-time'] if 'strict-time' in time.attrib else '' + strict_time = time.attrib['strict-time'] if 'strict-time' in time.attrib else 'false' - time_shift = time.attrib['time-shift'] if 'time-shift' in time.attrib else '' + time_shift = time.attrib['time-shift'] if 'time-shift' in time.attrib else '1' overlap_max = time.attrib['overlap-max'] if 'overlap-max' in time.attrib else '' diff --git a/pseudo_config.py b/pseudo_config.py index 4fc2be0..797d8ce 100644 --- a/pseudo_config.py +++ b/pseudo_config.py @@ -41,7 +41,6 @@ plexClients = ['RasPlex'] plexLibraries = { "TV Shows" : ["TV Shows"], "Movies" : ["Movies"], - "Music" : ["Music"], "Commercials" : ["Commercials"], } diff --git a/pseudo_schedule.xml b/pseudo_schedule.xml index 5d413f0..bac2e98 100644 --- a/pseudo_schedule.xml +++ b/pseudo_schedule.xml @@ -1,10 +1,117 @@ + + - - - - + + + + @@ -15,85 +122,85 @@ - - - - + + + + - - + + - + - - - - - + + + + + - + - - + + - - + + - + - - + + - + - - + + - - + + - - - - + + + + - - + + - + - + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - + - - + + From 2205cf723b6c473aa51da2c9c6dffffe398042fc Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Thu, 17 Aug 2017 14:52:17 -0700 Subject: [PATCH 12/34] Added in support for multiple movie filter values seperated by a comma, i,e, contentRating:G,PG,PG-13 --- PseudoChannel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PseudoChannel.py b/PseudoChannel.py index 05757f7..25a35cf 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -694,6 +694,9 @@ class PseudoChannel(): thestr = entry[13] regex = re.compile(r"\b(\w+)\s*:\s*([^:]*)(?=\s+\w+\s*:|$)") d = dict(regex.findall(thestr)) + # turn values into list + for key, val in d.iteritems(): + d[key] = val.split(',') for movie in movies.search(None, **d): movies_list.append(movie) From daf8ced89bbb19b1553b3daba164780eb7b2e004 Mon Sep 17 00:00:00 2001 From: Justin Emter Date: Fri, 18 Aug 2017 12:50:54 -0700 Subject: [PATCH 13/34] Cleaning up code base, etc. getting ready for release. --- PseudoChannel.py | 565 +-------------------------- pseudo_config.py | 18 +- pseudo_schedule.xml | 14 +- src/PseudoChannelCommercial.py | 70 +--- src/PseudoChannelDatabase.py | 108 +---- src/PseudoDailyScheduleController.py | 213 +--------- 6 files changed, 51 insertions(+), 937 deletions(-) diff --git a/PseudoChannel.py b/PseudoChannel.py index 25a35cf..e649fb5 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -1,18 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from src import PseudoChannelDatabase -from src import Movie -from src import Commercial -from src import Episode -from src import Music -from src import Video -from src import PseudoDailyScheduleController -from src import GoogleCalendar -from src import PseudoChannelCommercial - -from plexapi.server import PlexServer - import sys import datetime from datetime import time @@ -28,11 +16,18 @@ import json from pprint import pprint import random import re - +from plexapi.server import PlexServer import schedule - from time import sleep - +from src import PseudoChannelDatabase +from src import Movie +from src import Commercial +from src import Episode +from src import Music +from src import Video +from src import PseudoDailyScheduleController +from src import GoogleCalendar +from src import PseudoChannelCommercial import pseudo_config as config reload(sys) @@ -43,32 +38,21 @@ class PseudoChannel(): PLEX = PlexServer(config.baseurl, config.token) MEDIA = [] GKEY = config.gkey - USING_GOOGLE_CALENDAR = config.useGoogleCalendar - USING_COMMERCIAL_INJECTION = config.useCommercialInjection - DAILY_UPDATE_TIME = config.dailyUpdateTime - APP_TIME_FORMAT_STR = '%I:%M:%S %p' - COMMERCIAL_PADDING_IN_SECONDS = config.commercialPadding - CONTROLLER_SERVER_PATH = config.controllerServerPath CONTROLLER_SERVER_PORT = config.controllerServerPort - USE_OVERRIDE_CACHE = config.useDailyOverlapCache - DEBUG = config.debug_mode - ROTATE_LOG = config.rotateLog def __init__(self): logging.basicConfig(filename="pseudo-channel.log", level=logging.INFO) - self.db = PseudoChannelDatabase("pseudo-channel.db") - self.controller = PseudoDailyScheduleController( config.baseurl, config.token, @@ -77,17 +61,11 @@ class PseudoChannel(): self.CONTROLLER_SERVER_PORT, self.DEBUG ) - """Database functions. - update_db(): Grab the media from the Plex DB and store it in the local pseudo-channel.db. - drop_db(): Drop the local database. Fresh start. - update_schedule(): Update schedule with user defined times. - drop_schedule(): Drop the user defined schedule table. - generate_daily_schedule(): Generates daily schedule based on the "schedule" table. """ @@ -117,27 +95,16 @@ class PseudoChannel(): def update_db(self): print("#### Updating Local Database") - self.db.create_tables() - libs_dict = config.plexLibraries - sections = self.PLEX.library.sections() - for section in sections: - for correct_lib_name, user_lib_name in libs_dict.items(): - if section.title.lower() in [x.lower() for x in user_lib_name]: - if correct_lib_name == "Movies": - sectionMedia = self.PLEX.library.section(section.title).all() - for i, media in enumerate(sectionMedia): - self.db.add_movies_to_db(1, media.title, media.duration, media.key) - self.print_progress( i + 1, len(sectionMedia), @@ -145,24 +112,14 @@ class PseudoChannel(): suffix = 'Complete', bar_length = 40 ) - - elif correct_lib_name == "TV Shows": - sectionMedia = self.PLEX.library.section(section.title).all() - for i, media in enumerate(sectionMedia): - backgroundImagePath = self.PLEX.library.section(section.title).get(media.title) - backgroundImgURL = '' - if isinstance(backgroundImagePath.art, str): - backgroundImgURL = config.baseurl+backgroundImagePath.art+"?X-Plex-Token="+config.token - self.db.add_shows_to_db(2, media.title, media.duration, '', backgroundImgURL, media.key) - self.print_progress( i + 1, len(sectionMedia), @@ -170,16 +127,11 @@ class PseudoChannel(): suffix = 'Complete', bar_length = 40 ) - #add all episodes of each tv show to episodes table episodes = self.PLEX.library.section(section.title).get(media.title).episodes() - for episode in episodes: - duration = episode.duration - if duration: - self.db.add_episodes_to_db( 4, episode.title, @@ -189,9 +141,7 @@ class PseudoChannel(): media.title, episode.key ) - else: - self.db.add_episodes_to_db( 4, episode.title, @@ -201,17 +151,11 @@ class PseudoChannel(): media.title, episode.key ) - elif correct_lib_name == "Commercials": - sectionMedia = self.PLEX.library.section(section.title).all() - media_length = len(sectionMedia) - for i, media in enumerate(sectionMedia): - self.db.add_commercials_to_db(3, media.title, media.duration, media.key) - self.print_progress( i + 1, media_length, @@ -223,13 +167,9 @@ class PseudoChannel(): def update_schedule_from_google_calendar(self): self.gcal = GoogleCalendar(self.GKEY) - events = self.gcal.get_entries() - self.db.create_tables() - self.db.remove_all_scheduled_items() - scheduled_days_list = [ "mondays", "tuesdays", @@ -242,14 +182,12 @@ class PseudoChannel(): "weekends", "everyday" ] - section_dict = { "TV Shows" : ["series", "shows", "tv", "episodes", "tv shows", "show"], "Movies" : ["movie", "movies", "films", "film"], "Videos" : ["video", "videos", "vid"], "Music" : ["music", "songs", "song", "tune", "tunes"] } - weekday_dict = { "0" : ["mondays", "weekdays", "everyday"], "1" : ["tuesdays", "weekdays", "everyday"], @@ -259,56 +197,32 @@ class PseudoChannel(): "5" : ["saturdays", "weekends", "everyday"], "6" : ["sundays", "weekends", "everyday"], } - for event in events: - titlelist = [x.strip() for x in event['summary'].split(',')] - start = event['start'].get('dateTime', event['start'].get('date')) - s = datetime.datetime.strptime(start,"%Y-%m-%dT%H:%M:%S-07:00") - weekno = s.weekday() - for key, value in section_dict.items(): - if str(titlelist[0]).lower() == key or str(titlelist[0]).lower() in value: - print "Adding {} to schedule.".format(titlelist[1]) - title = titlelist[1] - # s.strftime('%I:%M'), event["summary"] natural_start_time = self.translate_time(s.strftime(self.APP_TIME_FORMAT_STR)) - natural_end_time = 0 - section = key - for dnum, daylist in weekday_dict.items(): - #print int(weekno), int(dnum) - if int(weekno) == int(dnum): - day_of_week = daylist[0] - strict_time = titlelist[2] if len(titlelist) > 2 else "true" - #strict_time = "true" - time_shift = "5" - overlap_max = "" - print natural_start_time - start_time_unix = datetime.datetime.strptime( self.translate_time(natural_start_time), '%I:%M:%S %p').strftime('%Y-%m-%d %H:%M:%S') - #print "Adding: ", time.tag, section, time.text, time.attrib['title'] - self.db.add_schedule_to_db( 0, # mediaID title, # title @@ -329,11 +243,8 @@ class PseudoChannel(): abspath = os.path.abspath(__file__) dname = os.path.dirname(abspath) os.chdir(dname) - self.db.create_tables() - self.db.remove_all_scheduled_items() - scheduled_days_list = [ "mondays", "tuesdays", @@ -346,52 +257,31 @@ class PseudoChannel(): "weekends", "everyday" ] - section_dict = { "TV Shows" : ["series", "shows", "tv", "episodes", "tv shows", "show"], "Movies" : ["movie", "movies", "films", "film"], "Videos" : ["video", "videos", "vid"], "Music" : ["music", "songs", "song", "tune", "tunes"] } - tree = ET.parse('pseudo_schedule.xml') - root = tree.getroot() - for child in root: - if child.tag in scheduled_days_list: - for time in child.iter("time"): - for key, value in section_dict.items(): - if time.attrib['type'] == key or time.attrib['type'] in value: - title = time.attrib['title'] if 'title' in time.attrib else '' - natural_start_time = self.translate_time(time.text) - natural_end_time = 0 - section = key - day_of_week = child.tag - strict_time = time.attrib['strict-time'] if 'strict-time' in time.attrib else 'false' - time_shift = time.attrib['time-shift'] if 'time-shift' in time.attrib else '1' - overlap_max = time.attrib['overlap-max'] if 'overlap-max' in time.attrib else '' - seriesOffset = time.attrib['series-offset'] if 'series-offset' in time.attrib else '' - xtra = time.attrib['xtra'] if 'xtra' in time.attrib else '' - start_time_unix = self.translate_time(time.text) - print "Adding: ", time.tag, section, time.text, time.attrib['title'] - self.db.add_schedule_to_db( 0, # mediaID title, # title @@ -419,53 +309,20 @@ class PseudoChannel(): self.db.remove_all_scheduled_items() - - - """App functions. - - generate_daily_schedule(): Generate the daily_schedule table. - """ - - ''' - * - * Using datetime to figure out when the media item will end based on the scheduled start time or the offset - * generated by the previous media item. - - * Returns time - * - ''' - ''' - * - * Returns time difference in minutes - * - ''' - def translate_time(self, timestr): try: - return datetime.datetime.strptime(timestr, '%I:%M %p').strftime(self.APP_TIME_FORMAT_STR) - except ValueError as e: - pass - try: - return datetime.datetime.strptime(timestr, '%I:%M:%S %p').strftime(self.APP_TIME_FORMAT_STR) - except ValueError as e: - pass - try: - return datetime.datetime.strptime(timestr, '%H:%M').strftime(self.APP_TIME_FORMAT_STR) - except ValueError as e: - pass - return timestr def time_diff(self, time1,time2): @@ -476,12 +333,9 @@ class PseudoChannel(): ''' timeA = datetime.datetime.strptime(time1, '%I:%M:%S %p') timeB = datetime.datetime.strptime(time2, '%I:%M:%S %p') - timeAEpoch = calendar.timegm(timeA.timetuple()) timeBEpoch = calendar.timegm(timeB.timetuple()) - tdelta = abs(timeAEpoch) - abs(timeBEpoch) - return int(tdelta/60) @@ -495,26 +349,16 @@ class PseudoChannel(): def calculate_start_time(self, prevEndTime, intendedStartTime, timeGap, overlapMax): self.TIME_GAP = timeGap - self.OVERLAP_GAP = timeGap - self.OVERLAP_MAX = overlapMax - time1 = prevEndTime.strftime('%I:%M:%S %p') - timeB = datetime.datetime.strptime(intendedStartTime, '%I:%M:%S %p').strftime(self.APP_TIME_FORMAT_STR) - print "++++ Previous End Time: ", time1, "Intended start time: ", timeB - timeDiff = self.time_diff(time1, timeB) - """print("timeDiff "+ str(timeDiff)) print("startTimeUNIX: "+ str(intendedStartTime))""" - newTimeObj = timeB - newStartTime = timeB - ''' * * If time difference is negative, then we know there is overlap @@ -527,32 +371,14 @@ class PseudoChannel(): * ''' timeset=[datetime.time(h,m).strftime("%H:%M") for h,m in itertools.product(xrange(0,24),xrange(0,60,int(self.OVERLAP_GAP)))] - - #print(timeset) - timeSetToUse = None - for time in timeset: - - #print(time) theTimeSetInterval = datetime.datetime.strptime(time, '%H:%M') - - # print(theTimeSetInterval) - - # print(prevEndTime) - if theTimeSetInterval >= prevEndTime: - print "++++ There is overlap. Setting new time-interval:", theTimeSetInterval - newStartTime = theTimeSetInterval - break - - #newStartTime = newTimeObj + datetime.timedelta(minutes=abs(timeDiff + overlapGap)) - elif (timeDiff >= 0) and (self.TIME_GAP != -1): - ''' * * If there this value is configured, then the timeGap var in config will determine the next increment. @@ -560,65 +386,39 @@ class PseudoChannel(): * ''' timeset=[datetime.time(h,m).strftime("%H:%M") for h,m in itertools.product(xrange(0,24),xrange(0,60,int(self.TIME_GAP)))] - - # print(timeset) - for time in timeset: - theTimeSetInterval = datetime.datetime.strptime(time, '%H:%M') - tempTimeTwoStr = datetime.datetime.strptime(time1, self.APP_TIME_FORMAT_STR).strftime('%H:%M') - formatted_time_two = datetime.datetime.strptime(tempTimeTwoStr, '%H:%M') - if theTimeSetInterval >= formatted_time_two: - print "++++ Setting new time-interval:", theTimeSetInterval - newStartTime = theTimeSetInterval - break - else: - print("Not sure what to do here") - return newStartTime.strftime('%I:%M:%S %p') def get_end_time_from_duration(self, startTime, duration): time = datetime.datetime.strptime(startTime, '%I:%M:%S %p') - show_time_plus_duration = time + datetime.timedelta(milliseconds=duration) - - #print(show_time_plus_duration.minute) - return show_time_plus_duration def generate_daily_schedule(self): print("#### Generating Daily Schedule") - logging.info("##### Dropping previous daily_schedule database") - - """A fix for the duplicate entries problem that comes up occasionally.""" self.db.drop_daily_schedule_table() - sleep(1) - self.db.create_daily_schedule_table() - sleep(1) - if self.USING_COMMERCIAL_INJECTION: self.commercials = PseudoChannelCommercial( self.db.get_commercials(), self.COMMERCIAL_PADDING_IN_SECONDS ) - schedule = self.db.get_schedule() - weekday_dict = { "0" : ["mondays", "weekdays", "everyday"], "1" : ["tuesdays", "weekdays", "everyday"], @@ -628,33 +428,19 @@ class PseudoChannel(): "5" : ["saturdays", "weekends", "everyday"], "6" : ["sundays", "weekends", "everyday"], } - weekno = datetime.datetime.today().weekday() - schedule_advance_watcher = 0 - for entry in schedule: - schedule_advance_watcher += 1 - section = entry[9] - for key, val in weekday_dict.iteritems(): - if str(entry[7]) in str(val) and int(weekno) == int(key): - if section == "TV Shows": - if str(entry[3]).lower() == "random": - next_episode = self.db.get_random_episode() - else: - next_episode = self.db.get_next_episode(entry[3]) - if next_episode != None: - episode = Episode( section, # section_type next_episode[3], # title @@ -670,27 +456,15 @@ class PseudoChannel(): next_episode[5], # episode_number next_episode[6] # season_number ) - self.MEDIA.append(episode) - else: - print("Cannot find TV Show Episode, {} in the local db".format(entry[3])) - - #print(episode) - elif section == "Movies": - if str(entry[3]).lower() == "random": - if(entry[13] != ''): - movies = self.PLEX.library.section('Movies') - movies_list = [] - try: - thestr = entry[13] regex = re.compile(r"\b(\w+)\s*:\s*([^:]*)(?=\s+\w+\s*:|$)") d = dict(regex.findall(thestr)) @@ -699,25 +473,15 @@ class PseudoChannel(): d[key] = val.split(',') for movie in movies.search(None, **d): movies_list.append(movie) - the_movie = self.db.get_movie(random.choice(movies_list).title) - except: - print("For some reason, I've failed getting movie with xtra args.") - the_movie = self.db.get_random_movie() - else: - the_movie = self.db.get_random_movie() - else: - the_movie = self.db.get_movie(entry[3]) - if the_movie != None: - movie = Movie( section, # section_type the_movie[3], # title @@ -730,21 +494,12 @@ class PseudoChannel(): entry[12], # overlap_max the_movie[6] # plex id ) - - #print(movie.natural_end_time) - self.MEDIA.append(movie) - else: - print str("Cannot find Movie, {} in the local db".format(entry[3])).encode('UTF-8') - elif section == "Music": - the_music = self.db.get_music(entry[3]) - if the_music != None: - music = Music( section, # section_type the_music[3], # title @@ -757,21 +512,12 @@ class PseudoChannel(): entry[12], # overlap_max the_music[6], # plex id ) - - #print(music.natural_end_time) - self.MEDIA.append(music) - else: - print str("Cannot find Music, {} in the local db".format(entry[3])).encode('UTF-8') - elif section == "Video": - the_video = self.db.get_video(entry[3]) - if the_music != None: - video = Video( section, # section_type the_video[3], # title @@ -784,270 +530,145 @@ class PseudoChannel(): entry[12], # overlap_max the_video[6] # plex id ) - - #print(music.natural_end_time) - self.MEDIA.append(video) - else: - print str("Cannot find Video, {} in the local db".format(entry[3])).encode('UTF-8') - else: - pass - """If we reached the end of the scheduled items for today, add them to the daily schedule """ if schedule_advance_watcher >= len(schedule): - print "+++++ Finished processing time entries, recreating daily_schedule" - previous_episode = None - for entry in self.MEDIA: - - #print entry.natural_end_time - if previous_episode != None: - natural_start_time = datetime.datetime.strptime(entry.natural_start_time, self.APP_TIME_FORMAT_STR) - natural_end_time = entry.natural_end_time - if entry.is_strict_time.lower() == "true": - print "++++ Strict-time: {}".format(str(entry.title)) - entry.end_time = self.get_end_time_from_duration( self.translate_time(entry.start_time), entry.duration ) - """Get List of Commercials to inject""" - if self.USING_COMMERCIAL_INJECTION: - list_of_commercials = self.commercials.get_commercials_to_place_between_media( previous_episode, entry ) - for commercial in list_of_commercials: - self.db.add_media_to_daily_schedule(commercial) - self.db.add_media_to_daily_schedule(entry) - previous_episode = entry - else: - try: print "++++ NOT strict-time: {}".format(str(entry.title).encode(sys.stdout.encoding, errors='replace')) - except: - pass - new_starttime = self.calculate_start_time( previous_episode.end_time, entry.natural_start_time, previous_episode.time_shift, previous_episode.overlap_max ) - print "++++ New start time:", new_starttime - entry.start_time = datetime.datetime.strptime(new_starttime, self.APP_TIME_FORMAT_STR).strftime('%I:%M:%S %p') - entry.end_time = self.get_end_time_from_duration(entry.start_time, entry.duration) - """Get List of Commercials to inject""" if self.USING_COMMERCIAL_INJECTION: list_of_commercials = self.commercials.get_commercials_to_place_between_media( previous_episode, entry ) - for commercial in list_of_commercials: - self.db.add_media_to_daily_schedule(commercial) - self.db.add_media_to_daily_schedule(entry) - previous_episode = entry - else: - self.db.add_media_to_daily_schedule(entry) - previous_episode = entry - self.make_xml_schedule() def run_commercial_injection(self): - """print "#### Running commercial injection." - - self.commercials = PseudoChannelCommercial( - self.db.get_commercials(), - self.db.get_daily_schedule() - ) - - commercials_to_inject = self.commercials.get_commercials_to_inject() - - print commercials_to_inject""" - - pass - - def run(self): - - """print datetime.datetime.now() - threading.Timer(1, self.run()).start()""" pass def make_xml_schedule(self): self.controller.make_xml_schedule(self.db.get_daily_schedule()) - def get_daily_schedule_as_media_object_list(self): - - for i, item in enumerate(self.db.get_daily_schedule(), start=0): - - if item[11] == "TV Shows": - - """episode = Episode( - - )""" - pass - - elif item[11] == "Movies": - - pass - - elif item[11] == "Music": - - pass - - elif item[11] == "Commercials": - - pass - - elif item[11] == "Videos": - - pass - - else: - - pass - def show_clients(self): print "##### Connected Clients:" - for i, client in enumerate(self.PLEX.clients()): - print "+++++", str(i + 1)+".", "Client:", client.title def show_schedule(self): print "##### Daily Pseudo Schedule:" - daily_schedule = self.db.get_daily_schedule() - for i , entry in enumerate(daily_schedule): - print str("+++++ {} {} {} {} {} {}".format(str(i + 1)+".", entry[8], entry[11], entry[6], " - ", entry[3])).encode(sys.stdout.encoding, errors='replace') def write_json_to_file(self, data): - fileName = "pseudo-schedule.json" - + fileName = "pseudo-queue.json" writepath = './' - if os.path.exists(writepath+fileName): - os.remove(writepath+fileName) - mode = 'a' if os.path.exists(writepath) else 'w' - with open(writepath+fileName, mode) as f: - f.write(data) def export_queue(self): shows_table = self.db.get_shows_table() - json_string = json.dumps(shows_table) - - print "+++++ Exporting queue ", json_string - + print "+++++ Exporting queue " self.write_json_to_file(json_string) + print "+++++ Done." def import_queue(self): """Dropping previous shows table before adding the imported data""" - self.db.clear_shows_table() - - with open('pseudo-schedule.json') as data_file: + with open('pseudo-queue.json') as data_file: data = json.load(data_file) - - pprint(data) - + #pprint(data) for row in data: - print row - self.db.import_shows_table_by_row(row[2], row[3], row[4], row[5], row[6], row[7]) + print "+++++ Done. Imported queue." def get_daily_schedule_cache_as_json(self): data = [] - try: with open('../.pseudo-cache/daily-schedule.json') as data_file: data = json.load(data_file) - #pprint(data) - except IOError: - print ("----- Having issues opening the pseudo-cache file.") - return data def save_daily_schedule_as_json(self): daily_schedule_table = self.db.get_daily_schedule() - json_string = json.dumps(daily_schedule_table) - print "+++++ Saving Daily Schedule Cache " - self.save_file(json_string, 'daily-schedule.json', '../.pseudo-cache/') def save_file(self, data, filename, path="./"): fileName = filename - writepath = path - if not os.path.exists(writepath): - os.makedirs(writepath) - if os.path.exists(writepath+fileName): - os.remove(writepath+fileName) - mode = 'a' if os.path.exists(writepath) else 'w' - with open(writepath+fileName, mode) as f: - f.write(data) def rotate_log(self): @@ -1056,7 +677,6 @@ class PseudoChannel(): os.remove('../pseudo-channel.log') except OSError: pass - try: os.remove('./pseudo-channel.log') except OSError: @@ -1065,31 +685,16 @@ class PseudoChannel(): def exit_app(self): print " - Exiting Pseudo TV & cleaning up." - for i in self.MEDIA: - del i - self.MEDIA = None - self.controller = None - self.db = None - sleep(1) if __name__ == '__main__': pseudo_channel = PseudoChannel() - - #pseudo_channel.db.create_tables() - - #pseudo_channel.update_db() - - #pseudo_channel.update_schedule() - - #pseudo_channel.generate_daily_schedule() - banner = textwrap.dedent('''\ # __ __ # |__)_ _ _| _ / |_ _ _ _ _| _ @@ -1098,17 +703,14 @@ if __name__ == '__main__': A Custom TV Channel for Plex ''') - parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description = banner) - ''' * * Primary arguments: "python PseudoChannel.py -u -xml -g -r" * ''' - parser.add_argument('-u', '--update', action='store_true', help='Update the local database with Plex libraries.') @@ -1123,14 +725,12 @@ if __name__ == '__main__': parser.add_argument('-gc', '--google_calendar', action='store_true', help='Update the local database with entries in the google calendar.') - parser.add_argument('-g', '--generate_schedule', action='store_true', help='Generate the daily schedule.') parser.add_argument('-r', '--run', action='store_true', help='Run this program.') - ''' * * Show connected clients: "python PseudoChannel.py -c" @@ -1139,7 +739,6 @@ if __name__ == '__main__': parser.add_argument('-c', '--show_clients', action='store_true', help='Show Plex clients.') - ''' * * Show schedule (daily): "python PseudoChannel.py -s" @@ -1148,7 +747,6 @@ if __name__ == '__main__': parser.add_argument('-s', '--show_schedule', action='store_true', help='Show scheduled media for today.') - ''' * * Make XML / HTML Schedule: "python PseudoChannel.py -m" @@ -1157,7 +755,6 @@ if __name__ == '__main__': parser.add_argument('-m', '--make_html', action='store_true', help='Makes the XML / HTML schedule based on the daily_schedule table.') - ''' * * Export queue: "python PseudoChannel.py -e" @@ -1166,7 +763,6 @@ if __name__ == '__main__': parser.add_argument('-e', '--export_queue', action='store_true', help='Exports the current queue for episodes.') - ''' * * Import queue: "python PseudoChannel.py -i" @@ -1175,55 +771,30 @@ if __name__ == '__main__': parser.add_argument('-i', '--import_queue', action='store_true', help='Imports the current queue for episodes.') - globals().update(vars(parser.parse_args())) - args = parser.parse_args() - - #print(args) - if args.update: - pseudo_channel.update_db() - if args.xml: - pseudo_channel.update_schedule() - if args.google_calendar: - pseudo_channel.update_schedule_from_google_calendar() - if args.generate_schedule: - pseudo_channel.generate_daily_schedule() - if args.show_clients: - pseudo_channel.show_clients() - if args.show_schedule: - pseudo_channel.show_schedule() - if args.make_html: - pseudo_channel.make_xml_schedule() - if args.export_queue: - pseudo_channel.export_queue() - if args.import_queue: - pseudo_channel.import_queue() - if args.run: - print banner print "+++++ To run this in the background:" print "+++++", "screen -d -m bash -c 'python PseudoChannel.py -r; exec sh'" - """Every minute on the minute check the DB startTimes of all media to determine whether or not to play. Also, check the now_time to see if it's midnight (or 23.59), if so then generate a new daily_schedule @@ -1232,215 +803,124 @@ if __name__ == '__main__': """Every rotate log""" dayToRotateLog = pseudo_channel.ROTATE_LOG.lower() schedule.every().friday.at("00:00").do(pseudo_channel.rotate_log) - logging.info("+++++ Running PseudoChannel.py -r") - def trigger_what_should_be_playing_now(): def nearest(items, pivot): return min(items, key=lambda x: abs(x - pivot)) daily_schedule = pseudo_channel.db.get_daily_schedule() - dates_list = [datetime.datetime.strptime(''.join(str(date[8])), "%I:%M:%S %p") for date in daily_schedule] - now = datetime.datetime.now() - now = now.replace(year=1900, month=1, day=1) - closest_media = nearest(dates_list, now) - print closest_media - prevItem = None for item in daily_schedule: - item_time = datetime.datetime.strptime(''.join(str(item[8])), "%I:%M:%S %p") if item_time == closest_media: - #print "Line 1088, Here", item - elapsed_time = closest_media - now - print elapsed_time.total_seconds() - try: - endTime = datetime.datetime.strptime(item[9], '%Y-%m-%d %H:%M:%S.%f') - except ValueError: - endTime = datetime.datetime.strptime(item[9], '%Y-%m-%d %H:%M:%S') - # we need to play the content and add an offest if elapsed_time.total_seconds() < 0 and \ endTime > now: - print str("+++++ Queueing up {} to play right away.".format(item[3])).encode('UTF-8') - offset = int(abs(elapsed_time.total_seconds() * 1000)) - pseudo_channel.controller.play(item, daily_schedule, offset) - break - elif elapsed_time.total_seconds() >= 0: - for itemTwo in daily_schedule: - item_timeTwo = datetime.datetime.strptime(''.join(str(itemTwo[8])), "%I:%M:%S %p") - try: - endTime = datetime.datetime.strptime(itemTwo[9], '%Y-%m-%d %H:%M:%S.%f') - except ValueError: - endTime = datetime.datetime.strptime(itemTwo[9], '%Y-%m-%d %H:%M:%S') - if item_timeTwo == closest_media and prevItem != None and \ endTime > now: - prevItem_time = datetime.datetime.strptime(''.join(str(prevItem[8])), "%I:%M:%S %p") - elapsed_timeTwo = prevItem_time - now - offsetTwo = int(abs(elapsed_timeTwo.total_seconds() * 1000)) - if pseudo_channel.DEBUG: print "+++++ Closest media was the next media " \ "but we were in the middle of something so triggering that instead." - print str("+++++ Queueing up '{}' to play right away.".format(prevItem[3])).encode('UTF-8') - pseudo_channel.controller.play(prevItem, daily_schedule, offsetTwo) - break - prevItem = itemTwo - - def job_that_executes_once(item, schedulelist): print str("##### Readying media: '{}'".format(item[3])).encode('UTF-8') - next_start_time = datetime.datetime.strptime(item[8], "%I:%M:%S %p") - now = datetime.datetime.now() - now = now.replace(year=1900, month=1, day=1) - time_diff = next_start_time - now if time_diff.total_seconds() > 0: - - print "+++++ Sleeping for {} seconds before playing: '{}'".format(time_diff.total_seconds(), item[3]) - sleep(int(time_diff.total_seconds())) - if pseudo_channel.DEBUG: print "+++++ Woke up!" - pseudo_channel.controller.play(item, schedulelist) - else: - pseudo_channel.controller.play(item, schedulelist) - return schedule.CancelJob - def generate_memory_schedule(schedulelist, isforupdate=False): print "##### Generating Memory Schedule." - now = datetime.datetime.now() - now = now.replace(year=1900, month=1, day=1) - pseudo_cache = pseudo_channel.get_daily_schedule_cache_as_json() - prev_end_time_to_watch_for = None - if pseudo_channel.USE_OVERRIDE_CACHE and isforupdate: - for cached_item in pseudo_cache: - prev_start_time = datetime.datetime.strptime(cached_item[8], "%I:%M:%S %p") - try: - prev_end_time = datetime.datetime.strptime(cached_item[9], '%Y-%m-%d %H:%M:%S.%f') - except ValueError: - prev_end_time = datetime.datetime.strptime(cached_item[9], '%Y-%m-%d %H:%M:%S') - """If update time is in between the prev media start / stop then there is overlap""" if prev_start_time < now and prev_end_time > now: - try: - print "+++++ It looks like there is update schedule overlap", cached_item[3] - except: - pass - prev_end_time_to_watch_for = prev_end_time - for item in schedulelist: - trans_time = datetime.datetime.strptime(item[8], "%I:%M:%S %p").strftime("%H:%M") - new_start_time = datetime.datetime.strptime(item[8], "%I:%M:%S %p") - if prev_end_time_to_watch_for == None: - schedule.every().day.at(trans_time).do(job_that_executes_once, item, schedulelist).tag('daily-tasks') - else: - """If prev end time is more then the start time of this media, skip it""" if prev_end_time_to_watch_for > new_start_time: - try: - print "Skipping scheduling item do to cached overlap.", item[3] - except: - pass - continue - else: - schedule.every().day.at(trans_time).do(job_that_executes_once, item, schedulelist).tag('daily-tasks') - - print "+++++ Done." - generate_memory_schedule(pseudo_channel.db.get_daily_schedule()) - daily_update_time = datetime.datetime.strptime( pseudo_channel.translate_time( pseudo_channel.DAILY_UPDATE_TIME ), pseudo_channel.APP_TIME_FORMAT_STR ).strftime('%H:%M') - if pseudo_channel.USING_GOOGLE_CALENDAR: - schedule.every().day.at(daily_update_time).do( pseudo_channel.update_schedule_from_google_calendar ).tag('daily-update-gc') - else: - pass def go_generate_daily_sched(): @@ -1455,26 +935,15 @@ if __name__ == '__main__': schedule.every().day.at(daily_update_time).do( go_generate_daily_sched ).tag('daily-update') - sleep_before_triggering_play_now = 1 try: - while True: - schedule.run_pending() - sleep(1) - if sleep_before_triggering_play_now: - logging.info("+++++ Successfully started PseudoChannel.py") - trigger_what_should_be_playing_now() - sleep_before_triggering_play_now = 0 - except KeyboardInterrupt: - - print(' Manual break by user') - \ No newline at end of file + print(' Manual break by user') \ No newline at end of file diff --git a/pseudo_config.py b/pseudo_config.py index 797d8ce..db743c1 100644 --- a/pseudo_config.py +++ b/pseudo_config.py @@ -20,7 +20,9 @@ "Movies" : ["Films"], - 6) *Skip this feature for now* For Google Calendar integration add your "gkey" to the "plex_token.py" file + 6) *Skip this feature for now* + + For Google Calendar integration add your "gkey" to the "plex_token.py" file ...(https://docs.simplecalendar.io/find-google-calendar-id/): gkey = "the key" @@ -36,7 +38,7 @@ * List of plex clients to use (add multiple clients to control multiple TV's) * ''' -plexClients = ['RasPlex'] +plexClients = ['RasPlex2'] plexLibraries = { "TV Shows" : ["TV Shows"], @@ -68,18 +70,22 @@ useDailyOverlapCache = True dailyUpdateTime = "12:00 AM" -"""Debug mode will give you more output in your terminal to help problem solve issues.""" -debug_mode = True - """---""" useGoogleCalendar = False """When to delete / remake the pseudo-channel.log - right at midnight, (i.e. 'friday') """ rotateLog = "friday" +"""Debug mode will give you more output in your terminal to help problem solve issues.""" +debug_mode = True + + + + + """ -##### Do not edit below this line. +##### Do not edit below this line--------------------------------------------------------------- Below is logic to grab your Plex 'token' & Plex 'baseurl'. If you are following along and have created a 'plex_token.py' file as instructed, you do not need to edit below this line. diff --git a/pseudo_schedule.xml b/pseudo_schedule.xml index bac2e98..3090b8c 100644 --- a/pseudo_schedule.xml +++ b/pseudo_schedule.xml @@ -24,7 +24,7 @@ by adding a '' block within the part of the week you want it to be below I have "Looney Tunes" scheduled to play "everyday" starting at "6:00 AM", whereas I have "Garfield & Friends" playing only on "weekday" mornings scheduled for after Looney Tunes starting at "8:00 AM". Also notice that "Garfield & Friends" below is actually written as, "Garfield & Friends". This is especially important -to those new to editing XML. In XML, "UTF-8", you are forbidden from using certain characters like the "&" +to those new to editing XML. In XML, "UTF-8", you are forbidden from using certain characters like the and character. It is important to convert your titles to XML friendly text (this is also important for non-english characters). You can find all of this information by googling "xml ascii character conversion". @@ -36,7 +36,7 @@ to either "series" or "movie". The attribute "strict-time" can either be "true" whether or not the particular "