mirror of
https://github.com/FakeTV/pseudo-channel.git
synced 2025-12-18 01:03:31 +00:00
701 lines
16 KiB
Python
701 lines
16 KiB
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 pseudo_config import *
|
|
|
|
from plexapi.server import PlexServer
|
|
|
|
import sys
|
|
import datetime
|
|
from datetime import time
|
|
import calendar
|
|
import itertools
|
|
import argparse
|
|
from xml.dom import minidom
|
|
import xml.etree.ElementTree as ET
|
|
|
|
from time import sleep
|
|
|
|
class PseudoChannel():
|
|
|
|
PLEX = PlexServer(baseurl, token)
|
|
|
|
MEDIA = []
|
|
|
|
def __init__(self):
|
|
|
|
self.db = PseudoChannelDatabase("pseudo-channel.db")
|
|
|
|
self.controller = PseudoDailyScheduleController(baseurl, token, plexClients)
|
|
|
|
"""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.
|
|
"""
|
|
|
|
# Print iterations progress
|
|
def print_progress(self, iteration, total, prefix='', suffix='', decimals=1, bar_length=100):
|
|
"""
|
|
Call in a loop to create terminal progress bar
|
|
@params:
|
|
iteration - Required : current iteration (Int)
|
|
total - Required : total iterations (Int)
|
|
prefix - Optional : prefix string (Str)
|
|
suffix - Optional : suffix string (Str)
|
|
decimals - Optional : positive number of decimals in percent complete (Int)
|
|
bar_length - Optional : character length of bar (Int)
|
|
"""
|
|
str_format = "{0:." + str(decimals) + "f}"
|
|
percents = str_format.format(100 * (iteration / float(total)))
|
|
filled_length = int(round(bar_length * iteration / float(total)))
|
|
bar = '█' * filled_length + '-' * (bar_length - filled_length)
|
|
|
|
sys.stdout.write('\r%s |%s| %s%s %s' % (prefix, bar, percents, '%', suffix)),
|
|
|
|
if iteration == total:
|
|
sys.stdout.write('\n')
|
|
sys.stdout.flush()
|
|
|
|
def update_db(self):
|
|
|
|
print("#### Updating Local Database")
|
|
|
|
self.db.create_tables()
|
|
|
|
sections = self.PLEX.library.sections()
|
|
|
|
for section in sections:
|
|
|
|
if section.title == "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)
|
|
|
|
self.print_progress(
|
|
i + 1,
|
|
len(sectionMedia),
|
|
prefix = 'Progress '+section.title+": ",
|
|
suffix = 'Complete',
|
|
bar_length = 40
|
|
)
|
|
|
|
|
|
elif section.title == "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 = baseurl+backgroundImagePath.art+"?X-Plex-Token="+token
|
|
|
|
self.db.add_shows_to_db(2, media.title, media.duration, '', backgroundImgURL)
|
|
|
|
self.print_progress(
|
|
i + 1,
|
|
len(sectionMedia),
|
|
prefix = 'Progress '+section.title+": ",
|
|
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,
|
|
duration,
|
|
episode.index,
|
|
episode.parentIndex,
|
|
media.title
|
|
)
|
|
|
|
else:
|
|
|
|
self.db.add_episodes_to_db(
|
|
4,
|
|
episode.title,
|
|
0,
|
|
episode.index,
|
|
episode.parentIndex,
|
|
media.title
|
|
)
|
|
|
|
elif section.title == "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)
|
|
|
|
self.print_progress(
|
|
i + 1,
|
|
media_length,
|
|
prefix = 'Progress '+section.title+":",
|
|
suffix = 'Complete',
|
|
bar_length = 40
|
|
)
|
|
|
|
def interpret_time_input(self, input):
|
|
|
|
try:
|
|
|
|
return datetime.datetime.strptime(time.text, '%I:%M %p')
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
return datetime.datetime.strptime(time.text, '%H:%M')
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
def update_schedule(self):
|
|
|
|
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"],
|
|
"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']
|
|
|
|
natural_start_time = time.text
|
|
|
|
natural_end_time = 0
|
|
|
|
section = key
|
|
|
|
day_of_week = child.tag
|
|
|
|
strict_time = time.attrib['strict-time']
|
|
|
|
time_shift = time.attrib['time-shift']
|
|
|
|
overlap_max = time.attrib['overlap-max']
|
|
|
|
#start_time_unix = datetime.datetime.strptime(time.text, '%I:%M %p')
|
|
|
|
start_time_unix = self.interpret_time_input(time.text)
|
|
|
|
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 drop_db(self):
|
|
|
|
self.db.drop_db()
|
|
|
|
def drop_schedule(self):
|
|
|
|
self.db.drop_schedule()
|
|
|
|
def remove_all_scheduled_items():
|
|
|
|
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 time_diff(self, time1,time2):
|
|
'''
|
|
*
|
|
* Getting the offest by comparing both times from the unix epoch time and getting the difference.
|
|
*
|
|
'''
|
|
timeA = datetime.datetime.strptime(time1, "%I:%M %p")
|
|
timeB = datetime.datetime.strptime(time2, "%I:%M %p")
|
|
|
|
timeAEpoch = calendar.timegm(timeA.timetuple())
|
|
timeBEpoch = calendar.timegm(timeB.timetuple())
|
|
|
|
tdelta = abs(timeAEpoch) - abs(timeBEpoch)
|
|
|
|
return int(tdelta/60)
|
|
|
|
|
|
'''
|
|
*
|
|
* Passing in the endtime from the prev episode and desired start time of this episode, calculate the best start time
|
|
|
|
* Returns time - for new start time
|
|
*
|
|
'''
|
|
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 %p')
|
|
|
|
timeB = datetime.datetime.strptime(intendedStartTime, '%I:%M %p').strftime('%-I:%M %p')
|
|
|
|
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
|
|
*
|
|
'''
|
|
if timeDiff < 0:
|
|
'''
|
|
*
|
|
* If there is an overlap, then the overlapGap var in config will determine the next increment. If it is set to "15", then the show will will bump up to the next 15 minute interval past the hour.
|
|
*
|
|
'''
|
|
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. If it is set to "15", then the show will will bump up to the next 15 minute interval past the hour.
|
|
*
|
|
'''
|
|
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, '%I:%M %p').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 %p')
|
|
|
|
def get_end_time_from_duration(self, startTime, duration):
|
|
|
|
time = datetime.datetime.strptime(startTime, '%I:%M %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")
|
|
|
|
schedule = self.db.get_schedule()
|
|
|
|
schedule_advance_watcher = 0
|
|
|
|
for entry in schedule:
|
|
|
|
schedule_advance_watcher += 1
|
|
|
|
section = entry[9]
|
|
|
|
if section == "TV Shows":
|
|
|
|
if entry[3] == "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
|
|
entry[5], # natural_start_time
|
|
self.get_end_time_from_duration(entry[5], next_episode[4]), # natural_end_time
|
|
next_episode[4], # duration
|
|
entry[7], # day_of_week
|
|
entry[10], # is_strict_time
|
|
entry[11], # time_shift
|
|
entry[12], # overlap_max
|
|
entry[3], # show_series_title
|
|
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 entry[3] == "random":
|
|
|
|
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
|
|
entry[5], # natural_start_time
|
|
self.get_end_time_from_duration(entry[5], the_movie[4]), # natural_end_time
|
|
the_movie[4], # duration
|
|
entry[7], # day_of_week
|
|
entry[10], # is_strict_time
|
|
entry[11], # time_shift
|
|
entry[12] # overlap_max
|
|
)
|
|
|
|
#print(movie.natural_end_time)
|
|
|
|
self.MEDIA.append(movie)
|
|
|
|
else:
|
|
|
|
print("Cannot find Movie, {} in the local db".format(entry[3]))
|
|
|
|
elif section == "Music":
|
|
|
|
the_music = self.db.get_music(entry[3])
|
|
|
|
if the_music != None:
|
|
|
|
music = Music(
|
|
section, # section_type
|
|
the_music[3], # title
|
|
entry[5], # natural_start_time
|
|
self.get_end_time_from_duration(entry[5], the_music[4]), # natural_end_time
|
|
the_music[4], # duration
|
|
entry[7], # day_of_week
|
|
entry[10], # is_strict_time
|
|
entry[11], # time_shift
|
|
entry[12] # overlap_max
|
|
)
|
|
|
|
#print(music.natural_end_time)
|
|
|
|
self.MEDIA.append(music)
|
|
|
|
else:
|
|
|
|
print("Cannot find Music, {} in the local db".format(entry[3]))
|
|
|
|
elif section == "Video":
|
|
|
|
the_video = self.db.get_video(entry[3])
|
|
|
|
if the_music != None:
|
|
|
|
video = Video(
|
|
section, # section_type
|
|
the_video[3], # title
|
|
entry[5], # natural_start_time
|
|
self.get_end_time_from_duration(entry[5], the_video[4]), # natural_end_time
|
|
the_video[4], # duration
|
|
entry[7], # day_of_week
|
|
entry[10], # is_strict_time
|
|
entry[11], # time_shift
|
|
entry[12] # overlap_max
|
|
)
|
|
|
|
#print(music.natural_end_time)
|
|
|
|
self.MEDIA.append(video)
|
|
|
|
else:
|
|
|
|
print("Cannot find Video, {} in the local db".format(entry[3]))
|
|
|
|
else:
|
|
|
|
pass
|
|
|
|
"""If we reached the end of the schedule we are ready to kick off the daily_schedule
|
|
|
|
"""
|
|
if schedule_advance_watcher >= len(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, '%I:%M %p')
|
|
|
|
natural_end_time = entry.natural_end_time
|
|
|
|
if entry.is_strict_time.lower() == "true":
|
|
|
|
print "++++ Strict-time: {}".format(entry.title)
|
|
|
|
entry.end_time = self.get_end_time_from_duration(entry.start_time, entry.duration)
|
|
|
|
self.db.add_media_to_daily_schedule(entry)
|
|
|
|
previous_episode = entry
|
|
|
|
else:
|
|
|
|
print "++++ NOT strict-time: {}".format(entry.title)
|
|
|
|
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, '%I:%M %p').strftime('%-I:%M %p')
|
|
|
|
entry.end_time = self.get_end_time_from_duration(entry.start_time, entry.duration)
|
|
|
|
self.db.add_media_to_daily_schedule(entry)
|
|
|
|
previous_episode = entry
|
|
|
|
else:
|
|
|
|
self.db.add_media_to_daily_schedule(entry)
|
|
|
|
previous_episode = entry
|
|
|
|
if __name__ == '__main__':
|
|
|
|
pseudo_channel = PseudoChannel()
|
|
|
|
#pseudo_channel.db.create_tables()
|
|
|
|
#pseudo_channel.update_db()
|
|
|
|
#pseudo_channel.update_schedule()
|
|
|
|
#pseudo_channel.generate_daily_schedule()
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Pseudo Channel for Plex. Update pseduo_config.py & pseudo_schedule.xml before this step.",
|
|
usage="PseudoChannel.py [-u] update local db with plex db [-xml] update db with xml schedule data [-g] generate daily schedule [-r] run the app"
|
|
)
|
|
|
|
parser.add_argument('-u', action='store_true')
|
|
parser.add_argument('-xml', action='store_true')
|
|
parser.add_argument('-g', action='store_true')
|
|
parser.add_argument('-r', action='store_true')
|
|
|
|
'''
|
|
*
|
|
* Show connected clients: "python PseudoChannel.py -u -xml -g -r"
|
|
*
|
|
'''
|
|
parser.add_argument('-sc', action='store_true')
|
|
|
|
globals().update(vars(parser.parse_args()))
|
|
|
|
args = parser.parse_args()
|
|
|
|
print(args)
|
|
|
|
if args.u:
|
|
|
|
pseudo_channel.update_db()
|
|
|
|
if args.xml:
|
|
|
|
pseudo_channel.update_schedule()
|
|
|
|
if args.g:
|
|
|
|
pseudo_channel.generate_daily_schedule()
|
|
|
|
if args.r:
|
|
|
|
try:
|
|
|
|
print "++++ Running TV Controller"
|
|
|
|
"""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
|
|
|
|
"""
|
|
while True:
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
now_time = now.time()
|
|
|
|
if now_time == time(23,59):
|
|
|
|
pseudo_channel.generate_daily_schedule()
|
|
|
|
pseudo_channel.controller.tv_controller(pseudo_channel.db.get_daily_schedule())
|
|
|
|
t = datetime.datetime.utcnow()
|
|
|
|
sleeptime = 60 - (t.second + t.microsecond/1000000.0)
|
|
|
|
sleep(sleeptime)
|
|
|
|
except KeyboardInterrupt, e:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|