diff --git a/.gitignore b/.gitignore index 8eaab71..219a946 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ pseudo-channel.db env/ *.log *.pid +*.json +.cache/ +.prevplaying diff --git a/PseudoChannel.py b/PseudoChannel.py index 6496977..adb4880 100644 --- a/PseudoChannel.py +++ b/PseudoChannel.py @@ -1,6 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import sys +import signal +import datetime +from datetime import time +from time import mktime as mktime +import logging +import calendar +import itertools +import argparse +import textwrap +import os, sys +from xml.dom import minidom +import xml.etree.ElementTree as ET +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 @@ -10,25 +30,6 @@ 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 -import logging -import calendar -import itertools -import argparse -import textwrap -import os, sys -from xml.dom import minidom -import xml.etree.ElementTree as ET - -import schedule - -from time import sleep - import pseudo_config as config reload(sys) @@ -39,28 +40,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, @@ -69,17 +63,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. """ @@ -109,27 +97,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), @@ -137,24 +114,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), @@ -162,16 +129,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, @@ -181,9 +143,7 @@ class PseudoChannel(): media.title, episode.key ) - else: - self.db.add_episodes_to_db( 4, episode.title, @@ -193,17 +153,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, @@ -212,120 +166,14 @@ class PseudoChannel(): bar_length = 40 ) - 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", - "wednesdays", - "thursdays", - "fridays", - "saturdays", - "sundays", - "weekdays", - "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"], - "2" : ["wednesdays", "weekdays", "everyday"], - "3" : ["thursdays", "weekdays", "everyday"], - "4" : ["fridays", "weekdays", "everyday"], - "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 - 0, # duration - natural_start_time, # startTime - natural_end_time, # endTime - day_of_week, # dayOfWeek - start_time_unix, # startTimeUnix - section, # section - strict_time, # strictTime - time_shift, # timeShift - overlap_max, # overlapMax - ) - def update_schedule(self): """Changing dir to the schedules dir.""" 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", @@ -338,50 +186,42 @@ 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 '' - - time_shift = time.attrib['time-shift'] if 'time-shift' 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 '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) + + now = datetime.datetime.now() + + start_time_unix = mktime( + datetime.datetime.strptime( + self.translate_time(natural_start_time), + self.APP_TIME_FORMAT_STR).replace(day=now.day, month=now.month, year=now.year).timetuple() + ) - 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 @@ -394,6 +234,7 @@ class PseudoChannel(): strict_time, # strictTime time_shift, # timeShift overlap_max, # overlapMax + xtra, # xtra kargs (i.e. 'director=director') ) def drop_db(self): @@ -408,53 +249,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): @@ -465,12 +273,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) @@ -484,26 +289,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 @@ -516,32 +311,15 @@ 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) - + #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. @@ -549,55 +327,40 @@ 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) - + 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"], @@ -607,33 +370,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 @@ -649,27 +398,32 @@ 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": - - 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)) + # 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) + 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 @@ -682,21 +436,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 @@ -709,21 +454,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 @@ -736,213 +472,178 @@ 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 - - self.db.remove_all_daily_scheduled_items() - 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-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 " + 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-queue.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]) + 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): + + try: + os.remove('../pseudo-channel.log') + except OSError: + pass + try: + os.remove('./pseudo-channel.log') + except OSError: + pass + + def signal_term_handler(self, signal, frame): + + logging.info('+++++ got SIGTERM') + self.controller.stop_media() + self.exit_app() + sys.exit(0) + def exit_app(self): - print " - Exiting Pseudo TV & cleaning up." - + logging.info(' - 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('''\ # __ __ # |__)_ _ _| _ / |_ _ _ _ _| _ @@ -951,17 +652,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.') @@ -976,14 +674,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" @@ -992,7 +688,6 @@ if __name__ == '__main__': parser.add_argument('-c', '--show_clients', action='store_true', help='Show Plex clients.') - ''' * * Show schedule (daily): "python PseudoChannel.py -s" @@ -1001,7 +696,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" @@ -1010,246 +704,198 @@ if __name__ == '__main__': parser.add_argument('-m', '--make_html', action='store_true', help='Makes the XML / HTML schedule based on the daily_schedule table.') - ''' * - * 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())) - 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.inject_commercials: - - pseudo_channel.run_commercial_injection() - + 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 """ - + """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): + 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") - - 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." - 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(): + """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()) + generate_memory_schedule(pseudo_channel.db.get_daily_schedule(), True) schedule.every().day.at(daily_update_time).do( go_generate_daily_sched ).tag('daily-update') - sleep_before_triggering_play_now = 1 + '''When the process is killed, stop any currently playing media & cleanup''' + signal.signal(signal.SIGTERM, pseudo_channel.signal_term_handler) + 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/README.md b/README.md index cbbe0c3..bc9cba2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ -# PseudoChannel.py - a Plex Controller for Home-Brewed TV Channels +# PseudoChannel.py - Your Home-Brewed TV Channels + +Joined by the author of [Fake TV](https://medium.com/@Fake.TV), this project aims at tackling one issue: creating a fake tv channel experience with your own media library (movies, tv shows, commercials, etc.). The idea is super simple... when you turn on your TV, rather than hopping straight to Netflix, you can choose to watch your own channel of curated media like a real channel, with randomized movie time blocks, weekend morning cartoons, 90's commercials to fill up gaps and more. We aim to add a ton of neat features but the basic idea is to have something that feels like a real TV channel. That being said it isn't supposed to "pause" nor are you supposed to intervene too much. Just like a real channel you are presented with a channel that you define once and let it go as it advances in series episodes, playing random movies where specified (defined by various parameters like genre, "Kevin Bacon", etc.). Think: weekday movie nights @ 8:00 PM. Or perhaps you want to further specify your weekly Wednesday evening movie be a movie in your Plex library that stars "Will Smith". PseudoChannel is built to interface with the Plex media server. So if you want to have your very own PseudoChannel, you first need to set up your home server using [Plex](https://www.plex.tv/). After that you can come back here to learn how to setup everything else. Although this app runs using Python and the command line, we aim to make all of it as easy as possible to understand for those who are intimidated by this sort of technology. If you have a question, are troubleshooting or have feature ideas, just leave an 'issue' here in this repository. -Joined by the author of [Fake TV](https://medium.com/@Fake.TV), this project aims at tackling one issue: creating a fake tv channel experience with your own media library (movies, tv shows, commercials, etc.). The idea is super simple... when you turn on your TV, rather than hopping straight to Netflix, you can choose to watch your own channel of curated media like a real channel, with randomized movie time blocks, weekend morning cartoons, 90's commercials to fill up gaps and more. We aim to add a ton of neat features but the basic idea is to have something that feels like a real TV channel. That being said it isn't supposed to "pause" nor are you supposed to intervene too much. Just like a real channel you are presented with a channel that you define once and let it go as it advances in series episodes, playing random movies where specified (defined by various parameters like genre, "Kevin Bacon", etc.). Think: weekday movie nights @ 8:00 PM. Or perhaps you want to further specify your weekly Wednesday evening movie be a movie in your Plex library that stars "Will Smith". Currently the latter feature among many others are being developed but this is the goal. PseudoChannel is built to interface with the Plex media server. So if you want to have your very own PseudoChannel, you first need to set up your home server using [Plex](https://www.plex.tv/). After that you can come back here to learn how to setup everything else. Please note that we just started this project so everything is evolving rapidly. Check back often. We aim to have a decent working "alpha" version within a week or so. This readme / the how-to guide will all be very user friendly. Although this app runs using Python and the command line, we aim to make all of it as easy as possible to understand for those who are intimidated by this sort of technology. ![Generated HTML schedule](http://i.imgur.com/uTGRYIp.png) -If interested in this project, check back very soon when the alpha is up. It's close and a tiny bit more user friendly. :) - -## How to Use (in the case someone stumbles across this and wants to try it before its polished): +## How to Use: - The instructions below are all for configuring the **"controller"** device (i.e. a laptop or raspberry pi running linux). This is the device this app runs on to control the Plex client. The **"client"** device should be a Raspberry Pi running Rasplex hooked up to your TV via HDMI - although I'm sure other devices work great too (never tried). @@ -25,21 +24,22 @@ baseurl = 'http://192.168.1.28:32400' ``` *This file is important as it tells PseudoChannel.py how/where to connect to your Plex server. It should sit just outside of this /pseudo-channel/ directory.* -3. Edit the `pseudo_config.py` / `the pseudo_schedule.xml` to your liking. You can specify your plex media library names within the `pseudo_config.py` file... the default assumes that you have these libraries in your Plex server named like so: "TV Shows", "Movies" & "Commercials". If you do not intend on using commercials just set the `useCommercialInjection` flag to `False`. There are a few other experimental options like using Google Calendar rather than an XML. It is an arduous process to initially set up and I've found the XML method to be the easiest method for organizing your schedule - so stick with that for now. +3. Edit the `pseudo_config.py` / `the pseudo_schedule.xml` to your liking. You can specify your plex media library names within the `pseudo_config.py` file... If you do not intend on using commercials just set the `useCommercialInjection` flag to `False`. There are a few other experimental options like using Google Calendar rather than an XML. It is an arduous process to initially set up and I've found the XML method to be the easiest method for organizing your schedule - so stick with that for now. Finally, setup your schedule in the xml file. There are some detailed instructions commented at the top of that file. 4. Run the `PseudoChannel.py` file with the following flags: ```bash -% python PseudoChannel.py -u -xml -g -r +% python PseudoChannel.py -u -xml -g -m -r ``` -*You can also run `-h` to view all the options. Keep in mind not all options are operational & some are experimental. Stick with the ones above and use `-c` to find the name(s) of your Plex client(s).* +*You can also run `-h` to view all the options. Use `-c` to find the name(s) of your Plex client(s) to add to the config.* - The `-u` flag will prepare & update (& create if not exists) the local `pseudo-channel.db`, you only need to run this once in the beginning or later when you have added new media to your Plex libraries. - The `-xml` flag will update the newly created local db with your schedule from the xml file - you should run this everytime you make changes to the xml. - The `-g` file will generate the daily schedule (for today) based on the xml. This is useful for the first run or testing (or manually advancing the daily queue forward). Running this flag say, 15 times will advance the play queue forward 15 days. It is automatically run every night at midnight to generate the daily schedule. -- Finally, the `-r` flag will run the app, checking the time / triggering the playstate of any media that is scheduled. It will also update the daily schedule when the clock hits 11.59 (or whatever time you've configured in the config file). The xml schedule is a bit tempermental at the moment so if you see errors, check your entries there first. Make sure all of your movie names / TV Series names are correct. +- The `-m` flag makes both the .html/.xml files and starts a simple html web server in the `./schedules` directory. +- Finally, the `-r` flag will run the app, checking the time / triggering the playstate of any media that is scheduled. It will also update the daily schedule when the clock hits 11.59 (or whatever time you've configured in the config file). If you see errors, check your entries in the xml first. Check your times, check for overlaps & make sure your are using ascii characters to replace foreign characters like umlauts and '&' characters, etc. Make sure all of your movie names / TV Series names are correct. -You can run `% python PseudoChannel.py` with the following options. The order is important (i.e. `% python PseudoChannel.py -u -xml -g -m -r`): +You can run `% python PseudoChannel.py` with the following options. The order is important depending on what you are doing (i.e. `% python PseudoChannel.py -u -xml -g -m -r`): | Flag | Description | | ------------------------|--------------| @@ -50,16 +50,22 @@ You can run `% python PseudoChannel.py` with the following options. The order is | -c, --show_clients | Show connected Plex clients. | | -s, --show_schedule | Output the generated "Daily Schedule" to your terminal. | | -m, --make_html | Manually generate both html / xml docs based on the "Daily Schedule". | +| -e, --export | Export the current queue of your "TV Shows" episodes. Useful when redoing your local DB. | +| -i, --import | Import the previously exported queue of your "TV Shows" episodes. | + +## `startstop.sh` - Alternative Way of Running the Application: + +Within the app directory, there is a file named, `startstop.sh`. This bash script is a convenient way to start/stop the application. When run, it will start the application and save the "pid" of the process to the application directory. When run again, it will check to see if that process is running, if so it will stop it. All you have to do is run: + +```bash +% ./startstop.sh +``` + +When you start the application with this bash script, you can close your terminal as it will keep running in the background. Later, when you come back and want to stop it... you can just execute that file once more and it will stop the running process. Please note: It's good to test the application and your configurations using the manual process above before running this bash script. Although there is a `pseudo-channel.log` that is created within the application directory, it is easier to just view the output in your terminal window - something that won't happen when using the bash script. ## Futher Info: -Features are being added to the xml but as of now there are a few. Within the XML `