# HDHR Viewer V2 import time import string from datetime import datetime import urllib import urllib2 import os import re from lxml import etree from DumbTools import DumbPrefs DEBUGMODE = True TITLE = 'HDHR Viewer 2 (1.1.5)' PREFIX = '/video/hdhrv2' VERSION = '1.1.5' #GRAPHICS ART = 'art-default.jpg' ICON = 'icon-default.png' ICON_SUBBED_LIST = 'icon-subscribed.png' ICON_FAV_LIST = 'icon-fav.png' ICON_DEFAULT_CHANNEL = 'icon-subscribed.png' ICON_SETTINGS = 'icon-settings.png' ICON_ERROR = 'icon-error.png' ICON_UNKNOWN = 'icon-unknown.png' #PREFS PREFS_HDHR_IP = 'hdhomerun_ip' PREFS_HDHR_TUNER = 'hdhomerun_tuner' PREFS_TRANSCODE = 'transcode' PREFS_XMLTV_MODE = 'xmltv_mode' PREFS_XMLTV_FILE = 'xmltv_file' PREFS_LOGO_MATCH = 'channellogo' PREFS_XMLTV_MATCH = 'xmltv_match' PREFS_XMLTV_APIURL = 'xmltv_api_url' PREFS_VCODEC = 'videocodec' PREFS_ACODEC = 'audiocodec' PREFS_ICONDIR = 'icon_directory' PREFS_AUTODISCOVER = 'autodiscover' #XMLTV Modes XMLTV_MODE_RESTAPI = 'restapi' XMLTV_MODE_HDHOMERUN = 'hdhomerun' XMLTV_MODE_FILE = 'file' #DATE/TIME FORMATS TIME_FORMAT = '%H:%M' DATE_FORMAT = '%Y%m%d' #HDHOMERUN GUIDE URL URL_HDHR_DISCOVER = 'http://{ip}/discover.json' URL_HDHR_DISCOVER_DEVICES = 'http://my.hdhomerun.com/discover' URL_HDHR_GUIDE = 'http://my.hdhomerun.com/api/guide.php?DeviceAuth={deviceAuth}' URL_HDHR_LINEUP = 'http://{ip}/lineup.json' URL_HDHR_TUNER_STATUS = 'http://{ip}/tuners.html' #URL_HDHR_STREAM = 'http://{ip}:5004/{tuner}/v{guideNumber}' CACHETIME_HDHR_GUIDE = 3600 # (s) Default: 3600 = 1 hour #DEBUG DEBUG_URL_HDHR_DISCOVER_DEVICES = 'http://192.168.1.11/discover' DEBUG_URL_HDHR_GUIDE = 'http://192.168.1.11/api/guide.php?DeviceAuth={deviceAuth}' #CONSTANTS/PARAMETERS TIMEOUT = 5 # XML Timeout (s); Default = 5 TIMEOUT_LAN = 1 # LAN Timeout (s); Default = 1 CACHETIME = 5 # Cache Time (s); Default = 5 MAX_FAVORITES = 10 # Max number of favorites supported; Default = 10 VIDEO_DURATION = 14400000 # Duration for Transcoder (ms); Default = 14400000 (4 hours) MAX_SIZE = 90971520 # [Bytes] 20971520 = 20MB; Default: 90971520 (100MB) AUDIO_CHANNELS = 6 # Audio Channels = 2 - stereo; 6 - 5.1 MEDIA_CONTAINER = 'mpegts' # Default media container = 'mpegts' VIDEO_CODEC = 'mpeg2video' # Default video codec = 'mpeg2video' AUDIO_CODEC = 'ac3' # Default audio codec = 'ac3' ################################################################################################### # Entry point - set up default values for all containers ################################################################################################### def Start(): ObjectContainer.title1 = TITLE ObjectContainer.art = R(ART) DirectoryObject.thumb = R(ICON) DirectoryObject.art = R(ART) HTTP.CacheTime = CACHETIME ################################################################################################### # Main Menu ################################################################################################### @handler(PREFIX, TITLE, art=ART, thumb=ICON) def MainMenu(): getInfo() tuners=[] HDHRV2 = Devices() tuners = HDHRV2.tunerDevices totalTuners = len(tuners) logInfo('Total Tuners: '+xstr(totalTuners)) oc = ObjectContainer() last_known = False # If totally fail to discover... if totalTuners==0: tuners=Dict['tuners'] if tuners!=None and len(tuners)>0: totalTuners = len(tuners) logError('No Tuners Found: Using last know tuners:' + xstr(tuners)) else: logError('No Tuners Found: Unable to load last known tuners') # If tuners exist, show favorites, all-channels, search. if totalTuners>0: Dict['tuners'] = tuners # Add any enabled favorites favoritesList = LoadEnabledFavorites() for favorite in favoritesList: ocTitle = favorite.name+' ('+xstr(favorite.totalChannels)+')' oc.add(DirectoryObject(key=Callback(FavoriteChannelsMenu, favidx=favorite.index), title=ocTitle, thumb=R(ICON_FAV_LIST))) # All Channels - Multi-Tuner support for tuneridx, tuner in enumerate(tuners): ocTitle = tuner['LocalIP']+' ('+xstr(getTunerTotalChannels(tuner))+')' # Append M: to Manually defined tuners. if not tuner['autoDiscover']: ocTitle='M:'+ocTitle oc.add(DirectoryObject(key=Callback(AllChannelsMenu, tuneridx=tuneridx), title=ocTitle, thumb=R(ICON_SUBBED_LIST))) # Search Option/Menu oc.add(InputDirectoryObject(key=Callback(SearchResultsChannelsMenu), title='Search Playing Now', thumb=R(ICON_SUBBED_LIST))) # If No Tuners were found. Show error message. else: logError('No Tuners Found: Check IP or internet connection...') errmsg = 'No Tuners Found.' oc.add(PopupDirectoryObject(key=Callback(errorMessage,message=errmsg), title=errmsg, art=R(ART), thumb=R(ICON_ERROR))) if last_known: errmsg = 'Using last known tuners' oc.add(PopupDirectoryObject(key=Callback(errorMessage,message=errmsg), title=errmsg, art=R(ART), thumb=R(ICON_ERROR))) # Settings / Preference Menu #oc.add(PrefsObject(title='Settings', thumb=R(ICON_SETTINGS))) if Client.Product in DumbPrefs.clients: DumbPrefs(PREFIX, oc,title = L('Settings'),thumb = R(ICON_SETTINGS)) else: oc.add(PrefsObject(title = L('Settings'),thumb = R(ICON_SETTINGS))) # Load Channel Icons Resources_iconpath = Core.storage.join_path(Core.bundle_path,'Contents','Resources') Local_iconpath = Prefs[PREFS_ICONDIR] if dirExists(Local_iconpath): #Only Show Reload Icons if directory properly configured. oc.add(DirectoryObject(key=Callback(LoadChannelIcons,force=True), title='Reload Icons', art=R(ART), thumb=R(ICON_SETTINGS))) LoadChannelIcons(force=False) # Dev/Debug purpose #oc.add(CreateVO(tuneridx=1, url="http://192.168.1.11/TestVideos/v5134.mpeg" ,title="TestTitle", year="2010", tagline="Tag", summary="summary", starRating=3.5, thumb=ICON_FAV_LIST, videoCodec=None, audioCodec=None,transcode="default")) return oc ################################################################################################### # Show all channels for specified tuner ################################################################################################### @route(PREFIX + '/all-channels') def AllChannelsMenu(tuneridx): tuners = Dict['tuners'] tuneridx=int(tuneridx) tuner=tuners[tuneridx] tuner_name=tuner.get('LocalIP','unknown') oc = ObjectContainer(title1=tuner_name) try: allChannels = LoadAllChannels(tuneridx) PopulateProgramInfo(tuneridx, allChannels.list, False) return AddChannelObjectContainer(oc,tuneridx,tuner['LocalIP'], allChannels.list,False) #return AddChannelObjectContainer(oc,tuneridx,'test', allChannels.list) except Exception as inst: logError('AllChannelsMenu('+xstr(tuneridx)+')') logError(strError(inst)) return AddErrorObjectContainer(oc,'AllChannelsMenu('+xstr(tuneridx)+');'+strError(inst)) return oc ################################################################################################### # This function produces a directory for all channels the user is subscribed to # Note, we only show program info for the favorites, because the full channel list can be a bit too # large (well, for folks subscribing to cable) ################################################################################################### @route(PREFIX + '/favorite-channels') def FavoriteChannelsMenu(favidx): allChannels = [] channelList = [] tuner_defined=False tuners = Dict['tuners'] favorite = LoadFavorite(favidx) ocTitle = favorite.name oc = ObjectContainer(title1=ocTitle) try: # If tuner IP is defined in Fav list, and exist in Tuner list for tuneridx, tuner in enumerate(tuners): if tuner['LocalIP']==favorite.tuner: allChannels=LoadAllChannels(tuneridx) tuner_defined=True break # If tuner IP not defined in Fav list, assume 1st tuner. if not tuner_defined: logDebug('Tuner not defined/found in favorite list. Assuming tuner: '+tuners[0]['LocalIP']) # Use first tuner... tuneridx=0 allChannels=LoadAllChannels(tuneridx) # Filter favorite list for channelNumber in favorite.channels: channel = allChannels.map.get(channelNumber) if (channel is not None): channelList.append(channel) # Populate the program info for all of the channels PopulateProgramInfo(tuneridx, channelList, True) return AddChannelObjectContainer(oc,tuneridx,favorite.name,channelList,False) except Exception as inst: logError('FavoriteChannelsMenu('+xstr(favidx)+')'+strError(inst)) return AddErrorObjectContainer(oc,strError(inst)) return oc ################################################################################################### # This function produces a directory for all channels whose programs match the specified query # key words ################################################################################################### @route(PREFIX + '/search-channels') def SearchResultsChannelsMenu(query): oc = ObjectContainer(title1='Search: '+query,no_cache=True) try: tuners = Dict['tuners'] for tuneridx, tuner in enumerate(tuners): if not tuner['autoDiscover']: if isXmlTvModeHDHomeRun(): logInfo('HDHomeRun Search') oc = QueryChannelsHDHomeRun(oc,tuneridx,query) elif isXmlTvModeFile(): logInfo('XMLTV Search') oc = QueryChannelsFile(oc,tuneridx,query) elif isXmlTvModeRestApi(): logInfo('RestAPI Search') oc = QueryChannelsRestAPI(oc,tuneridx,query) elif tuner['autoDiscover']: logInfo('autoDisover Search...') oc = QueryChannelsHDHomeRun(oc,tuneridx,query) # Log Total Results per Tuner logInfo('Total Search Results ('+getDeviceInfo(tuner,'LocalIP')+') results:'+xstr(len(oc))) # Log Total Results (all tuners) logInfo('Total Search Results (all tuners):'+xstr(len(oc))) except Exception as inst: logError('SearchResultsChannelsMenu(\''+query+'\'): '+strError(inst)) return AddErrorObjectContainer(oc,strError(inst)) return oc @route(PREFIX + '/load-channel-icons') def LoadChannelIcons(force=False): oc = ObjectContainer(title1='Load Channel Icons',no_cache=True) try: logDebug('LoadChannelIcons') Resources_iconpath = Core.storage.join_path(Core.bundle_path,'Contents','Resources') Local_iconpath = Prefs[PREFS_ICONDIR] for filename in Core.storage.list_dir(Local_iconpath): src = Core.storage.join_path(Local_iconpath,filename) dest = Core.storage.join_path(Resources_iconpath,filename) if os.path.isfile(src) and (force or not os.path.isfile(dest)): Core.storage.copy(src,dest) return AddErrorObjectContainer(oc,'Done!') except Exception as inst: logError('Reset: '+strError(inst)) return AddErrorObjectContainer(oc,strError(inst)) return oc ################################################################################################### # This function produces a directory for all channels whose programs match the specified query # key words ################################################################################################### def QueryChannelsRestAPI(oc,tuneridx,query): logInfo('Searching RestAPI for:'+query) channels = [] allProgramsMap = {} try: allChannels = LoadAllChannels(tuneridx) xmltvApiUrl = ConstructApiUrl(None,False,query) jsonChannelPrograms = JSON.ObjectFromURL(xmltvApiUrl) if jsonChannelPrograms==None: return {} allProgramsMap = ProgramMap_RestAPI(jsonChannelPrograms) for channel in allChannels.list: try: program = allProgramsMap[channel.number] channel.setProgramInfo(program) channels.append(channel) except KeyError: pass return AddChannelObjectContainer(oc,tuneridx,"Search: " + query,channels,True) except Exception as inst: logError('QueryChannelsRestAPI(tuneridx,query)'+strError(inst)) return BuildErrorObjectContainer(strError(inst)) def QueryChannelsHDHomeRun(oc,tuneridx,query): logInfo('Searching HDHomeRun for:'+query) channels = [] allProgramsMap = {} try: allChannels = LoadAllChannels(tuneridx) tunerGuideURL = getGuideURL(tuneridx) jsonChannelPrograms = JSON.ObjectFromURL(tunerGuideURL,cacheTime=CACHETIME_HDHR_GUIDE) if jsonChannelPrograms==None: return {} allProgramsMap = ProgramMap_HDHomeRun(jsonChannelPrograms,query) for channel in allChannels.list: try: program = allProgramsMap[channel.number] channel.setProgramInfo(program) channels.append(channel) except KeyError: pass return AddChannelObjectContainer(oc,tuneridx,"Search: " + query,channels,True) except Exception as inst: logError('QueryChannelsHDHomeRun(tuneridx,query)'+strError(inst)) return BuildErrorObjectContainer(strError(inst)) def QueryChannelsFile(oc,tuneridx,query): logInfo('Searching XMLTV for:'+query) channels = [] allProgramsMap = {} try: channelList = [] allChannels = LoadAllChannels(tuneridx) channellist=allChannels.list for channel in channellist: if Prefs[PREFS_XMLTV_MATCH] == 'name': channelList.append(channel.name) else: channelList.append(channel.number) allProgramsMap = ProgramSearch_File(channelList,query) for channel in channellist: try: program = allProgramsMap[channel.number] channel.setProgramInfo(program) channels.append(channel) except KeyError: pass return AddChannelObjectContainer(oc,tuneridx,"Search: " + query,channels,True) except Exception as inst: logError('QueryChannelsFile(oc,tuner,query)'+strError(inst)) return BuildErrorObjectContainer(strError(inst)) ################################################################################################### # Return error message ################################################################################################### def BuildErrorObjectContainer(errormsg): oc = ObjectContainer(title2=errormsg) oc.add(DirectoryObject(title=errormsg,tagline=errormsg,summary=errormsg,thumb=R(ICON_ERROR))) return oc def AddErrorObjectContainer(oc,errormsg): oc.add(DirectoryObject(title=errormsg,tagline=errormsg,summary=errormsg,thumb=R(ICON_ERROR))) return oc ################################################################################################### # This function populates the channel with XMLTV program info coming from the xmltv rest service ################################################################################################### def PopulateProgramInfo(tuneridx, channels, partialQuery): allProgramsMap = {} tuners = Dict['tuners'] if Prefs[PREFS_XMLTV_MODE] != 'disable': tuner=tuners[tuneridx] try: # If automatically discovered, force HDHomeRun guide. if tuner['autoDiscover']: xmltvApiUrl = getGuideURL(tuneridx) jsonChannelPrograms = JSON.ObjectFromURL(xmltvApiUrl,cacheTime=CACHETIME_HDHR_GUIDE) allProgramsMap = ProgramMap_HDHomeRun(jsonChannelPrograms) # Manual Tuners, use Settings else: #HDHomeRun if Prefs[PREFS_XMLTV_MODE]==XMLTV_MODE_HDHOMERUN: xmltvApiUrl = getGuideURL(tuneridx) jsonChannelPrograms = JSON.ObjectFromURL(xmltvApiUrl,cacheTime=CACHETIME_HDHR_GUIDE) allProgramsMap = ProgramMap_HDHomeRun(jsonChannelPrograms) #RestAPI if Prefs[PREFS_XMLTV_MODE]==XMLTV_MODE_RESTAPI: xmltvApiUrl = ConstructApiUrl(channels,partialQuery) jsonChannelPrograms = JSON.ObjectFromURL(xmltvApiUrl) allProgramsMap = ProgramMap_RestAPI(jsonChannelPrograms) #XMLTV if Prefs[PREFS_XMLTV_MODE]==XMLTV_MODE_FILE: channelList = [] try: for channel in channels: if Prefs[PREFS_XMLTV_MATCH] == 'name': channelList.append(channel.name) else: channelList.append(channel.number) allProgramsMap = ProgramMap_File(channelList) except Exception as inst: logError('XMLTV Mode Channel List'+strError(inst)) return except Exception as inst: Log.Error(xstr(type(inst)) + ": " + xstr(inst.args) + ": " + xstr(inst)) return # go through all channels and set the program for channel in channels: try: if Prefs[PREFS_XMLTV_MATCH] == 'name': program = allProgramsMap[channel.name] else: program = allProgramsMap[channel.number] channel.setProgramInfo(program) except KeyError: pass return ################################################################################################### # This function parses the given program json, and then builds a map from the channel display # name (all of them) to the Program object ################################################################################################### def ProgramMap_RestAPI(jsonChannelPrograms,query=None): allProgramsMap = {} t = time.time() for jsonChannelProgram in jsonChannelPrograms: # parse the program and the next programs if they exist program = ParseProgramJson(XMLTV_MODE_RESTAPI,jsonChannelProgram['program']) jsonNextPrograms = jsonChannelProgram['nextPrograms'] if jsonNextPrograms is not None: for jsonNextProgram in jsonNextPrograms: program.next.append(ParseProgramJson(XMLTV_MODE_RESTAPI,jsonNextProgram)) # now associate all channel display names with that same program object jsonChannelDisplayNames = jsonChannelProgram['channel']['displayNames'] for displayName in jsonChannelDisplayNames: allProgramsMap[displayName] = program logInfo("Time taken to parse RestAPI JSON: "+str(time.time()-t)) return allProgramsMap def ProgramMap_HDHomeRun(jsonChannelPrograms,query=None): allProgramsMap = {} t = time.time() for jsonChannelProgram in jsonChannelPrograms: program=None guideNumber = jsonChannelProgram['GuideNumber'] nextCount=0 for i, guide in enumerate(jsonChannelProgram.get('Guide','')): guideData = ParseProgramJson(XMLTV_MODE_HDHOMERUN,guide) guideTitle = guideData.title # For Search Func. guideDesc = guideData.desc # For Search Func. # Current Program if(guideData.startTime < t and t < guideData.stopTime): if query is None: program = guideData # For Search Func. elif (query.lower() in guideTitle.lower()) or (query.lower() in guideTitle.lower()): program = guideData # Next Programs if (guideData.startTime > t) and (program is not None) and (query is None) and (nextCount<int(Prefs["xmltv_show_next_programs_count"])) : program.next.append(guideData) nextCount+=1 #If Programs exist if program!=None: # If no program icon, try to get channel icon, else leave it empty... if program.icon=='': program.icon=jsonChannelProgram.get('ImageURL','') # Map all programs, or searched programs. allProgramsMap[guideNumber] = program logInfo('Time taken to parse/search HDHomeRun JSON: '+str(time.time()-t)) return allProgramsMap def ProgramMap_File(channellist): allProgramsMap = {} t = time.time() channels = [] channelIDs = [] channelID = None channelNumber = None c_channelID = None p_channelID = None program=None i=0 for event, elem in etree.iterparse(Prefs[PREFS_XMLTV_FILE],events=("start", "end")): # get channelIDs that are requested. if elem.tag == 'channel' and event=='start': channelID = elem.attrib.get('id') for dispname in elem.findall('display-name'): if dispname.text in channellist: channels.append(dispname.text) channelIDs.append(channelID) elem.clear() # get programs if elem.tag == 'programme' and event=='start' and len(channelIDs)>0: currTime = int(datetime.fromtimestamp(time.time()).strftime('%Y%m%d%H%M%S')) stopTime = int(elem.attrib.get('stop')[:14]) c_channelID = elem.attrib.get('channel') if currTime<stopTime and c_channelID==p_channelID and i<=int(Prefs["xmltv_show_next_programs_count"]) and c_channelID in channelIDs: channelindex = channelIDs.index(c_channelID) channelmap = channels[channelindex] stopTime = time.mktime(datetime.strptime(str(stopTime),'%Y%m%d%H%M%S').timetuple()) startTime=int(elem.attrib.get('start')[:14]) startTime=time.mktime(datetime.strptime(str(startTime),'%Y%m%d%H%M%S').timetuple()) title=xstr(elem.findtext('title')) subTitle=xstr(elem.findtext('sub-title')) desc=xstr(elem.findtext('desc')) date=xstr(elem.findtext('date')) icon_e=elem.find('icon') icon=None if icon_e!=None: icon=xstr(icon_e.attrib.get('src')) starRating=0.0 if i==0: # current listing program = Program(startTime,stopTime,title,date,subTitle,desc,icon,starRating) else: #next listing program.next.append(Program(startTime,stopTime,title,date,subTitle,desc,icon,starRating)) if program!=None: allProgramsMap[channelmap] = program i+=1 elem.clear() elif c_channelID!=p_channelID: i=0 elem.clear() else: elem.clear() p_channelID=c_channelID logInfo("Time taken to parse XMLTV: "+str(time.time()-t)) return allProgramsMap ##################### def ProgramSearch_File(channellist,query): t = time.time() allProgramsMap = {} channels = [] channelIDs = [] channelID = None channelNumber = None c_channelID = None p_channelID = None program=None i=0 for event, elem in etree.iterparse(Prefs[PREFS_XMLTV_FILE],events=("start", "end")): # get channelIDs that are requested. if elem.tag == 'channel' and event=='start': channelID = elem.attrib.get('id') for dispname in elem.findall('display-name'): if dispname.text in channellist: channels.append(dispname.text) channelIDs.append(channelID) elem.clear() # get programs if elem.tag == 'programme' and event=='start' and len(channelIDs)>0: currTime = int(datetime.fromtimestamp(time.time()).strftime('%Y%m%d%H%M%S')) startTime = int(elem.attrib.get('start')[:14]) stopTime = int(elem.attrib.get('stop')[:14]) c_channelID = elem.attrib.get('channel') if startTime<currTime and currTime<stopTime and c_channelID==p_channelID and c_channelID in channelIDs: channelindex = channelIDs.index(c_channelID) channelmap = channels[channelindex] stopTime = time.mktime(datetime.strptime(str(stopTime),'%Y%m%d%H%M%S').timetuple()) #startTime=int(elem.attrib.get('start')[:14]) startTime=time.mktime(datetime.strptime(str(startTime),'%Y%m%d%H%M%S').timetuple()) title=xstr(elem.findtext('title')) subTitle=xstr(elem.findtext('sub-title')) desc=xstr(elem.findtext('desc')) date=xstr(elem.findtext('date')) icon_e=elem.find('icon') icon=None if icon_e!=None: icon=xstr(icon_e.attrib.get('src')) starRating=0.0 #query if query.lower() in title.lower() or query.lower() in desc.lower(): program = Program(startTime,stopTime,title,date,subTitle,desc,icon,starRating) allProgramsMap[channelmap] = program elem.clear() elif c_channelID!=p_channelID: elem.clear() else: elem.clear() p_channelID=c_channelID logInfo("Time taken to search XMLTV File: "+str(time.time()-t)) return allProgramsMap ################################################################################################### # This function returns whether the xmltv_mode is set to restapi or hdhomerun ################################################################################################### def isXmlTvModeRestApi(): xmltv_mode = xstr(Prefs[PREFS_XMLTV_MODE]) return (xmltv_mode == XMLTV_MODE_RESTAPI) def isXmlTvModeHDHomeRun(): xmltv_mode = xstr(Prefs[PREFS_XMLTV_MODE]) return (xmltv_mode == XMLTV_MODE_HDHOMERUN) def isXmlTvModeFile(): xmltv_mode = xstr(Prefs[PREFS_XMLTV_MODE]) return (xmltv_mode == XMLTV_MODE_FILE) ################################################################################################### # This function constructs the url with query to obtain the currently playing programs ################################################################################################### def ConstructApiUrl(channels, partialQuery, filterText = None): xmltvApiUrl = Prefs["xmltv_api_url"] showNextProgramsCount = int(Prefs["xmltv_show_next_programs_count"]) # construct the parameter map, and then use the url encode function to ensure we are compliant with the spec paramMap = {} paramMap["show_next"] = str(showNextProgramsCount) if filterText is not None: paramMap["filter_text"] = filterText # if partialQuery, then we want to include a channels parameter with the csv of the channel numbers if partialQuery: if Prefs[PREFS_XMLTV_MATCH] == "name": csv = ",".join([channel.name for channel in channels]) else: csv = ",".join([channel.number for channel in channels]) paramMap["channels"] = csv xmltvApiUrl += "?" + urllib.urlencode(paramMap) return xmltvApiUrl ################################################################################################### # This function parses a Program json object ################################################################################################### def ParseProgramJson(mode,jsonProgram): #isXmlTvModeRestApi if mode==XMLTV_MODE_RESTAPI: startTime = int(jsonProgram.get('start'))/1000 stopTime = int(jsonProgram.get('stop'))/1000 title = xstr(jsonProgram.get('title','')) date = xstr(jsonProgram.get('date',0)) subTitle = xstr(jsonProgram.get('subtitle','')) desc = xstr(jsonProgram.get('desc','')) starRating = xstr(jsonProgram.get('starRating','')) icon = xstr(jsonProgram.get('icon','')) else: startTime = int(jsonProgram.get('StartTime')) stopTime = int(jsonProgram.get('EndTime')) title = xstr(jsonProgram.get('Title')) date = GetDateDisplay(jsonProgram.get('OriginalAirdate',0)) subTitle = xstr(jsonProgram.get('Affiliate','')) desc = xstr(jsonProgram.get('Synopsis','')) starRating = xstr('') icon = xstr(jsonProgram.get('ImageURL','')) return Program(startTime,stopTime,title,date,subTitle,desc,icon,starRating) ################################################################################################### # This function returns the title to be used with the VideoClipObject ################################################################################################### def GetVcoTitle(channel): title = xstr(channel.number) + " - " + xstr(channel.name) if (channel.hasProgramInfo() and channel.program.title is not None): title += ": " + channel.program.title return title ################################################################################################### # This function returns the tagline to be used with the VideoClipObject ################################################################################################### def GetVcoTagline(program): tagline = " " if (program is not None): startTimeDisplay = GetTimeDisplay(program.startTime) stopTimeDisplay = GetTimeDisplay(program.stopTime) tagline = startTimeDisplay + " - " + stopTimeDisplay + ": " + xstr(program.title) if (program.subTitle): tagline += " - " + program.subTitle return tagline ################################################################################################### # This function returns the summary to be used with the VideoClipObject ################################################################################################### def GetVcoSummary(program): summary = " " if (program is not None): if (program.desc is not None): summary += program.desc if (len(program.next) > 0): summary += "\nNext:\n" for nextProgram in program.next: summary += GetVcoTagline(nextProgram) + "\n" return summary ################################################################################################### # This function returns the star rating (float value) for the given progam ################################################################################################### def GetVcoStarRating(program): starRating = 0.0 if (program is not None): if (program.starRating is not None): try: textArray = program.starRating.split("/") numerator = float(textArray[0]) denominator = float(textArray[1]) starRating = float(10.0*numerator / denominator) except: starRating = 0.0 return starRating ################################################################################################### # This function returns the star rating (float value) for the given progam ################################################################################################### def GetVcoYear(program): year = " " if (program is not None and program.date is not None): year = program.date return year ################################################################################################### # This function returns the icon for the given progam ################################################################################################### def GetVcoIcon(channel,program): # Create safe names icon_channelname = makeSafeFilename(channel.name)+'.png' icon_channelnumber = makeSafeFilename(channel.number)+'.png' # Default icon icon = R(ICON_UNKNOWN) # Icon detection if (program is not None and program.icon is not None and program.icon.strip() != ''): icon = program.icon elif resourceExists(icon_channelname): icon = R(icon_channelname) elif resourceExists('logo-'+icon_channelname): icon = R('logo-'+icon_channelname) elif resourceExists(icon_channelnumber): icon = R(icon_channelnumber) elif resourceExists('logo-'+icon_channelnumber): icon = R('logo-'+icon_channelnumber) #logDebug(icon) return icon ################################################################################################### # Check tuner availibility ################################################################################################### def CheckTunerAvail(ip): try: htmlData = urllib2.urlopen(URL_HDHR_TUNER_STATUS.format(ip=ip),timeout=TIMEOUT_LAN).read() tuner_avail = re.findall('>none<|>not in use<',htmlData) return len(tuner_avail) except Exception as inst: logError('CheckTunerAvail(ip):'+strError(inst)) return -1 ################################################################################################### # This function converts a time in milliseconds to a time text ################################################################################################### def GetTimeDisplay(timeInMs): timeInSeconds = timeInMs return datetime.fromtimestamp(timeInSeconds).strftime(TIME_FORMAT) ################################################################################################### # This function converts a time in milliseconds to a time text ################################################################################################### def GetDateDisplay(timeInSeconds): if timeInSeconds==0: return "" return datetime.fromtimestamp(timeInSeconds).strftime(DATE_FORMAT) ################################################################################################### # This function loads the list of all enabled favorites ################################################################################################### def LoadEnabledFavorites(): favorites = [] for favidx in range(1,MAX_FAVORITES+1): favorite = LoadFavorite(favidx) if (favorite.enable): favorites.append(favorite) return favorites ################################################################################################### # This function loads the favorite identified by the index i ################################################################################################### def LoadFavorite(i): enable = Prefs['favorites.' + str(i) + '.enable'] name = Prefs['favorites.' + str(i) + '.name'] list = Prefs['favorites.' + str(i) + '.list'] sortBy = Prefs['favorites.' + str(i) + '.sortby'] return Favorite(i,enable,name,list, sortBy) ################################################################################################### # This function loads the full channel list from the configured hdhrviewer host ################################################################################################### def LoadAllChannels(tuneridx): # Devices.tunerDevices[tuneridx] allChannelsList = [] allChannelsMap = {} tuner = Dict['tuners'][tuneridx] try: jsonLineupUrl = tuner['LineupURL'] jsonLineup = JSON.ObjectFromURL(jsonLineupUrl,timeout=TIMEOUT_LAN) for channel in jsonLineup: guideNumber = channel.get('GuideNumber') guideName = channel.get('GuideName','') videoCodec = channel.get('VideoCodec','') audioCodec = channel.get('AudioCodec','') streamUrl = channel.get('URL','') HD = xstr(channel.get('HD','')) Fav = xstr(channel.get('Favorite','')) DRM = xstr(channel.get('DRM','')) channelLogo = ICON_DEFAULT_CHANNEL channel = Channel(guideNumber,guideName,streamUrl,channelLogo,videoCodec,audioCodec,HD,Fav,DRM) allChannelsList.append(channel) allChannelsMap[guideNumber] = channel except Exception as inst: logError('LoadAllChannels(tuneridx): '+strError(inst)) logError('tuner='+xstr(tuneridx)) return ChannelCollection(allChannelsList,allChannelsMap) ################################################################################################### # Utility function to populate the channels, including the program info if enabled in preferences ################################################################################################### def AddChannelObjectContainer(oc, tuneridx, title, channels, search=False): tuneridx_int=int(tuneridx) tuner = Dict['tuners'][tuneridx_int] jsonData = getDeviceInfoJsonData(tuner) modelNumber = jsonData.get('ModelNumber','unknown') firmwareName = jsonData.get('FirmwareName','unknown') firmwareVersion = jsonData.get('FirmwareVersion','unknown') #localIP = tuner['LocalIP'] localIP = tuner.get('LocalIP','') deviceID = jsonData.get('DeviceID','unknown') if CheckTunerAvail(localIP)==0: errmsg='Warning: Tuner not available!' oc.add(PopupDirectoryObject(key=Callback(errorMessage,message=errmsg), title=errmsg, art=R(ART), thumb=R(ICON_ERROR))) #Debugging info logInfo('********************[Tuner]***********************') logInfo('Model :'+modelNumber) logInfo('Firmware :'+firmwareName+' '+firmwareVersion) #Overwrite PreTranscode setting according to model. if modelNumber=='HDTC-2US' and Prefs['transcode'] in ['none','default']: logInfo('Transcode:'+Prefs['transcode']+'; Overwrite to none') transcode = 'none' elif modelNumber=='HDTC-2US': logInfo('Transcode:'+Prefs['transcode']) transcode = Prefs['transcode'] else: logInfo('Transcode:default/ignore') transcode = 'default' if Prefs[PREFS_VCODEC]!="default": logInfo('VideoCodec Override:'+Prefs[PREFS_VCODEC]) if Prefs[PREFS_ACODEC]!="default": logInfo('AudioCodec Override:'+Prefs[PREFS_ACODEC]) logInfo('**************************************************') logDebug('ch.no'.ljust(6)+'|'+'RptCodec'.ljust(12)+'|'+'CptCodec'.ljust(18)+'|'+'HD'.ljust(2)+'|'+'Fav'.ljust(3)+'|'+'DRM'.ljust(3)+'|'+'url') # setup the VideoClipObjects from the channel list for channel in channels: program = channel.program videoCodec = channel.videoCodec audioCodec = channel.audioCodec RvideoCodec = channel.videoCodec RaudioCodec = channel.audioCodec HD = channel.HD DRM = channel.DRM Fav = channel.Fav url = channel.streamUrl vcoTitle = GetVcoTitle(channel) year = GetVcoYear(program) tagline = GetVcoTagline(program) summary = GetVcoSummary(program) starRating = GetVcoStarRating(program) thumb = GetVcoIcon(channel,program) # Only append device ip for search results if search and localIP!='': vcoTitle = vcoTitle+' ['+localIP+']' #If codec not defined/available (older firmware), assume default codec. if RvideoCodec in ['',None]: logError('Video Codec not defined. Are you running the latest firmware? Trying videoCodec='+VIDEO_CODEC) videoCodec=VIDEO_CODEC RvideoCodec=VIDEO_CODEC if RaudioCodec in ['',None]: logError('Audio Codec not defined. Are you running the latest firmware? Trying audioCodec='+AUDIO_CODEC) audioCodec=AUDIO_CODEC RaudioCodec=AUDIO_CODEC #VideoCodec correction if modelNumber=='HDTC-2US' and transcode not in ['default','none']: videoCodec=VideoCodec.H264 elif RvideoCodec.lower()=='mpeg2': videoCodec='mpeg2video' else: videoCodec=RvideoCodec.lower() #AudioCodec correction if RaudioCodec.lower()=='aac': audioCodec='aac_latm' elif RaudioCodec.lower()=='mpeg': audioCodec='mp2' else: audioCodec=audioCodec.lower() audioCodec=RaudioCodec.lower() #VideoCodec override if Prefs[PREFS_VCODEC]=='plex': videoCodec=None elif Prefs[PREFS_VCODEC]!='default': videoCodec=Prefs[PREFS_VCODEC] #AudioCodec override if Prefs[PREFS_ACODEC]=='plex': audioCodec=None elif Prefs[PREFS_ACODEC]!='default': audioCodec=Prefs[PREFS_ACODEC] #tempfix for iOS Plex 4.4 if iOSPlex44(): vcoTitle = vcoTitle.replace(' ',' ') if tagline is not None: tagline = tagline.replace(' ',' ') if summary is not None: summary = summary.replace(' ',' ') #debugging purposes logDebug(channel.number.ljust(6)+'|'+(RvideoCodec+'/'+RaudioCodec).ljust(12)+'|'+(xstr(videoCodec)+'/'+xstr(audioCodec)).ljust(18)+'|'+HD.ljust(2)+'|'+Fav.ljust(3)+'|'+DRM.ljust(3)+'|'+url) oc.add(CreateVO(tuneridx=tuneridx, url=url, title=vcoTitle, year=year, tagline=tagline, summary=summary, starRating=starRating, thumb=thumb, number=channel.number, name=channel.name, thumb_url=thumb, videoCodec=videoCodec, audioCodec=audioCodec,transcode=transcode)) return oc ################################################################################################### # Create Video Object ################################################################################################### @route(PREFIX + '/CreateVO') def CreateVO(tuneridx, url, title, year=None, tagline='', summary='', thumb=None, starRating=0, videoCodec=None,audioCodec=None,transcode='default', number='None',name='None', include_container=False, thumb_url=None, #checkFiles=0, includeBandwidths=1, **kwargs): tuneridx_int=int(tuneridx) tuner = Dict['tuners'][tuneridx_int] localIP = tuner.get('LocalIP','') #If tuner is not available: if include_container and CheckTunerAvail(localIP)==0: errmsg='Warning: Tuner not available!\n' summary=errmsg + xstr(summary) uniquekey = str(tuneridx)+url if transcode=='auto': #Auto Transcode for HDTC-2US vo = VideoClipObject( rating_key = uniquekey, key = Callback(CreateVO, tuneridx=tuneridx, url=url, title=title, year=year, tagline=tagline, summary=summary, starRating=starRating, videoCodec=videoCodec,audioCodec=audioCodec,transcode=transcode, thumb=thumb, #checkFiles=checkFiles, includeBandwidths=includeBandwidths, include_container=True), rating = float(starRating), title = xstr(title), year = xint(year), summary = xstr(summary), #Plex.tv & Roku3 tagline = xstr(tagline), source_title = xstr(tagline), #without duration, transcoding will not work... duration = VIDEO_DURATION, thumb = thumb, items = [ MediaObject( parts = [PartObject(key=(url+"?transcode=heavy"))], container = MEDIA_CONTAINER, video_resolution = 1080, bitrate = 8000, #8000 #12000 video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ), MediaObject( parts = [PartObject(key=(url+"?transcode=mobile"))], container = MEDIA_CONTAINER, video_resolution = 720, bitrate = 2000, #2000 #8000 video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ), MediaObject( parts = [PartObject(key=(url+"?transcode=internet480"))], container = MEDIA_CONTAINER, video_resolution = 480, bitrate = 1500, #1500 #2000 video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ), MediaObject( parts = [PartObject(key=(url+"?transcode=internet240"))], container = MEDIA_CONTAINER, video_resolution = 240, bitrate = 720, # 720 #1500 video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ), ] ) elif transcode in ['default','none']: #For HDTC-2US(transcode=none) or other tuners. if transcode=='none': mo_url=url+'?transcode=none' else: mo_url=url vo = VideoClipObject( rating_key = uniquekey, key = Callback(CreateVO, tuneridx=tuneridx, url=url, title=title, year=year, tagline=tagline, summary=summary, thumb=thumb, starRating=starRating, videoCodec=videoCodec,audioCodec=audioCodec,transcode=transcode, #checkFiles=checkFiles, includeBandwidths=includeBandwidths, include_container=True), rating = float(starRating), title = xstr(title), year = xint(year), summary = xstr(summary), #Plex.tv & Roku3 tagline = xstr(tagline), source_title = xstr(tagline), duration = VIDEO_DURATION, thumb = thumb, items = [ MediaObject( parts = [PartObject(key=(mo_url))], container = MEDIA_CONTAINER, video_resolution = 1080, #bitrate = 20000, video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ) ] ) else: #For HDTC-2US H264 vo = VideoClipObject( rating_key = uniquekey, key = Callback(CreateVO, tuneridx=tuneridx, url=url, title=title, year=year, tagline=tagline, summary=summary, thumb=thumb, starRating=starRating, videoCodec=videoCodec,audioCodec=audioCodec,transcode=transcode, #checkFiles=checkFiles, includeBandwidths=includeBandwidths, include_container=True), rating = float(starRating), title = xstr(title), year = xint(year), summary = xstr(summary), #Plex.tv & Roku3 tagline = xstr(tagline), source_title = xstr(tagline), #without duration, transcoding will not work... duration = VIDEO_DURATION, thumb = thumb, items = [ MediaObject( parts = [PartObject(key=(url+'?transcode='+transcode))], container = MEDIA_CONTAINER, #video_resolution = 1080, video_codec = videoCodec, audio_codec = audioCodec, audio_channels = AUDIO_CHANNELS, optimized_for_streaming = True ) ] ) if include_container: return ObjectContainer(objects=[vo]) else: return vo ################################################################################################### # Utility to convert an object to a string (and mainly handle the NoneType case) # Credit: from a stackoverflow article ################################################################################################### def xstr(s): if s is None: return '' else: return str(s) ################################################################################################### # Utility to convert an object to an integer (and handle the NoneType case) ################################################################################################### def xint(s): if (s is None or len(s)==0): return None else: try: return int(s) except: return None ################################################################################################### # Make safe file name for channel logo ################################################################################################### def makeSafeFilename(inputFilename): try: safechars = string.letters + string.digits + "-_." return filter(lambda c: c in safechars, inputFilename) except: return "" ################################################################################################### # Check if resource exist ################################################################################################### def resourceExists(inputFilename): return Core.storage.resource_exists(inputFilename) def fileExists(inputFilepath): return os.path.exists(inputFilepath) def dirExists(inputDir): return Core.storage.dir_exists(inputDir) ################################################################################################### # python 'any' function ################################################################################################### def xany(iterable): for element in iterable: if element: return True return False ################################################################################################### # logging / debuging functions ################################################################################################### def strError(inst): return xstr(type(inst)) + ": " + xstr(inst.args) + ": " + xstr(inst) def logError(strmsg): Log.Error('########## ' + xstr(strmsg)) def logDebug(strmsg): Log.Debug('---------- ' + xstr(strmsg)) def logInfo(strmsg): Log.Info('********** ' + xstr(strmsg)) def logType(strmsg): Log.Debug('---------- ' + xstr(strmsg) + '; Type='+xstr(type(strmsg))) ################################################################################################### # Client detection ################################################################################################### def iOSPlex44(): return (Client.Product=='Plex for iOS' and Client.Version == '4.4') ################################################################################################### # Error Msg ################################################################################################### def errorMessage(message): return ObjectContainer(header="Error", message=message) ################################################################################################### # Client Information. ################################################################################################### def getInfo(): #svrOSver = Platform.OSVersion logInfo('******************[System Info]*******************') logInfo('Server: '+Platform.OS+' '+Platform.OSVersion+' ['+Platform.CPU+']') logInfo('PMS : '+Platform.ServerVersion) logInfo('Client: '+Client.Product+' '+Client.Version+' ['+Client.Platform+']') logInfo("HDHRV2: "+VERSION) logInfo('*******************[Settings]*********************') logInfo('HDHomerunIP........:'+Prefs[PREFS_HDHR_IP]) logInfo('Transcode..........:'+Prefs[PREFS_TRANSCODE]) logInfo('XMLTV Mode.........:'+Prefs[PREFS_XMLTV_MODE]) logInfo('XMLTV File.........:'+Prefs[PREFS_XMLTV_FILE]) logInfo('XMLTV URL..........:'+Prefs[PREFS_XMLTV_APIURL]) logInfo('XMLTV Match........:'+Prefs[PREFS_XMLTV_MATCH]) logInfo('VideoCodec Override:'+Prefs[PREFS_VCODEC]) logInfo('AudioCodec Override:'+Prefs[PREFS_ACODEC]) logInfo('**************************************************') ################################################################################################### # Get total channels from {ip}/lineup.json ################################################################################################### def getTunerTotalChannels(tuner): # (tuner) in Devices.tunerDevices try: jsonURL = tuner['LineupURL'] jsonData = JSON.ObjectFromURL(jsonURL,timeout=TIMEOUT_LAN) totalChannels = len(jsonData) except Exception as inst: logError('getTunerTotalChannels(tuner): '+strError(inst)) logError('tuner='+xstr(tuner)) totalChannels = 0 return totalChannels ################################################################################################### # Get info from {ip}/discover.json # tuner in Devices.tunerDevices # info = FriendlyName, ModelNumber, FirmwareName, FirmwareVersion, DeviceID, DeviceAuth, TunerCount # BaseURL, LineupURL ################################################################################################### def getDeviceInfo(tuner,info): try: jsonURL = tuner['DiscoverURL'] jsonData = JSON.ObjectFromURL(jsonURL,timeout=TIMEOUT_LAN) info = jsonData.get(info,'') except Exception as inst: logError('getDeviceInfo(tuner,info)'+strError(inst)) logError('tuner='+xstr(tuner)) logError('info='+xstr(info)) info = '' return info def getDeviceInfoJsonData(tuner): try: jsonURL = tuner['DiscoverURL'] jsonData = JSON.ObjectFromURL(jsonURL,timeout=TIMEOUT_LAN) return jsonData except Exception as inst: logError('getDeviceInfoJsonData(tuner,info)'+strError(inst)) logError('tuner='+xstr(tuner)) logError('info='+xstr(info)) return {} ################################################################################################### # Get guide url for tuner. # # (tuner) in Devices.tunerDevices # http://my.hdhomerun.com/api/guide.php?DeviceAuth={deviceAuth} ################################################################################################### def getGuideURL(tuneridx): try: tuners = Dict['tuners'] tuner=tuners[tuneridx] deviceAuth = getDeviceInfo(tuner,'DeviceAuth') info = URL_HDHR_GUIDE.format(deviceAuth=deviceAuth) except Exception as inst: logError('getGuideURL(tuneridx); tuneridx='+xstr(tuneridx)) logError(strError(inst)) info = '' return info ################################################################################################### # Get HDHomeRun Lineup details from {ip}/lineup.json # info = GuideNumber, GuideName, VideoCodec, AudioCodec, HD, URL ################################################################################################### def getLineupInfo(tuner,info): try: jsonURL = tuner['LineupURL'] jsonData = JSON.ObjectFromURL(jsonURL,timeout=TIMEOUT_LAN) info = jsonData.get(info,'') except Exception as inst: logError('getLineupInfo(tuner,info)'+strError(inst)) logError('tuner='+xstr(tuner)) logError('info='+xstr(info)) info = '' return info ################################################################################################### # Devices Class Definition # MultiTuner + Auto Discovery + Manual IP. Future: something to do with storageServers. ################################################################################################### class Devices: def __init__(self): self.storageServers = [] self.tunerDevices = [] self.manualTuner() self.autoDiscover() # Auto Discover devices def autoDiscover(self): cacheTime=None if Prefs[PREFS_AUTODISCOVER]: try: response = xstr(HTTP.Request(URL_HDHR_DISCOVER_DEVICES,timeout=TIMEOUT,cacheTime=cacheTime)) JSONdevices = JSON.ObjectFromString(''.join(response.splitlines())) for device in JSONdevices: StorageURL = device.get('StorageURL') LineupURL = device.get('LineupURL') if LineupURL is not None: if not xany(d['LocalIP']==device['LocalIP'] for d in self.tunerDevices): device['autoDiscover'] = True logInfo('Adding auto discovered tuner: '+device['LocalIP']) self.tunerDevices.append(device) else: logInfo('Auto discovered tuner skipped (duplicate): '+device['LocalIP']) #future if StorageURL is not None: self.storageServers.append(device) except Exception as inst: logError('Devices.autoDiscover(): '+strError(inst)) # Get manual tuners listed in Settings def manualTuner(self): try: manualTuners = Prefs[PREFS_HDHR_IP] if manualTuners is not None: # Only add tuners if not 'auto' if manualTuners != 'auto': for tunerIP in manualTuners.split(): if not xany(d['LocalIP']==tunerIP for d in self.tunerDevices): self.addManualTuner(tunerIP) else: # self.addManualTuner(tunerIP) #test logInfo('Manually defined tuner skipped: '+tunerIP) else: logInfo('No manually defined tuners') except Exception as inst: logError('Devices.manualTuner(): '+strError(inst)) # Add manual tuners def addManualTuner(self,tunerIP): try: tuner = {} tuner['autoDiscover'] = False tuner['DeviceID'] = 'Manual'+tunerIP tuner['LocalIP'] = tunerIP tuner['BaseURL'] = tunerIP tuner['DiscoverURL'] = URL_HDHR_DISCOVER.format(ip=tunerIP) tuner['LineupURL'] = URL_HDHR_LINEUP.format(ip=tunerIP) self.tunerDevices.append(tuner) logInfo('Adding manually defined tuner: '+xstr(tuner['LocalIP'])) except Exception as inst: logError('Devices.addManualTuner('+xstr(tunerIP)+'): '+strError(inst)) ################################################################################################### # Channel collection class definition, that supports both a map and list version of the same data ################################################################################################### class ChannelCollection: def __init__(self,list,map): self.list = list self.map = map ################################################################################################### # Channel class definition ################################################################################################### class Channel: def __init__(self,guideNumber,guideName,streamUrl,channelLogo,videoCodec,audioCodec,HD,Fav,DRM): self.number = guideNumber self.name = guideName self.streamUrl = streamUrl self.program = None self.logo = channelLogo self.videoCodec = videoCodec self.audioCodec = audioCodec self.HD = HD self.Fav = Fav self.DRM = DRM def setProgramInfo(self,program): self.program = program def hasProgramInfo(self): return (self.program is not None) ################################################################################################### # Channel class definition ################################################################################################### class Program: def __init__(self,startTime,stopTime,title,date,subTitle,desc,icon,starRating): self.startTime = startTime self.stopTime = stopTime self.title = title self.date = date self.subTitle = subTitle self.desc = desc self.icon = icon self.starRating = starRating self.next = [] ################################################################################################### # Favorite class definition ################################################################################################### class Favorite: def __init__(self,index,enable,name,textList,sortBy): self.index = index self.enable = enable self.name = name self.tuner = '' self.channels = [] self.totalChannels = 0 if textList is not None: textListItems = textList.split() self.tuner=textListItems[0] for item in textListItems: try: if isinstance(float(item), float): self.channels.append(item) self.totalChannels = self.totalChannels + 1 except ValueError: logInfo("Unable to parse the channel number " + item + " into a number.") if sortBy == 'Channel Number': try: self.channels.sort(key=float) except Exception as inst: logError('Favorite.channels.sort'+strError(inst))