diff --git a/.gitignore b/.gitignore index 601cfa61..4dfcc4c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,14 @@ - *.pyc - /ATVSettings.cfg - /Settings.cfg - PlexConnect.log - *.cer - *.key - *.pem - *.der - *.pyproj - *.sln - *.suo - /assets/fanartcache/*.jpg +.venv +.vscode diff --git a/ATVSettings.py b/ATVSettings.py index 2c61fd38..44ec06c7 100755 --- a/ATVSettings.py +++ b/ATVSettings.py @@ -3,84 +3,82 @@ import sys from os import sep, makedirs from os.path import isdir -import ConfigParser +import configparser import fnmatch from Debug import * # dprint() - -options = { \ - 'playlistsview' :('List', 'Tabbed List', 'Hide'), \ - 'libraryview' :('List', 'Grid', 'Bookcase', 'Hide'), \ - 'sharedlibrariesview' :('List', 'Grid', 'Bookcase', 'Hide'), \ - 'channelview' :('Hide', 'List', 'Tabbed List', 'Grid', 'Bookcase'), \ - 'sharedchannelsview' :('Hide', 'List', 'Tabbed List', 'Grid', 'Bookcase'), \ - 'globalsearch' :('Show', 'Hide'), \ - 'movieview' :('Grid', 'List', 'Detailed List'), \ - 'homevideoview' :('Grid', 'List', 'Detailed List'), \ - 'actorview' :('Movies', 'Portrait'), \ - 'showview' :('List', 'Detailed List', 'Grid', 'Bookcase'), \ - 'flattenseason' :('False', 'True'), \ - 'seasonview' :('List', 'Coverflow'), \ - 'durationformat' :('Hours/Minutes', 'Minutes'), \ - 'postertitles' :('Highlighted Only', 'Show All'), \ - 'fanart' :('Hide', 'Show'), \ - 'fanart_blur' :('0', '5', '10', '15', '20'), \ - 'allowdeletion' :('No', 'Yes'), \ - 'moviepreplay_bottomshelf' :('Extras', 'Related Movies'), \ - 'movies_navbar_ondeck' :('checked', 'unchecked'), \ - 'movies_navbar_unwatched' :('checked', 'unchecked'), \ - 'movies_navbar_byfolder' :('checked', 'unchecked'), \ - 'movies_navbar_collections' :('checked', 'unchecked'), \ - 'movies_navbar_genres' :('checked', 'unchecked'), \ - 'movies_navbar_decades' :('checked', 'unchecked'), \ - 'movies_navbar_directors' :('checked', 'unchecked'), \ - 'movies_navbar_actors' :('checked', 'unchecked'), \ - 'movies_navbar_more' :('checked', 'unchecked'), \ - 'homevideos_navbar_ondeck' :('checked', 'unchecked'), \ - 'homevideos_navbar_unwatched' :('checked', 'unchecked'), \ - 'homevideos_navbar_byfolder' :('checked', 'unchecked'), \ - 'homevideos_navbar_collections' :('checked', 'unchecked'), \ - 'homevideos_navbar_genres' :('checked', 'unchecked'), \ - 'music_navbar_recentlyadded' :('checked', 'unchecked'), \ - 'music_navbar_genre' :('checked', 'unchecked'), \ - 'music_navbar_decade' :('checked', 'unchecked'), \ - 'music_navbar_year' :('checked', 'unchecked'), \ - 'music_navbar_more' :('checked', 'unchecked'), \ - 'tv_navbar_ondeck' :('checked', 'unchecked'), \ - 'tv_navbar_unwatched' :('checked', 'unchecked'), \ - 'tv_navbar_genres' :('checked', 'unchecked'), \ - 'tv_navbar_more' :('checked', 'unchecked'), \ - 'transcodequality' :('1080p 12.0Mbps', \ - '1080p 20.0Mbps', \ - '1080p 40.0Mbps', \ - '480p 2.0Mbps', \ - '720p 3.0Mbps', '720p 4.0Mbps', \ - '1080p 8.0Mbps', '1080p 10.0Mbps'), \ - 'transcoderaction' :('Auto', 'DirectPlay', 'Transcode'), \ - 'remotebitrate' :('720p 3.0Mbps', '720p 4.0Mbps', \ - '1080p 8.0Mbps', '1080p 10.0Mbps', '1080p 12.0Mbps', '1080p 20.0Mbps', '1080p 40.0Mbps', \ - '480p 2.0Mbps'), \ - 'dolbydigital' :('Off', 'On'), \ - 'phototranscoderaction' :('Auto', 'Transcode'), \ - 'subtitlerenderer' :('Auto', 'iOS, PMS', 'PMS'), \ - 'subtitlesize' :('100', '125', '150', '50', '75'), \ - 'audioboost' :('100', '175', '225', '300'), \ - 'showunwatched' :('True', 'False'), \ - 'showsynopsis' :('Hide', 'Show'), \ - 'showplayerclock' :('True', 'False'), \ - 'overscanadjust' :('0', '1', '2', '3', '-3', '-2', '-1'), \ - 'clockposition' :('Center', 'Right', 'Left'), \ - 'showendtime' :('True', 'False'), \ - 'timeformat' :('24 Hour', '12 Hour'), \ - 'myplex_user' :('', ), \ - 'myplex_auth' :('', ), \ - 'plexhome_enable' :('False', 'True'), \ - 'plexhome_user' :('', ), \ - 'plexhome_auth' :('', ), \ - } - +options = { + 'playlistsview': ('List', 'Tabbed List', 'Hide'), + 'libraryview': ('List', 'Grid', 'Bookcase', 'Hide'), + 'sharedlibrariesview': ('List', 'Grid', 'Bookcase', 'Hide'), + 'channelview': ('Hide', 'List', 'Tabbed List', 'Grid', 'Bookcase'), + 'sharedchannelsview': ('Hide', 'List', 'Tabbed List', 'Grid', 'Bookcase'), + 'globalsearch': ('Show', 'Hide'), + 'movieview': ('Grid', 'List', 'Detailed List'), + 'homevideoview': ('Grid', 'List', 'Detailed List'), + 'actorview': ('Movies', 'Portrait'), + 'showview': ('List', 'Detailed List', 'Grid', 'Bookcase'), + 'flattenseason': ('False', 'True'), + 'seasonview': ('List', 'Coverflow'), + 'durationformat': ('Hours/Minutes', 'Minutes'), + 'postertitles': ('Highlighted Only', 'Show All'), + 'fanart': ('Hide', 'Show'), + 'fanart_blur': ('0', '5', '10', '15', '20'), + 'allowdeletion': ('No', 'Yes'), + 'moviepreplay_bottomshelf': ('Extras', 'Related Movies'), + 'movies_navbar_ondeck': ('checked', 'unchecked'), + 'movies_navbar_unwatched': ('checked', 'unchecked'), + 'movies_navbar_byfolder': ('checked', 'unchecked'), + 'movies_navbar_collections': ('checked', 'unchecked'), + 'movies_navbar_genres': ('checked', 'unchecked'), + 'movies_navbar_decades': ('checked', 'unchecked'), + 'movies_navbar_directors': ('checked', 'unchecked'), + 'movies_navbar_actors': ('checked', 'unchecked'), + 'movies_navbar_more': ('checked', 'unchecked'), + 'homevideos_navbar_ondeck': ('checked', 'unchecked'), + 'homevideos_navbar_unwatched': ('checked', 'unchecked'), + 'homevideos_navbar_byfolder': ('checked', 'unchecked'), + 'homevideos_navbar_collections': ('checked', 'unchecked'), + 'homevideos_navbar_genres': ('checked', 'unchecked'), + 'music_navbar_recentlyadded': ('checked', 'unchecked'), + 'music_navbar_genre': ('checked', 'unchecked'), + 'music_navbar_decade': ('checked', 'unchecked'), + 'music_navbar_year': ('checked', 'unchecked'), + 'music_navbar_more': ('checked', 'unchecked'), + 'tv_navbar_ondeck': ('checked', 'unchecked'), + 'tv_navbar_unwatched': ('checked', 'unchecked'), + 'tv_navbar_genres': ('checked', 'unchecked'), + 'tv_navbar_more': ('checked', 'unchecked'), + 'transcodequality': ('1080p 12.0Mbps', + '1080p 20.0Mbps', + '1080p 40.0Mbps', + '480p 2.0Mbps', + '720p 3.0Mbps', '720p 4.0Mbps', + '1080p 8.0Mbps', '1080p 10.0Mbps'), + 'transcoderaction': ('Auto', 'DirectPlay', 'Transcode'), + 'remotebitrate': ('720p 3.0Mbps', '720p 4.0Mbps', + '1080p 8.0Mbps', '1080p 10.0Mbps', '1080p 12.0Mbps', '1080p 20.0Mbps', '1080p 40.0Mbps', + '480p 2.0Mbps'), + 'dolbydigital': ('Off', 'On'), + 'phototranscoderaction': ('Auto', 'Transcode'), + 'subtitlerenderer': ('Auto', 'iOS, PMS', 'PMS'), + 'subtitlesize': ('100', '125', '150', '50', '75'), + 'audioboost': ('100', '175', '225', '300'), + 'showunwatched': ('True', 'False'), + 'showsynopsis': ('Hide', 'Show'), + 'showplayerclock': ('True', 'False'), + 'overscanadjust': ('0', '1', '2', '3', '-3', '-2', '-1'), + 'clockposition': ('Center', 'Right', 'Left'), + 'showendtime': ('True', 'False'), + 'timeformat': ('24 Hour', '12 Hour'), + 'myplex_user': ('', ), + 'myplex_auth': ('', ), + 'plexhome_enable': ('False', 'True'), + 'plexhome_user': ('', ), + 'plexhome_auth': ('', ), +} class CATVSettings(): @@ -89,27 +87,25 @@ def __init__(self, path): self.cfg = None self.path = path self.loadSettings() - - - + # load/save config + def loadSettings(self): dprint(__name__, 1, "load settings") # options -> default dflt = {} for opt in options: dflt[opt] = options[opt][0] - + # load settings - self.cfg = ConfigParser.SafeConfigParser(dflt) + self.cfg = configparser.ConfigParser(dflt) self.cfg.read(self.getSettingsFile()) - + def saveSettings(self): dprint(__name__, 1, "save settings") - f = open(self.getSettingsFile(), 'wb') - self.cfg.write(f) - f.close() - + with open(self.getSettingsFile(), 'w') as f: + self.cfg.write(f) + def getSettingsFile(self): if self.path.startswith('.'): # relative to current path @@ -120,61 +116,61 @@ def getSettingsFile(self): if not isdir(directory): makedirs(directory) return directory + sep + "ATVSettings.cfg" - + def checkSection(self, UDID): # check for existing UDID section sections = self.cfg.sections() if not UDID in sections: self.cfg.add_section(UDID) dprint(__name__, 0, "add section {0}", UDID) - - - + # access/modify AppleTV options + def getSetting(self, UDID, option): self.checkSection(UDID) dprint(__name__, 1, "getsetting {0}", self.cfg.get(UDID, option)) return self.cfg.get(UDID, option) - + def setSetting(self, UDID, option, val): self.checkSection(UDID) self.cfg.set(UDID, option, val) - + def checkSetting(self, UDID, option): self.checkSection(UDID) val = self.cfg.get(UDID, option) opts = options[option] - + # check val in list found = False for opt in opts: if fnmatch.fnmatch(val, opt): found = True - + # if not found, correct to default if not found: self.cfg.set(UDID, option, opts[0]) - dprint(__name__, 1, "checksetting: default {0} to {1}", option, opts[0]) - + dprint(__name__, 1, + "checksetting: default {0} to {1}", option, opts[0]) + def toggleSetting(self, UDID, option): self.checkSection(UDID) cur = self.cfg.get(UDID, option) opts = options[option] - + # find current in list - i=0 - for i,opt in enumerate(opts): - if opt==cur: + i = 0 + for i, opt in enumerate(opts): + if opt == cur: break - + # get next option (circle to first) - i=i+1 - if i>=len(opts): - i=0 - + i = i+1 + if i >= len(opts): + i = 0 + # set self.cfg.set(UDID, option, opts[i]) - + def setOptions(self, option, opts): global options if option in options: @@ -182,28 +178,27 @@ def setOptions(self, option, opts): dprint(__name__, 1, 'setOption: update {0} to {1}', option, opts) - -if __name__=="__main__": +if __name__ == "__main__": ATVSettings = CATVSettings() - + UDID = '007' ATVSettings.checkSection(UDID) - + option = 'transcodequality' - print ATVSettings.getSetting(UDID, option) - - print "setSetting" + print(ATVSettings.getSetting(UDID, option)) + + print("setSetting") ATVSettings.setSetting(UDID, option, 'True') # error - pick default - print ATVSettings.getSetting(UDID, option) + print(ATVSettings.getSetting(UDID, option)) ATVSettings.setSetting(UDID, option, '9') - print ATVSettings.getSetting(UDID, option) - - print "toggleSetting" + print(ATVSettings.getSetting(UDID, option)) + + print("toggleSetting") ATVSettings.toggleSetting(UDID, option) - print ATVSettings.getSetting(UDID, option) + print(ATVSettings.getSetting(UDID, option)) ATVSettings.toggleSetting(UDID, option) - print ATVSettings.getSetting(UDID, option) + print(ATVSettings.getSetting(UDID, option)) ATVSettings.toggleSetting(UDID, option) - print ATVSettings.getSetting(UDID, option) - + print(ATVSettings.getSetting(UDID, option)) + del ATVSettings diff --git a/DNSServer.py b/DNSServer.py index 54c553a8..25e2da12 100755 --- a/DNSServer.py +++ b/DNSServer.py @@ -1,457 +1,66 @@ -#!/usr/bin/env python +from dnslib.intercept import InterceptResolver +import dnslib.server +from Debug import * -""" -Source: -http://code.google.com/p/minidns/source/browse/minidns -""" -""" -Header - 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| ID | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -|QR| Opcode |AA|TC|RD|RA| Z | RCODE | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| QDCOUNT | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| ANCOUNT | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| NSCOUNT | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| ARCOUNT | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +class DNSServer(): + def __init__(self, param): -Query -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| | -/ QNAME / -| | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| QTYPE | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| QCLASS | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - -ResourceRecord -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| | -/ NAME / -| | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| TYPE | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| CLASS | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| TTL | -| | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ -| RDLENGTH | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| -| | -/ RDATA / -| | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ - -Source: http://doc-tcpip.org/Dns/named.dns.message.html -""" - -""" -prevent aTV update -Source: http://forum.xbmc.org/showthread.php?tid=93604 - -loopback to 127.0.0.1... - mesu.apple.com - appldnld.apple.com - appldnld.apple.com.edgesuite.net -""" - - -import sys -import socket -import struct -from multiprocessing import Pipe # inter process communication -import signal - -import Settings -from Debug import * # dprint() - - - -""" - Hostname/DNS conversion - Hostname: 'Hello.World' - DNSdata: 'HelloWorld -""" -def HostToDNS(Host): - DNSdata = '.'+Host+'\0' # python 2.6: bytearray() - i=0 - while i>3)&0x0F) - print "RCode "+str((ord(paket[3])>>0)&0x0F) - qdcount = (ord(paket[4])<<8)+ord(paket[5]) - ancount = (ord(paket[6])<<8)+ord(paket[7]) - nscount = (ord(paket[8])<<8)+ord(paket[9]) - arcount = (ord(paket[10])<<8)+ord(paket[11]) - print "Count - QD, AN, NS, AR:", qdcount, ancount, nscount, arcount - adr = 12 - - # QDCOUNT (query) - for i in range(qdcount): - print "QUERY" - host = DNSToHost(paket, adr) - - """ - for j in range(len(host)+2+4): - print ord(paket[adr+j]), - print - """ - - adr = adr + len(host) + 2 - print host - print "type "+str((ord(paket[adr+0])<<8)+ord(paket[adr+1])) - print "class "+str((ord(paket[adr+2])<<8)+ord(paket[adr+3])) - adr = adr + 4 - - # ANCOUNT (resource record) - for i in range(ancount): - print "ANSWER" - print ord(paket[adr]) - if ord(paket[adr]) & 0xC0: - print"link" - adr = adr + 2 - else: - host = DNSToHost(paket, adr) - adr = adr + len(host) + 2 - print host - _type = (ord(paket[adr+0])<<8)+ord(paket[adr+1]) - _class = (ord(paket[adr+2])<<8)+ord(paket[adr+3]) - print "type, class: ", _type, _class - adr = adr + 4 - print "ttl" - adr = adr + 4 - rdlength = (ord(paket[adr+0])<<8)+ord(paket[adr+1]) - print "rdlength", rdlength - adr = adr + 2 - if _type==1: - print "IP:", - for j in range(rdlength): - print ord(paket[adr+j]), - print - elif _type==5: - print "redirect:", DNSToHost(paket, adr) - else: - print "type unsupported:", - for j in range(rdlength): - print ord(paket[adr+j]), - print - adr = adr + rdlength - -def printDNSdata_raw(DNSdata): - # hex code - for i in range(len(DNSdata)): - if i % 16==0: - print - print "{0:02x}".format(ord(DNSdata[i])), - print - - # printable characters - for i in range(len(DNSdata)): - if i % 16==0: - print - if (ord(DNSdata[i])>32) & (ord(DNSdata[i])<128): - print DNSdata[i], - else: - print ".", - print - - - -def parseDNSdata(paket): - - def getWord(DNSdata, addr): - return (ord(DNSdata[addr])<<8)+ord(DNSdata[addr+1]) - - DNSstruct = {} - adr = 0 - - # header - DNSstruct['head'] = { \ - 'id': getWord(paket, adr+0), \ - 'flags': getWord(paket, adr+2), \ - 'qdcnt': getWord(paket, adr+4), \ - 'ancnt': getWord(paket, adr+6), \ - 'nscnt': getWord(paket, adr+8), \ - 'arcnt': getWord(paket, adr+10) } - adr = adr + 12 - - # query - DNSstruct['query'] = [] - for i in range(DNSstruct['head']['qdcnt']): - DNSstruct['query'].append({}) - host_nolink = DNSToHost(paket, adr, followlink=False) - host_link = DNSToHost(paket, adr, followlink=True) - DNSstruct['query'][i]['host'] = host_link - adr = adr + len(host_nolink)+2 - DNSstruct['query'][i]['type'] = getWord(paket, adr+0) - DNSstruct['query'][i]['class'] = getWord(paket, adr+2) - adr = adr + 4 - - # resource records - DNSstruct['resrc'] = [] - for i in range(DNSstruct['head']['ancnt'] + DNSstruct['head']['nscnt'] + DNSstruct['head']['arcnt']): - DNSstruct['resrc'].append({}) - host_nolink = DNSToHost(paket, adr, followlink=False) - host_link = DNSToHost(paket, adr, followlink=True) - DNSstruct['resrc'][i]['host'] = host_link - adr = adr + len(host_nolink)+2 - DNSstruct['resrc'][i]['type'] = getWord(paket, adr+0) - DNSstruct['resrc'][i]['class'] = getWord(paket, adr+2) - DNSstruct['resrc'][i]['ttl'] = (getWord(paket, adr+4)<<16)+getWord(paket, adr+6) - DNSstruct['resrc'][i]['rdlen'] = getWord(paket, adr+8) - adr = adr + 10 - DNSstruct['resrc'][i]['rdata'] = [] - if DNSstruct['resrc'][i]['type']==5: # 5=redirect, evaluate name - host = DNSToHost(paket, adr, followlink=True) - DNSstruct['resrc'][i]['rdata'] = host - adr = adr + DNSstruct['resrc'][i]['rdlen'] - DNSstruct['resrc'][i]['rdlen'] = len(host) - else: # 1=IP, ... - for j in range(DNSstruct['resrc'][i]['rdlen']): - DNSstruct['resrc'][i]['rdata'].append( paket[adr+j] ) - adr = adr + DNSstruct['resrc'][i]['rdlen'] - - return DNSstruct - -def encodeDNSstruct(DNSstruct): - - def appendWord(DNSdata, val): - DNSdata.append((val>>8) & 0xFF) - DNSdata.append( val & 0xFF) - - DNS = bytearray() - - # header - appendWord(DNS, DNSstruct['head']['id']) - appendWord(DNS, DNSstruct['head']['flags']) - appendWord(DNS, DNSstruct['head']['qdcnt']) - appendWord(DNS, DNSstruct['head']['ancnt']) - appendWord(DNS, DNSstruct['head']['nscnt']) - appendWord(DNS, DNSstruct['head']['arcnt']) - - # query - for i in range(DNSstruct['head']['qdcnt']): - host = HostToDNS(DNSstruct['query'][i]['host']) - DNS.extend(bytearray(host)) - appendWord(DNS, DNSstruct['query'][i]['type']) - appendWord(DNS, DNSstruct['query'][i]['class']) - - # resource records - for i in range(DNSstruct['head']['ancnt'] + DNSstruct['head']['nscnt'] + DNSstruct['head']['arcnt']): - host = HostToDNS(DNSstruct['resrc'][i]['host']) # no 'packing'/link - todo? - DNS.extend(bytearray(host)) - appendWord(DNS, DNSstruct['resrc'][i]['type']) - appendWord(DNS, DNSstruct['resrc'][i]['class']) - appendWord(DNS, (DNSstruct['resrc'][i]['ttl']>>16) & 0xFFFF) - appendWord(DNS, (DNSstruct['resrc'][i]['ttl'] ) & 0xFFFF) - appendWord(DNS, DNSstruct['resrc'][i]['rdlen']) - - if DNSstruct['resrc'][i]['type']==5: # 5=redirect, hostname - host = HostToDNS(DNSstruct['resrc'][i]['rdata']) - DNS.extend(bytearray(host)) - else: - DNS.extend(DNSstruct['resrc'][i]['rdata']) - - return DNS - -def printDNSstruct(DNSstruct): - for i in range(DNSstruct['head']['qdcnt']): - print "query:", DNSstruct['query'][i]['host'] - - for i in range(DNSstruct['head']['ancnt'] + DNSstruct['head']['nscnt'] + DNSstruct['head']['arcnt']): - print "resrc:", - print DNSstruct['resrc'][i]['host'] - if DNSstruct['resrc'][i]['type']==1: - print "->IP:", - for j in range(DNSstruct['resrc'][i]['rdlen']): - print ord(DNSstruct['resrc'][i]['rdata'][j]), - print - elif DNSstruct['resrc'][i]['type']==5: - print "->alias:", DNSstruct['resrc'][i]['rdata'] - else: - print "->unknown type" - - - -def Run(cmdPipe, param): - if not __name__ == '__main__': - signal.signal(signal.SIGINT, signal.SIG_IGN) - - dinit(__name__, param) # init logging, DNSServer process - - cfg_IP_self = param['IP_self'] - cfg_Port_DNSServer = param['CSettings'].getSetting('port_dnsserver') - cfg_IP_DNSMaster = param['CSettings'].getSetting('ip_dnsmaster') - - try: - DNS = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - DNS.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - DNS.settimeout(5.0) - DNS.bind((cfg_IP_self, int(cfg_Port_DNSServer))) - except Exception, e: - dprint(__name__, 0, "Failed to create socket on UDP port {0}: {1}", cfg_Port_DNSServer, e) - sys.exit(1) - - intercept = [param['HostToIntercept']] - restrain = [] - - if param['CSettings'].getSetting('use_custom_dns_bind_ip') == "True": - cfg_IP_self = param['CSettings'].getSetting('custom_dns_bind_ip') - else: + cfg = param['CSettings'] cfg_IP_self = param['IP_self'] - if param['CSettings'].getSetting('intercept_atv_icon')=='True': - intercept.append('a1.phobos.apple.com') - dprint(__name__, 0, "Intercept Atv Icon: Enabled") - if param['CSettings'].getSetting('prevent_atv_update')=='True': - restrain = ['mesu.apple.com', 'appldnld.apple.com', 'appldnld.apple.com.edgesuite.net'] - dprint(__name__, 0, "Prevent Atv Update: Enabled") - - dprint(__name__, 0, "***") - dprint(__name__, 0, "DNSServer: Serving DNS on {0} port {1}.", cfg_IP_self, cfg_Port_DNSServer) - dprint(__name__, 1, "intercept: {0} => {1}", intercept, cfg_IP_self) - dprint(__name__, 1, "restrain: {0} => 127.0.0.1", restrain) - dprint(__name__, 1, "forward other to higher level DNS: "+cfg_IP_DNSMaster) - dprint(__name__, 0, "***") - - try: - while True: - # check command - if cmdPipe.poll(): - cmd = cmdPipe.recv() - if cmd=='shutdown': - break - - # do your work (with timeout) - try: - data, addr = DNS.recvfrom(1024) - dprint(__name__, 1, "DNS request received!") - dprint(__name__, 1, "Source: "+str(addr)) - - #print "incoming:" - #printDNSdata(data) - - # analyse DNS request - # todo: how about multi-query messages? - opcode = (ord(data[2]) >> 3) & 0x0F # Opcode bits (query=0, inversequery=1, status=2) - if opcode == 0: # Standard query - domain = DNSToHost(data, 12) - dprint(__name__, 1, "Domain: "+domain) - - paket='' - if domain in intercept: - dprint(__name__, 1, "***intercept request") - paket+=data[:2] # 0:1 - ID - paket+="\x81\x80" # 2:3 - flags - paket+=data[4:6] # 4:5 - QDCOUNT - should be 1 for this code - paket+=data[4:6] # 6:7 - ANCOUNT - paket+='\x00\x00' # 8:9 - NSCOUNT - paket+='\x00\x00' # 10:11 - ARCOUNT - paket+=data[12:] # original query - paket+='\xc0\x0c' # pointer to domain name/original query - paket+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' # response type, ttl and resource data length -> 4 bytes - paket+=str.join('',map(lambda x: chr(int(x)), cfg_IP_self.split('.'))) # 4bytes of IP - dprint(__name__, 1, "-> DNS response: "+cfg_IP_self) - - elif domain in restrain: - dprint(__name__, 1, "***restrain request") - paket+=data[:2] # 0:1 - ID - paket+="\x81\x80" # 2:3 - flags - paket+=data[4:6] # 4:5 - QDCOUNT - should be 1 for this code - paket+=data[4:6] # 6:7 - ANCOUNT - paket+='\x00\x00' # 8:9 - NSCOUNT - paket+='\x00\x00' # 10:11 - ARCOUNT - paket+=data[12:] # original query - paket+='\xc0\x0c' # pointer to domain name/original query - paket+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' # response type, ttl and resource data length -> 4 bytes - paket+='\x7f\x00\x00\x01' # 4bytes of IP - 127.0.0.1, loopback - dprint(__name__, 1, "-> DNS response: "+cfg_IP_self) - - else: - dprint(__name__, 1, "***forward request") - - try: - DNS_forward = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - DNS_forward.settimeout(5.0) - except Exception, e: - dprint(__name__, 0, "Failed to create socket for DNS_forward): {0}", e) - continue - - DNS_forward.sendto(data, (cfg_IP_DNSMaster, 53)) - paket, addr_master = DNS_forward.recvfrom(1024) - DNS_forward.close() - # todo: double check: ID has to be the same! - # todo: spawn thread to wait in parallel - dprint(__name__, 1, "-> DNS response from higher level") - - #print "-> respond back:" - #printDNSdata(paket) - - # todo: double check: ID has to be the same! - DNS.sendto(paket, addr) - - except socket.timeout: - pass - - except socket.error as e: - dprint(__name__, 1, "Warning: DNS error ({0}): {1}", e.errno, e.strerror) - - except KeyboardInterrupt: - signal.signal(signal.SIGINT, signal.SIG_IGN) # we heard you! - dprint(__name__, 0, "^C received.") - finally: - dprint(__name__, 0, "Shutting down.") - DNS.close() - - - -if __name__ == '__main__': - cmdPipe = Pipe() - - cfg = Settings.CSettings() - param = {} - param['CSettings'] = cfg - - param['IP_self'] = '192.168.178.20' # IP_self? - param['baseURL'] = 'http://'+ param['IP_self'] +':'+ cfg.getSetting('port_webserver') - param['HostToIntercept'] = cfg.getSetting('hosttointercept') - - Run(cmdPipe[1], param) + if cfg.getSetting('use_custom_dns_bind_ip') == "True": + intercept_ip = cfg.getSetting('custom_dns_bind_ip') + else: + intercept_ip = cfg_IP_self + + cfg_Port_DNSServer = int(cfg.getSetting('port_dnsserver')) + if not cfg_Port_DNSServer: + cfg_Port_DNSServer = 53 + cfg_IP_DNSMaster = cfg.getSetting('ip_dnsmaster') + + intercept = [param['HostToIntercept']] + restrain = [] + if cfg.getSetting('intercept_atv_icon') == 'True': + intercept.append('a1.phobos.apple.com') + dprint(__name__, 0, "Intercept Atv Icon: Enabled") + if cfg.getSetting('prevent_atv_update') == 'True': + restrain = ['mesu.apple.com', 'appldnld.apple.com', + 'appldnld.apple.com.edgesuite.net'] + dprint(__name__, 0, "Prevent Atv Update: Enabled") + + dprint(__name__, 0, "***") + dprint(__name__, 0, + f"DNSServer: Serving DNS on {cfg_IP_self} port {cfg_Port_DNSServer}.") + dprint(__name__, 1, f"intercept: {intercept} => {intercept_ip}") + dprint(__name__, 1, f"restrain: {restrain} => NXDOMAIN") + dprint(__name__, 1, + f"forward other to higher level DNS: {cfg_IP_DNSMaster}") + dprint(__name__, 0, "***") + + intercept_records = [f"{i} 300 IN A {intercept_ip}" for i in intercept] + + resolver = InterceptResolver( + address=cfg_IP_DNSMaster, + port=53, + intercept=intercept_records, + nxdomain=restrain, + skip=[], + forward=[], + all_qtypes=True, + ttl="30s", + timeout=30 + ) + logger = dnslib.server.DNSLogger(prefix=False) + self.server = dnslib.server.DNSServer( + resolver, address=cfg_IP_self, port=cfg_Port_DNSServer, logger=logger, + tcp=False) + + def start_thread(self): + self.server.start_thread() + + def stop(self): + self.server.stop() + + def isAlive(self): + self.server.isAlive() diff --git a/Debug.py b/Debug.py index 987a1f85..6659c852 100755 --- a/Debug.py +++ b/Debug.py @@ -9,21 +9,19 @@ 2 - lower debug data, function input values, intermediate info... """ -dlevels = { "PlexConnect": 0, \ - "PlexAPI" : 0, \ - "DNSServer" : 1, \ - "WebServer" : 1, \ - "XMLConverter" : 0, \ - "Settings" : 0, \ - "ATVSettings": 0, \ - "Localize" : 0, \ - "ATVLogger" : 0, \ - "PILBackgrounds" : 0, \ - } - - - import time +dlevels = {"PlexConnect": 0, + "PlexAPI": 0, + "DNSServer": 1, + "WebServer": 1, + "XMLConverter": 0, + "Settings": 0, + "ATVSettings": 0, + "Localize": 0, + "ATVLogger": 0, + "PILBackgrounds": 0, + } + try: import xml.etree.cElementTree as etree @@ -31,98 +29,93 @@ import xml.etree.ElementTree as etree - g_logfile = '' g_loglevel = 0 -def dinit(src, param, newlog=False): + +def dinit(src, param, newlog=False): if 'LogFile' in param: global g_logfile g_logfile = param['LogFile'] - + if 'LogLevel' in param: global g_loglevel - g_loglevel = { "Normal": 0, "High": 2, "Off": -1 }.get(param['LogLevel'], 0) - - if not g_loglevel==-1 and not g_logfile=='' and newlog: + g_loglevel = {"Normal": 0, "High": 2, + "Off": -1}.get(param['LogLevel'], 0) + + if not g_loglevel == -1 and not g_logfile == '' and newlog: f = open(g_logfile, 'w') f.close() - - dprint(src, 0, "Started") + dprint(src, 0, "Started") def dprint(src, dlevel, *args): logToTerminal = not (src in dlevels) or dlevel <= dlevels[src] - logToFile = not g_loglevel==-1 and not g_logfile=='' and dlevel <= g_loglevel - + logToFile = not g_loglevel == -1 and not g_logfile == '' and dlevel <= g_loglevel + if logToTerminal or logToFile: asc_args = list(args) - - for i,arg in enumerate(asc_args): + + for i, arg in enumerate(asc_args): if etree.iselement(asc_args[i]): asc_args[i] = prettyXML(asc_args[i]) - - if isinstance(asc_args[i], str): - asc_args[i] = asc_args[i].decode('utf-8', 'replace') # convert as utf-8 just in case - if isinstance(asc_args[i], unicode): - asc_args[i] = asc_args[i].encode('ascii', 'replace') # back to ascii - + # print to file (if filename defined) if logToFile: - f = open(g_logfile, 'a') - f.write(time.strftime("%b %d,%Y %H:%M:%S ")) - if len(asc_args)==0: - f.write(src+":\n") - elif len(asc_args)==1: - f.write(src+": "+asc_args[0]+"\n") - else: - f.write(src+": "+asc_args[0].format(*asc_args[1:])+"\n") - f.close() - + with open(g_logfile, 'a') as f: + f.write(time.strftime("%b %d,%Y %H:%M:%S ")) + if len(asc_args) == 0: + f.write(f"{src}:\n") + elif len(asc_args) == 1: + f.write(f"{src}: {asc_args[0]}\n") + else: + f.write(f"{src}: {asc_args[0].format(*asc_args[1:])}\n") + # print to terminal window if logToTerminal: - print(time.strftime("%b %d,%Y %H:%M:%S")), - if len(asc_args)==0: - print src+":" - elif len(asc_args)==1: - print src+": "+str(asc_args[0]) + print((time.strftime("%b %d,%Y %H:%M:%S")), end=' ') + if len(asc_args) == 0: + print(f"{src}:") + elif len(asc_args) == 1: + print(f"{src}: {str(asc_args[0])}") else: - print src+": "+asc_args[0].format(*asc_args[1:]) - + print(f"{src}: {asc_args[0].format(*asc_args[1:])}") """ # XML in-place prettyprint formatter # Source: http://stackoverflow.com/questions/749796/pretty-printing-xml-in-python """ + + def indent(elem, level=0): - i = "\n" + level*" " + i = f"\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: - indent(elem, level+1) + indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i + def prettyXML(elem): indent(elem) return(etree.tostring(elem)) +if __name__ == "__main__": + dinit('Debug', {'LogFile': 'Debug.log'}, True) # True -> new file + dinit('unknown', {'Logfile': 'Debug.log'}) # False/Dflt -> append to file -if __name__=="__main__": - dinit('Debug', {'LogFile':'Debug.log'}, True) # True -> new file - dinit('unknown', {'Logfile':'Debug.log'}) # False/Dflt -> append to file - dprint('unknown', 0, "debugging {0}", __name__) dprint('unknown', 1, "level 1") - + dprint('PlexConnect', 0, "debugging {0}", 'PlexConnect') dprint('PlexConnect', 1, "level") diff --git a/Localize.py b/Localize.py index 7eca9efe..ec02017d 100755 --- a/Localize.py +++ b/Localize.py @@ -9,13 +9,14 @@ from Debug import * # dprint() - g_Translations = {} + def getTranslation(language): global g_Translations if language not in g_Translations: - filename = os.path.join(sys.path[0], 'assets', 'locales', language, 'plexconnect.mo') + filename = os.path.join( + sys.path[0], 'assets', 'locales', language, 'plexconnect.mo') try: fp = open(filename, 'rb') g_Translations[language] = gettext.GNUTranslations(fp) @@ -25,45 +26,45 @@ def getTranslation(language): return g_Translations[language] - def pickLanguage(languages): language = 'en' language_aliases = { 'zh_TW': 'zh_Hant', 'zh_CN': 'zh_Hans' } - - languages = re.findall('(\w{2}(?:[-_]\w{2,})?)(?:;q=(\d+(?:\.\d+)?))?', languages) - languages = [(lang.replace('-', '_'), float(quot) if quot else 1.) for (lang, quot) in languages] - languages = [(language_aliases.get(lang, lang), quot) for (lang, quot) in languages] + + languages = re.findall( + '(\w{2}(?:[-_]\w{2,})?)(?:;q=(\d+(?:\.\d+)?))?', languages) + languages = [(lang.replace('-', '_'), float(quot) if quot else 1.) + for (lang, quot) in languages] + languages = [(language_aliases.get(lang, lang), quot) + for (lang, quot) in languages] languages = sorted(languages, key=itemgetter(1), reverse=True) for lang, quot in languages: if os.path.exists(os.path.join(sys.path[0], 'assets', 'locales', lang, 'plexconnect.mo')): - language = lang - break + language = lang + break dprint(__name__, 1, "aTVLanguage: "+language) return(language) - def replaceTEXT(textcontent, language): translation = getTranslation(language) for msgid in set(re.findall(r'\{\{TEXT\((.+?)\)\}\}', textcontent)): - msgstr = translation.ugettext(msgid).replace('\"', '\\\"') + msgstr = translation.gettext(msgid).replace('\"', '\\\"') textcontent = textcontent.replace('{{TEXT(%s)}}' % msgid, msgstr) return textcontent - -if __name__=="__main__": +if __name__ == "__main__": languages = "de;q=0.9, en;q=0.8" language = pickLanguage(languages) - + Text = "Hello World" # doesn't translate - print getTranslation(language).ugettext(Text) - + print(getTranslation(language).gettext(Text)) + Text = "Library" # translates - print getTranslation(language).ugettext(Text) - + print(getTranslation(language).gettext(Text)) + Text = "{{TEXT(Channels)}}" # translates - print replaceTEXT(Text, language).encode('ascii', 'replace') + print(replaceTEXT(Text, language).encode('ascii', 'replace')) diff --git a/PILBackgrounds.py b/PILBackgrounds.py index 8d506dc6..ac031d6e 100755 --- a/PILBackgrounds.py +++ b/PILBackgrounds.py @@ -3,14 +3,14 @@ import re import sys import io -import urllib -import urllib2 -import urlparse +import urllib.request +import urllib.parse +import urllib.error import posixpath import traceback import os.path -from Debug import * +from Debug import * try: from PIL import Image, ImageFilter @@ -19,27 +19,30 @@ __isPILinstalled = False - def generate(PMS_uuid, url, authtoken, resolution, blurRadius, CSettings): cachepath = sys.path[0]+"/assets/fanartcache" stylepath = sys.path[0]+"/assets/thumbnails" # Create cache filename - id = re.search('/library/metadata/(?P\S+)/art/(?P\S+)', url) + id = re.search( + '/library/metadata/(?P\S+)/art/(?P\S+)', url) if id: # assumes URL in format "/library/metadata//art/fileId>" id = id.groupdict() - cachefile = urllib.quote_plus(PMS_uuid +"_"+ id['ratingKey'] +"_"+ id['fileId'] +"_"+ resolution +"_"+ blurRadius) + ".jpg" + cachefile = urllib.parse.quote_plus( + PMS_uuid + "_" + id['ratingKey'] + "_" + id['fileId'] + "_" + resolution + "_" + blurRadius) + ".jpg" else: - fileid = posixpath.basename(urlparse.urlparse(url).path) - cachefile = urllib.quote_plus(PMS_uuid +"_"+ fileid +"_"+ resolution +"_"+ blurRadius) + ".jpg" # quote: just to make sure... - + fileid = posixpath.basename(urllib.parse.urlparse(url).path) + # quote: just to make sure... + cachefile = urllib.parse.quote_plus( + PMS_uuid + "_" + fileid + "_" + resolution + "_" + blurRadius) + ".jpg" + # Already created? dprint(__name__, 1, 'Check for Cachefile.') # Debug if os.path.isfile(cachepath+"/"+cachefile): dprint(__name__, 1, 'Cachefile found.') # Debug return "/fanartcache/"+cachefile - + # No! Request Background from PMS dprint(__name__, 1, 'No Cachefile found. Generating Background.') # Debug try: @@ -47,21 +50,22 @@ def generate(PMS_uuid, url, authtoken, resolution, blurRadius, CSettings): xargs = {} if authtoken: xargs['X-Plex-Token'] = authtoken - request = urllib2.Request(url, None, xargs) - response = urllib2.urlopen(request).read() + request = urllib.request.Request(url, None, xargs) + response = urllib.request.urlopen(request).read() background = Image.open(io.BytesIO(response)) - except urllib2.URLError as e: + except urllib.error.URLError as e: dprint(__name__, 0, 'URLError: {0} // url: {1}', e.reason, url) return "/thumbnails/Background_blank_" + resolution + ".jpg" - except urllib2.HTTPError as e: - dprint(__name__, 0, 'HTTPError: {0} {1} // url: {2}', str(e.code), e.msg, url) + except urllib.error.HTTPError as e: + dprint(__name__, 0, + 'HTTPError: {0} {1} // url: {2}', str(e.code), e.msg, url) return "/thumbnails/Background_blank_" + resolution + ".jpg" except IOError as e: dprint(__name__, 0, 'IOError: {0} // url: {1}', str(e), url) return "/thumbnails/Background_blank_" + resolution + ".jpg" - + blurRadius = int(blurRadius) - + # Get gradient template dprint(__name__, 1, 'Merging Layers.') # Debug if resolution == '1080': @@ -75,47 +79,47 @@ def generate(PMS_uuid, url, authtoken, resolution, blurRadius, CSettings): blurRegion = (0, 342, 1280, 720) blurRadius = int(blurRadius / 1.5) layer = Image.open(stylepath + "/gradient_720.png") - + try: # Set background resolution and merge layers bgWidth, bgHeight = background.size - dprint(__name__,1 ,"Background size: {0}, {1}", bgWidth, bgHeight) - dprint(__name__,1 , "aTV Height: {0}, {1}", width, height) - + dprint(__name__, 1, "Background size: {0}, {1}", bgWidth, bgHeight) + dprint(__name__, 1, "aTV Height: {0}, {1}", width, height) + if bgHeight != height: - if CSettings.getSetting('fanart_quality')=='High': - background = background.resize((width, height), Image.ANTIALIAS) + if CSettings.getSetting('fanart_quality') == 'High': + background = background.resize( + (width, height), Image.ANTIALIAS) else: background = background.resize((width, height), Image.NEAREST) - dprint(__name__,1 , "Resizing background") - + dprint(__name__, 1, "Resizing background") + if blurRadius != 0: - dprint(__name__,1 , "Blurring Lower Region") + dprint(__name__, 1, "Blurring Lower Region") imgBlur = background.crop(blurRegion) imgBlur = imgBlur.filter(ImageFilter.GaussianBlur(blurRadius)) background.paste(imgBlur, blurRegion) - - background.paste(layer, ( 0, 0), layer) - + + background.paste(layer, (0, 0), layer) + # Save to Cache background.save(cachepath+"/"+cachefile) except: - dprint(__name__, 0, 'Error - Failed to generate background image.\n{0}', traceback.format_exc()) + dprint( + __name__, 0, 'Error - Failed to generate background image.\n{0}', traceback.format_exc()) return "/thumbnails/Background_blank_" + resolution + ".jpg" - + dprint(__name__, 1, 'Cachefile generated.') # Debug return "/fanartcache/"+cachefile - # HELPERS def isPILinstalled(): return __isPILinstalled - -if __name__=="__main__": +if __name__ == "__main__": url = "https://thetvdb.com/banners/fanart/original/95451-23.jpg" res = generate('uuid', url, 'authtoken', '1080') res = generate('uuid', url, 'authtoken', '720') diff --git a/PlexAPI.py b/PlexAPI.py index 4aaf8e33..097e5333 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -28,27 +28,28 @@ """ - import sys import struct import time -import urllib2, httplib, socket, StringIO, gzip +import urllib.request +import urllib.error +import urllib.parse +import http.client +import socket +import io +import gzip from threading import Thread -import Queue +import queue import traceback -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree +import xml.etree.ElementTree as etree -from urllib import urlencode, quote_plus +from urllib.parse import urlencode, quote_plus from Version import __VERSION__ from Debug import * # dprint(), prettyXML() - """ storage for PMS addresses and additional information - now per aTV! (replaces global PMS_list) syntax: PMS[][PMS_UUID][] @@ -65,68 +66,59 @@ uuid - PMS ID name, scheme, ip, port, type, owned, token """ + + def declarePMS(ATV_udid, uuid, name, scheme, ip, port): # store PMS information in g_PMS database global g_PMS - if not ATV_udid in g_PMS: + if ATV_udid not in g_PMS: g_PMS[ATV_udid] = {} - - address = ip + ':' + port - baseURL = scheme+'://'+ip+':'+port - g_PMS[ATV_udid][uuid] = { 'name': name, - 'scheme':scheme, 'ip': ip , 'port': port, - 'address': address, - 'baseURL': baseURL, - 'local': '1', - 'owned': '1', - 'accesstoken': '', - 'enableGzip': False - } + + address = f"{ip}:{port}" + baseURL = f"{scheme}://{address}" + g_PMS[ATV_udid][uuid] = {'name': name, + 'scheme': scheme, 'ip': ip, 'port': port, + 'address': address, + 'baseURL': baseURL, + 'local': '1', + 'owned': '1', + 'accesstoken': '', + 'enableGzip': False + } + def updatePMSProperty(ATV_udid, uuid, tag, value): # set property element of PMS by UUID - if not ATV_udid in g_PMS: + if ATV_udid not in g_PMS: return '' # no server known for this aTV - if not uuid in g_PMS[ATV_udid]: + if uuid not in g_PMS[ATV_udid]: return '' # requested PMS not available - g_PMS[ATV_udid][uuid][tag] = value + def getPMSProperty(ATV_udid, uuid, tag): - # get name of PMS by UUID - if not ATV_udid in g_PMS: - return '' # no server known for this aTV - if not uuid in g_PMS[ATV_udid]: - return '' # requested PMS not available - - return g_PMS[ATV_udid][uuid].get(tag, '') + return g_PMS.get(ATV_udid, {}).get(uuid, {}).get(tag, '') + def getPMSFromAddress(ATV_udid, address): # find PMS by IP, return UUID - if not ATV_udid in g_PMS: - return '' # no server known for this aTV - - for uuid in g_PMS[ATV_udid]: - if address in g_PMS[ATV_udid][uuid].get('address', None): + for uuid in g_PMS.get(ATV_udid, []): + if address in g_PMS[ATV_udid][uuid].get('address', []): return uuid return '' # IP not found + def getPMSAddress(ATV_udid, uuid): # get address of PMS by UUID - if not ATV_udid in g_PMS: - return '' # no server known for this aTV - if not uuid in g_PMS[ATV_udid]: - return '' # requested PMS not available - - return g_PMS[ATV_udid][uuid]['ip'] + ':' + g_PMS[ATV_udid][uuid]['port'] + retval = g_PMS.get(ATV_udid, {}).get(uuid, {}).get('ip', '') + if retval: + retval = f"{retval}:{g_PMS.get(ATV_udid, {}).get(uuid, {}).get('port', '')}" + return retval + def getPMSCount(ATV_udid): # get count of discovered PMS by UUID - if not ATV_udid in g_PMS: - return 0 # no server known for this aTV - - return len(g_PMS[ATV_udid]) - + return len(g_PMS.get(ATV_udid, [])) """ @@ -141,24 +133,24 @@ def getPMSCount(ATV_udid): Port_PlexGDM = 32414 Msg_PlexGDM = 'M-SEARCH * HTTP/1.0' + def PlexGDM(): dprint(__name__, 0, "***") dprint(__name__, 0, "PlexGDM - looking up Plex Media Server") dprint(__name__, 0, "***") - + # setup socket for discovery -> multicast message GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) GDM.settimeout(1.0) - # Set the time-to-live for messages to 1 for local network ttl = struct.pack('b', 1) GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - + returnData = [] try: # Send data to the multicast group - dprint(__name__, 1, "Sending discovery message: {0}", Msg_PlexGDM) - GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM)) + dprint(__name__, 1, f"Sending discovery message: {Msg_PlexGDM}") + GDM.sendto(bytes(Msg_PlexGDM, "utf8"), (IP_PlexGDM, Port_PlexGDM)) # Look for responses from all recipients while True: @@ -166,10 +158,12 @@ def PlexGDM(): data, server = GDM.recvfrom(1024) dprint(__name__, 1, "Received data from {0}", server) dprint(__name__, 1, "Data received:\n {0}", data) - returnData.append( { 'from' : server, - 'data' : data } ) + returnData.append({'from': server, + 'data': data}) except socket.timeout: break + except Exception as e: + print(str(e)) finally: GDM.close() @@ -178,41 +172,41 @@ def PlexGDM(): PMS_list = {} if returnData: for response in returnData: - update = { 'ip' : response.get('from')[0] } - - # Check if we had a positive HTTP response - if "200 OK" in response.get('data'): - for each in response.get('data').split('\n'): + update = {'ip': response.get('from')[0]} + + # Check if we had a positive HTTP response + if "200 OK" in response.get('data', b'').decode(): + for each in response.get('data').decode().split('\n'): # decode response data update['discovery'] = "auto" - #update['owned']='1' - #update['master']= 1 - #update['role']='master' - + # update['owned']='1' + # pdate['master']= 1 + # update['role']='master' + if "Content-Type:" in each: update['content-type'] = each.split(':')[1].strip() elif "Resource-Identifier:" in each: update['uuid'] = each.split(':')[1].strip() elif "Name:" in each: - update['serverName'] = each.split(':')[1].strip().decode('utf-8', 'replace') # store in utf-8 + update['serverName'] = each.split(':')[1].strip() elif "Port:" in each: update['port'] = each.split(':')[1].strip() elif "Updated-At:" in each: update['updated'] = each.split(':')[1].strip() elif "Version:" in each: update['version'] = each.split(':')[1].strip() - + PMS_list[update['uuid']] = update - - if PMS_list=={}: + + if PMS_list == {}: dprint(__name__, 0, "GDM: No servers discovered") else: dprint(__name__, 0, "GDM: Servers discovered: {0}", len(PMS_list)) for uuid in PMS_list: - dprint(__name__, 1, "{0} {1}:{2}", PMS_list[uuid]['serverName'], PMS_list[uuid]['ip'], PMS_list[uuid]['port']) - - return PMS_list + dprint(__name__, 1, "{0} {1}:{2}", + PMS_list[uuid]['serverName'], PMS_list[uuid]['ip'], PMS_list[uuid]['port']) + return PMS_list """ @@ -227,80 +221,90 @@ def PlexGDM(): result: g_PMS database for ATV_udid """ + + def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): global g_PMS g_PMS[ATV_udid] = {} - + # install plex.tv "virtual" PMS - for myPlex, PlexHome declarePMS(ATV_udid, 'plex.tv', 'plex.tv', 'https', 'plex.tv', '443') updatePMSProperty(ATV_udid, 'plex.tv', 'local', '-') updatePMSProperty(ATV_udid, 'plex.tv', 'owned', '-') - updatePMSProperty(ATV_udid, 'plex.tv', 'accesstoken', tokenDict.get('MyPlex', '')) - - #debug - #declarePMS(ATV_udid, '2ndServer', '2ndServer', 'http', '192.168.178.22', '32400', 'local', '1', 'token') - #declarePMS(ATV_udid, 'remoteServer', 'remoteServer', 'http', '127.0.0.1', '1234', 'myplex', '1', 'token') - #debug - + updatePMSProperty(ATV_udid, 'plex.tv', 'accesstoken', + tokenDict.get('MyPlex', '')) + + # # debug + # declarePMS(ATV_udid, '2ndServer', '2ndServer', 'http', '192.168.178.22', '32400', 'local', '1', 'token') + # declarePMS(ATV_udid, 'remoteServer', 'remoteServer', 'http', '127.0.0.1', '1234', 'myplex', '1', 'token') + # # debug + if 'PlexHome' in tokenDict: authtoken = tokenDict.get('PlexHome') else: authtoken = tokenDict.get('MyPlex', '') - - if authtoken=='': - # not logged into myPlex - # local PMS - if CSettings.getSetting('enable_plexgdm')=='False': - # defined in setting.cfg - ip = CSettings.getSetting('ip_pms') - # resolve hostname if needed - try: - ip2 = socket.gethostbyname(ip) - if ip != ip2: - dprint(__name__, 0, "PlexAPI - Hostname "+ip+" resolved to "+ip2) - ip = ip2 - except: - dprint(__name__, 0, "PlexAPI - ip_dns "+ip+" could not be resolved") - - port = CSettings.getSetting('port_pms') - XML = getXMLFromPMS('http://'+ip+':'+port, '/servers', None, '') - - if XML==False: - pass # no response from manual defined server (Settings.cfg) + + if authtoken == '': + # not logged into myPlex + # local PMS + if CSettings.getSetting('enable_plexgdm') == 'False': + dprint(__name__, 0, f"PlexAPI - Not using plexgdm") + # defined in setting.cfg + ip = CSettings.getSetting('ip_pms') + # resolve hostname if needed + try: + ip2 = socket.gethostbyname(ip) + if ip != ip2: + dprint(__name__, 0, + f"PlexAPI - Hostname {ip} resolved to {ip2}") + ip = ip2 + except Exception: + dprint(__name__, 0, + f"PlexAPI - ip_dns {ip} could not be resolved") + + port = CSettings.getSetting('port_pms') + XML = getXMLFromPMS(f'http://{ip}:{port}', '/servers', None, '') + + if not XML: + pass # no response from manual defined server (Settings.cfg) + else: + Server = XML.find('Server') + uuid = Server.get('machineIdentifier') + name = Server.get('name') + + # dflt: token='', local, owned + declarePMS(ATV_udid, uuid, name, 'http', ip, port) + # todo - check IP to verify "local"? + else: - Server = XML.find('Server') - uuid = Server.get('machineIdentifier') - name = Server.get('name') - - declarePMS(ATV_udid, uuid, name, 'http', ip, port) # dflt: token='', local, owned - # todo - check IP to verify "local"? - - else: - # PlexGDM - PMS_list = PlexGDM() - for uuid in PMS_list: - PMS = PMS_list[uuid] - declarePMS(ATV_udid, PMS['uuid'], PMS['serverName'], 'http', PMS['ip'], PMS['port']) # dflt: token='', local, owned - + # PlexGDM + dprint(__name__, 0, f"PlexAPI - trying the PlexGDM()") + + PMS_list = PlexGDM() + for (uuid, PMS) in PMS_list.items(): + # dflt: token='', local, owned + declarePMS( + ATV_udid, PMS['uuid'], PMS['serverName'], 'http', PMS['ip'], PMS['port']) + else: # MyPlex servers getPMSListFromMyPlex(ATV_udid, authtoken) - + # all servers - update enableGzip for uuid in g_PMS.get(ATV_udid, {}): # enable Gzip if not on same host, local&remote PMS depending on setting - enableGzip = (not getPMSProperty(ATV_udid, uuid, 'ip')==IP_self) and ( \ - (getPMSProperty(ATV_udid, uuid, 'local')=='1' and CSettings.getSetting('allow_gzip_pmslocal')=='True' ) or \ - (getPMSProperty(ATV_udid, uuid, 'local')=='0' and CSettings.getSetting('allow_gzip_pmsremote')=='True') ) + enableGzip = (not getPMSProperty(ATV_udid, uuid, 'ip') == IP_self) and ( + (getPMSProperty(ATV_udid, uuid, 'local') == '1' and CSettings.getSetting('allow_gzip_pmslocal') == 'True') or + (getPMSProperty(ATV_udid, uuid, 'local') == '0' and CSettings.getSetting('allow_gzip_pmsremote') == 'True')) updatePMSProperty(ATV_udid, uuid, 'enableGzip', enableGzip) - + # debug print all servers - dprint(__name__, 0, "Plex Media Servers found: {0}", len(g_PMS[ATV_udid])-1) + dprint(__name__, 0, "Plex Media Servers found: {0}", len( + g_PMS[ATV_udid]) - 1) for uuid in g_PMS[ATV_udid]: dprint(__name__, 1, str(g_PMS[ATV_udid][uuid])) - """ getPMSListFromMyPlex @@ -308,55 +312,55 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): poke every PMS at every given address (plex.tv tends to cache a LOT...) -> by design this leads to numerous threads ending in URLErrors like or """ + + def getPMSListFromMyPlex(ATV_udid, authtoken): dprint(__name__, 0, "***") dprint(__name__, 0, "poke plex.tv - request Plex Media Server list") dprint(__name__, 0, "***") - - XML = getXMLFromPMS('https://plex.tv', '/api/resources?includeHttps=1', {}, authtoken) - - if XML==False: + + XML = getXMLFromPMS('https://plex.tv', + '/api/resources?includeHttps=1', {}, authtoken) + if not XML: pass # no data from MyPlex else: - queue = Queue.Queue() + q = queue.Queue() threads = [] PMSsPoked = 0 - - for Dir in XML.getiterator('Device'): - if Dir.get('product','') == "Plex Media Server" and Dir.get('provides','') == "server": + for Dir in XML.iter('Device'): + if Dir.get('product', '') == "Plex Media Server" and Dir.get('provides', '') == "server": uuid = Dir.get('clientIdentifier') name = Dir.get('name') token = Dir.get('accessToken', authtoken) owned = Dir.get('owned', '0') - - if Dir.find('Connection') == None: + + if not Dir.findall('Connection'): continue # no valid connection - skip - - PMSsPoked +=1 - + + PMSsPoked += 1 # multiple connection possible - poke either one, fastest response wins - for Con in Dir.getiterator('Connection'): + for Con in Dir.iter('Connection'): protocol = Con.get('protocol') ip = Con.get('address') port = Con.get('port') uri = Con.get('uri') local = Con.get('local') - + # change protocol and uri if in local if local == "1": protocol = "http" uri = protocol + "://" + ip + ":" + port - # poke PMS, own thread for each poke - PMSInfo = { 'uuid': uuid, 'name': name, 'token': token, 'owned': owned, 'local': local, \ - 'protocol': protocol, 'ip': ip, 'port': port, 'uri': uri } - PMS = { 'baseURL': uri, 'path': '/', 'options': None, 'token': token, \ - 'data': PMSInfo } - dprint(__name__, 0, "poke {0} ({1}) at {2}", name, uuid, uri) - t = Thread(target=getXMLFromPMSToQueue, args=(PMS, queue)) + PMSInfo = {'uuid': uuid, 'name': name, 'token': token, 'owned': owned, 'local': local, + 'protocol': protocol, 'ip': ip, 'port': port, 'uri': uri} + PMS = {'baseURL': uri, 'path': '/', 'options': None, 'token': token, + 'data': PMSInfo} + dprint(__name__, 0, + "poke {0} ({1}) at {2}", name, uuid, uri) + t = Thread(target=getXMLFromPMSToQueue, args=(PMS, q)) t.start() threads.append(t) - + # wait for requests being answered # - either all communication threads done # - or at least one response received from every PMS (early exit) @@ -366,25 +370,24 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): # check for "living" threads - basically a manual t.join() ThreadsAlive = 0 for t in threads: - if t.isAlive(): + if t.is_alive(): ThreadsAlive += 1 - + # analyse PMS/http response - declare new PMS - if not queue.empty(): - (PMSInfo, PMS) = queue.get() - - if PMS==False: + if not q.empty(): + (PMSInfo, PMS) = q.get() + if not PMS: # communication error - skip this connection continue - + uuid = PMSInfo['uuid'] name = PMSInfo['name'] - + if uuid != PMS.getroot().get('machineIdentifier') or \ name != PMS.getroot().get('friendlyName'): # response from someone - but not the poked PMS - skip this connection continue - + token = PMSInfo['token'] owned = PMSInfo['owned'] local = PMSInfo['local'] @@ -392,28 +395,34 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): ip = PMSInfo['ip'] port = PMSInfo['port'] uri = PMSInfo['uri'] - - if not uuid in g_PMS[ATV_udid]: # PMS uuid not yet handled, so take it + + # PMS uuid not yet handled, so take it + if uuid not in g_PMS[ATV_udid]: PMSsCnt += 1 - - dprint(__name__, 0, "response {0} ({1}) at {2}", name, uuid, uri) - - declarePMS(ATV_udid, uuid, name, protocol, ip, port) # dflt: token='', local, owned - updated later + + dprint(__name__, 0, + "response {0} ({1}) at {2}", name, uuid, uri) + + # dflt: token='', local, owned - updated later + declarePMS(ATV_udid, uuid, name, protocol, ip, port) updatePMSProperty(ATV_udid, uuid, 'accesstoken', token) updatePMSProperty(ATV_udid, uuid, 'owned', owned) updatePMSProperty(ATV_udid, uuid, 'local', local) - updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) # set in declarePMS, overwrite for https encryption - elif local=='1': # Update udid if local instance is found - - dprint(__name__, 0, "update to {0} ({1}) at {2}", name, uuid, uri) - - declarePMS(ATV_udid, uuid, name, protocol, ip, port) # dflt: token='', local, owned - updated later + # set in declarePMS, overwrite for https encryption + updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) + elif local == '1': # Update udid if local instance is found + + dprint(__name__, 0, + "update to {0} ({1}) at {2}", name, uuid, uri) + + # dflt: token='', local, owned - updated later + declarePMS(ATV_udid, uuid, name, protocol, ip, port) updatePMSProperty(ATV_udid, uuid, 'accesstoken', token) updatePMSProperty(ATV_udid, uuid, 'owned', owned) updatePMSProperty(ATV_udid, uuid, 'local', local) - updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) # set in declarePMS, overwrite for https encryption - - + # set in declarePMS, overwrite for https encryption + updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) + """ Plex Media Server communication @@ -426,13 +435,15 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): result: returned XML or 'False' in case of error """ + + def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): xargs = {} - if not options==None: + if options: xargs = getXArgsDeviceInfo(options) - if not authtoken=='': + if authtoken: xargs['X-Plex-Token'] = authtoken - + dprint(__name__, 1, "URL: {0}{1}", baseURL, path) dprint(__name__, 1, "xargs: {0}", xargs) @@ -440,61 +451,59 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): if options is not None and 'PlexConnectMethod' in options: dprint(__name__, 1, 'Custom method ' + method) method = options['PlexConnectMethod'] - - request = urllib2.Request(baseURL+path , None, xargs) + + request = urllib.request.Request(baseURL + path, None, xargs) request.add_header('User-agent', 'PlexConnect') request.get_method = lambda: method if enableGzip: request.add_header('Accept-encoding', 'gzip') - + try: - response = urllib2.urlopen(request, timeout=20) - except (urllib2.URLError, httplib.HTTPException) as e: + response = urllib.request.urlopen(request, timeout=20) + except (urllib.error.URLError, http.client.HTTPException) as e: dprint(__name__, 1, 'No Response from Plex Media Server') if hasattr(e, 'reason'): - dprint(__name__, 1, "We failed to reach a server. Reason: {0}", e.reason) + dprint(__name__, 1, + "We failed to reach a server. Reason: {0}", e.reason) elif hasattr(e, 'code'): - dprint(__name__, 1, "The server couldn't fulfill the request. Error code: {0}", e.code) + dprint( + __name__, 1, "The server couldn't fulfill the request. Error code: {0}", e.code) dprint(__name__, 1, 'Traceback:\n{0}', traceback.format_exc()) return False except IOError: - dprint(__name__, 0, 'Error loading response XML from Plex Media Server:\n{0}', traceback.format_exc()) + dprint( + __name__, 0, 'Error loading response XML from Plex Media Server:\n{0}', traceback.format_exc()) return False - if response.info().get('Content-Encoding') == 'gzip': - buf = StringIO.StringIO(response.read()) + buf = io.StringIO(response.read()) file = gzip.GzipFile(fileobj=buf) XML = etree.parse(file) else: - # parse into etree XML = etree.parse(response) - dprint(__name__, 1, "====== received PMS-XML ======") dprint(__name__, 1, XML.getroot()) dprint(__name__, 1, "====== PMS-XML finished ======") - - #XMLTree = etree.ElementTree(etree.fromstring(response)) - return XML -def getXMLFromPMSToQueue(PMS, queue): - XML = getXMLFromPMS(PMS['baseURL'],PMS['path'],PMS['options'],PMS['token']) - queue.put( (PMS['data'], XML) ) - +def getXMLFromPMSToQueue(PMS, q): + XML = getXMLFromPMS(PMS['baseURL'], PMS['path'], + PMS['options'], PMS['token']) + q.put((PMS['data'], XML)) def getXArgsDeviceInfo(options={}): xargs = dict() xargs['X-Plex-Device'] = 'AppleTV' - xargs['X-Plex-Model'] = '2,3' # Base it on AppleTV model. - #if not options is None: + xargs['X-Plex-Model'] = '2,3' # Base it on AppleTV model. if 'PlexConnectUDID' in options: - xargs['X-Plex-Client-Identifier'] = options['PlexConnectUDID'] # UDID for MyPlex device identification + # UDID for MyPlex device identification + xargs['X-Plex-Client-Identifier'] = options['PlexConnectUDID'] if 'PlexConnectATVName' in options: - xargs['X-Plex-Device-Name'] = options['PlexConnectATVName'] # "friendly" name: aTV-Settings->General->Name. + # "friendly" name: aTV-Settings->General->Name. + xargs['X-Plex-Device-Name'] = options['PlexConnectATVName'] xargs['X-Plex-Platform'] = 'iOS' xargs['X-Plex-Client-Platform'] = 'iOS' xargs['X-Plex-Client-Profile-Extra'] = 'add-transcode-target(type=MusicProfile&context=streaming&protocol=hls&container=mpegts&audioCodec=aac)+add-transcode-target(type=videoProfile&context=streaming&protocol=hls&container=mpegts&videoCodec=h264&audioCodec=aac,mp3&replace=true)' @@ -505,9 +514,8 @@ def getXArgsDeviceInfo(options={}): xargs['X-Plex-Platform-Version'] = options['aTVFirmwareVersion'] xargs['X-Plex-Product'] = 'PlexConnect' xargs['X-Plex-Version'] = __VERSION__ - - return xargs + return xargs """ @@ -521,117 +529,134 @@ def getXArgsDeviceInfo(options={}): result: XML """ + + def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): - queue = Queue.Queue() + q = queue.Queue() threads = [] - + root = etree.Element("MediaConverter") - root.set('friendlyName', type+' Servers') - + root.set('friendlyName', type + ' Servers') + for uuid in g_PMS.get(ATV_udid, {}): - if (type=='all' and getPMSProperty(ATV_udid, uuid, 'name')!='plex.tv') or \ - (type=='owned' and getPMSProperty(ATV_udid, uuid, 'owned')=='1') or \ - (type=='shared' and getPMSProperty(ATV_udid, uuid, 'owned')=='0') or \ - (type=='local' and getPMSProperty(ATV_udid, uuid, 'local')=='1') or \ - (type=='remote' and getPMSProperty(ATV_udid, uuid, 'local')=='0'): + if (type == 'all' and getPMSProperty(ATV_udid, uuid, 'name') != 'plex.tv') or \ + (type == 'owned' and getPMSProperty(ATV_udid, uuid, 'owned') == '1') or \ + (type == 'shared' and getPMSProperty(ATV_udid, uuid, 'owned') == '0') or \ + (type == 'local' and getPMSProperty(ATV_udid, uuid, 'local') == '1') or \ + (type == 'remote' and getPMSProperty(ATV_udid, uuid, 'local') == '0'): Server = etree.SubElement(root, 'Server') # create "Server" node - Server.set('name', getPMSProperty(ATV_udid, uuid, 'name')) + Server.set('name', getPMSProperty(ATV_udid, uuid, 'name')) Server.set('address', getPMSProperty(ATV_udid, uuid, 'ip')) - Server.set('port', getPMSProperty(ATV_udid, uuid, 'port')) + Server.set('port', getPMSProperty(ATV_udid, uuid, 'port')) Server.set('baseURL', getPMSProperty(ATV_udid, uuid, 'baseURL')) - Server.set('local', getPMSProperty(ATV_udid, uuid, 'local')) - Server.set('owned', getPMSProperty(ATV_udid, uuid, 'owned')) - + Server.set('local', getPMSProperty(ATV_udid, uuid, 'local')) + Server.set('owned', getPMSProperty(ATV_udid, uuid, 'owned')) + baseURL = getPMSProperty(ATV_udid, uuid, 'baseURL') token = getPMSProperty(ATV_udid, uuid, 'accesstoken') PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')' - - Server.set('searchKey', PMS_mark + getURL('', '', '/Search/Entry.xml')) - + + Server.set('searchKey', PMS_mark + + getURL('', '', '/Search/Entry.xml')) + # request XMLs, one thread for each - PMS = { 'baseURL':baseURL, 'path':path, 'options':options, 'token':token, \ - 'data': {'uuid': uuid, 'Server': Server} } - t = Thread(target=getXMLFromPMSToQueue, args=(PMS, queue)) + PMS = {'baseURL': baseURL, 'path': path, 'options': options, 'token': token, + 'data': {'uuid': uuid, 'Server': Server}} + t = Thread(target=getXMLFromPMSToQueue, args=(PMS, q)) t.start() threads.append(t) - + # wait for requests being answered for t in threads: t.join() - + # add new data to root XML, individual Server - while not queue.empty(): - (data, XML) = queue.get() - uuid = data['uuid'] - Server = data['Server'] + while not q.empty(): + (data, XML) = q.get() + uuid = data['uuid'] + Server = data['Server'] - baseURL = getPMSProperty(ATV_udid, uuid, 'baseURL') - token = getPMSProperty(ATV_udid, uuid, 'accesstoken') - PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')' - - if XML==False: - Server.set('size', '0') - else: - Server.set('size', XML.getroot().get('size', '0')) - - for Dir in XML.getiterator('Directory'): # copy "Directory" content, add PMS to links - - if Dir.get('key') is not None and (Dir.get('agent') is not None or Dir.get('share') is not None): - key = Dir.get('key') # absolute path - Dir.set('key', PMS_mark + getURL('', path, key)) - Dir.set('refreshKey', getURL(baseURL, path, key) + '/refresh') - if 'thumb' in Dir.attrib: - Dir.set('thumb', PMS_mark + getURL('', path, Dir.get('thumb'))) - if 'art' in Dir.attrib: - Dir.set('art', PMS_mark + getURL('', path, Dir.get('art'))) - # print Dir.get('type') + baseURL = getPMSProperty(ATV_udid, uuid, 'baseURL') + token = getPMSProperty(ATV_udid, uuid, 'accesstoken') + PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')' + + if not XML: + Server.set('size', '0') + else: + Server.set('size', XML.getroot().get('size', '0')) + + # copy "Directory" content, add PMS to links + for Dir in XML.iter('Directory'): + + if Dir.get('key') is not None and (Dir.get('agent') is not None or Dir.get('share') is not None): + key = Dir.get('key') # absolute path + Dir.set('key', PMS_mark + getURL('', path, key)) + Dir.set('refreshKey', getURL( + baseURL, path, key) + '/refresh') + if 'thumb' in Dir.attrib: + Dir.set('thumb', PMS_mark + + getURL('', path, Dir.get('thumb'))) + if 'art' in Dir.attrib: + Dir.set('art', PMS_mark + + getURL('', path, Dir.get('art'))) + # print Dir.get('type') + Server.append(Dir) + elif Dir.get('title') == 'Live TV & DVR': + mp = None + for MediaProvider in XML.iter('MediaProvider'): + if MediaProvider.get('protocols') == 'livetv': + mp = MediaProvider + break + if mp is not None: + Dir.set('key', PMS_mark + + getURL('', '', mp.get('identifier'))) + Dir.set('refreshKey', getURL( + baseURL, '/livetv/dvrs', mp.get('parentID')) + '/reloadGuide') + Dir.set( + 'scanner', 'PlexConnect LiveTV Scanner Placeholder') + Dir.set('type', 'livetv') + Dir.set('thumbType', 'video') Server.append(Dir) - elif Dir.get('title') == 'Live TV & DVR': - mp = None - for MediaProvider in XML.getiterator('MediaProvider'): - if MediaProvider.get('protocols') == 'livetv': - mp = MediaProvider - break - if mp is not None: - Dir.set('key', PMS_mark + getURL('', '', mp.get('identifier'))) - Dir.set('refreshKey', getURL(baseURL, '/livetv/dvrs', mp.get('parentID')) + '/reloadGuide') - Dir.set('scanner', 'PlexConnect LiveTV Scanner Placeholder') - Dir.set('type', 'livetv') - Dir.set('thumbType', 'video') - Server.append(Dir) - - for Playlist in XML.getiterator('Playlist'): # copy "Playlist" content, add PMS to links - key = Playlist.get('key') # absolute path - Playlist.set('key', PMS_mark + getURL('', path, key)) - if 'composite' in Playlist.attrib: - Playlist.set('composite', PMS_mark + getURL('', path, Playlist.get('composite'))) - Server.append(Playlist) - - for Video in XML.getiterator('Video'): # copy "Video" content, add PMS to links - key = Video.get('key') # absolute path - Video.set('key', PMS_mark + getURL('', path, key)) - if 'thumb' in Video.attrib: - Video.set('thumb', PMS_mark + getURL('', path, Video.get('thumb'))) - if 'parentKey' in Video.attrib: - Video.set('parentKey', PMS_mark + getURL('', path, Video.get('parentKey'))) - if 'parentThumb' in Video.attrib: - Video.set('parentThumb', PMS_mark + getURL('', path, Video.get('parentThumb'))) - if 'grandparentKey' in Video.attrib: - Video.set('grandparentKey', PMS_mark + getURL('', path, Video.get('grandparentKey'))) - if 'grandparentThumb' in Video.attrib: - Video.set('grandparentThumb', PMS_mark + getURL('', path, Video.get('grandparentThumb'))) - Server.append(Video) - + + # copy "Playlist" content, add PMS to links + for Playlist in XML.iter('Playlist'): + key = Playlist.get('key') # absolute path + Playlist.set('key', PMS_mark + getURL('', path, key)) + if 'composite' in Playlist.attrib: + Playlist.set('composite', PMS_mark + + getURL('', path, Playlist.get('composite'))) + Server.append(Playlist) + + # copy "Video" content, add PMS to links + for Video in XML.iter('Video'): + key = Video.get('key') # absolute path + Video.set('key', PMS_mark + getURL('', path, key)) + if 'thumb' in Video.attrib: + Video.set('thumb', PMS_mark + + getURL('', path, Video.get('thumb'))) + if 'parentKey' in Video.attrib: + Video.set('parentKey', PMS_mark + + getURL('', path, Video.get('parentKey'))) + if 'parentThumb' in Video.attrib: + Video.set('parentThumb', PMS_mark + + getURL('', path, Video.get('parentThumb'))) + if 'grandparentKey' in Video.attrib: + Video.set('grandparentKey', PMS_mark + + getURL('', path, Video.get('grandparentKey'))) + if 'grandparentThumb' in Video.attrib: + Video.set('grandparentThumb', PMS_mark + + getURL('', path, Video.get('grandparentThumb'))) + Server.append(Video) + root.set('size', str(len(root.findall('Server')))) - + XML = etree.ElementTree(root) - + dprint(__name__, 1, "====== Local Server/Sections XML ======") dprint(__name__, 1, XML.getroot()) dprint(__name__, 1, "====== Local Server/Sections XML finished ======") - - return XML # XML representation - created "just in time". Do we need to cache it? + return XML # XML representation - created "just in time". Do we need to cache it? def getURL(baseURL, path, key): @@ -643,9 +668,8 @@ def getURL(baseURL, path, key): URL = baseURL + path else: # internal path, add-on URL = baseURL + path + '/' + key - - return URL + return URL """ @@ -659,17 +683,20 @@ def getURL(baseURL, path, key): username authtoken - token for subsequent communication with MyPlex """ + + def MyPlexSignIn(username, password, options): # MyPlex web address MyPlexHost = 'plex.tv' MyPlexSignInPath = '/users/sign_in.xml' MyPlexURL = 'https://' + MyPlexHost + MyPlexSignInPath - + # create POST request xargs = getXArgsDeviceInfo(options) - request = urllib2.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - + request = urllib.request.Request(MyPlexURL, None, xargs) + # turn into 'POST' - done automatically with data!=None. But we don't have data. + request.get_method = lambda: 'POST' + # no certificate, will fail with "401 - Authentification required" """ try: @@ -679,35 +706,35 @@ def MyPlexSignIn(username, password, options): print "has WWW_Authenticate:", e.headers.has_key('WWW-Authenticate') print """ - + # provide credentials - ### optional... when 'realm' is unknown - ##passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() - ##passmanager.add_password(None, address, username, password) # None: default "realm" - passmanager = urllib2.HTTPPasswordMgr() - passmanager.add_password(MyPlexHost, MyPlexURL, username, password) # realm = 'plex.tv' - authhandler = urllib2.HTTPBasicAuthHandler(passmanager) - urlopener = urllib2.build_opener(authhandler) - + # optional... when 'realm' is unknown + # # passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() + # passmanager.add_password(None, address, username, password) # None: default "realm" + passmanager = urllib.request.HTTPPasswordMgr() + passmanager.add_password(MyPlexHost, MyPlexURL, + username, password) # realm = 'plex.tv' + authhandler = urllib.request.HTTPBasicAuthHandler(passmanager) + urlopener = urllib.request.build_opener(authhandler) + # sign in, get MyPlex response try: response = urlopener.open(request).read() - except urllib2.HTTPError, e: - if e.code==401: + except urllib.error.HTTPError as e: + if e.code == 401: dprint(__name__, 0, 'Authentication failed') return ('', '') else: raise - dprint(__name__, 1, "====== MyPlex sign in XML ======") dprint(__name__, 1, response) dprint(__name__, 1, "====== MyPlex sign in XML finished ======") - + # analyse response XMLTree = etree.ElementTree(etree.fromstring(response)) - + el_username = XMLTree.find('username') - el_authtoken = XMLTree.find('authentication-token') + el_authtoken = XMLTree.find('authentication-token') if el_username is None or \ el_authtoken is None: username = '' @@ -717,9 +744,8 @@ def MyPlexSignIn(username, password, options): username = el_username.text authtoken = el_authtoken.text dprint(__name__, 0, 'MyPlex Sign In successfull') - - return (username, authtoken) + return (username, authtoken) def MyPlexSignOut(authtoken): @@ -727,56 +753,56 @@ def MyPlexSignOut(authtoken): MyPlexHost = 'plex.tv' MyPlexSignOutPath = '/users/sign_out.xml' MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath - + # create POST request - xargs = { 'X-Plex-Token': authtoken } - request = urllib2.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - - response = urllib2.urlopen(request).read() - + xargs = {'X-Plex-Token': authtoken} + request = urllib.request.Request(MyPlexURL, None, xargs) + # turn into 'POST' - done automatically with data!=None. But we don't have data. + request.get_method = lambda: 'POST' + + response = urllib.request.urlopen(request).read() + dprint(__name__, 1, "====== MyPlex sign out XML ======") dprint(__name__, 1, response) dprint(__name__, 1, "====== MyPlex sign out XML finished ======") dprint(__name__, 0, 'MyPlex Sign Out done') - def MyPlexSwitchHomeUser(id, pin, options, authtoken): MyPlexHost = 'https://plex.tv' MyPlexURL = MyPlexHost + '/api/home/users/' + id + '/switch' - + if pin: MyPlexURL += '?pin=' + pin - + xargs = {} if options: xargs = getXArgsDeviceInfo(options) xargs['X-Plex-Token'] = authtoken - - request = urllib2.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - - response = urllib2.urlopen(request).read() - + + request = urllib.request.Request(MyPlexURL, None, xargs) + # turn into 'POST' - done automatically with data!=None. But we don't have data. + request.get_method = lambda: 'POST' + + response = urllib.request.urlopen(request).read() + dprint(__name__, 1, "====== MyPlexHomeUser XML ======") dprint(__name__, 1, response) dprint(__name__, 1, "====== MyPlexHomeUser XML finished ======") - + # analyse response XMLTree = etree.ElementTree(etree.fromstring(response)) - + el_user = XMLTree.getroot() # root=. double check? username = el_user.attrib.get('title', '') authtoken = el_user.attrib.get('authenticationToken', '') - + if username and authtoken: dprint(__name__, 0, 'MyPlex switch HomeUser change successfull') else: dprint(__name__, 0, 'MyPlex switch HomeUser change failed') - - return (username, authtoken) + return (username, authtoken) """ @@ -793,18 +819,22 @@ def MyPlexSwitchHomeUser(id, pin, options, authtoken): result: final path to pull in PMS transcoder """ + + def getTranscodeVideoPath(path, AuthToken, options, action, quality, subtitle, audio, partIndex): UDID = options['PlexConnectUDID'] - + transcodePath = '/video/:/transcode/universal/start.m3u8?' - + vRes = quality[0] vQ = quality[1] mVB = quality[2] - dprint(__name__, 1, "Setting transcode quality Res:{0} Q:{1} {2}Mbps", vRes, vQ, mVB) - dprint(__name__, 1, "Subtitle: selected {0}, dontBurnIn {1}, size {2}", subtitle['selected'], subtitle['dontBurnIn'], subtitle['size']) + dprint(__name__, 1, + "Setting transcode quality Res:{0} Q:{1} {2}Mbps", vRes, vQ, mVB) + dprint(__name__, 1, "Subtitle: selected {0}, dontBurnIn {1}, size {2}", + subtitle['selected'], subtitle['dontBurnIn'], subtitle['size']) dprint(__name__, 1, "Audio: boost {0}", audio['boost']) - + args = dict() args['session'] = UDID args['protocol'] = 'hls' @@ -812,22 +842,23 @@ def getTranscodeVideoPath(path, AuthToken, options, action, quality, subtitle, a args['maxVideoBitrate'] = mVB args['videoQuality'] = vQ args['mediaBufferSize'] = '80000' - args['directStream'] = '0' if action=='Transcode' else '1' + args['directStream'] = '0' if action == 'Transcode' else '1' # 'directPlay' - handled by the client in MEDIARUL() args['subtitleSize'] = subtitle['size'] - args['skipSubtitles'] = subtitle['dontBurnIn'] #'1' # shut off PMS subtitles. Todo: skip only for aTV native/SRT (or other supported) + # '1' # shut off PMS subtitles. Todo: skip only for aTV native/SRT (or other supported) + args['skipSubtitles'] = subtitle['dontBurnIn'] args['audioBoost'] = audio['boost'] args['fastSeek'] = '1' args['path'] = path args['partIndex'] = partIndex - + xargs = getXArgsDeviceInfo(options) - xargs['X-Plex-Client-Capabilities'] = "protocols=http-live-streaming,http-mp4-streaming,http-streaming-video,http-streaming-video-720p,http-mp4-video,http-mp4-video-720p;videoDecoders=h264{profile:high&resolution:1080&level:41};audioDecoders=mp3,aac{bitrate:160000}" - if not AuthToken=='': + xargs[ + 'X-Plex-Client-Capabilities'] = "protocols=http-live-streaming,http-mp4-streaming,http-streaming-video,http-streaming-video-720p,http-mp4-video,http-mp4-video-720p;videoDecoders=h264{profile:high&resolution:1080&level:41};audioDecoders=mp3,aac{bitrate:160000}" + if not AuthToken == '': xargs['X-Plex-Token'] = AuthToken - - return transcodePath + urlencode(args) + '&' + urlencode(xargs) + return transcodePath + urlencode(args) + '&' + urlencode(xargs) """ @@ -841,22 +872,23 @@ def getTranscodeVideoPath(path, AuthToken, options, action, quality, subtitle, a result: final path to media file """ + + def getDirectVideoPath(key, AuthToken): if key.startswith('http://') or key.startswith('https://'): # external address - keep path = key else: - if AuthToken=='': + if AuthToken == '': path = key else: xargs = dict() xargs['X-Plex-Token'] = AuthToken - if key.find('?')==-1: + if key.find('?') == -1: path = key + '?' + urlencode(xargs) else: path = key + '&' + urlencode(xargs) - - return path + return path """ @@ -871,28 +903,30 @@ def getDirectVideoPath(key, AuthToken): result: final path to image file """ + + def getTranscodeImagePath(key, AuthToken, path, width, height): - if key.startswith('http://') or key.startswith('https://'): # external address - can we get a transcoding request for external images? + # external address - can we get a transcoding request for external images? + if key.startswith('http://') or key.startswith('https://'): path = key elif key.startswith('/'): # internal full path. path = 'http://127.0.0.1:32400' + key else: # internal path, add-on path = 'http://127.0.0.1:32400' + path + '/' + key path = path.encode('utf8') - + args = dict() args['width'] = width args['height'] = height args['url'] = path - - if not AuthToken=='': + + if not AuthToken == '': args['X-Plex-Token'] = AuthToken - + # ATV's cache ignores query strings, it does not ignore fragments though, so append the query as fragment as well. return '/photo/:/transcode' + '?' + urlencode(args) + '#' + urlencode(args) - """ Direct Image support @@ -902,17 +936,18 @@ def getTranscodeImagePath(key, AuthToken, path, width, height): result: final path to image file """ + + def getDirectImagePath(path, AuthToken): - if not AuthToken=='': + if not AuthToken == '': xargs = dict() xargs['X-Plex-Token'] = AuthToken - if path.find('?')==-1: + if path.find('?') == -1: path = path + '?' + urlencode(xargs) else: path = path + '&' + urlencode(xargs) - - return path + return path """ @@ -926,23 +961,24 @@ def getDirectImagePath(path, AuthToken): result: final path to pull in PMS transcoder """ + + def getTranscodeAudioPath(path, AuthToken, options, maxAudioBitrate): UDID = options['PlexConnectUDID'] - + transcodePath = '/music/:/transcode/universal/start.mp3?' - + args = dict() args['path'] = path args['session'] = UDID args['protocol'] = 'hls' args['maxAudioBitrate'] = maxAudioBitrate - + xargs = getXArgsDeviceInfo(options) - if not AuthToken=='': + if not AuthToken == '': xargs['X-Plex-Token'] = AuthToken - - return transcodePath + urlencode(args) + '&' + urlencode(xargs) + return transcodePath + urlencode(args) + '&' + urlencode(xargs) """ @@ -954,17 +990,18 @@ def getTranscodeAudioPath(path, AuthToken, options, maxAudioBitrate): result: final path to audio file """ + + def getDirectAudioPath(path, AuthToken): - if not AuthToken=='': + if not AuthToken == '': xargs = dict() xargs['X-Plex-Token'] = AuthToken - if path.find('?')==-1: + if path.find('?') == -1: path = path + '?' + urlencode(xargs) else: path = path + '&' + urlencode(xargs) - - return path + return path if __name__ == '__main__': @@ -974,55 +1011,50 @@ def getDirectAudioPath(path, AuthToken): testMyPlexXML = 0 testMyPlexSignIn = 0 testMyPlexSignOut = 0 - + username = 'abc' password = 'def' token = 'xyz' - - + # test PlexGDM if testPlexGDM: dprint('', 0, "*** PlexGDM") PMS_list = PlexGDM() dprint('', 0, PMS_list) - - + # test XML from local PMS if testLocalPMS: dprint('', 0, "*** XML from local PMS") XML = getXMLFromPMS('http://127.0.0.1:32400', '/library/sections') - - + # test local Server/Sections if testSectionXML: dprint('', 0, "*** local Server/Sections") PMS_list = PlexGDM() - XML = getSectionXML(PMS_list, {}, '') - - + XML = getXMLFromMultiplePMS(PMS_list, {}, '') + # test XML from MyPlex if testMyPlexXML: dprint('', 0, "*** XML from MyPlex") XML = getXMLFromPMS('https://plex.tv', '/pms/servers', None, token) - XML = getXMLFromPMS('https://plex.tv', '/pms/system/library/sections', None, token) - - + XML = getXMLFromPMS('https://plex.tv', + '/pms/system/library/sections', None, token) + # test MyPlex Sign In if testMyPlexSignIn: dprint('', 0, "*** MyPlex Sign In") - options = {'PlexConnectUDID':'007'} - + options = {'PlexConnectUDID': '007'} + (user, token) = MyPlexSignIn(username, password, options) - if user=='' and token=='': + if user == '' and token == '': dprint('', 0, "Authentication failed") else: dprint('', 0, "logged in: {0}, {1}", user, token) - - + # test MyPlex Sign out if testMyPlexSignOut: dprint('', 0, "*** MyPlex Sign Out") MyPlexSignOut(token) dprint('', 0, "logged out") - + # test transcoder diff --git a/PlexConnect.py b/PlexConnect.py index 21bb46d9..79cf2bf1 100755 --- a/PlexConnect.py +++ b/PlexConnect.py @@ -8,40 +8,44 @@ """ -import sys, time +import sys +import time from os import sep import socket from multiprocessing import Process, Pipe from multiprocessing.managers import BaseManager -import signal, errno +import signal +import errno import argparse from Version import __VERSION__ -import DNSServer, WebServer -import Settings, ATVSettings +import DNSServer +import WebServer +import Settings +import ATVSettings from PILBackgrounds import isPILinstalled from Debug import * # dprint() CONFIG_PATH = '.' + def getIP_self(): cfg = param['CSettings'] - if cfg.getSetting('enable_plexgdm')=='False': - dprint('PlexConnect', 0, "IP_PMS: "+cfg.getSetting('ip_pms')) - if cfg.getSetting('enable_plexconnect_autodetect')=='True': + if cfg.getSetting('enable_plexgdm') == 'False': + dprint('PlexConnect', 0, f"IP_PMS: {cfg.getSetting('ip_pms')}") + if cfg.getSetting('enable_plexconnect_autodetect') == 'True': # get public ip of machine running PlexConnect s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('1.2.3.4', 1000)) IP = s.getsockname()[0] - dprint('PlexConnect', 0, "IP_self: "+IP) + dprint('PlexConnect', 0, f"IP_self: {IP}") else: # manual override from "settings.cfg" IP = cfg.getSetting('ip_plexconnect') - dprint('PlexConnect', 0, "IP_self (from settings): "+IP) - - return IP + dprint('PlexConnect', 0, f"IP_self (from settings): {IP}") + return IP # initializer for Manager, proxy-ing ATVSettings to WebServer/XMLConverter @@ -49,22 +53,22 @@ def initProxy(): signal.signal(signal.SIGINT, signal.SIG_IGN) - procs = {} pipes = {} param = {} running = False + def startup(): global procs global pipes global param global running - + # Settings cfg = Settings.CSettings(CONFIG_PATH) param['CSettings'] = cfg - + # Logfile if cfg.getSetting('logpath').startswith('.'): # relative to current path @@ -72,49 +76,43 @@ def startup(): else: # absolute path logpath = cfg.getSetting('logpath') - + param['LogFile'] = logpath + sep + 'PlexConnect.log' param['LogLevel'] = cfg.getSetting('loglevel') dinit('PlexConnect', param, True) # init logging, new file, main process - + dprint('PlexConnect', 0, "Version: {0}", __VERSION__) dprint('PlexConnect', 0, "Python: {0}", sys.version) dprint('PlexConnect', 0, "Host OS: {0}", sys.platform) - dprint('PlexConnect', 0, "PILBackgrounds: Is PIL installed? {0}", isPILinstalled()) - + dprint('PlexConnect', 0, + "PILBackgrounds: Is PIL installed? {0}", isPILinstalled()) + # more Settings param['IP_self'] = getIP_self() param['HostToIntercept'] = cfg.getSetting('hosttointercept') - param['baseURL'] = 'http://'+ param['HostToIntercept'] - + param['baseURL'] = 'http://' + param['HostToIntercept'] + # proxy for ATVSettings proxy = BaseManager() proxy.register('ATVSettings', ATVSettings.CATVSettings) proxy.start(initProxy) param['CATVSettings'] = proxy.ATVSettings(CONFIG_PATH) - running = True - + # init DNSServer - if cfg.getSetting('enable_dnsserver')=='True': - master, slave = Pipe() # endpoint [0]-PlexConnect, [1]-DNSServer - proc = Process(target=DNSServer.Run, args=(slave, param)) - proc.start() - - time.sleep(0.1) - if proc.is_alive(): - procs['DNSServer'] = proc - pipes['DNSServer'] = master - else: - dprint('PlexConnect', 0, "DNSServer not alive. Shutting down.") - running = False - + if cfg.getSetting('enable_dnsserver') == 'True': + dnsserver = DNSServer.DNSServer(param) + dnsserver.start_thread() + # if not dnsserver.isAlive(): + # dprint('PlexConnect', 0, "DNSServer not alive. Shutting down.") + # running = False + # init WebServer if running: master, slave = Pipe() # endpoint [0]-PlexConnect, [1]-WebServer proc = Process(target=WebServer.Run, args=(slave, param)) proc.start() - + time.sleep(0.1) if proc.is_alive(): procs['WebServer'] = proc @@ -122,14 +120,14 @@ def startup(): else: dprint('PlexConnect', 0, "WebServer not alive. Shutting down.") running = False - + # init WebServer_SSL if running and \ - cfg.getSetting('enable_webserver_ssl')=='True': + cfg.getSetting('enable_webserver_ssl') == 'True': master, slave = Pipe() # endpoint [0]-PlexConnect, [1]-WebServer proc = Process(target=WebServer.Run_SSL, args=(slave, param)) proc.start() - + time.sleep(0.1) if proc.is_alive(): procs['WebServer_SSL'] = proc @@ -137,14 +135,15 @@ def startup(): else: dprint('PlexConnect', 0, "WebServer_SSL not alive. Shutting down.") running = False - + # not started successful - clean up if not running: cmdShutdown() shutdown() - + return running + def run(timeout=60): # do something important try: @@ -154,16 +153,18 @@ def run(timeout=60): pass # mask "IOError: [Errno 4] Interrupted function call" else: raise - + return running + def shutdown(): for slave in procs: procs[slave].join() param['CATVSettings'].saveSettings() - + dprint('PlexConnect', 0, "Shutdown") + def cmdShutdown(): global running running = False @@ -173,14 +174,12 @@ def cmdShutdown(): dprint('PlexConnect', 0, "Shutting down.") - def sighandler_shutdown(signum, frame): signal.signal(signal.SIGINT, signal.SIG_IGN) # we heard you! cmdShutdown() - -if __name__=="__main__": +if __name__ == "__main__": signal.signal(signal.SIGINT, sighandler_shutdown) signal.signal(signal.SIGTERM, sighandler_shutdown) parser = argparse.ArgumentParser() @@ -189,15 +188,15 @@ def sighandler_shutdown(signum, frame): args = parser.parse_args() if args.config_path: CONFIG_PATH = args.config_path - + dprint('PlexConnect', 0, "***") dprint('PlexConnect', 0, "PlexConnect") dprint('PlexConnect', 0, "Press CTRL-C to shut down.") dprint('PlexConnect', 0, "***") - + running = startup() - + while running: running = run() - + shutdown() diff --git a/PlexConnect_WinService.py b/PlexConnect_WinService.py index b0545ecc..81e5778f 100755 --- a/PlexConnect_WinService.py +++ b/PlexConnect_WinService.py @@ -22,30 +22,28 @@ import PlexConnect - class AppServerSvc(win32serviceutil.ServiceFramework): _svc_name_ = "PlexConnect-Service" _svc_display_name_ = "PlexConnect-Service" _svc_description_ = "Description" - - def __init__(self,args): - win32serviceutil.ServiceFramework.__init__(self,args) - + + def __init__(self, args): + win32serviceutil.ServiceFramework.__init__(self, args) + def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) PlexConnect.cmdShutdown() - + def SvcDoRun(self): self.ReportServiceStatus(win32service.SERVICE_RUNNING) running = PlexConnect.startup() - + while running: running = PlexConnect.run(timeout=10) - + PlexConnect.shutdown() - - self.ReportServiceStatus(win32service.SERVICE_STOPPED) + self.ReportServiceStatus(win32service.SERVICE_STOPPED) if __name__ == '__main__': diff --git a/PlexConnect_daemon.py b/PlexConnect_daemon.py index 3290f151..e90fd89e 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -12,6 +12,7 @@ import argparse import atexit from PlexConnect import startup, shutdown, run, cmdShutdown +from contextlib import redirect_stderr, redirect_stdout def daemonize(args): @@ -25,7 +26,7 @@ def daemonize(args): pid = os.fork() if pid != 0: sys.exit(0) - except OSError, e: + except OSError as e: raise RuntimeError("1st fork failed: %s [%d]" % (e.strerror, e.errno)) # decouple from parent environment @@ -40,26 +41,18 @@ def daemonize(args): pid = os.fork() if pid != 0: sys.exit(0) - except OSError, e: + except OSError as e: raise RuntimeError("2nd fork failed: %s [%d]" % (e.strerror, e.errno)) - # redirect standard file descriptors - sys.stdout.flush() - sys.stderr.flush() - si = file('/dev/null', 'r') - so = file('/dev/null', 'a+') - se = file('/dev/null', 'a+', 0) - os.dup2(si.fileno(), sys.stdin.fileno()) - os.dup2(so.fileno(), sys.stdout.fileno()) - os.dup2(se.fileno(), sys.stderr.fileno()) - if args.pidfile: try: atexit.register(delpid) pid = str(os.getpid()) - file(args.pidfile, 'w').write("%s\n" % pid) - except IOError, e: - raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) + with open(args.pidfile, 'w') as fh: + fh.write(f"{pid}") + except IOError as e: + raise SystemExit( + "Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) def delpid(): @@ -71,6 +64,7 @@ def sighandler_shutdown(signum, frame): signal.signal(signal.SIGINT, signal.SIG_IGN) # we heard you! cmdShutdown() + if __name__ == '__main__': signal.signal(signal.SIGINT, sighandler_shutdown) signal.signal(signal.SIGTERM, sighandler_shutdown) @@ -79,11 +73,13 @@ def sighandler_shutdown(signum, frame): parser.add_argument('--pidfile', dest='pidfile') args = parser.parse_args() - daemonize(args) + with redirect_stdout(open(os.devnull, "a")): + with redirect_stderr(open(os.devnull, "a")): + daemonize(args) - running = startup() + running = startup() - while running: - running = run() + while running: + running = run() - shutdown() + shutdown() diff --git a/README.md b/README.md index d156f696..10e6f837 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,7 @@ The basic idea is, to... ## Requirements -- Python 2.6.x with minor issues: ElementTree doesn't support tag indices. -- Python 2.7.18 recommended. - +- Python 3 (Tested on 3.9.2) ## Installation ```sh @@ -42,6 +40,7 @@ See the [Wiki - Install Guide][] for additional documentation. ## Usage ```sh +pip install -r requirements.txt # Run with root privileges sudo ./PlexConnect.py ``` diff --git a/Settings.py b/Settings.py index 5b8ca03d..050eba87 100755 --- a/Settings.py +++ b/Settings.py @@ -3,13 +3,12 @@ import sys from os import sep, makedirs from os.path import isdir -import ConfigParser +import configparser import re from Debug import * # dprint() - """ Global Settings... syntax: 'setting': ('default', 'regex to validate') @@ -23,44 +22,43 @@ intercept_atv_icon: changes atv icon to plex icon """ g_settings = [ - ('enable_plexgdm' , ('True', '((True)|(False))')), - ('ip_pms' , ('192.168.178.10', '[a-zA-Z0-9_.-]+')), - ('port_pms' , ('32400', '[0-9]{1,5}')), + ('enable_plexgdm', ('True', '((True)|(False))')), + ('ip_pms', ('192.168.178.10', '[a-zA-Z0-9_.-]+')), + ('port_pms', ('32400', '[0-9]{1,5}')), \ ('enable_dnsserver', ('True', '((True)|(False))')), - ('port_dnsserver' , ('53', '[0-9]{1,5}')), - ('ip_dnsmaster' , ('8.8.8.8', '([0-9]{1,3}\.){3}[0-9]{1,3}')), - ('prevent_atv_update' , ('True', '((True)|(False))')), - ('intercept_atv_icon' , ('True', '((True)|(False))')), + ('port_dnsserver', ('53', '[0-9]{1,5}')), + ('ip_dnsmaster', ('8.8.8.8', '([0-9]{1,3}\.){3}[0-9]{1,3}')), + ('prevent_atv_update', ('True', '((True)|(False))')), + ('intercept_atv_icon', ('True', '((True)|(False))')), \ ('enable_plexconnect_autodetect', ('True', '((True)|(False))')), - ('ip_plexconnect' , ('0.0.0.0', '([0-9]{1,3}\.){3}[0-9]{1,3}')), - ('use_custom_dns_bind_ip' , ('False', '((True)|(False))')), - ('custom_dns_bind_ip' , ('0.0.0.0', '([0-9]{1,3}\.){3}[0-9]{1,3}')), + ('ip_plexconnect', ('0.0.0.0', '([0-9]{1,3}\.){3}[0-9]{1,3}')), + ('use_custom_dns_bind_ip', ('False', '((True)|(False))')), + ('custom_dns_bind_ip', ('0.0.0.0', '([0-9]{1,3}\.){3}[0-9]{1,3}')), \ - ('hosttointercept' , ('trailers.apple.com', '[a-zA-Z0-9_.-]+')), + ('hosttointercept', ('trailers.apple.com', '[a-zA-Z0-9_.-]+')), ('icon', ('movie-trailers', '[a-zA-Z0-9_.-]+')), - ('certfile' , ('./assets/certificates/trailers.pem', '.+.pem')), + ('certfile', ('./assets/certificates/trailers.pem', '.+.pem')), \ - ('port_webserver' , ('80', '[0-9]{1,5}')), - ('enable_webserver_ssl' , ('True', '((True)|(False))')), - ('port_ssl' , ('443', '[0-9]{1,5}')), + ('port_webserver', ('80', '[0-9]{1,5}')), + ('enable_webserver_ssl', ('True', '((True)|(False))')), + ('port_ssl', ('443', '[0-9]{1,5}')), \ - ('allow_gzip_atv' , ('False', '((True)|(False))')), - ('allow_gzip_pmslocal' , ('False', '((True)|(False))')), - ('allow_gzip_pmsremote' , ('True', '((True)|(False))')), - ('fanart_quality' , ('High', '((Low)|(High))')), + ('allow_gzip_atv', ('False', '((True)|(False))')), + ('allow_gzip_pmslocal', ('False', '((True)|(False))')), + ('allow_gzip_pmsremote', ('True', '((True)|(False))')), + ('fanart_quality', ('High', '((Low)|(High))')), \ - ('loglevel' , ('Normal', '((Off)|(Normal)|(High))')), - ('logpath' , ('.', '.+')), - ] - + ('loglevel', ('Normal', '((Off)|(Normal)|(High))')), + ('logpath', ('.', '.+')), +] class CSettings(): def __init__(self, path): dprint(__name__, 1, "init class CSettings") - self.cfg = ConfigParser.SafeConfigParser() + self.cfg = configparser.ConfigParser() self.section = 'PlexConnect' self.path = path @@ -68,23 +66,20 @@ def __init__(self, path): self.cfg.add_section(self.section) for (opt, (dflt, vldt)) in g_settings: self.cfg.set(self.section, opt, '\0') - + self.loadSettings() self.checkSection() - - - + # load/save config def loadSettings(self): dprint(__name__, 1, "load settings") self.cfg.read(self.getSettingsFile()) - + def saveSettings(self): dprint(__name__, 1, "save settings") - f = open(self.getSettingsFile(), 'wb') - self.cfg.write(f) - f.close() - + with open(self.getSettingsFile(), 'w') as f: + self.cfg.write(f) + def getSettingsFile(self): if self.path.startswith('.'): # relative to current path @@ -95,7 +90,7 @@ def getSettingsFile(self): if not isdir(directory): makedirs(directory) return directory + "/Settings.cfg" - + def checkSection(self): modify = False # check for existing section @@ -103,41 +98,41 @@ def checkSection(self): modify = True self.cfg.add_section(self.section) dprint(__name__, 0, "add section {0}", self.section) - + for (opt, (dflt, vldt)) in g_settings: setting = self.cfg.get(self.section, opt) - if setting=='\0': + if setting == '\0': # check settings - add if new modify = True self.cfg.set(self.section, opt, dflt) dprint(__name__, 0, "add setting {0}={1}", opt, dflt) - + elif not re.search('\A'+vldt+'\Z', setting): # check settings - default if unknown modify = True self.cfg.set(self.section, opt, dflt) - dprint(__name__, 0, "bad setting {0}={1} - set default {2}", opt, setting, dflt) - + dprint( + __name__, 0, "bad setting {0}={1} - set default {2}", opt, setting, dflt) + # save if changed if modify: self.saveSettings() - - - + # access/modify PlexConnect settings + def getSetting(self, option): - dprint(__name__, 1, "getsetting {0}={1}", option, self.cfg.get(self.section, option)) + dprint(__name__, 1, "getsetting {0}={1}", + option, self.cfg.get(self.section, option)) return self.cfg.get(self.section, option) - -if __name__=="__main__": +if __name__ == "__main__": Settings = CSettings() - + option = 'enable_plexgdm' - print Settings.getSetting(option) - + print(Settings.getSetting(option)) + option = 'enable_dnsserver' - print Settings.getSetting(option) - + print(Settings.getSetting(option)) + del Settings diff --git a/Subtitle.py b/Subtitle.py index b50200e7..7b7d2362 100755 --- a/Subtitle.py +++ b/Subtitle.py @@ -6,16 +6,16 @@ """ - import re -import urllib2 +import urllib.request +import urllib.error +import urllib.parse import json from Debug import * # dprint(), prettyXML() import PlexAPI - """ Plex Media Server: get subtitle, return as aTV subtitle JSON @@ -26,6 +26,8 @@ result: aTV subtitle JSON or 'False' in case of error """ + + def getSubtitleJSON(PMS_address, path, options): """ # double check aTV UDID, redo from client IP if needed/possible @@ -36,59 +38,61 @@ def getSubtitleJSON(PMS_address, path, options): """ path = path + ('?' if not '?' in path else '&') path = path + 'encoding=utf-8' - + if not 'PlexConnectUDID' in options: # aTV unidentified, UDID not known return False - + UDID = options['PlexConnectUDID'] - + # determine PMS_uuid, PMSBaseURL from IP (PMS_mark) xargs = {} PMS_uuid = PlexAPI.getPMSFromAddress(UDID, PMS_address) PMS_baseURL = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'baseURL') - xargs['X-Plex-Token'] = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'accesstoken') - + xargs['X-Plex-Token'] = PlexAPI.getPMSProperty( + UDID, PMS_uuid, 'accesstoken') + dprint(__name__, 1, "subtitle URL: {0}{1}", PMS_baseURL, path) dprint(__name__, 1, "xargs: {0}", xargs) - - request = urllib2.Request(PMS_baseURL+path , None, xargs) + + request = urllib.request.Request(PMS_baseURL+path, None, xargs) try: - response = urllib2.urlopen(request, timeout=20) - except urllib2.URLError as e: + response = urllib.request.urlopen(request, timeout=20) + except urllib.error.URLError as e: dprint(__name__, 0, 'No Response from Plex Media Server') if hasattr(e, 'reason'): - dprint(__name__, 0, "We failed to reach a server. Reason: {0}", e.reason) + dprint(__name__, 0, + "We failed to reach a server. Reason: {0}", e.reason) elif hasattr(e, 'code'): - dprint(__name__, 0, "The server couldn't fulfill the request. Error code: {0}", e.code) + dprint( + __name__, 0, "The server couldn't fulfill the request. Error code: {0}", e.code) return False except IOError: dprint(__name__, 0, 'Error loading response XML from Plex Media Server') return False - + # Todo: Deal with ANSI files. How to select used "codepage"? subtitleFile = response.read() - - print response.headers - + + print(response.headers) + dprint(__name__, 1, "====== received Subtitle ======") dprint(__name__, 1, "{0} [...]", subtitleFile[:255]) dprint(__name__, 1, "====== Subtitle finished ======") - - if options['PlexConnectSubtitleFormat']=='srt': + + if options['PlexConnectSubtitleFormat'] == 'srt': subtitle = parseSRT(subtitleFile) else: return False - + JSON = json.dumps(subtitle) - + dprint(__name__, 1, "====== generated subtitle aTV subtitle JSON ======") dprint(__name__, 1, "{0} [...]", JSON[:255]) dprint(__name__, 1, "====== aTV subtitle JSON finished ======") return(JSON) - """ parseSRT - decode SRT file, create aTV subtitle structure @@ -97,58 +101,72 @@ def getSubtitleJSON(PMS_address, path, options): result: JSON - subtitle encoded into .js tree to feed PlexConnect's updateSubtitle() (see application.js) """ + + def parseSRT(SRT): - subtitle = { 'Timestamp': [] } - - srtPart = re.split(r'(\r\n|\n\r|\n|\r)\1+(?=[0-9]+)', SRT.strip())[::2]; # trim whitespaces, split at multi-newline, check for following number + subtitle = {'Timestamp': []} + + # trim whitespaces, split at multi-newline, check for following number + srtPart = re.split(r'(\r\n|\n\r|\n|\r)\1+(?=[0-9]+)', SRT.strip())[::2] timeHide_last = 0 - + for Item in srtPart: - ItemPart = re.split(r'\r\n|\n\r|\n|\r', Item.strip()); # trim whitespaces, split at newline - - timePart = re.split(r':|,|-->', ItemPart[1]); # --> split at : , or --> + # trim whitespaces, split at newline + ItemPart = re.split(r'\r\n|\n\r|\n|\r', Item.strip()) + + # --> split at : , or --> + timePart = re.split(r':|,|-->', ItemPart[1]) timeShow = int(timePart[0])*1000*60*60 +\ - int(timePart[1])*1000*60 +\ - int(timePart[2])*1000 +\ - int(timePart[3]); + int(timePart[1])*1000*60 +\ + int(timePart[2])*1000 +\ + int(timePart[3]) timeHide = int(timePart[4])*1000*60*60 +\ - int(timePart[5])*1000*60 +\ - int(timePart[6])*1000 +\ - int(timePart[7]); - + int(timePart[5])*1000*60 +\ + int(timePart[6])*1000 +\ + int(timePart[7]) + # switch off? skip if new msg at same point in time. - if timeHide_last!=timeShow: - subtitle['Timestamp'].append({ 'time': timeHide_last }) + if timeHide_last != timeShow: + subtitle['Timestamp'].append({'time': timeHide_last}) timeHide_last = timeHide - + # current time - subtitle['Timestamp'].append({ 'time': timeShow, 'Line': [] }) + subtitle['Timestamp'].append({'time': timeShow, 'Line': []}) #JSON += ' { "time":'+str(timeHide_last)+', "Line": [\n' - + # analyse format: <...> - i_talics (light), b_old (heavy), u_nderline (?), font color (?) frmt_i = False frmt_b = False for i, line in enumerate(ItemPart[2:]): # evaluate each text line - for frmt in re.finditer(r'<([^/]*?)>', line): # format switch on in current line - if frmt.group(1)=='i': frmt_i = True - if frmt.group(1)=='b': frmt_b = True - + # format switch on in current line + for frmt in re.finditer(r'<([^/]*?)>', line): + if frmt.group(1) == 'i': + frmt_i = True + if frmt.group(1) == 'b': + frmt_b = True + weight = '' # determine aTV font - from previous line or current - if frmt_i: weight = 'light' - if frmt_b: weight = 'heavy' - + if frmt_i: + weight = 'light' + if frmt_b: + weight = 'heavy' + for frmt in re.finditer(r'', line): # format switch off - if frmt.group(1)=='i': frmt_i = False - if frmt.group(1)=='b': frmt_b = False - - line = re.sub('<.*?>', "", line); # remove the formatting identifiers - - subtitle['Timestamp'][-1]['Line'].append({ 'text': line }) - if weight: subtitle['Timestamp'][-1]['Line'][-1]['weight'] = weight - - subtitle['Timestamp'].append({ 'time': timeHide_last }) # switch off last subtitle - return subtitle + if frmt.group(1) == 'i': + frmt_i = False + if frmt.group(1) == 'b': + frmt_b = False + + # remove the formatting identifiers + line = re.sub('<.*?>', "", line) + subtitle['Timestamp'][-1]['Line'].append({'text': line}) + if weight: + subtitle['Timestamp'][-1]['Line'][-1]['weight'] = weight + + # switch off last subtitle + subtitle['Timestamp'].append({'time': timeHide_last}) + return subtitle if __name__ == '__main__': @@ -167,14 +185,14 @@ def parseSRT(SRT): Yes, Python works!\n\ \n\ " - + dprint('', 0, "SRT file") dprint('', 0, SRT[:1000]) subtitle = parseSRT(SRT) JSON = json.dumps(subtitle) dprint('', 0, "aTV subtitle JSON") dprint('', 0, JSON[:1000]) - + """ JSON result (about): { "Timestamp": [ diff --git a/Version.py b/Version.py index 6f2265f7..8a71995b 100755 --- a/Version.py +++ b/Version.py @@ -5,6 +5,5 @@ """ - # Version string - globally available __VERSION__ = '0.7.4-210621' diff --git a/WebServer.py b/WebServer.py index 31cd697f..986da556 100755 --- a/WebServer.py +++ b/WebServer.py @@ -14,19 +14,26 @@ import sys -import string, cgi, time +import string +import cgi +import time from os import sep, path -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from SocketServer import ThreadingMixIn +from http.server import BaseHTTPRequestHandler, HTTPServer +from socketserver import ThreadingMixIn import ssl from multiprocessing import Pipe # inter process communication -import urllib, StringIO, gzip +import urllib.request +import urllib.parse +import urllib.error +import io +import gzip import signal import traceback import datetime -import Settings, ATVSettings +import Settings +import ATVSettings from Debug import * # dprint() import XMLConverter # XML_PMS2aTV, XML_PlayVideo import re @@ -34,89 +41,92 @@ import Subtitle - g_param = {} + + def setParams(param): global g_param g_param = param - def JSConverter(file, options): f = open(sys.path[0] + "/assets/js/" + file) JS = f.read() f.close() - + # PlexConnect {{URL()}}->baseURL for path in set(re.findall(r'\{\{URL\((.*?)\)\}\}', JS)): JS = JS.replace('{{URL(%s)}}' % path, g_param['baseURL']+path) - + # localization JS = Localize.replaceTEXT(JS, options['aTVLanguage']).encode('utf-8') - - return JS + return JS class MyHandler(BaseHTTPRequestHandler): - + # Fixes slow serving speed under Windows def address_string(self): - host, port = self.client_address[:2] - #return socket.getfqdn(host) - return host - + host, port = self.client_address[:2] + # return socket.getfqdn(host) + return host + def log_message(self, format, *args): - pass - + pass + def compress(self, data): - buf = StringIO.StringIO() + buf = io.StringIO() zfile = gzip.GzipFile(mode='wb', fileobj=buf, compresslevel=9) zfile.write(data) zfile.close() return buf.getvalue() - + def sendResponse(self, data, type, enableGzip): self.send_response(200) self.send_header('Server', 'PlexConnect') self.send_header('Content-type', type) try: - accept_encoding = map(string.strip, string.split(self.headers["accept-encoding"], ",")) + accept_encoding = [x.strip() + for x in self.headers.get("accept-encoding", "").split(",")] except KeyError: accept_encoding = [] if enableGzip and \ - g_param['CSettings'].getSetting('allow_gzip_atv')=='True' and \ + g_param['CSettings'].getSetting('allow_gzip_atv') == 'True' and \ 'gzip' in accept_encoding: self.send_header('Content-encoding', 'gzip') self.end_headers() self.wfile.write(self.compress(data)) else: self.end_headers() + if not isinstance(data, bytes): + data = bytes(data, 'utf8') self.wfile.write(data) - + def do_GET(self): global g_param try: dprint(__name__, 2, "http request header:\n{0}", self.headers) dprint(__name__, 2, "http request path:\n{0}", self.path) - + # check for PMS address PMSaddress = '' pms_end = self.path.find(')') - if self.path.startswith('/PMS(') and pms_end>-1: - PMSaddress = urllib.unquote_plus(self.path[5:pms_end]) + if self.path.startswith('/PMS(') and pms_end > -1: + PMSaddress = urllib.parse.unquote_plus(self.path[5:pms_end]) self.path = self.path[pms_end+1:] - + # break up path, separate PlexConnect options # clean path needed for filetype decoding - parts = re.split(r'[?&]', self.path, 1) # should be '?' only, but we do some things different :-) - if len(parts)==1: + # should be '?' only, but we do some things different :-) + parts = re.split(r'[?&]', self.path, 1) + if len(parts) == 1: self.path = parts[0] options = {} query = '' else: self.path = parts[0] - + # break up query string options = {} query = '' @@ -125,70 +135,78 @@ def do_GET(self): if part.startswith('PlexConnect'): # get options[] opt = part.split('=', 1) - if len(opt)==1: + if len(opt) == 1: options[opt[0]] = '' else: - options[opt[0]] = urllib.unquote(opt[1]) + options[opt[0]] = urllib.parse.unquote(opt[1]) else: # recreate query string (non-PlexConnect) - has to be merged back when forwarded - if query=='': + if query == '': query = '?' + part else: query += '&' + part - + # get aTV language setting - options['aTVLanguage'] = Localize.pickLanguage(self.headers.get('Accept-Language', 'en')) + options['aTVLanguage'] = Localize.pickLanguage( + self.headers.get('Accept-Language', 'en')) query = query.replace("yyltyy", "<").replace("yygtyy", ">") - + # add client address - to be used in case UDID is unknown if 'X-Forwarded-For' in self.headers: - options['aTVAddress'] = self.headers['X-Forwarded-For'].split(',', 1)[0] + options['aTVAddress'] = self.headers['X-Forwarded-For'].split(',', 1)[ + 0] else: options['aTVAddress'] = self.client_address[0] - + # get aTV hard-/software parameters - options['aTVFirmwareVersion'] = self.headers.get('X-Apple-TV-Version', '5.1') - options['aTVScreenResolution'] = self.headers.get('X-Apple-TV-Resolution', '720') - + options['aTVFirmwareVersion'] = self.headers.get( + 'X-Apple-TV-Version', '5.1') + options['aTVScreenResolution'] = self.headers.get( + 'X-Apple-TV-Resolution', '720') + dprint(__name__, 2, "pms address:\n{0}", PMSaddress) dprint(__name__, 2, "cleaned path:\n{0}", self.path) dprint(__name__, 2, "PlexConnect options:\n{0}", options) dprint(__name__, 2, "additional arguments:\n{0}", query) - + if 'User-Agent' in self.headers and \ 'AppleTV' in self.headers['User-Agent']: - + # recieve simple logging messages from the ATV if 'PlexConnectATVLogLevel' in options: - dprint('ATVLogger', int(options['PlexConnectATVLogLevel']), options['PlexConnectLog']) + dprint('ATVLogger', int( + options['PlexConnectATVLogLevel']), options['PlexConnectLog']) self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() return - + # serve "*.cer" - Serve up certificate file to atv if self.path.endswith(".cer"): dprint(__name__, 1, "serving *.cer: "+self.path) if g_param['CSettings'].getSetting('certfile').startswith('.'): # relative to current path - cfg_certfile = sys.path[0] + sep + g_param['CSettings'].getSetting('certfile') + cfg_certfile = sys.path[0] + sep + \ + g_param['CSettings'].getSetting('certfile') else: # absolute path - cfg_certfile = g_param['CSettings'].getSetting('certfile') + cfg_certfile = g_param['CSettings'].getSetting( + 'certfile') cfg_certfile = path.normpath(cfg_certfile) - + cfg_certfile = path.splitext(cfg_certfile)[0] + '.cer' try: f = open(cfg_certfile, "rb") except: - dprint(__name__, 0, "Failed to access certificate: {0}", cfg_certfile) + dprint( + __name__, 0, "Failed to access certificate: {0}", cfg_certfile) return - + self.sendResponse(f.read(), 'text/xml', False) f.close() - return - + return + # serve .js files to aTV # application, main: ignore path, send /assets/js/application.js # otherwise: path should be '/js', send /assets/js/*.js @@ -208,12 +226,14 @@ def do_GET(self): resource = self.headers['Host']+self.path icon = g_param['CSettings'].getSetting('icon') if basename.startswith(icon): - icon_res = basename[len(icon):] # cut string from settings, keeps @720.png/@1080.png + # cut string from settings, keeps @720.png/@1080.png + icon_res = basename[len(icon):] resource = sys.path[0] + '/assets/icons/icon'+icon_res - dprint(__name__, 1, "serving "+self.headers['Host']+self.path+" with "+resource) + dprint(__name__, 1, "serving " + + self.headers['Host']+self.path+" with "+resource) r = open(resource, "rb") else: - r = urllib.urlopen('http://'+resource) + r = urllib.request.urlopen('http://'+resource) self.sendResponse(r.read(), 'image/png', False) r.close() return @@ -225,7 +245,7 @@ def do_GET(self): self.sendResponse(f.read(), 'image/jpeg', False) f.close() return - + # serve "*.png" - only png's support transparent colors if self.path.endswith(".png"): dprint(__name__, 1, "serving *.png: "+self.path) @@ -233,27 +253,29 @@ def do_GET(self): self.sendResponse(f.read(), 'image/png', False) f.close() return - + # serve subtitle file - transcoded to aTV subtitle json if 'PlexConnect' in options and \ - options['PlexConnect']=='Subtitle': + options['PlexConnect'] == 'Subtitle': dprint(__name__, 1, "serving subtitle: "+self.path) - XML = Subtitle.getSubtitleJSON(PMSaddress, self.path + query, options) + XML = Subtitle.getSubtitleJSON( + PMSaddress, self.path + query, options) self.sendResponse(XML, 'application/json', True) return - + # get everything else from XMLConverter - formerly limited to trailing "/" and &PlexConnect Cmds if True: dprint(__name__, 1, "serving .xml: "+self.path) - XML = XMLConverter.XML_PMS2aTV(PMSaddress, self.path + query, options) + XML = XMLConverter.XML_PMS2aTV( + PMSaddress, self.path + query, options) self.sendResponse(XML, 'text/xml', True) return - + """ # unexpected request self.send_error(403,"Access denied: %s" % self.path) """ - + else: """ Added Up Page for docker helthcheck @@ -266,138 +288,144 @@ def do_GET(self): except IOError: dprint(__name__, 0, 'File Not Found:\n{0}', traceback.format_exc()) - self.send_error(404,"File Not Found: %s" % self.path) + self.send_error(404, "File Not Found: %s" % self.path) except: - dprint(__name__, 0, 'Internal Server Error:\n{0}', traceback.format_exc()) - self.send_error(500,"Internal Server Error: %s" % self.path) - + dprint(__name__, 0, + 'Internal Server Error:\n{0}', traceback.format_exc()) + self.send_error(500, "Internal Server Error: %s" % self.path) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """Handle requests in a separate thread.""" - def Run(cmdPipe, param): if not __name__ == '__main__': signal.signal(signal.SIGINT, signal.SIG_IGN) - + dinit(__name__, param) # init logging, WebServer process - + cfg_IP_WebServer = param['IP_self'] cfg_Port_WebServer = param['CSettings'].getSetting('port_webserver') try: - server = ThreadedHTTPServer((cfg_IP_WebServer,int(cfg_Port_WebServer)), MyHandler) + server = ThreadedHTTPServer( + (cfg_IP_WebServer, int(cfg_Port_WebServer)), MyHandler) server.timeout = 1 - except Exception, e: - dprint(__name__, 0, "Failed to connect to HTTP on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_WebServer, e) + except Exception as e: + dprint( + __name__, 0, "Failed to connect to HTTP on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_WebServer, e) sys.exit(1) - + socketinfo = server.socket.getsockname() - + dprint(__name__, 0, "***") - dprint(__name__, 0, "WebServer: Serving HTTP on {0} port {1}.", socketinfo[0], socketinfo[1]) + dprint(__name__, 0, + "WebServer: Serving HTTP on {0} port {1}.", socketinfo[0], socketinfo[1]) dprint(__name__, 0, "***") - + setParams(param) XMLConverter.setParams(param) XMLConverter.setATVSettings(param['CATVSettings']) - + try: while True: # check command if cmdPipe.poll(): cmd = cmdPipe.recv() - if cmd=='shutdown': + if cmd == 'shutdown': break - + # do your work (with timeout) server.handle_request() - + except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # we heard you! - dprint(__name__, 0,"^C received.") + dprint(__name__, 0, "^C received.") finally: dprint(__name__, 0, "Shutting down (HTTP).") server.socket.close() - def Run_SSL(cmdPipe, param): if not __name__ == '__main__': signal.signal(signal.SIGINT, signal.SIG_IGN) - + dinit(__name__, param) # init logging, WebServer process - + cfg_IP_WebServer = param['IP_self'] cfg_Port_SSL = param['CSettings'].getSetting('port_ssl') - + if param['CSettings'].getSetting('certfile').startswith('.'): # relative to current path - cfg_certfile = sys.path[0] + sep + param['CSettings'].getSetting('certfile') + cfg_certfile = sys.path[0] + sep + \ + param['CSettings'].getSetting('certfile') else: # absolute path cfg_certfile = param['CSettings'].getSetting('certfile') cfg_certfile = path.normpath(cfg_certfile) - + try: certfile = open(cfg_certfile, 'r') except: dprint(__name__, 0, "Failed to access certificate: {0}", cfg_certfile) sys.exit(1) certfile.close() - + try: - server = ThreadedHTTPServer((cfg_IP_WebServer,int(cfg_Port_SSL)), MyHandler) - server.socket = ssl.wrap_socket(server.socket, certfile=cfg_certfile, server_side=True) + server = ThreadedHTTPServer( + (cfg_IP_WebServer, int(cfg_Port_SSL)), MyHandler) + server.socket = ssl.wrap_socket( + server.socket, certfile=cfg_certfile, server_side=True) server.timeout = 1 - except Exception, e: - dprint(__name__, 0, "Failed to connect to HTTPS on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_SSL, e) + except Exception as e: + dprint( + __name__, 0, "Failed to connect to HTTPS on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_SSL, e) sys.exit(1) - + socketinfo = server.socket.getsockname() - + dprint(__name__, 0, "***") - dprint(__name__, 0, "WebServer: Serving HTTPS on {0} port {1}.", socketinfo[0], socketinfo[1]) + dprint(__name__, 0, + "WebServer: Serving HTTPS on {0} port {1}.", socketinfo[0], socketinfo[1]) dprint(__name__, 0, "***") - + setParams(param) XMLConverter.setParams(param) XMLConverter.setATVSettings(param['CATVSettings']) - + try: while True: # check command if cmdPipe.poll(): cmd = cmdPipe.recv() - if cmd=='shutdown': + if cmd == 'shutdown': break - + # do your work (with timeout) server.handle_request() - + except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # we heard you! - dprint(__name__, 0,"^C received.") + dprint(__name__, 0, "^C received.") finally: dprint(__name__, 0, "Shutting down (HTTPS).") server.socket.close() - -if __name__=="__main__": +if __name__ == "__main__": cmdPipe = Pipe() - + cfg = Settings.CSettings() param = {} param['CSettings'] = cfg param['CATVSettings'] = ATVSettings.CATVSettings() - + param['IP_self'] = '192.168.178.20' # IP_self? - param['baseURL'] = 'http://'+ param['IP_self'] +':'+ cfg.getSetting('port_webserver') + param['baseURL'] = 'http://' + param['IP_self'] + \ + ':' + cfg.getSetting('port_webserver') param['HostToIntercept'] = cfg.getSetting('hosttointercept') - if len(sys.argv)==1: + if len(sys.argv) == 1: Run(cmdPipe[1], param) - elif len(sys.argv)==2 and sys.argv[1]=='SSL': + elif len(sys.argv) == 2 and sys.argv[1] == 'SSL': Run_SSL(cmdPipe[1], param) diff --git a/XMLConverter.py b/XMLConverter.py index ac358863..aa0dd59d 100755 --- a/XMLConverter.py +++ b/XMLConverter.py @@ -19,8 +19,9 @@ import os import sys import traceback -import inspect -import string, random +import inspect +import string +import random import copy # deepcopy() import json @@ -31,13 +32,20 @@ except ImportError: import xml.etree.ElementTree as etree -import time, uuid, hmac, hashlib, base64 -from urllib import quote_plus, unquote_plus, urlencode -import urllib2 -import urlparse +import time +import uuid +import hmac +import hashlib +import base64 +from urllib.parse import quote_plus, unquote_plus, urlencode +import urllib.request +import urllib.error +import urllib.parse +import urllib.parse from Version import __VERSION__ # for {{EVAL()}}, display in settings page -import Settings, ATVSettings +import Settings +import ATVSettings import PlexAPI from Debug import * # dprint(), prettyXML() import Localize @@ -46,20 +54,26 @@ from PILBackgrounds import isPILinstalled g_param = {} + + def setParams(param): global g_param g_param = param + g_ATVSettings = None + + def setATVSettings(cfg): global g_ATVSettings g_ATVSettings = cfg - """ # aTV XML ErrorMessage - hardcoded XML File """ + + def XML_Error(title, desc): errorXML = '\ \n\ @@ -71,11 +85,10 @@ def XML_Error(title, desc): \n\ \n\ \n\ -'; +' return errorXML - def XML_PlayVideo_ChannelsV1(baseURL, path): XML = '\ \n\ @@ -106,12 +119,11 @@ def XML_PlayVideo_ChannelsV1(baseURL, path): \n\ \n\ \n\ -'; - dprint(__name__,2 , XML) +' + dprint(__name__, 2, XML) return XML - """ global list of known aTVs - to look up UDID by IP if needed @@ -121,6 +133,7 @@ def XML_PlayVideo_ChannelsV1(baseURL, path): """ g_ATVList = {} + def declareATV(udid, ip): global g_ATVList if udid in g_ATVList: @@ -128,15 +141,15 @@ def declareATV(udid, ip): else: g_ATVList[udid] = {'ip': ip} + def getATVFromIP(ip): # find aTV by IP, return UDID for udid in g_ATVList: - if ip==g_ATVList[udid].get('ip', None): + if ip == g_ATVList[udid].get('ip', None): return udid return None # IP not found - """ # XML converter functions # - translate aTV request and send to PMS @@ -144,6 +157,8 @@ def getATVFromIP(ip): # - select XML template # - translate to aTV XML """ + + def XML_PMS2aTV(PMS_address, path, options): # double check aTV UDID, redo from client IP if needed/possible if not 'PlexConnectUDID' in options: @@ -151,34 +166,35 @@ def XML_PMS2aTV(PMS_address, path, options): if UDID: options['PlexConnectUDID'] = UDID else: - # aTV unidentified, UDID not known - return XML_Error('PlexConnect','Unexpected error - unidentified ATV') + # aTV unidentified, UDID not known + return XML_Error('PlexConnect', 'Unexpected error - unidentified ATV') else: - declareATV(options['PlexConnectUDID'], options['aTVAddress']) # update with latest info - + # update with latest info + declareATV(options['PlexConnectUDID'], options['aTVAddress']) + UDID = options['PlexConnectUDID'] - + # determine PMS_uuid, PMSBaseURL from IP (PMS_mark) PMS_uuid = PlexAPI.getPMSFromAddress(UDID, PMS_address) PMS_baseURL = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'baseURL') - + # check cmd to work on cmd = '' dir = '' channelsearchURL = '' - + if 'PlexConnect' in options: cmd = options['PlexConnect'] - + dprint(__name__, 1, "---------------------------------------------") dprint(__name__, 1, "PMS, path: {0} {1} ", PMS_address, path) dprint(__name__, 1, "Initial Command: {0}", cmd) - + # check aTV language setting if not 'aTVLanguage' in options: dprint(__name__, 1, "no aTVLanguage - pick en") options['aTVLanguage'] = 'en' - + # XML Template selector # - PlexConnect command # - path @@ -186,7 +202,7 @@ def XML_PMS2aTV(PMS_address, path, options): XMLtemplate = '' PMS = None PMSroot = None - + # XML direct request or # XMLtemplate defined by solely PlexConnect Cmd if path.endswith(".xml"): @@ -197,182 +213,206 @@ def XML_PMS2aTV(PMS_address, path, options): if cmd.startswith('SettingsToggle:'): opt = cmd[len('SettingsToggle:'):] # cut command: parts = opt.split('+') - g_ATVSettings.toggleSetting(options['PlexConnectUDID'], parts[0].lower()) + g_ATVSettings.toggleSetting( + options['PlexConnectUDID'], parts[0].lower()) parts1 = parts[1].split('_', 1) dir = parts1[0] cmd = parts1[1] XMLtemplate = dir + '/' + cmd + '.xml' - dprint(__name__, 2, "ATVSettings->Toggle: {0} in template: {1}", parts[0], parts[1]) - path = '' # clear path - we don't need PMS-XML - - elif cmd=='SaveSettings': - g_ATVSettings.saveSettings(); - return XML_Error('PlexConnect', 'SaveSettings!') # not an error - but aTV won't care anyways. - - elif cmd=='PlayVideo_ChannelsV1': + dprint(__name__, 2, + "ATVSettings->Toggle: {0} in template: {1}", parts[0], parts[1]) + path = '' # clear path - we don't need PMS-XML + + elif cmd == 'SaveSettings': + g_ATVSettings.saveSettings() + # not an error - but aTV won't care anyways. + return XML_Error('PlexConnect', 'SaveSettings!') + + elif cmd == 'PlayVideo_ChannelsV1': dprint(__name__, 1, "playing Channels XML Version 1: {0}".format(path)) auth_token = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'accesstoken') path = PlexAPI.getDirectVideoPath(path, auth_token) - return XML_PlayVideo_ChannelsV1(PMS_baseURL, path) # direct link, no PMS XML available - - elif cmd=='PlayTrailer': + # direct link, no PMS XML available + return XML_PlayVideo_ChannelsV1(PMS_baseURL, path) + + elif cmd == 'PlayTrailer': trailerID = options['PlexConnectTrailerID'] - info = urllib2.urlopen("https://youtube.com/get_video_info?html5=1&video_id=" + trailerID).read() - parsed = urlparse.parse_qs(info) - + info = urllib.request.urlopen( + "https://youtube.com/get_video_info?html5=1&video_id=" + trailerID).read() + parsed = urllib.parse.parse_qs(info) + key = 'player_response' if not key in parsed: return XML_Error('PlexConnect', 'Youtube: No Trailer Info available') streams_dict = json.loads(parsed[key][0]) - streams = streams_dict['streamingData']['formats'] - + streams = streams_dict['streamingData']['formats'] + url = '' for i in range(len(streams)): stream = streams[i] - # 18: "medium", 22: hd720 + # 18: "medium", 22: hd720 if stream['itag'] == 18: url = stream['url'] - # if there is also a "22" (720p) stream, let's upgrade to that one + # if there is also a "22" (720p) stream, let's upgrade to that one if stream['itag'] == 22: url = stream['url'] if url == '': - return XML_Error('PlexConnect','Youtube: ATV compatible Trailer not available') - - return XML_PlayVideo_ChannelsV1('', url.replace('&','&')) - - elif cmd=='MyPlexLogin': + return XML_Error('PlexConnect', 'Youtube: ATV compatible Trailer not available') + + return XML_PlayVideo_ChannelsV1('', url.replace('&', '&')) + + elif cmd == 'MyPlexLogin': dprint(__name__, 2, "MyPlex->Logging In...") if not 'PlexConnectCredentials' in options: return XML_Error('PlexConnect', 'MyPlex Sign In called without Credentials.') - - parts = options['PlexConnectCredentials'].split(':',1) - (username, auth_token) = PlexAPI.MyPlexSignIn(parts[0], parts[1], options) - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': auth_token}) - + + parts = options['PlexConnectCredentials'].split(':', 1) + (username, auth_token) = PlexAPI.MyPlexSignIn( + parts[0], parts[1], options) + PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], { + 'MyPlex': auth_token}) + g_ATVSettings.setSetting(UDID, 'myplex_user', username) g_ATVSettings.setSetting(UDID, 'myplex_auth', auth_token) g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'False') g_ATVSettings.setSetting(UDID, 'plexhome_user', '') g_ATVSettings.setSetting(UDID, 'plexhome_auth', '') - + XMLtemplate = 'Settings/Main.xml' path = '' # clear path - we don't need PMS-XML - - elif cmd=='MyPlexLogout': + + elif cmd == 'MyPlexLogout': dprint(__name__, 2, "MyPlex->Logging Out...") - + auth_token = g_ATVSettings.getSetting(UDID, 'myplex_auth') PlexAPI.MyPlexSignOut(auth_token) - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': ''}) - + PlexAPI.discoverPMS( + UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': ''}) + g_ATVSettings.setSetting(UDID, 'myplex_user', '') g_ATVSettings.setSetting(UDID, 'myplex_auth', '') g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'False') g_ATVSettings.setSetting(UDID, 'plexhome_user', '') g_ATVSettings.setSetting(UDID, 'plexhome_auth', '') - + XMLtemplate = 'Settings/Main.xml' path = '' # clear path - we don't need PMS-XML - - elif cmd=='MyPlexSwitchHomeUser': + + elif cmd == 'MyPlexSwitchHomeUser': dprint(__name__, 2, "MyPlex->switch HomeUser...") if not 'PlexConnectCredentials' in options: return XML_Error('PlexConnect', 'MyPlex HomeUser called without Credentials.') - - parts = options['PlexConnectCredentials'].split(':',1) - if len(parts)!=2: + + parts = options['PlexConnectCredentials'].split(':', 1) + if len(parts) != 2: return XML_Error('PlexConnect', 'MyPlex HomeUser called with bad Credentials.') - + auth_token = g_ATVSettings.getSetting(UDID, 'myplex_auth') - (plexHome_user, plexHome_auth) = PlexAPI.MyPlexSwitchHomeUser(parts[0], parts[1], options, auth_token) - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': auth_token, 'PlexHome': plexHome_auth}) - + (plexHome_user, plexHome_auth) = PlexAPI.MyPlexSwitchHomeUser( + parts[0], parts[1], options, auth_token) + PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], { + 'MyPlex': auth_token, 'PlexHome': plexHome_auth}) + g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'True') g_ATVSettings.setSetting(UDID, 'plexhome_user', plexHome_user) g_ATVSettings.setSetting(UDID, 'plexhome_auth', plexHome_auth) - + XMLtemplate = 'Settings/PlexHome.xml' - - elif cmd=='MyPlexLogoutHomeUser': + + elif cmd == 'MyPlexLogoutHomeUser': dprint(__name__, 2, "MyPlex->Logging Out HomeUser...") - + auth_token = g_ATVSettings.getSetting(UDID, 'myplex_auth') - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': auth_token, 'PlexHome': ''}) - - g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'True') # stays at PlexHome mode + PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], { + 'MyPlex': auth_token, 'PlexHome': ''}) + + # stays at PlexHome mode + g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'True') g_ATVSettings.setSetting(UDID, 'plexhome_user', '') g_ATVSettings.setSetting(UDID, 'plexhome_auth', '') - + XMLtemplate = 'Settings/PlexHome.xml' - - elif cmd=='MyPlexLeaveHome': + + elif cmd == 'MyPlexLeaveHome': dprint(__name__, 2, "MyPlex->Leave Home...") - + auth_token = g_ATVSettings.getSetting(UDID, 'myplex_auth') - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], {'MyPlex': auth_token}) - - g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'False') # exit PlexHome mode completely + PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], { + 'MyPlex': auth_token}) + + # exit PlexHome mode completely + g_ATVSettings.setSetting(UDID, 'plexhome_enable', 'False') g_ATVSettings.setSetting(UDID, 'plexhome_user', '') g_ATVSettings.setSetting(UDID, 'plexhome_auth', '') - + XMLtemplate = 'Settings/PlexHome.xml' - + elif cmd.startswith('Discover'): tokenDict = {} tokenDict['MyPlex'] = g_ATVSettings.getSetting(UDID, 'myplex_auth') if g_ATVSettings.getSetting(UDID, 'plexhome_enable') == 'True': - tokenDict['PlexHome'] = g_ATVSettings.getSetting(UDID, 'plexhome_auth') - PlexAPI.discoverPMS(UDID, g_param['CSettings'], g_param['IP_self'], tokenDict) - + tokenDict['PlexHome'] = g_ATVSettings.getSetting( + UDID, 'plexhome_auth') + PlexAPI.discoverPMS( + UDID, g_param['CSettings'], g_param['IP_self'], tokenDict) + # sanitize aTV settings from not-working combinations # fanart only with PIL/pillow installed, only with iOS>=6.0 # watch out: this check will make trouble with iOS10 if not PILBackgrounds.isPILinstalled() or \ not options['aTVFirmwareVersion'] >= '6.0': - dprint(__name__, 2, "disable fanart (PIL not installed or aTVFirmwareVersion<6.0)") + dprint( + __name__, 2, "disable fanart (PIL not installed or aTVFirmwareVersion<6.0)") g_ATVSettings.setSetting(UDID, 'fanart', 'Hide') - - return XML_Error('PlexConnect', 'Discover!') # not an error - but aTV won't care anyways. - + + # not an error - but aTV won't care anyways. + return XML_Error('PlexConnect', 'Discover!') + elif path.find('serviceSearch') != -1 or (path.find('video') != -1 and path.lower().find('search') != -1): XMLtemplate = 'Channels/VideoSearchResults.xml' - + elif path.find('SearchResults') != -1: XMLtemplate = 'Channels/VideoSearchResults.xml' # Special case scanners - if cmd=='S_-_BABS' or cmd=='BABS': + if cmd == 'S_-_BABS' or cmd == 'BABS': dprint(__name__, 1, "Found S - BABS / BABS") dir = 'TVShow' cmd = 'NavigationBar' - elif cmd=='Plex_Music': + elif cmd == 'Plex_Music': dprint(__name__, 1, "Found Plex_Music") dir = 'Music' cmd = 'NavigationBar' - elif cmd=='Plex_Movie': + elif cmd == 'Plex_Movie': dprint(__name__, 1, "Found Plex_Movie") dir = 'Movie' cmd = 'NavigationBar' - elif cmd=='Plex_TV_Series': + elif cmd == 'Plex_TV_Series': dprint(__name__, 1, "Found Plex_TV_Series") dir = 'TVShow' cmd = 'NavigationBar' elif cmd.find('Scanner') != -1: dprint(__name__, 1, "Found Scanner.") - if cmd.find('Series') != -1: dir = 'TVShow' - elif cmd.find('Movie') != -1: dir = 'Movie' + if cmd.find('Series') != -1: + dir = 'TVShow' + elif cmd.find('Movie') != -1: + dir = 'Movie' elif cmd.find('Video') != -1 or cmd.find('Personal_Media') != -1: # Plex Video Files Scanner # Extended Personal Media Scanner dir = 'HomeVideo' - elif cmd.find('Photo') != -1: dir = 'Photo' - elif cmd.find('Premium_Music') != -1: dir = 'Music' - elif cmd.find('Music') != -1 or cmd.find('iTunes') != -1: dir ='Music' - elif cmd.find('LiveTV') != -1: dir = 'LiveTV' + elif cmd.find('Photo') != -1: + dir = 'Photo' + elif cmd.find('Premium_Music') != -1: + dir = 'Music' + elif cmd.find('Music') != -1 or cmd.find('iTunes') != -1: + dir = 'Music' + elif cmd.find('LiveTV') != -1: + dir = 'LiveTV' else: return XML_Error('PlexConnect', 'Unknown scanner: '+cmd) - + cmd = 'NavigationBar' - # Not a special command so split it + # Not a special command so split it elif cmd.find('_') != -1: parts = cmd.split('_', 1) dir = parts[0] @@ -381,16 +421,17 @@ def XML_PMS2aTV(PMS_address, path, options): # Commands that contain a directory if dir != '': XMLtemplate = dir + '/' + cmd + '.xml' - if path == '/': path = '' - + if path == '/': + path = '' + dprint(__name__, 1, "Split Directory: {0} Command: {1}", dir, cmd) dprint(__name__, 1, "XMLTemplate: {0}", XMLtemplate) dprint(__name__, 1, "---------------------------------------------") - + PMSroot = None while True: # request PMS-XML - if not path=='' and not PMSroot and PMS_address: + if not path == '' and not PMSroot and PMS_address: if PMS_address in ['all', 'owned', 'shared', 'local', 'remote']: # owned, shared PMSs type = PMS_address @@ -398,209 +439,231 @@ def XML_PMS2aTV(PMS_address, path, options): else: # IP:port or plex.tv # PMS_uuid derived earlier from PMSaddress - auth_token = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'accesstoken') - enableGzip = PlexAPI.getPMSProperty(UDID, PMS_uuid, 'enableGzip') - PMS = PlexAPI.getXMLFromPMS(PMS_baseURL, path, options, auth_token, enableGzip) - - if PMS==False: + auth_token = PlexAPI.getPMSProperty( + UDID, PMS_uuid, 'accesstoken') + enableGzip = PlexAPI.getPMSProperty( + UDID, PMS_uuid, 'enableGzip') + PMS = PlexAPI.getXMLFromPMS( + PMS_baseURL, path, options, auth_token, enableGzip) + + if PMS == False: return XML_Error('PlexConnect', 'No Response from Plex Media Server') - + PMSroot = PMS.getroot() - + # get XMLtemplate aTVTree = etree.parse(sys.path[0]+'/assets/templates/'+XMLtemplate) aTVroot = aTVTree.getroot() - + # convert PMS XML to aTV XML using provided XMLtemplate - CommandCollection = CCommandCollection(options, PMSroot, PMS_address, path) + CommandCollection = CCommandCollection( + options, PMSroot, PMS_address, path) XML_ExpandTree(CommandCollection, aTVroot, PMSroot, 'main') XML_ExpandAllAttrib(CommandCollection, aTVroot, PMSroot, 'main') del CommandCollection - + # no redirect? exit loop! redirect = aTVroot.find('redirect') - if redirect==None: - break; - + if redirect == None: + break + # redirect to new PMS-XML - if necessary path_rdrct = redirect.get('newPath') if path_rdrct: path = path_rdrct PMSroot = None # force PMS-XML reload dprint(__name__, 1, "PMS-XML redirect: {0}", path) - + # redirect to new XMLtemplate - if necessary XMLtemplate_rdrct = redirect.get('template') if XMLtemplate_rdrct: XMLtemplate = XMLtemplate_rdrct.replace(" ", "") dprint(__name__, 1, "XMLTemplate redirect: {0}", XMLtemplate) - + dprint(__name__, 1, "====== generated aTV-XML ======") dprint(__name__, 1, aTVroot) dprint(__name__, 1, "====== aTV-XML finished ======") dprint(__name__, 1, "====== generated aTV-XML ======") dprint(__name__, 1, aTVroot) dprint(__name__, 1, "====== aTV-XML finished ======") - - return etree.tostring(aTVroot) + return etree.tostring(aTVroot) def XML_ExpandTree(CommandCollection, elem, src, srcXML): # unpack template 'COPY'/'CUT' command in children res = False while True: - if list(elem)==[]: # no sub-elements, stop recursion + if list(elem) == []: # no sub-elements, stop recursion break - + for child in elem: - res = XML_ExpandNode(CommandCollection, elem, child, src, srcXML, 'TEXT') - if res==True: # tree modified: restart from 1st elem + res = XML_ExpandNode(CommandCollection, elem, + child, src, srcXML, 'TEXT') + if res == True: # tree modified: restart from 1st elem break # "for child" - + # recurse into children XML_ExpandTree(CommandCollection, child, src, srcXML) - - res = XML_ExpandNode(CommandCollection, elem, child, src, srcXML, 'TAIL') - if res==True: # tree modified: restart from 1st elem + + res = XML_ExpandNode(CommandCollection, elem, + child, src, srcXML, 'TAIL') + if res == True: # tree modified: restart from 1st elem break # "for child" - - if res==False: # complete tree parsed with no change, stop recursion - break # "while True" + if res == False: # complete tree parsed with no change, stop recursion + break # "while True" def XML_ExpandNode(CommandCollection, elem, child, src, srcXML, text_tail): - if text_tail=='TEXT': # read line from text or tail + if text_tail == 'TEXT': # read line from text or tail line = child.text - elif text_tail=='TAIL': + elif text_tail == 'TAIL': line = child.tail else: - dprint(__name__, 0, "XML_ExpandNode - text_tail badly specified: {0}", text_tail) + dprint(__name__, 0, + "XML_ExpandNode - text_tail badly specified: {0}", text_tail) return False - + pos = 0 - while line!=None: - cmd_start = line.find('{{',pos) - cmd_end = line.find('}}',pos) - next_start = line.find('{{',cmd_start+2) - while next_start!=-1 and next_startcmd_end: + while line != None: + cmd_start = line.find('{{', pos) + cmd_end = line.find('}}', pos) + next_start = line.find('{{', cmd_start+2) + while next_start != -1 and next_start < cmd_end: + cmd_end = line.find('}}', cmd_end+2) + next_start = line.find('{{', next_start+2) + if cmd_start == -1 or cmd_end == -1 or cmd_start > cmd_end: return False # tree not touched, line unchanged - + dprint(__name__, 2, "XML_ExpandNode: {0}", line) - + cmd = line[cmd_start+2:cmd_end] - if cmd[-1]!=')': - dprint(__name__, 0, "XML_ExpandNode - closing bracket missing: {0} ", line) - - parts = cmd.split('(',1) + if cmd[-1] != ')': + dprint(__name__, 0, + "XML_ExpandNode - closing bracket missing: {0} ", line) + + parts = cmd.split('(', 1) cmd = parts[0] param = parts[1].strip(')') # remove ending bracket - + res = False if hasattr(CCommandCollection, 'TREE_'+cmd): # expand tree, work COPY, CUT - line = line[:cmd_start] + line[cmd_end+2:] # remove cmd from text and tail - if text_tail=='TEXT': + # remove cmd from text and tail + line = line[:cmd_start] + line[cmd_end+2:] + if text_tail == 'TEXT': child.text = line - elif text_tail=='TAIL': + elif text_tail == 'TAIL': child.tail = line - + try: - param = XML_ExpandLine(CommandCollection, src, srcXML, param) # expand any attributes in the parameter - res = getattr(CommandCollection, 'TREE_'+cmd)(elem, child, src, srcXML, param) + # expand any attributes in the parameter + param = XML_ExpandLine(CommandCollection, src, srcXML, param) + res = getattr(CommandCollection, 'TREE_' + + cmd)(elem, child, src, srcXML, param) except: - dprint(__name__, 0, "XML_ExpandNode - Error in cmd {0}, line {1}\n{2}", cmd, line, traceback.format_exc()) - - if res==True: + dprint( + __name__, 0, "XML_ExpandNode - Error in cmd {0}, line {1}\n{2}", cmd, line, traceback.format_exc()) + + if res == True: return True # tree modified, node added/removed: restart from 1st elem - - elif hasattr(CCommandCollection, 'ATTRIB_'+cmd): # check other known cmds: VAL, EVAL... - dprint(__name__, 2, "XML_ExpandNode - Stumbled over {0} in line {1}", cmd, line) + + # check other known cmds: VAL, EVAL... + elif hasattr(CCommandCollection, 'ATTRIB_'+cmd): + dprint(__name__, 2, + "XML_ExpandNode - Stumbled over {0} in line {1}", cmd, line) pos = cmd_end else: - dprint(__name__, 0, "XML_ExpandNode - Found unknown cmd {0} in line {1}", cmd, line) - line = line[:cmd_start] + "((UNKNOWN:"+cmd+"))" + line[cmd_end+2:] # mark unknown cmd in text or tail - if text_tail=='TEXT': + dprint( + __name__, 0, "XML_ExpandNode - Found unknown cmd {0} in line {1}", cmd, line) + # mark unknown cmd in text or tail + line = line[:cmd_start] + "((UNKNOWN:"+cmd+"))" + line[cmd_end+2:] + if text_tail == 'TEXT': child.text = line - elif text_tail=='TAIL': + elif text_tail == 'TAIL': child.tail = line - + dprint(__name__, 2, "XML_ExpandNode: {0} - done", line) return False - def XML_ExpandAllAttrib(CommandCollection, elem, src, srcXML): # unpack template commands in elem.text line = elem.text - if line!=None: - elem.text = XML_ExpandLine(CommandCollection, src, srcXML, line.strip()) - + if line != None: + elem.text = XML_ExpandLine( + CommandCollection, src, srcXML, line.strip()) + # unpack template commands in elem.tail line = elem.tail - if line!=None: - elem.tail = XML_ExpandLine(CommandCollection, src, srcXML, line.strip()) - + if line != None: + elem.tail = XML_ExpandLine( + CommandCollection, src, srcXML, line.strip()) + # unpack template commands in elem.attrib.value for attrib in elem.attrib: line = elem.get(attrib) - elem.set(attrib, XML_ExpandLine(CommandCollection, src, srcXML, line.strip())) - + elem.set(attrib, XML_ExpandLine( + CommandCollection, src, srcXML, line.strip())) + # recurse into children for el in elem: XML_ExpandAllAttrib(CommandCollection, el, src, srcXML) - def XML_ExpandLine(CommandCollection, src, srcXML, line): pos = 0 while True: - cmd_start = line.find('{{',pos) - cmd_end = line.find('}}',pos) - next_start = line.find('{{',cmd_start+2) - while next_start!=-1 and next_startcmd_end: - break; - + cmd_start = line.find('{{', pos) + cmd_end = line.find('}}', pos) + next_start = line.find('{{', cmd_start+2) + while next_start != -1 and next_start < cmd_end: + cmd_end = line.find('}}', cmd_end+2) + next_start = line.find('{{', next_start+2) + + if cmd_start == -1 or cmd_end == -1 or cmd_start > cmd_end: + break + dprint(__name__, 2, "XML_ExpandLine: {0}", line) - + cmd = line[cmd_start+2:cmd_end] - if cmd[-1]!=')': - dprint(__name__, 0, "XML_ExpandLine - closing bracket missing: {0} ", line) - - parts = cmd.split('(',1) + if cmd[-1] != ')': + dprint(__name__, 0, + "XML_ExpandLine - closing bracket missing: {0} ", line) + + parts = cmd.split('(', 1) cmd = parts[0] param = parts[1][:-1] # remove ending bracket - - if hasattr(CCommandCollection, 'ATTRIB_'+cmd): # expand line, work VAL, EVAL... - + + # expand line, work VAL, EVAL... + if hasattr(CCommandCollection, 'ATTRIB_'+cmd): + try: - param = XML_ExpandLine(CommandCollection, src, srcXML, param) # expand any attributes in the parameter - res = getattr(CommandCollection, 'ATTRIB_'+cmd)(src, srcXML, param) + # expand any attributes in the parameter + param = XML_ExpandLine(CommandCollection, src, srcXML, param) + res = getattr(CommandCollection, 'ATTRIB_' + + cmd)(src, srcXML, param) line = line[:cmd_start] + res + line[cmd_end+2:] pos = cmd_start+len(res) except: - dprint(__name__, 0, "XML_ExpandLine - Error in {0}\n{1}", line, traceback.format_exc()) - line = line[:cmd_start] + "((ERROR:"+cmd+"))" + line[cmd_end+2:] - + dprint( + __name__, 0, "XML_ExpandLine - Error in {0}\n{1}", line, traceback.format_exc()) + line = line[:cmd_start] + \ + "((ERROR:"+cmd+"))" + line[cmd_end+2:] + elif hasattr(CCommandCollection, 'TREE_'+cmd): # check other known cmds: COPY, CUT - dprint(__name__, 2, "XML_ExpandLine - stumbled over {0} in line {1}", cmd, line) + dprint(__name__, 2, + "XML_ExpandLine - stumbled over {0} in line {1}", cmd, line) pos = cmd_end else: - dprint(__name__, 0, "XML_ExpandLine - Found unknown cmd {0} in line {1}", cmd, line) - line = line[:cmd_start] + "((UNKNOWN:"+cmd+"))" + line[cmd_end+2:] - + dprint( + __name__, 0, "XML_ExpandLine - Found unknown cmd {0} in line {1}", cmd, line) + line = line[:cmd_start] + "((UNKNOWN:"+cmd+"))" + line[cmd_end+2:] + dprint(__name__, 2, "XML_ExpandLine: {0} - done", line) return line - """ # Command expander classes # CCommandHelper(): @@ -610,162 +673,175 @@ def XML_ExpandLine(CommandCollection, src, srcXML, line): # cmds with effect on the tree structure (COPY, CUT) - must be expanded first # cmds dealing with single node keys, text, tail only (VAL, EVAL, ADDR_PMS ,...) """ + + class CCommandHelper(): def __init__(self, options, PMSroot, PMS_address, path): self.options = options self.PMSroot = {'main': PMSroot} self.PMS_address = PMS_address # default PMS if nothing else specified self.path = {'main': path} - + self.ATV_udid = options['PlexConnectUDID'] self.PMS_uuid = PlexAPI.getPMSFromAddress(self.ATV_udid, PMS_address) - self.PMS_baseURL = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'baseURL') + self.PMS_baseURL = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'baseURL') self.variables = {} - + # internal helper functions def getParam(self, src, param): - parts = param.split(':',1) + parts = param.split(':', 1) param = parts[0] - leftover='' - if len(parts)>1: + leftover = '' + if len(parts) > 1: leftover = parts[1] - - param = param.replace('&col;',':') # colon # replace XML_template special chars - param = param.replace('&ocb;','{') # opening curly brace - param = param.replace('&ccb;','}') # closinging curly brace - - param = param.replace('"','"') # replace XML special chars - param = param.replace(''',"'") - param = param.replace('<','<') - param = param.replace('>','>') - param = param.replace('&','&') # must be last + + # colon # replace XML_template special chars + param = param.replace('&col;', ':') + param = param.replace('&ocb;', '{') # opening curly brace + param = param.replace('&ccb;', '}') # closinging curly brace + + param = param.replace('"', '"') # replace XML special chars + param = param.replace(''', "'") + param = param.replace('<', '<') + param = param.replace('>', '>') + param = param.replace('&', '&') # must be last sevenDate = datetime.datetime.now().replace(hour=19) elevenDate = datetime.datetime.now().replace(hour=23) - param = param.replace("7pmtimestamp", str(int(time.mktime(sevenDate.timetuple())))) - param = param.replace("11pmtimestamp", str(int(time.mktime(elevenDate.timetuple())))) - + param = param.replace("7pmtimestamp", str( + int(time.mktime(sevenDate.timetuple())))) + param = param.replace("11pmtimestamp", str( + int(time.mktime(elevenDate.timetuple())))) + dprint(__name__, 2, "CCmds_getParam: {0}, {1}", param, leftover) return [param, leftover] - + def getKey(self, src, srcXML, param): attrib, leftover = self.getParam(src, param) default, leftover = self.getParam(src, leftover) - - el, srcXML, attrib = self.getBase(src, srcXML, attrib) - + + el, srcXML, attrib = self.getBase(src, srcXML, attrib) + # walk the path if neccessary - while '/' in attrib and el!=None: - parts = attrib.split('/',1) - if parts[0].startswith('#') and attrib[1:] in self.variables: # internal variable in path + while '/' in attrib and el != None: + parts = attrib.split('/', 1) + # internal variable in path + if parts[0].startswith('#') and attrib[1:] in self.variables: el = el.find(self.variables[parts[0][1:]]) elif parts[0].startswith('$'): # setting - el = el.find(g_ATVSettings.getSetting(self.ATV_udid, parts[0][1:])) + el = el.find(g_ATVSettings.getSetting( + self.ATV_udid, parts[0][1:])) elif parts[0].startswith('%'): # PMS property - el = el.find(PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, parts[0][1:])) + el = el.find(PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, parts[0][1:])) else: el = el.find(parts[0]) attrib = parts[1] - + # check element and get attribute - if attrib.startswith('#') and attrib[1:] in self.variables: # internal variable + # internal variable + if attrib.startswith('#') and attrib[1:] in self.variables: res = self.variables[attrib[1:]] dfltd = False elif attrib.startswith('$'): # setting res = g_ATVSettings.getSetting(self.ATV_udid, attrib[1:]) dfltd = False elif attrib.startswith('%'): # PMS property - res = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, attrib[1:]) + res = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, attrib[1:]) dfltd = False - elif attrib.startswith('^') and attrib[1:] in self.options: # aTV property, http request options + # aTV property, http request options + elif attrib.startswith('^') and attrib[1:] in self.options: res = self.options[attrib[1:]] dfltd = False - elif el!=None and attrib in el.attrib: + elif el != None and attrib in el.attrib: res = el.get(attrib) dfltd = False - + else: # path/attribute not found res = default dfltd = True - - dprint(__name__, 2, "CCmds_getKey: {0},{1},{2}", res, leftover,dfltd) - return [res,leftover,dfltd] - + + dprint(__name__, 2, "CCmds_getKey: {0},{1},{2}", res, leftover, dfltd) + return [res, leftover, dfltd] + def getElement(self, src, srcXML, param): tag, leftover = self.getParam(src, param) - + el, srcXML, tag = self.getBase(src, srcXML, tag) - + # walk the path if neccessary - while len(tag)>0: - parts = tag.split('/',1) + while len(tag) > 0: + parts = tag.split('/', 1) el = el.find(parts[0]) - if not '/' in tag or el==None: + if not '/' in tag or el == None: break tag = parts[1] return [el, leftover] - + def getBase(self, src, srcXML, param): # get base element if param.startswith('@'): # redirect to additional XML - parts = param.split('/',1) + parts = param.split('/', 1) srcXML = parts[0][1:] src = self.PMSroot[srcXML] - leftover='' - if len(parts)>1: + leftover = '' + if len(parts) > 1: leftover = parts[1] elif param.startswith('/'): # start at root src = self.PMSroot['main'] leftover = param[1:] else: leftover = param - + return [src, srcXML, leftover] - + def getConversion(self, src, param): conv, leftover = self.getParam(src, param) - + # build conversion "dictionary" convlist = [] - if conv!='': + if conv != '': parts = conv.split('|') for part in parts: convstr = part.split('=') - convlist.append((unquote_plus(convstr[0]), unquote_plus(convstr[1]))) - + convlist.append( + (unquote_plus(convstr[0]), unquote_plus(convstr[1]))) + dprint(__name__, 2, "CCmds_getConversion: {0},{1}", convlist, leftover) return [convlist, leftover] - + def applyConversion(self, val, convlist): # apply string conversion - encodedval = val.replace(" ", "+") - if convlist!=[]: + encodedval = val.replace(" ", "+") + if convlist != []: for part in reversed(sorted(convlist)): - if encodedval>=part[0]: + if encodedval >= part[0]: val = part[1] break - + dprint(__name__, 2, "CCmds_applyConversion: {0}", val) return val - + def applyMath(self, val, math, frmt): # apply math function - eval try: x = eval(val) - if math!='': + if math != '': x = eval(math) val = ('{0'+frmt+'}').format(x) except: - dprint(__name__, 0, "CCmds_applyMath: Error in math {0}, frmt {1}\n{2}", math, frmt, traceback.format_exc()) + dprint( + __name__, 0, "CCmds_applyMath: Error in math {0}, frmt {1}\n{2}", math, frmt, traceback.format_exc()) # apply format specifier - + dprint(__name__, 2, "CCmds_applyMath: {0}", val) return val - - def _(self, msgid): - return Localize.getTranslation(self.options['aTVLanguage']).ugettext(msgid) + def _(self, msgid): + return Localize.getTranslation(self.options['aTVLanguage']).gettext(msgid) class CCommandCollection(CCommandHelper): @@ -774,29 +850,29 @@ class CCommandCollection(CCommandHelper): def TREE_COPY(self, elem, child, src, srcXML, param): tag, param_enbl = self.getParam(src, param) - src, srcXML, tag = self.getBase(src, srcXML, tag) - + src, srcXML, tag = self.getBase(src, srcXML, tag) + # walk the src path if neccessary - while '/' in tag and src!=None: - parts = tag.split('/',1) + while '/' in tag and src != None: + parts = tag.split('/', 1) src = src.find(parts[0]) tag = parts[1] - + # find index of child in elem - to keep consistent order for ix, el in enumerate(list(elem)): - if el==child: + if el == child: break - + # duplicate child and add to tree cnt = 0 for elemSRC in src.findall(tag): key = 'COPY' - if param_enbl!='': + if param_enbl != '': key, leftover, dfltd = self.getKey(elemSRC, srcXML, param_enbl) conv, leftover = self.getConversion(elemSRC, leftover) if not dfltd: key = self.applyConversion(key, conv) - + if key: self.PMSroot['copy_'+tag] = elemSRC self.variables['copy_ix'] = str(cnt) @@ -804,86 +880,89 @@ def TREE_COPY(self, elem, child, src, srcXML, param): el = copy.deepcopy(child) XML_ExpandTree(self, el, elemSRC, srcXML) XML_ExpandAllAttrib(self, el, elemSRC, srcXML) - - if el.tag=='__COPY__': + + if el.tag == '__COPY__': for el_child in list(el): elem.insert(ix, el_child) ix += 1 else: elem.insert(ix, el) ix += 1 - + # remove template child elem.remove(child) return True # tree modified, nodes updated: restart from 1st elem - - #syntax: Video, playType (Single|Continuous), key to match (^PlexConnectRatingKey), ratingKey + + # syntax: Video, playType (Single|Continuous), key to match (^PlexConnectRatingKey), ratingKey def TREE_COPY_PLAYLIST(self, elem, child, src, srcXML, param): - tag, leftover = self.getParam(src, param) - playType, leftover, dfltd = self.getKey(src, srcXML, leftover) # Single (default), Continuous + tag, leftover = self.getParam(src, param) + playType, leftover, dfltd = self.getKey( + src, srcXML, leftover) # Single (default), Continuous key, leftover, dfltd = self.getKey(src, srcXML, leftover) param_key = leftover - + src, srcXML, tag = self.getBase(src, srcXML, tag) - + # walk the src path if neccessary - while '/' in tag and src!=None: - parts = tag.split('/',1) + while '/' in tag and src != None: + parts = tag.split('/', 1) src = src.find(parts[0]) tag = parts[1] - + # find index of child in elem - to keep consistent order for ix, el in enumerate(list(elem)): - if el==child: + if el == child: break - + # filter elements to copy cnt = 0 copy_enbl = False elemsSRC = [] for elemSRC in src.findall(tag): - child_key, leftover, dfltd = self.getKey(elemSRC, srcXML, param_key) + child_key, leftover, dfltd = self.getKey( + elemSRC, srcXML, param_key) if not key: copy_enbl = True # copy all - elif playType == 'Continuous' or playType== 'Shuffle': - copy_enbl = copy_enbl or (key==child_key) # [0 0 1 1 1 1] + elif playType == 'Continuous' or playType == 'Shuffle': + copy_enbl = copy_enbl or (key == child_key) # [0 0 1 1 1 1] else: # 'Single' (default) - copy_enbl = (key==child_key) # [0 0 1 0 0 0] - + copy_enbl = (key == child_key) # [0 0 1 0 0 0] + if copy_enbl: elemsSRC.append(elemSRC) - + # shuffle elements if playType == 'Shuffle': if not key: random.shuffle(elemsSRC) # shuffle all else: - elems = elemsSRC[1:] # keep first element fix + # keep first element fix + elems = elemsSRC[1:] random.shuffle(elems) elemsSRC = [elemsSRC[0]] + elems - + # duplicate child and add to tree cnt = 0 for elemSRC in elemsSRC: - self.PMSroot['copy_'+tag] = elemSRC - self.variables['copy_ix'] = str(cnt) - cnt = cnt+1 - el = copy.deepcopy(child) - XML_ExpandTree(self, el, elemSRC, srcXML) - XML_ExpandAllAttrib(self, el, elemSRC, srcXML) - - if el.tag=='__COPY__': - for el_child in list(el): - elem.insert(ix, el_child) - ix += 1 - else: - elem.insert(ix, el) + self.PMSroot['copy_'+tag] = elemSRC + self.variables['copy_ix'] = str(cnt) + cnt = cnt+1 + el = copy.deepcopy(child) + XML_ExpandTree(self, el, elemSRC, srcXML) + XML_ExpandAllAttrib(self, el, elemSRC, srcXML) + + if el.tag == '__COPY__': + for el_child in list(el): + elem.insert(ix, el_child) ix += 1 - + else: + elem.insert(ix, el) + ix += 1 + # remove template child elem.remove(child) return True # tree modified, nodes updated: restart from 1st elem - + def TREE_CUT(self, elem, child, src, srcXML, param): key, leftover, dfltd = self.getKey(src, srcXML, param) conv, leftover = self.getConversion(src, leftover) @@ -894,115 +973,132 @@ def TREE_CUT(self, elem, child, src, srcXML, param): return True # tree modified, node removed: restart from 1st elem else: return False # tree unchanged - + def TREE_ADDXML(self, elem, child, src, srcXML, param): tag, leftover = self.getParam(src, param) key, leftover, dfltd = self.getKey(src, srcXML, leftover) - + PMS_address = self.PMS_address - + if key.startswith('//'): # local servers signature - pathstart = key.find('/',3) - PMS_address= key[:pathstart] + pathstart = key.find('/', 3) + PMS_address = key[:pathstart] path = key[pathstart:] elif key.startswith('/'): # internal full path. path = key - #elif key.startswith('http://'): # external address + # elif key.startswith('http://'): # external address # path = key elif key == '': # internal path path = self.path[srcXML] else: # internal path, add-on path = self.path[srcXML] + '/' + key - + if PMS_address in ['all', 'owned', 'shared', 'local', 'remote']: # owned, shared PMSs type = PMS_address - PMS = PlexAPI.getXMLFromMultiplePMS(self.ATV_udid, path, type, self.options) + PMS = PlexAPI.getXMLFromMultiplePMS( + self.ATV_udid, path, type, self.options) else: # IP:port or plex.tv - auth_token = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'accesstoken') - enableGzip = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'enableGzip') - PMS = PlexAPI.getXMLFromPMS(self.PMS_baseURL, path, self.options, auth_token, enableGzip) - + auth_token = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'accesstoken') + enableGzip = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'enableGzip') + PMS = PlexAPI.getXMLFromPMS( + self.PMS_baseURL, path, self.options, auth_token, enableGzip) + self.PMSroot[tag] = PMS.getroot() # store additional PMS XML self.path[tag] = path # store base path - - return False # tree unchanged (well, source tree yes. but that doesn't count...) - + + # tree unchanged (well, source tree yes. but that doesn't count...) + return False + def TREE_VAR(self, elem, child, src, srcXML, param): var, leftover = self.getParam(src, param) key, leftover, dfltd = self.getKey(src, srcXML, leftover) conv, leftover = self.getConversion(src, leftover) if not dfltd: key = self.applyConversion(key, conv) - + self.variables[var] = key return False # tree unchanged - + def TREE_MEDIABADGES(self, elem, child, src, srcXML, param): - resolution, leftover, dfltd = self.getKey(src, srcXML, param + "/videoResolution") - container, leftover, dfltd = self.getKey(src, srcXML, param + "/container") - vCodec, leftover, dfltd = self.getKey(src, srcXML, param + "/videoCodec") - aCodec, leftover, dfltd = self.getKey(src, srcXML, param + "/audioCodec") - channels, leftover, dfltd = self.getKey(src, srcXML, param + "/audioChannels") - + resolution, leftover, dfltd = self.getKey( + src, srcXML, param + "/videoResolution") + container, leftover, dfltd = self.getKey( + src, srcXML, param + "/container") + vCodec, leftover, dfltd = self.getKey( + src, srcXML, param + "/videoCodec") + aCodec, leftover, dfltd = self.getKey( + src, srcXML, param + "/audioCodec") + channels, leftover, dfltd = self.getKey( + src, srcXML, param + "/audioChannels") + additionalBadges = etree.Element("additionalMediaBadges") index = 0 attribs = {'insertIndex': '0', 'required': 'true', 'src': ''} - + # Resolution if resolution not in ['720', '1080', '2k', '4k']: - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/sd.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/sd.png' else: - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/' + resolution + '.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/' + resolution + '.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) index += 1 # Special case iTunes DRM if vCodec == 'drmi' or aCodec == 'drms': attribs['insertIndex'] = str(index) - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/iTunesDRM.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/iTunesDRM.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) child.append(additionalBadges) - return True # Finish, no more info needed + return True # Finish, no more info needed # File container if container != '' and self.options['aTVFirmwareVersion'] >= '7.0': attribs['insertIndex'] = str(index) - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/' + container + '.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/' + container + '.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) index += 1 # Video Codec if vCodec != '' and self.options['aTVFirmwareVersion'] >= '7.0': if vCodec == 'mpeg4': - vCodec = 'xvid' # Are there any other mpeg4-part 2 codecs? + vCodec = 'xvid' # Are there any other mpeg4-part 2 codecs? attribs['insertIndex'] = str(index) - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/' + vCodec + '.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/' + vCodec + '.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) - index += 1 + index += 1 # Audio Codec if aCodec != '': attribs['insertIndex'] = str(index) - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/' + aCodec + '.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/' + aCodec + '.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) - index += 1 + index += 1 # Audio Channels if channels != '': attribs['insertIndex'] = str(index) - attribs['src'] = g_param['baseURL'] + '/thumbnails/MediaBadges/' + channels + '.png' + attribs['src'] = g_param['baseURL'] + \ + '/thumbnails/MediaBadges/' + channels + '.png' urlBadge = etree.SubElement(additionalBadges, "urlBadge", attribs) # Append XML child.append(additionalBadges) - return True # Tree changed - - + return True # Tree changed + # XML ATTRIB modifier commands # add new commands to this list! + def ATTRIB_VAL(self, src, srcXML, param): key, leftover, dfltd = self.getKey(src, srcXML, param) conv, leftover = self.getConversion(src, leftover) if not dfltd: key = self.applyConversion(key, conv) return key - + def ATTRIB_EVAL(self, src, srcXML, param): return str(eval(param)) @@ -1011,12 +1107,12 @@ def ATTRIB_VAL_QUOTED(self, src, srcXML, param): conv, leftover = self.getConversion(src, leftover) if not dfltd: key = self.applyConversion(key, conv) - return quote_plus(unicode(key).encode("utf-8")) + return quote_plus(str(key).encode("utf-8")) def ATTRIB_SETTING(self, src, srcXML, param): opt, leftover = self.getParam(src, param) return g_ATVSettings.getSetting(self.ATV_udid, opt) - + def ATTRIB_ADDPATH(self, src, srcXML, param): addpath, leftover, dfltd = self.getKey(src, srcXML, param) if addpath.startswith('/'): @@ -1026,67 +1122,74 @@ def ATTRIB_ADDPATH(self, src, srcXML, param): else: res = self.path[srcXML]+'/'+addpath return res - + def ATTRIB_IMAGEURL(self, src, srcXML, param): key, leftover, dfltd = self.getKey(src, srcXML, param) width, leftover = self.getParam(src, leftover) height, leftover = self.getParam(src, leftover) - if height=='': + if height == '': height = width - + PMS_uuid = self.PMS_uuid PMS_baseURL = self.PMS_baseURL cmd_start = key.find('PMS(') cmd_end = key.find(')', cmd_start) - if cmd_start>-1 and cmd_end>-1 and cmd_end>cmd_start: + if cmd_start > -1 and cmd_end > -1 and cmd_end > cmd_start: PMS_address = key[cmd_start+4:cmd_end] PMS_uuid = PlexAPI.getPMSFromAddress(self.ATV_udid, PMS_address) - PMS_baseURL = PlexAPI.getPMSProperty(self.ATV_udid, PMS_uuid, 'baseURL') + PMS_baseURL = PlexAPI.getPMSProperty( + self.ATV_udid, PMS_uuid, 'baseURL') key = key[cmd_end+1:] - - AuthToken = PlexAPI.getPMSProperty(self.ATV_udid, PMS_uuid, 'accesstoken') - + + AuthToken = PlexAPI.getPMSProperty( + self.ATV_udid, PMS_uuid, 'accesstoken') + # transcoder action - transcoderAction = g_ATVSettings.getSetting(self.ATV_udid, 'phototranscoderaction') - + transcoderAction = g_ATVSettings.getSetting( + self.ATV_udid, 'phototranscoderaction') + # image orientation - orientation, leftover, dfltd = self.getKey(src, srcXML, 'Media/Part/orientation') - normalOrientation = (not orientation) or orientation=='1' - + orientation, leftover, dfltd = self.getKey( + src, srcXML, 'Media/Part/orientation') + normalOrientation = (not orientation) or orientation == '1' + # aTV native filetypes - parts = key.rsplit('.',1) - photoATVNative = parts[-1].lower() in ['jpg','jpeg','tif','tiff','gif','png'] + parts = key.rsplit('.', 1) + photoATVNative = parts[-1].lower() in ['jpg', + 'jpeg', 'tif', 'tiff', 'gif', 'png'] dprint(__name__, 2, "photo: ATVNative - {0}", photoATVNative) - - if width=='' and \ - transcoderAction=='Auto' and \ + + if width == '' and \ + transcoderAction == 'Auto' and \ normalOrientation and \ photoATVNative: # direct play res = PlexAPI.getDirectImagePath(key, AuthToken) else: - if width=='': + if width == '': width = 1920 # max for HDTV. Relate to aTV version? Increase for KenBurns effect? - if height=='': + if height == '': height = 1080 # as above # request transcoding - res = PlexAPI.getTranscodeImagePath(key, AuthToken, self.path[srcXML], width, height) - + res = PlexAPI.getTranscodeImagePath( + key, AuthToken, self.path[srcXML], width, height) + if res.startswith('/'): # internal full path. res = PMS_baseURL + res elif res.startswith('http://') or key.startswith('https://'): # external address pass else: # internal path, add-on res = PMS_baseURL + self.path[srcXML] + '/' + res - + dprint(__name__, 1, 'ImageURL: {0}', res) return res - + def ATTRIB_MUSICURL(self, src, srcXML, param): Track, leftover = self.getElement(src, srcXML, param) - - AuthToken = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'accesstoken') - + + AuthToken = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'accesstoken') + if not Track: # not a complete audio/track structure - take key directly and build direct-play path key, leftover, dfltd = self.getKey(src, srcXML, param) @@ -1094,25 +1197,25 @@ def ATTRIB_MUSICURL(self, src, srcXML, param): res = PlexAPI.getURL(self.PMS_baseURL, self.path[srcXML], res) dprint(__name__, 1, 'MusicURL - direct: {0}', res) return res - + # complete track structure - request transcoding if needed Media = Track.find('Media') - + # check "Media" element and get key - if Media!=None: + if Media != None: # transcoder action setting? # transcoder bitrate setting [kbps] - eg. 128, 256, 384, 512? maxAudioBitrateCompressed = '320' - + audioATVNative = \ - Media.get('audioCodec','-') in ("mp3", "aac", "ac3", "drms") and \ - int(Media.get('bitrate','0')) <= int(maxAudioBitrateCompressed) \ + Media.get('audioCodec', '-') in ("mp3", "aac", "ac3", "drms") and \ + int(Media.get('bitrate', '0')) <= int(maxAudioBitrateCompressed) \ or \ - Media.get('audioCodec','-') in ("alac", "aiff", "wav") + Media.get('audioCodec', '-') in ("alac", "aiff", "wav") # check Media.get('container') as well - mp3, m4a, ...? - + dprint(__name__, 2, "audio: ATVNative - {0}", audioATVNative) - + if audioATVNative: # direct play res, leftover, dfltd = self.getKey(Media, srcXML, 'Part/key') @@ -1120,33 +1223,34 @@ def ATTRIB_MUSICURL(self, src, srcXML, param): else: # request transcoding res, leftover, dfltd = self.getKey(Track, srcXML, 'key') - res = PlexAPI.getTranscodeAudioPath(res, AuthToken, self.options, maxAudioBitrateCompressed) - + res = PlexAPI.getTranscodeAudioPath( + res, AuthToken, self.options, maxAudioBitrateCompressed) + else: dprint(__name__, 0, "MEDIAPATH - element not found: {0}", param) res = 'FILE_NOT_FOUND' # not found? - + res = PlexAPI.getURL(self.PMS_baseURL, self.path[srcXML], res) dprint(__name__, 1, 'MusicURL: {0}', res) return res - + def ATTRIB_URL(self, src, srcXML, param): key, leftover, dfltd = self.getKey(src, srcXML, param) addPath, leftover = self.getParam(src, leftover) addOpt, leftover = self.getParam(src, leftover) - + # compare PMS_mark in PlexAPI/getXMLFromMultiplePMS() PMS_mark = '/PMS(' + self.PMS_address + ')' - + # overwrite with URL embedded PMS address cmd_start = key.find('PMS(') cmd_end = key.find(')', cmd_start) - if cmd_start>-1 and cmd_end>-1 and cmd_end>cmd_start: + if cmd_start > -1 and cmd_end > -1 and cmd_end > cmd_start: PMS_mark = '/'+key[cmd_start:cmd_end+1] key = key[cmd_end+1:] - + res = g_param['baseURL'] # base address to PlexConnect - + if key.endswith('.js'): # link to PlexConnect owned .js stuff res = res + key elif key.startswith('http://') or key.startswith('https://'): # external server @@ -1164,169 +1268,190 @@ def ATTRIB_URL(self, src, srcXML, param): res = res + PMS_mark + self.path[srcXML] else: # internal path, add-on res = res + PMS_mark + self.path[srcXML] + '/' + key - + if addPath: res = res + addPath - + if addOpt: if not '?' in res: - res = res +'?'+ addOpt + res = res + '?' + addOpt else: - res = res +'&'+ addOpt - + res = res + '&' + addOpt + return res - + def ATTRIB_VIDEOURL(self, src, srcXML, param): Video, leftover = self.getElement(src, srcXML, param) partIndex, leftover, dfltd = self.getKey(src, srcXML, leftover) partIndex = int(partIndex) if partIndex else 0 - - AuthToken = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'accesstoken') - + + AuthToken = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'accesstoken') + if not Video: - dprint(__name__, 0, "VIDEOURL - VIDEO element not found: {0}", param) + dprint(__name__, 0, + "VIDEOURL - VIDEO element not found: {0}", param) res = 'VIDEO_ELEMENT_NOT_FOUND' # not found? return res - + # complete video structure - request transcoding if needed Media = Video.find('Media') - + # check "Media" element and get key - if Media!=None: + if Media != None: # transcoder action - transcoderAction = g_ATVSettings.getSetting(self.ATV_udid, 'transcoderaction') - # transcoderAction = "Transcode" - + transcoderAction = g_ATVSettings.getSetting( + self.ATV_udid, 'transcoderaction') + # transcoderAction = "Transcode" + # video format # HTTP live stream # or native aTV media videoATVNative = \ - Media.get('protocol','-') in ("hls") \ + Media.get('protocol', '-') in ("hls") \ or \ - Media.get('container','-') in ("mov", "mp4") and \ - Media.get('videoCodec','-') in ("mpeg4", "h264", "drmi") and \ - Media.get('audioCodec','-') in ("aac", "drms") # remove AC3 when Dolby Digital is Off - - # determine if Dolby Digital is active - DolbyDigital = g_ATVSettings.getSetting(self.ATV_udid, 'dolbydigital') - if DolbyDigital=='On': - self.options['DolbyDigital'] = True + Media.get('container', '-') in ("mov", "mp4") and \ + Media.get('videoCodec', '-') in ("mpeg4", "h264", "drmi") and \ + Media.get('audioCodec', '-') in ("aac", + "drms") # remove AC3 when Dolby Digital is Off + + # determine if Dolby Digital is active + DolbyDigital = g_ATVSettings.getSetting( + self.ATV_udid, 'dolbydigital') + if DolbyDigital == 'On': + self.options['DolbyDigital'] = True videoATVNative = \ - Media.get('protocol','-') in ("hls") \ + Media.get('protocol', '-') in ("hls") \ or \ - Media.get('container','-') in ("mov", "mp4") and \ - Media.get('videoCodec','-') in ("mpeg4", "h264", "drmi") and \ - Media.get('audioCodec','-') in ("aac", "ac3", "drms") - + Media.get('container', '-') in ("mov", "mp4") and \ + Media.get('videoCodec', '-') in ("mpeg4", "h264", "drmi") and \ + Media.get('audioCodec', '-') in ("aac", "ac3", "drms") + for Stream in Media.find('Part').findall('Stream'): - if Stream.get('streamType','') == '1' and\ - Stream.get('codec','-') in ("mpeg4", "h264"): + if Stream.get('streamType', '') == '1' and\ + Stream.get('codec', '-') in ("mpeg4", "h264"): if Stream.get('profile', '-') == 'high 10' or \ - int(Stream.get('refFrames','0')) > 8: - videoATVNative = False - break + int(Stream.get('refFrames', '0')) > 8: + videoATVNative = False + break if Stream.get('scanType', '') == 'interlaced' or Stream.get('codec') == 'mpeg2video': videoATVNative = False break - + dprint(__name__, 2, "video: ATVNative - {0}", videoATVNative) - + # quality limits: quality=(resolution, quality, bitrate) - qLookup = { '480p 2.0Mbps' :('720x480', '60', '2000'), \ - '720p 3.0Mbps' :('1280x720', '75', '3000'), \ - '720p 4.0Mbps' :('1280x720', '100', '4000'), \ - '1080p 8.0Mbps' :('1920x1080', '60', '8000'), \ - '1080p 10.0Mbps' :('1920x1080', '75', '10000'), \ - '1080p 12.0Mbps' :('1920x1080', '90', '12000'), \ - '1080p 20.0Mbps' :('1920x1080', '100', '20000'), \ - '1080p 40.0Mbps' :('1920x1080', '100', '40000') } - if PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'local')=='1': - qLimits = qLookup[g_ATVSettings.getSetting(self.ATV_udid, 'transcodequality')] + qLookup = {'480p 2.0Mbps': ('720x480', '60', '2000'), + '720p 3.0Mbps': ('1280x720', '75', '3000'), + '720p 4.0Mbps': ('1280x720', '100', '4000'), + '1080p 8.0Mbps': ('1920x1080', '60', '8000'), + '1080p 10.0Mbps': ('1920x1080', '75', '10000'), + '1080p 12.0Mbps': ('1920x1080', '90', '12000'), + '1080p 20.0Mbps': ('1920x1080', '100', '20000'), + '1080p 40.0Mbps': ('1920x1080', '100', '40000')} + if PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'local') == '1': + qLimits = qLookup[g_ATVSettings.getSetting( + self.ATV_udid, 'transcodequality')] else: - qLimits = qLookup[g_ATVSettings.getSetting(self.ATV_udid, 'remotebitrate')] - + qLimits = qLookup[g_ATVSettings.getSetting( + self.ATV_udid, 'remotebitrate')] + # subtitle renderer, subtitle selection - subtitleRenderer = g_ATVSettings.getSetting(self.ATV_udid, 'subtitlerenderer') - + subtitleRenderer = g_ATVSettings.getSetting( + self.ATV_udid, 'subtitlerenderer') + subtitleId = '' subtitleKey = '' subtitleCodec = '' - for Stream in Media.find('Part').findall('Stream'): # Todo: check 'Part' existance, deal with multi part video - if Stream.get('streamType','') == '3' and\ - Stream.get('selected','0') == '1': - subtitleId = Stream.get('id','') - subtitleKey = Stream.get('key','') - subtitleCodec = Stream.get('codec','') + # Todo: check 'Part' existance, deal with multi part video + for Stream in Media.find('Part').findall('Stream'): + if Stream.get('streamType', '') == '3' and\ + Stream.get('selected', '0') == '1': + subtitleId = Stream.get('id', '') + subtitleKey = Stream.get('key', '') + subtitleCodec = Stream.get('codec', '') break - + subtitleIOSNative = \ - subtitleKey=='' and (subtitleCodec=="mov_text" or subtitleCodec=="ttxt" or subtitleCodec=="tx3g" or subtitleCodec=="text") # embedded + subtitleKey == '' and (subtitleCodec == "mov_text" or subtitleCodec == + "ttxt" or subtitleCodec == "tx3g" or subtitleCodec == "text") # embedded subtitlePlexConnect = \ - subtitleKey!='' and subtitleCodec=="srt" # external - + subtitleKey != '' and subtitleCodec == "srt" # external + # subtitle suitable for direct play? # no subtitle # or 'Auto' with subtitle by iOS or PlexConnect # or 'iOS,PMS' with subtitle by iOS subtitleDirectPlay = \ - subtitleId=='' \ + subtitleId == '' \ or \ - subtitleRenderer=='Auto' and \ - ( (videoATVNative and subtitleIOSNative) or subtitlePlexConnect ) \ + subtitleRenderer == 'Auto' and \ + ((videoATVNative and subtitleIOSNative) or subtitlePlexConnect) \ or \ - subtitleRenderer=='iOS, PMS' and \ + subtitleRenderer == 'iOS, PMS' and \ (videoATVNative and subtitleIOSNative) - dprint(__name__, 2, "subtitle: IOSNative - {0}, PlexConnect - {1}, DirectPlay - {2}", subtitleIOSNative, subtitlePlexConnect, subtitleDirectPlay) - + dprint(__name__, 2, "subtitle: IOSNative - {0}, PlexConnect - {1}, DirectPlay - {2}", + subtitleIOSNative, subtitlePlexConnect, subtitleDirectPlay) + # determine video URL - if transcoderAction=='DirectPlay' \ + if transcoderAction == 'DirectPlay' \ or \ - transcoderAction=='Auto' and \ + transcoderAction == 'Auto' and \ videoATVNative and \ - int(Media.get('bitrate','0')) < int(qLimits[2]) and \ + int(Media.get('bitrate', '0')) < int(qLimits[2]) and \ subtitleDirectPlay: # direct play for... # force direct play # or videoATVNative (HTTP live stream m4v/h264/aac...) # limited by quality setting # with aTV supported subtitle (iOS embedded tx3g, PlexConnext external srt) - res, leftover, dfltd = self.getKey(Media, srcXML, 'Part['+str(partIndex+1)+']/key') - - if Media.get('indirect', False): # indirect... todo: select suitable resolution, today we just take first Media - PMS = PlexAPI.getXMLFromPMS(self.PMS_baseURL, res, self.options, AuthToken) # todo... check key for trailing '/' or even 'http' - res, leftover, dfltd = self.getKey(PMS.getroot(), srcXML, 'Video/Media/Part['+str(partIndex+1)+']/key') - + res, leftover, dfltd = self.getKey( + Media, srcXML, 'Part['+str(partIndex+1)+']/key') + + # indirect... todo: select suitable resolution, today we just take first Media + if Media.get('indirect', False): + # todo... check key for trailing '/' or even 'http' + PMS = PlexAPI.getXMLFromPMS( + self.PMS_baseURL, res, self.options, AuthToken) + res, leftover, dfltd = self.getKey( + PMS.getroot(), srcXML, 'Video/Media/Part['+str(partIndex+1)+']/key') + res = PlexAPI.getDirectVideoPath(res, AuthToken) else: # request transcoding - res = Video.get('key','') - + res = Video.get('key', '') + # misc settings: subtitlesize, audioboost - subtitle = { 'selected': '1' if subtitleId else '0', \ - 'dontBurnIn': '1' if subtitleDirectPlay else '0', \ - 'size': g_ATVSettings.getSetting(self.ATV_udid, 'subtitlesize') } - audio = { 'boost': g_ATVSettings.getSetting(self.ATV_udid, 'audioboost') } - res = PlexAPI.getTranscodeVideoPath(res, AuthToken, self.options, transcoderAction, qLimits, subtitle, audio, partIndex) - + subtitle = {'selected': '1' if subtitleId else '0', + 'dontBurnIn': '1' if subtitleDirectPlay else '0', + 'size': g_ATVSettings.getSetting(self.ATV_udid, 'subtitlesize')} + audio = {'boost': g_ATVSettings.getSetting( + self.ATV_udid, 'audioboost')} + res = PlexAPI.getTranscodeVideoPath( + res, AuthToken, self.options, transcoderAction, qLimits, subtitle, audio, partIndex) + else: - dprint(__name__, 0, "VIDEOURL - MEDIA element not found: {0}", param) + dprint(__name__, 0, + "VIDEOURL - MEDIA element not found: {0}", param) res = 'MEDIA_ELEMENT_NOT_FOUND' # not found? - + if res.startswith('/'): # internal full path. res = self.PMS_baseURL + res elif res.startswith('http://') or res.startswith('https://'): # external address pass else: # internal path, add-on res = self.PMS_baseURL + self.path[srcXML] + res - + dprint(__name__, 1, 'VideoURL: {0}', res) return res - + def ATTRIB_episodestring(self, src, srcXML, param): - parentIndex, leftover, dfltd = self.getKey(src, srcXML, param) # getKey "defaults" if nothing found. + # getKey "defaults" if nothing found. + parentIndex, leftover, dfltd = self.getKey(src, srcXML, param) index, leftover, dfltd = self.getKey(src, srcXML, leftover) title, leftover, dfltd = self.getKey(src, srcXML, leftover) - out = self._("{0:0d}x{1:02d} {2}").format(int(parentIndex), int(index), title) + out = self._("{0:0d}x{1:02d} {2}").format( + int(parentIndex), int(index), title) return out def ATTRIB_durationToString(self, src, srcXML, param): @@ -1338,20 +1463,22 @@ def ATTRIB_durationToString(self, src, srcXML, param): return self._("{0:d} Minutes").format(min) else: if len(duration) > 0: - hour = min/60 - min = min%60 - if hour == 0: return self._("{0:d} Minutes").format(min) - else: return self._("{0:d}hr {1:d}min").format(hour, min) - + hour = int(min / 60) + min = int(min % 60) + if hour == 0: + return self._(f"{min} Minutes") + else: + return self._(f"{hour}hr {min}min") + if type == 'Audio': secs = int(duration)/1000 if len(duration) > 0: mins = secs/60 - secs = secs%60 + secs = secs % 60 return self._("{0:d}:{1:0>2d}").format(mins, secs) - + return "" - + def ATTRIB_contentRating(self, src, srcXML, param): rating, leftover, dfltd = self.getKey(src, srcXML, param) if rating.find('/') != -1: @@ -1359,75 +1486,79 @@ def ATTRIB_contentRating(self, src, srcXML, param): return parts[1] else: return rating - + def ATTRIB_unwatchedCountGrid(self, src, srcXML, param): total, leftover, dfltd = self.getKey(src, srcXML, param) viewed, leftover, dfltd = self.getKey(src, srcXML, leftover) unwatched = int(total) - int(viewed) return str(unwatched) - + def ATTRIB_unwatchedCountList(self, src, srcXML, param): total, leftover, dfltd = self.getKey(src, srcXML, param) viewed, leftover, dfltd = self.getKey(src, srcXML, leftover) unwatched = int(total) - int(viewed) - if unwatched > 0: return self._("{0} unwatched").format(unwatched) - else: return "" - + if unwatched > 0: + return self._("{0} unwatched").format(unwatched) + else: + return "" + def ATTRIB_TEXT(self, src, srcXML, param): return self._(param) - + def ATTRIB_PMSCOUNT(self, src, srcXML, param): - return str(PlexAPI.getPMSCount(self.ATV_udid) - 1) # -1: correct for plex.tv - + # -1: correct for plex.tv + return str(PlexAPI.getPMSCount(self.ATV_udid) - 1) + def ATTRIB_PMSNAME(self, src, srcXML, param): PMS_name = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'name') - if PMS_name=='': + if PMS_name == '': return "No Server in Proximity" else: return PMS_name - + def ATTRIB_BACKGROUNDURL(self, src, srcXML, param): key, leftover, dfltd = self.getKey(src, srcXML, param) - + if key.startswith('/'): # internal full path. key = self.PMS_baseURL + key elif key.startswith('http://') or key.startswith('https://'): # external address pass else: # internal path, add-on key = self.PMS_baseURL + self.path[srcXML] + key - - auth_token = PlexAPI.getPMSProperty(self.ATV_udid, self.PMS_uuid, 'accesstoken') - + + auth_token = PlexAPI.getPMSProperty( + self.ATV_udid, self.PMS_uuid, 'accesstoken') + dprint(__name__, 1, "Background (Source): {0}", key) res = g_param['baseURL'] # base address to PlexConnect - res = res + PILBackgrounds.generate(self.PMS_uuid, key, auth_token, self.options['aTVScreenResolution'], g_ATVSettings.getSetting(self.ATV_udid, 'fanart_blur'), g_param['CSettings']) + res = res + PILBackgrounds.generate(self.PMS_uuid, key, auth_token, self.options['aTVScreenResolution'], g_ATVSettings.getSetting( + self.ATV_udid, 'fanart_blur'), g_param['CSettings']) dprint(__name__, 1, "Background: {0}", res) return res - -if __name__=="__main__": +if __name__ == "__main__": cfg = Settings.CSettings() param = {} param['CSettings'] = cfg param['HostToIntercept'] = cfg.getSetting('hosttointercept') setParams(param) - + cfg = ATVSettings.CATVSettings() setATVSettings(cfg) - - print "load PMS XML" + + print("load PMS XML") _XML = ' \ \ \ ' PMSroot = etree.fromstring(_XML) PMSTree = etree.ElementTree(PMSroot) - print prettyXML(PMSroot) - - print - print "load aTV XML template" + print(prettyXML(PMSroot)) + + print() + print("load aTV XML template") _XML = ' \ Info \ \ @@ -1443,27 +1574,28 @@ def ATTRIB_BACKGROUNDURL(self, src, srcXML, param): ' aTVroot = etree.fromstring(_XML) aTVTree = etree.ElementTree(aTVroot) - print prettyXML(aTVroot) - - print - print "unpack PlexConnect COPY/CUT commands" + print(prettyXML(aTVroot)) + + print() + print("unpack PlexConnect COPY/CUT commands") options = {} options['PlexConnectUDID'] = '007' PMS_address = 'PMS_IP' - CommandCollection = CCommandCollection(options, PMSroot, PMS_address, '/library/sections') + CommandCollection = CCommandCollection( + options, PMSroot, PMS_address, '/library/sections') XML_ExpandTree(CommandCollection, aTVroot, PMSroot, 'main') XML_ExpandAllAttrib(CommandCollection, aTVroot, PMSroot, 'main') del CommandCollection - - print - print "resulting aTV XML" - print prettyXML(aTVroot) - - print - #print "store aTV XML" + + print() + print("resulting aTV XML") + print(prettyXML(aTVroot)) + + print() + # print "store aTV XML" #str = prettyXML(aTVTree) #f=open(sys.path[0]+'/XML/aTV_fromTmpl.xml', 'w') - #f.write(str) - #f.close() - + # f.write(str) + # f.close() + del cfg diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..9b764861 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +dnslib==0.9.16 +pillow==8.3.2 \ No newline at end of file