From 7eb4ea0e998a9f54217011ec9bc2dbd4b99cc40d Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 08:33:26 -0500 Subject: [PATCH 01/19] first pass at python3 --- .gitignore | 14 ++------ ATVSettings.py | 20 +++++------ DNSServer.py | 82 +++++++++++++++++++++---------------------- Debug.py | 13 +++---- Localize.py | 6 ++-- PILBackgrounds.py | 18 +++++----- PlexAPI.py | 36 +++++++++---------- PlexConnect_daemon.py | 6 ++-- Settings.py | 47 +++++++++++-------------- Subtitle.py | 10 +++--- WebServer.py | 20 +++++------ XMLConverter.py | 56 ++++++++++++++--------------- 12 files changed, 153 insertions(+), 175 deletions(-) diff --git a/.gitignore b/.gitignore index 601cfa619..4dfcc4c8a 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 2c61fd386..653a42f4f 100755 --- a/ATVSettings.py +++ b/ATVSettings.py @@ -3,7 +3,7 @@ import sys from os import sep, makedirs from os.path import isdir -import ConfigParser +import configparser import fnmatch from Debug import * # dprint() @@ -101,7 +101,7 @@ def loadSettings(self): 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): @@ -190,20 +190,20 @@ def setOptions(self, option, opts): ATVSettings.checkSection(UDID) option = 'transcodequality' - print ATVSettings.getSetting(UDID, option) + print(ATVSettings.getSetting(UDID, option)) - print "setSetting" + 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(ATVSettings.getSetting(UDID, option)) - print "toggleSetting" + 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 54c553a86..ec4d7a273 100755 --- a/DNSServer.py +++ b/DNSServer.py @@ -113,20 +113,20 @@ def DNSToHost(DNSdata, i, followlink=True): def printDNSdata(paket): # HEADER - print "ID {0:04x}".format((ord(paket[0])<<8)+ord(paket[1])) - print "flags {0:02x} {1:02x}".format(ord(paket[2]), ord(paket[3])) - print "OpCode "+str((ord(paket[2])>>3)&0x0F) - print "RCode "+str((ord(paket[3])>>0)&0x0F) + print("ID {0:04x}".format((ord(paket[0])<<8)+ord(paket[1]))) + print("flags {0:02x} {1:02x}".format(ord(paket[2]), ord(paket[3]))) + print("OpCode "+str((ord(paket[2])>>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 + print("Count - QD, AN, NS, AR:", qdcount, ancount, nscount, arcount) adr = 12 # QDCOUNT (query) for i in range(qdcount): - print "QUERY" + print("QUERY") host = DNSToHost(paket, adr) """ @@ -136,62 +136,62 @@ def printDNSdata(paket): """ 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])) + 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]) + print("ANSWER") + print(ord(paket[adr])) if ord(paket[adr]) & 0xC0: - print"link" + print("link") adr = adr + 2 else: host = DNSToHost(paket, adr) adr = adr + len(host) + 2 - print host + 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 + print("type, class: ", _type, _class) adr = adr + 4 - print "ttl" + print("ttl") adr = adr + 4 rdlength = (ord(paket[adr+0])<<8)+ord(paket[adr+1]) - print "rdlength", rdlength + print("rdlength", rdlength) adr = adr + 2 if _type==1: - print "IP:", + print("IP:", end=' ') for j in range(rdlength): - print ord(paket[adr+j]), - print + print(ord(paket[adr+j]), end=' ') + print() elif _type==5: - print "redirect:", DNSToHost(paket, adr) + print("redirect:", DNSToHost(paket, adr)) else: - print "type unsupported:", + print("type unsupported:", end=' ') for j in range(rdlength): - print ord(paket[adr+j]), - print + print(ord(paket[adr+j]), end=' ') + 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 + print() + print("{0:02x}".format(ord(DNSdata[i])), end=' ') + print() # printable characters for i in range(len(DNSdata)): if i % 16==0: - print + print() if (ord(DNSdata[i])>32) & (ord(DNSdata[i])<128): - print DNSdata[i], + print(DNSdata[i], end=' ') else: - print ".", - print + print(".", end=' ') + print() @@ -294,20 +294,20 @@ def appendWord(DNSdata, val): def printDNSstruct(DNSstruct): for i in range(DNSstruct['head']['qdcnt']): - print "query:", DNSstruct['query'][i]['host'] + 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'] + print("resrc:", end=' ') + print(DNSstruct['resrc'][i]['host']) if DNSstruct['resrc'][i]['type']==1: - print "->IP:", + print("->IP:", end=' ') for j in range(DNSstruct['resrc'][i]['rdlen']): - print ord(DNSstruct['resrc'][i]['rdata'][j]), - print + print(ord(DNSstruct['resrc'][i]['rdata'][j]), end=' ') + print() elif DNSstruct['resrc'][i]['type']==5: - print "->alias:", DNSstruct['resrc'][i]['rdata'] + print("->alias:", DNSstruct['resrc'][i]['rdata']) else: - print "->unknown type" + print("->unknown type") @@ -326,7 +326,7 @@ def Run(cmdPipe, param): 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: + except Exception as e: dprint(__name__, 0, "Failed to create socket on UDP port {0}: {1}", cfg_Port_DNSServer, e) sys.exit(1) @@ -388,7 +388,7 @@ def Run(cmdPipe, param): 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 + paket+=str.join('',[chr(int(x)) for x in cfg_IP_self.split('.')]) # 4bytes of IP dprint(__name__, 1, "-> DNS response: "+cfg_IP_self) elif domain in restrain: @@ -411,7 +411,7 @@ def Run(cmdPipe, param): try: DNS_forward = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) DNS_forward.settimeout(5.0) - except Exception, e: + except Exception as e: dprint(__name__, 0, "Failed to create socket for DNS_forward): {0}", e) continue diff --git a/Debug.py b/Debug.py index 987a1f85b..1e036e994 100755 --- a/Debug.py +++ b/Debug.py @@ -63,11 +63,6 @@ def dprint(src, dlevel, *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') @@ -82,13 +77,13 @@ def dprint(src, dlevel, *args): # print to terminal window if logToTerminal: - print(time.strftime("%b %d,%Y %H:%M:%S")), + print((time.strftime("%b %d,%Y %H:%M:%S")), end=' ') if len(asc_args)==0: - print src+":" + print(src+":") elif len(asc_args)==1: - print src+": "+str(asc_args[0]) + print(src+": "+str(asc_args[0])) else: - print src+": "+asc_args[0].format(*asc_args[1:]) + print(src+": "+asc_args[0].format(*asc_args[1:])) diff --git a/Localize.py b/Localize.py index 7eca9efe7..f510c0575 100755 --- a/Localize.py +++ b/Localize.py @@ -60,10 +60,10 @@ def replaceTEXT(textcontent, language): language = pickLanguage(languages) Text = "Hello World" # doesn't translate - print getTranslation(language).ugettext(Text) + print(getTranslation(language).ugettext(Text)) Text = "Library" # translates - print getTranslation(language).ugettext(Text) + print(getTranslation(language).ugettext(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 8d506dc6e..dde396f77 100755 --- a/PILBackgrounds.py +++ b/PILBackgrounds.py @@ -3,9 +3,7 @@ import re import sys import io -import urllib -import urllib2 -import urlparse +import urllib.request, urllib.parse, urllib.error import posixpath import traceback @@ -29,10 +27,10 @@ def generate(PMS_uuid, url, authtoken, resolution, blurRadius, CSettings): 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) + cachefile = urllib.parse.quote_plus(PMS_uuid +"_"+ fileid +"_"+ resolution +"_"+ blurRadius) + ".jpg" # quote: just to make sure... # Already created? dprint(__name__, 1, 'Check for Cachefile.') # Debug @@ -47,13 +45,13 @@ 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: + 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: diff --git a/PlexAPI.py b/PlexAPI.py index 4aaf8e33a..0d45571ae 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -32,9 +32,9 @@ import sys import struct import time -import urllib2, httplib, socket, StringIO, gzip +import urllib.request, urllib.error, urllib.parse, http.client, socket, io, gzip from threading import Thread -import Queue +import queue import traceback try: @@ -42,7 +42,7 @@ except ImportError: 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() @@ -318,7 +318,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): if XML==False: pass # no data from MyPlex else: - queue = Queue.Queue() + queue = queue.Queue() threads = [] PMSsPoked = 0 @@ -441,7 +441,7 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): 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 @@ -449,8 +449,8 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): 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) @@ -463,7 +463,7 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): 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: @@ -522,7 +522,7 @@ def getXArgsDeviceInfo(options={}): XML """ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): - queue = Queue.Queue() + queue = queue.Queue() threads = [] root = etree.Element("MediaConverter") @@ -667,7 +667,7 @@ def MyPlexSignIn(username, password, options): # create POST request xargs = getXArgsDeviceInfo(options) - request = urllib2.Request(MyPlexURL, None, xargs) + request = urllib.request.Request(MyPlexURL, None, xargs) request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. # no certificate, will fail with "401 - Authentification required" @@ -684,15 +684,15 @@ def MyPlexSignIn(username, password, options): ### optional... when 'realm' is unknown ##passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() ##passmanager.add_password(None, address, username, password) # None: default "realm" - passmanager = urllib2.HTTPPasswordMgr() + passmanager = urllib.request.HTTPPasswordMgr() passmanager.add_password(MyPlexHost, MyPlexURL, username, password) # realm = 'plex.tv' - authhandler = urllib2.HTTPBasicAuthHandler(passmanager) - urlopener = urllib2.build_opener(authhandler) + 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: + except urllib.error.HTTPError as e: if e.code==401: dprint(__name__, 0, 'Authentication failed') return ('', '') @@ -730,10 +730,10 @@ def MyPlexSignOut(authtoken): # create POST request xargs = { 'X-Plex-Token': authtoken } - request = urllib2.Request(MyPlexURL, None, xargs) + request = urllib.request.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() + response = urllib.request.urlopen(request).read() dprint(__name__, 1, "====== MyPlex sign out XML ======") dprint(__name__, 1, response) @@ -754,10 +754,10 @@ def MyPlexSwitchHomeUser(id, pin, options, authtoken): xargs = getXArgsDeviceInfo(options) xargs['X-Plex-Token'] = authtoken - request = urllib2.Request(MyPlexURL, None, xargs) + request = urllib.request.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() + response = urllib.request.urlopen(request).read() dprint(__name__, 1, "====== MyPlexHomeUser XML ======") dprint(__name__, 1, response) diff --git a/PlexConnect_daemon.py b/PlexConnect_daemon.py index 3290f151c..747b7c0f9 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -25,7 +25,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,7 +40,7 @@ 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 @@ -58,7 +58,7 @@ def daemonize(args): atexit.register(delpid) pid = str(os.getpid()) file(args.pidfile, 'w').write("%s\n" % pid) - except IOError, e: + except IOError as e: raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) diff --git a/Settings.py b/Settings.py index 5b8ca03d8..0aecf9bc0 100755 --- a/Settings.py +++ b/Settings.py @@ -3,7 +3,7 @@ import sys from os import sep, makedirs from os.path import isdir -import ConfigParser +import configparser import re from Debug import * # dprint() @@ -60,7 +60,7 @@ 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 +68,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 +92,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 +100,39 @@ 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) - + # 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)) 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 b50200e7c..8fa0403d3 100755 --- a/Subtitle.py +++ b/Subtitle.py @@ -8,7 +8,7 @@ import re -import urllib2 +import urllib.request, urllib.error, urllib.parse import json from Debug import * # dprint(), prettyXML() @@ -52,10 +52,10 @@ def getSubtitleJSON(PMS_address, path, options): 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) @@ -69,7 +69,7 @@ def getSubtitleJSON(PMS_address, path, options): # 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]) diff --git a/WebServer.py b/WebServer.py index 31cd697fc..0fa3548a1 100755 --- a/WebServer.py +++ b/WebServer.py @@ -16,11 +16,11 @@ import sys import string, cgi, 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, urllib.parse, urllib.error, io, gzip import signal import traceback @@ -70,7 +70,7 @@ def log_message(self, format, *args): 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() @@ -81,7 +81,7 @@ def sendResponse(self, data, type, enableGzip): 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 = list(map(string.strip, string.split(self.headers["accept-encoding"], ","))) except KeyError: accept_encoding = [] if enableGzip and \ @@ -104,7 +104,7 @@ def do_GET(self): PMSaddress = '' pms_end = self.path.find(')') if self.path.startswith('/PMS(') and pms_end>-1: - PMSaddress = urllib.unquote_plus(self.path[5:pms_end]) + PMSaddress = urllib.parse.unquote_plus(self.path[5:pms_end]) self.path = self.path[pms_end+1:] # break up path, separate PlexConnect options @@ -128,7 +128,7 @@ def do_GET(self): 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=='': @@ -213,7 +213,7 @@ def do_GET(self): 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 @@ -289,7 +289,7 @@ def Run(cmdPipe, param): try: server = ThreadedHTTPServer((cfg_IP_WebServer,int(cfg_Port_WebServer)), MyHandler) server.timeout = 1 - except Exception, 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) @@ -351,7 +351,7 @@ def Run_SSL(cmdPipe, param): 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: + 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) diff --git a/XMLConverter.py b/XMLConverter.py index ac358863e..1daf121c7 100755 --- a/XMLConverter.py +++ b/XMLConverter.py @@ -32,9 +32,9 @@ import xml.etree.ElementTree as etree import time, uuid, hmac, hashlib, base64 -from urllib import quote_plus, unquote_plus, urlencode -import urllib2 -import urlparse +from urllib.parse import quote_plus, unquote_plus, urlencode +import urllib.request, urllib.error, urllib.parse +import urllib.parse from Version import __VERSION__ # for {{EVAL()}}, display in settings page import Settings, ATVSettings @@ -217,22 +217,22 @@ def XML_PMS2aTV(PMS_address, path, options): 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 == '': @@ -739,7 +739,7 @@ def getConversion(self, src, param): def applyConversion(self, val, convlist): # apply string conversion - encodedval = val.replace(" ", "+") + encodedval = val.replace(" ", "+") if convlist!=[]: for part in reversed(sorted(convlist)): if encodedval>=part[0]: @@ -1011,7 +1011,7 @@ 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) @@ -1050,8 +1050,8 @@ def ATTRIB_IMAGEURL(self, src, srcXML, param): 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) @@ -1195,7 +1195,7 @@ def ATTRIB_VIDEOURL(self, src, srcXML, param): if Media!=None: # transcoder action transcoderAction = g_ATVSettings.getSetting(self.ATV_udid, 'transcoderaction') - # transcoderAction = "Transcode" + # transcoderAction = "Transcode" # video format # HTTP live stream @@ -1207,10 +1207,10 @@ def ATTRIB_VIDEOURL(self, src, srcXML, param): 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 + # 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") \ or \ @@ -1417,17 +1417,17 @@ def ATTRIB_BACKGROUNDURL(self, src, srcXML, 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(prettyXML(PMSroot)) - print - print "load aTV XML template" + print() + print("load aTV XML template") _XML = ' \ Info \ \ @@ -1443,10 +1443,10 @@ def ATTRIB_BACKGROUNDURL(self, src, srcXML, param): ' aTVroot = etree.fromstring(_XML) aTVTree = etree.ElementTree(aTVroot) - print prettyXML(aTVroot) + print(prettyXML(aTVroot)) - print - print "unpack PlexConnect COPY/CUT commands" + print() + print("unpack PlexConnect COPY/CUT commands") options = {} options['PlexConnectUDID'] = '007' PMS_address = 'PMS_IP' @@ -1455,11 +1455,11 @@ def ATTRIB_BACKGROUNDURL(self, src, srcXML, param): XML_ExpandAllAttrib(CommandCollection, aTVroot, PMSroot, 'main') del CommandCollection - print - print "resulting aTV XML" - print prettyXML(aTVroot) + print() + print("resulting aTV XML") + print(prettyXML(aTVroot)) - print + print() #print "store aTV XML" #str = prettyXML(aTVTree) #f=open(sys.path[0]+'/XML/aTV_fromTmpl.xml', 'w') From 9e95978ffeecf135209dd56cb1e768d0bedb6ef6 Mon Sep 17 00:00:00 2001 From: dlgoodr Date: Wed, 29 Sep 2021 08:37:32 -0500 Subject: [PATCH 02/19] fix another write --- ATVSettings.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ATVSettings.py b/ATVSettings.py index 653a42f4f..64d0e5cd6 100755 --- a/ATVSettings.py +++ b/ATVSettings.py @@ -106,9 +106,8 @@ def loadSettings(self): 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('.'): From a6b42b57ae2f6aef7992c14dd5be055316b10715 Mon Sep 17 00:00:00 2001 From: dlgoodr Date: Wed, 29 Sep 2021 10:30:03 -0500 Subject: [PATCH 03/19] fix accept_encoding processing --- WebServer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebServer.py b/WebServer.py index 0fa3548a1..18dd32341 100755 --- a/WebServer.py +++ b/WebServer.py @@ -81,7 +81,7 @@ def sendResponse(self, data, type, enableGzip): self.send_header('Server', 'PlexConnect') self.send_header('Content-type', type) try: - accept_encoding = list(map(string.strip, string.split(self.headers["accept-encoding"], ","))) + accept_encoding = [x.strip() for x in self.headers["accept-encoding"].split(",")] except KeyError: accept_encoding = [] if enableGzip and \ @@ -92,7 +92,7 @@ def sendResponse(self, data, type, enableGzip): self.wfile.write(self.compress(data)) else: self.end_headers() - self.wfile.write(data) + self.wfile.write(data.encode()) def do_GET(self): global g_param From bb0b9bbe5896ed080bee8d7ef1996195842c9820 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 10:38:35 -0500 Subject: [PATCH 04/19] autopep8 --- ATVSettings.py | 206 ++++--- DNSServer.py | 289 ++++----- Debug.py | 75 ++- Localize.py | 31 +- PILBackgrounds.py | 62 +- PlexAPI.py | 688 ++++++++++++---------- PlexConnect.py | 80 +-- PlexConnect_WinService.py | 18 +- PlexConnect_daemon.py | 4 +- Settings.py | 56 +- Subtitle.py | 134 +++-- Version.py | 1 - WebServer.py | 228 +++---- XMLConverter.py | 1176 +++++++++++++++++++++---------------- 14 files changed, 1657 insertions(+), 1391 deletions(-) diff --git a/ATVSettings.py b/ATVSettings.py index 64d0e5cd6..44ec06c75 100755 --- a/ATVSettings.py +++ b/ATVSettings.py @@ -9,78 +9,76 @@ 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,26 +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.ConfigParser(dflt) self.cfg.read(self.getSettingsFile()) - + def saveSettings(self): dprint(__name__, 1, "save settings") with open(self.getSettingsFile(), 'w') as f: self.cfg.write(f) - + def getSettingsFile(self): if self.path.startswith('.'): # relative to current path @@ -119,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: @@ -181,22 +178,21 @@ 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") ATVSettings.setSetting(UDID, option, 'True') # error - pick default print(ATVSettings.getSetting(UDID, option)) ATVSettings.setSetting(UDID, option, '9') print(ATVSettings.getSetting(UDID, option)) - + print("toggleSetting") ATVSettings.toggleSetting(UDID, option) print(ATVSettings.getSetting(UDID, option)) @@ -204,5 +200,5 @@ def setOptions(self, option, opts): print(ATVSettings.getSetting(UDID, option)) ATVSettings.toggleSetting(UDID, option) print(ATVSettings.getSetting(UDID, option)) - + del ATVSettings diff --git a/DNSServer.py b/DNSServer.py index ec4d7a273..6daf455ae 100755 --- a/DNSServer.py +++ b/DNSServer.py @@ -67,80 +67,83 @@ """ +""" + Hostname/DNS conversion + Hostname: 'Hello.World' + DNSdata: 'HelloWorld +""" + + + + 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("OpCode "+str((ord(paket[2]) >> 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]))) + 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") @@ -152,21 +155,21 @@ def printDNSdata(paket): 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]) + _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]) + rdlength = (ord(paket[adr+0]) << 8)+ord(paket[adr+1]) print("rdlength", rdlength) adr = adr + 2 - if _type==1: + if _type == 1: print("IP:", end=' ') for j in range(rdlength): print(ord(paket[adr+j]), end=' ') print() - elif _type==5: + elif _type == 5: print("redirect:", DNSToHost(paket, adr)) else: print("type unsupported:", end=' ') @@ -175,44 +178,44 @@ def printDNSdata(paket): print() adr = adr + rdlength + def printDNSdata_raw(DNSdata): # hex code for i in range(len(DNSdata)): - if i % 16==0: + if i % 16 == 0: print() print("{0:02x}".format(ord(DNSdata[i])), end=' ') print() - + # printable characters for i in range(len(DNSdata)): - if i % 16==0: + if i % 16 == 0: print() - if (ord(DNSdata[i])>32) & (ord(DNSdata[i])<128): + if (ord(DNSdata[i]) > 32) & (ord(DNSdata[i]) < 128): print(DNSdata[i], end=' ') else: print(".", end=' ') print() - def parseDNSdata(paket): - + def getWord(DNSdata, addr): - return (ord(DNSdata[addr])<<8)+ord(DNSdata[addr+1]) - + 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) } + 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']): @@ -224,7 +227,7 @@ def getWord(DNSdata, addr): 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']): @@ -235,30 +238,32 @@ def getWord(DNSdata, addr): 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]['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 + 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] ) + 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) - + DNSdata.append((val >> 8) & 0xFF) + DNSdata.append(val & 0xFF) + DNS = bytearray() - + # header appendWord(DNS, DNSstruct['head']['id']) appendWord(DNS, DNSstruct['head']['flags']) @@ -266,70 +271,72 @@ def appendWord(DNSdata, val): 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? + # no 'packing'/link - todo? + host = HostToDNS(DNSstruct['resrc'][i]['host']) 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]['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 + + 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:", end=' ') print(DNSstruct['resrc'][i]['host']) - if DNSstruct['resrc'][i]['type']==1: + if DNSstruct['resrc'][i]['type'] == 1: print("->IP:", end=' ') for j in range(DNSstruct['resrc'][i]['rdlen']): print(ord(DNSstruct['resrc'][i]['rdata'][j]), end=' ') print() - elif DNSstruct['resrc'][i]['type']==5: + 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 as e: - dprint(__name__, 0, "Failed to create socket on UDP port {0}: {1}", cfg_Port_DNSServer, e) + dprint( + __name__, 0, "Failed to create socket on UDP port {0}: {1}", cfg_Port_DNSServer, e) sys.exit(1) - + intercept = [param['HostToIntercept']] restrain = [] @@ -338,102 +345,118 @@ def Run(cmdPipe, param): else: cfg_IP_self = param['IP_self'] - if param['CSettings'].getSetting('intercept_atv_icon')=='True': + 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'] + 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__, 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': + 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) - + + # 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) + # Opcode bits (query=0, inversequery=1, status=2) + opcode = (ord(data[2]) >> 3) & 0x0F if opcode == 0: # Standard query domain = DNSToHost(data, 12) dprint(__name__, 1, "Domain: "+domain) - - paket='' + + 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('',[chr(int(x)) for x in cfg_IP_self.split('.')]) # 4bytes of IP + paket += data[:2] # 0:1 - ID + paket += "\x81\x80" # 2:3 - flags + # 4:5 - QDCOUNT - should be 1 for this code + paket += data[4:6] + paket += data[4:6] # 6:7 - ANCOUNT + paket += '\x00\x00' # 8:9 - NSCOUNT + paket += '\x00\x00' # 10:11 - ARCOUNT + # original query + paket += data[12:] + # pointer to domain name/original query + paket += '\xc0\x0c' + # response type, ttl and resource data length -> 4 bytes + paket += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' + # 4bytes of IP + paket += str.join('', [chr(int(x)) + for x in cfg_IP_self.split('.')]) 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 + paket += data[:2] # 0:1 - ID + paket += "\x81\x80" # 2:3 - flags + # 4:5 - QDCOUNT - should be 1 for this code + paket += data[4:6] + paket += data[4:6] # 6:7 - ANCOUNT + paket += '\x00\x00' # 8:9 - NSCOUNT + paket += '\x00\x00' # 10:11 - ARCOUNT + # original query + paket += data[12:] + # pointer to domain name/original query + paket += '\xc0\x0c' + # response type, ttl and resource data length -> 4 bytes + paket += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' + 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 = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM) DNS_forward.settimeout(5.0) except Exception as e: - dprint(__name__, 0, "Failed to create socket for DNS_forward): {0}", 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) - + + # 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) - + 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.") @@ -442,16 +465,16 @@ def Run(cmdPipe, param): 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['baseURL'] = 'http://' + param['IP_self'] + \ + ':' + cfg.getSetting('port_webserver') param['HostToIntercept'] = cfg.getSetting('hosttointercept') Run(cmdPipe[1], param) diff --git a/Debug.py b/Debug.py index 1e036e994..4a240fb28 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,66 +29,67 @@ 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]) - + # 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: + if len(asc_args) == 0: f.write(src+":\n") - elif len(asc_args)==1: + 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() - + # print to terminal window if logToTerminal: print((time.strftime("%b %d,%Y %H:%M:%S")), end=' ') - if len(asc_args)==0: + if len(asc_args) == 0: print(src+":") - elif len(asc_args)==1: + elif len(asc_args) == 1: print(src+": "+str(asc_args[0])) else: print(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*" " if len(elem): @@ -106,18 +105,18 @@ def indent(elem, level=0): 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 f510c0575..2214ae9f6 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,27 +26,28 @@ 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)): @@ -54,16 +56,15 @@ def replaceTEXT(textcontent, language): 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)) - + Text = "Library" # translates print(getTranslation(language).ugettext(Text)) - + Text = "{{TEXT(Channels)}}" # translates print(replaceTEXT(Text, language).encode('ascii', 'replace')) diff --git a/PILBackgrounds.py b/PILBackgrounds.py index dde396f77..ac031d6ee 100755 --- a/PILBackgrounds.py +++ b/PILBackgrounds.py @@ -3,12 +3,14 @@ import re import sys import io -import urllib.request, urllib.parse, urllib.error +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 @@ -17,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.parse.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(urllib.parse.urlparse(url).path) - cachefile = urllib.parse.quote_plus(PMS_uuid +"_"+ fileid +"_"+ resolution +"_"+ blurRadius) + ".jpg" # quote: just to make sure... - + # 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: @@ -52,14 +57,15 @@ def generate(PMS_uuid, url, authtoken, resolution, blurRadius, CSettings): dprint(__name__, 0, 'URLError: {0} // url: {1}', e.reason, url) return "/thumbnails/Background_blank_" + resolution + ".jpg" except urllib.error.HTTPError as e: - dprint(__name__, 0, 'HTTPError: {0} {1} // url: {2}', str(e.code), e.msg, url) + 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': @@ -73,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 0d45571ae..3b021a2d8 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -28,11 +28,16 @@ """ - import sys import struct import time -import urllib.request, urllib.error, urllib.parse, http.client, socket, io, 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 traceback @@ -48,7 +53,6 @@ from Debug import * # dprint(), prettyXML() - """ storage for PMS addresses and additional information - now per aTV! (replaces global PMS_list) syntax: PMS[][PMS_UUID][] @@ -65,23 +69,26 @@ 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: 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 - } + 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 @@ -89,44 +96,47 @@ def updatePMSProperty(ATV_udid, uuid, tag, value): return '' # no server known for this aTV if not uuid 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, '') + 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): 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'] + 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[ATV_udid]) """ @@ -141,19 +151,20 @@ 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 @@ -166,8 +177,8 @@ 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 finally: @@ -178,41 +189,42 @@ def PlexGDM(): PMS_list = {} if returnData: for response in returnData: - update = { 'ip' : response.get('from')[0] } - - # Check if we had a positive HTTP response + 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'): + for each in response.get('data').split('\n'): # decode response data update['discovery'] = "auto" - #update['owned']='1' + # update['owned']='1' #update['master']= 1 - #update['role']='master' - + # 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().decode( + 'utf-8', 'replace') # store in utf-8 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 +239,88 @@ 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 + 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 - + # 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': + # 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) + 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 + PMS_list = PlexGDM() + for uuid in PMS_list: + PMS = PMS_list[uuid] + # 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,32 +328,35 @@ 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 XML == False: pass # no data from MyPlex else: queue = queue.Queue() threads = [] PMSsPoked = 0 - + for Dir in XML.getiterator('Device'): - if Dir.get('product','') == "Plex Media Server" and Dir.get('provides','') == "server": + 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: continue # no valid connection - skip - - PMSsPoked +=1 - + + PMSsPoked += 1 + # multiple connection possible - poke either one, fastest response wins for Con in Dir.getiterator('Connection'): protocol = Con.get('protocol') @@ -341,22 +364,23 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): 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) + 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)) 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) @@ -368,23 +392,23 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): for t in threads: if t.isAlive(): ThreadsAlive += 1 - + # analyse PMS/http response - declare new PMS if not queue.empty(): (PMSInfo, PMS) = queue.get() - - if PMS==False: + + if PMS == False: # 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 +416,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 not uuid 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 +456,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 not options == None: xargs = getXArgsDeviceInfo(options) - if not authtoken=='': + if not authtoken == '': xargs['X-Plex-Token'] = authtoken - + dprint(__name__, 1, "URL: {0}{1}", baseURL, path) dprint(__name__, 1, "xargs: {0}", xargs) @@ -440,28 +472,31 @@ 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 = urllib.request.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 = 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 = io.StringIO(response.read()) file = gzip.GzipFile(fileobj=buf) @@ -469,32 +504,33 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): 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 + return XML def getXMLFromPMSToQueue(PMS, queue): - XML = getXMLFromPMS(PMS['baseURL'],PMS['path'],PMS['options'],PMS['token']) - queue.put( (PMS['data'], XML) ) - + XML = getXMLFromPMS(PMS['baseURL'], PMS['path'], + PMS['options'], PMS['token']) + queue.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 not options is None: 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 +541,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,19 +556,21 @@ def getXArgsDeviceInfo(options={}): result: XML """ + + def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): queue = queue.Queue() threads = [] - + root = etree.Element("MediaConverter") 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('address', getPMSProperty(ATV_udid, uuid, 'ip')) @@ -541,97 +578,112 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): Server.set('baseURL', getPMSProperty(ATV_udid, uuid, 'baseURL')) 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} } + PMS = {'baseURL': baseURL, 'path': path, 'options': options, 'token': token, + 'data': {'uuid': uuid, 'Server': Server}} t = Thread(target=getXMLFromPMSToQueue, args=(PMS, queue)) 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'] + (data, XML) = queue.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 XML == False: + Server.set('size', '0') + else: + Server.set('size', XML.getroot().get('size', '0')) + + # copy "Directory" content, add PMS to links + for Dir in XML.getiterator('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.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) - 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.getiterator('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.getiterator('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 +695,8 @@ def getURL(baseURL, path, key): URL = baseURL + path else: # internal path, add-on URL = baseURL + path + '/' + key - - return URL + return URL """ @@ -659,17 +710,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 = urllib.request.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - + # 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 +733,36 @@ def MyPlexSignIn(username, password, options): print "has WWW_Authenticate:", e.headers.has_key('WWW-Authenticate') print """ - + # provide credentials - ### optional... when 'realm' is unknown + # optional... when 'realm' is unknown ##passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() - ##passmanager.add_password(None, address, username, password) # None: default "realm" + # passmanager.add_password(None, address, username, password) # None: default "realm" passmanager = urllib.request.HTTPPasswordMgr() - passmanager.add_password(MyPlexHost, MyPlexURL, username, password) # realm = 'plex.tv' + 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 urllib.error.HTTPError as e: - if e.code==401: + 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 +772,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 +781,56 @@ def MyPlexSignOut(authtoken): MyPlexHost = 'plex.tv' MyPlexSignOutPath = '/users/sign_out.xml' MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath - + # create POST request - xargs = { 'X-Plex-Token': authtoken } + xargs = {'X-Plex-Token': authtoken} request = urllib.request.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - + # 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 = urllib.request.Request(MyPlexURL, None, xargs) - request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data. - + # 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 +847,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 +870,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 +900,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 +931,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 +964,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 +989,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 +1018,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 +1039,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, {}, '') - - + # 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 21bb46d96..043e9ee65 100755 --- a/PlexConnect.py +++ b/PlexConnect.py @@ -8,28 +8,33 @@ """ -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': + 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_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)) @@ -39,9 +44,8 @@ def getIP_self(): # manual override from "settings.cfg" IP = cfg.getSetting('ip_plexconnect') dprint('PlexConnect', 0, "IP_self (from settings): "+IP) - - return 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,35 +76,36 @@ 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': + 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 @@ -108,13 +113,13 @@ def startup(): else: 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 +127,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 +142,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 +160,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 +181,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 +195,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 b0545ecc2..81e5778fe 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 747b7c0f9..e42b7437d 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -59,7 +59,8 @@ def daemonize(args): pid = str(os.getpid()) file(args.pidfile, 'w').write("%s\n" % pid) except IOError as e: - raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) + raise SystemExit( + "Unable to write PID file: %s [%d]" % (e.strerror, e.errno)) def delpid(): @@ -71,6 +72,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) diff --git a/Settings.py b/Settings.py index 0aecf9bc0..050eba876 100755 --- a/Settings.py +++ b/Settings.py @@ -9,7 +9,6 @@ from Debug import * # dprint() - """ Global Settings... syntax: 'setting': ('default', 'regex to validate') @@ -23,38 +22,37 @@ 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(): @@ -108,21 +106,23 @@ def checkSection(self): 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) diff --git a/Subtitle.py b/Subtitle.py index 8fa0403d3..7b7d23627 100755 --- a/Subtitle.py +++ b/Subtitle.py @@ -6,16 +6,16 @@ """ - import re -import urllib.request, urllib.error, urllib.parse +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 = urllib.request.Request(PMS_baseURL+path , None, xargs) + + request = urllib.request.Request(PMS_baseURL+path, None, xargs) try: 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) - + 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 6f2265f7c..8a71995b6 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 18dd32341..658cdba81 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 http.server import BaseHTTPRequestHandler, HTTPServer from socketserver import ThreadingMixIn import ssl from multiprocessing import Pipe # inter process communication -import urllib.request, urllib.parse, urllib.error, io, 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,58 +41,58 @@ 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 = 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 = [x.strip() for x in self.headers["accept-encoding"].split(",")] + accept_encoding = [x.strip() + for x in self.headers["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() @@ -93,30 +100,31 @@ def sendResponse(self, data, type, enableGzip): else: self.end_headers() self.wfile.write(data.encode()) - + 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: + 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 +133,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.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,9 +224,11 @@ 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.request.urlopen('http://'+resource) @@ -225,7 +243,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 +251,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 +286,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 as e: - dprint(__name__, 0, "Failed to connect to HTTP on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_WebServer, 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 as e: - dprint(__name__, 0, "Failed to connect to HTTPS on {0} port {1}: {2}", cfg_IP_WebServer, cfg_Port_SSL, 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 1daf121c7..3953cc0d8 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 +import time +import uuid +import hmac +import hashlib +import base64 from urllib.parse import quote_plus, unquote_plus, urlencode -import urllib.request, urllib.error, urllib.parse +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,35 +213,40 @@ 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 = urllib.request.urlopen("https://youtube.com/get_video_info?html5=1&video_id=" + trailerID).read() + 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'] - + url = '' for i in range(len(streams)): stream = streams[i] @@ -236,143 +257,162 @@ def XML_PMS2aTV(PMS_address, path, options): 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,193 +673,206 @@ 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!=[]: + 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) - class CCommandCollection(CCommandHelper): # XML TREE modifier commands # add new commands to this list! 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)) @@ -1016,7 +1112,7 @@ def ATTRIB_VAL_QUOTED(self, src, srcXML, param): 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 = 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 + 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': + 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): @@ -1339,19 +1464,21 @@ def ATTRIB_durationToString(self, src, srcXML, param): 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) - + 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) + 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,64 +1486,68 @@ 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") _XML = ' \ \ @@ -1425,7 +1556,7 @@ def ATTRIB_BACKGROUNDURL(self, src, srcXML, param): PMSroot = etree.fromstring(_XML) PMSTree = etree.ElementTree(PMSroot) print(prettyXML(PMSroot)) - + print() print("load aTV XML template") _XML = ' \ @@ -1444,26 +1575,27 @@ 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") 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 "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 From 2ffda9b7bc9148d03770f00f8e1f033f5174b972 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 13:43:06 -0500 Subject: [PATCH 05/19] use dnslib --- DNSServer.py | 535 ++++++----------------------------------------- PlexConnect.py | 26 +-- README.md | 5 +- requirements.txt | 1 + 4 files changed, 74 insertions(+), 493 deletions(-) create mode 100644 requirements.txt diff --git a/DNSServer.py b/DNSServer.py index 6daf455ae..1e0639e99 100755 --- a/DNSServer.py +++ b/DNSServer.py @@ -1,480 +1,65 @@ -#!/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 | -+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + cfg = param['CSettings'] -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 -""" - - -""" - Hostname/DNS conversion - Hostname: 'Hello.World' - DNSdata: 'HelloWorld -""" - - - - -import sys -import socket -import struct -from multiprocessing import Pipe # inter process communication -import signal -import Settings -from Debug import * # dprint() -def HostToDNS(Host): - DNSdata = '.'+Host+'\0' # python 2.6: bytearray() - i = 0 - while i < len(DNSdata)-1: - next = DNSdata.find('.', i+1) - if next == -1: - next = len(DNSdata)-1 - # python 2.6: DNSdata[i] = next-i-1 - DNSdata = DNSdata[:i] + chr(next-i-1) + DNSdata[i+1:] - i = next - - return DNSdata - - -def DNSToHost(DNSdata, i, followlink=True): - Host = '' - while DNSdata[i] != '\0': - nlen = ord(DNSdata[i]) - if nlen & 0xC0: - if followlink: - Host = Host + \ - DNSToHost( - DNSdata, ((ord(DNSdata[i]) & 0x3F) << 8) + ord(DNSdata[i+1]), True)+'.' - break - else: - Host = Host + DNSdata[i+1:i+nlen+1]+'.' - i += nlen+1 - Host = Host[:-1] - return Host - - -def printDNSdata(paket): - # HEADER - print("ID {0:04x}".format((ord(paket[0]) << 8)+ord(paket[1]))) - print("flags {0:02x} {1:02x}".format(ord(paket[2]), ord(paket[3]))) - print("OpCode "+str((ord(paket[2]) >> 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:", end=' ') - for j in range(rdlength): - print(ord(paket[adr+j]), end=' ') - print() - elif _type == 5: - print("redirect:", DNSToHost(paket, adr)) - else: - print("type unsupported:", end=' ') - for j in range(rdlength): - print(ord(paket[adr+j]), end=' ') - 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])), end=' ') - 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], end=' ') - else: - print(".", end=' ') - 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']): - # no 'packing'/link - todo? - host = HostToDNS(DNSstruct['resrc'][i]['host']) - 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:", end=' ') - print(DNSstruct['resrc'][i]['host']) - if DNSstruct['resrc'][i]['type'] == 1: - print("->IP:", end=' ') - for j in range(DNSstruct['resrc'][i]['rdlen']): - print(ord(DNSstruct['resrc'][i]['rdata'][j]), end=' ') - print() - elif DNSstruct['resrc'][i]['type'] == 5: - print("->alias:", DNSstruct['resrc'][i]['rdata']) + if cfg.getSetting('use_custom_dns_bind_ip') == "True": + cfg_IP_self = cfg.getSetting('custom_dns_bind_ip') 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 as 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_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 bits (query=0, inversequery=1, status=2) - opcode = (ord(data[2]) >> 3) & 0x0F - 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 - # 4:5 - QDCOUNT - should be 1 for this code - paket += data[4:6] - paket += data[4:6] # 6:7 - ANCOUNT - paket += '\x00\x00' # 8:9 - NSCOUNT - paket += '\x00\x00' # 10:11 - ARCOUNT - # original query - paket += data[12:] - # pointer to domain name/original query - paket += '\xc0\x0c' - # response type, ttl and resource data length -> 4 bytes - paket += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' - # 4bytes of IP - paket += str.join('', [chr(int(x)) - for x in cfg_IP_self.split('.')]) - 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 - # 4:5 - QDCOUNT - should be 1 for this code - paket += data[4:6] - paket += data[4:6] # 6:7 - ANCOUNT - paket += '\x00\x00' # 8:9 - NSCOUNT - paket += '\x00\x00' # 10:11 - ARCOUNT - # original query - paket += data[12:] - # pointer to domain name/original query - paket += '\xc0\x0c' - # response type, ttl and resource data length -> 4 bytes - paket += '\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04' - 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 as 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) + cfg_IP_self = param['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} => {cfg_IP_self}") + 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 {cfg_IP_self}" 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/PlexConnect.py b/PlexConnect.py index 043e9ee65..60a1d4010 100755 --- a/PlexConnect.py +++ b/PlexConnect.py @@ -33,17 +33,17 @@ def getIP_self(): cfg = param['CSettings'] if cfg.getSetting('enable_plexgdm') == 'False': - dprint('PlexConnect', 0, "IP_PMS: "+cfg.getSetting('ip_pms')) + 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) + dprint('PlexConnect', 0, f"IP_self (from settings): {IP}") return IP @@ -97,22 +97,15 @@ def startup(): 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 + 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: @@ -167,6 +160,9 @@ def run(timeout=60): def shutdown(): for slave in procs: procs[slave].join() + if param['CATVSettings'].getSetting('enable_dnsserver') == 'True': + if dnsserver: + dnsserver.stop() param['CATVSettings'].saveSettings() dprint('PlexConnect', 0, "Shutdown") diff --git a/README.md b/README.md index d156f6960..10e6f8370 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/requirements.txt b/requirements.txt new file mode 100644 index 000000000..6ced7d790 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +dnslib==0.9.16 \ No newline at end of file From 2d6493a9945c9f5ef91fded0f5246894072818b1 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 15:14:01 -0500 Subject: [PATCH 06/19] differentiate between bind IP and response IP --- DNSServer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/DNSServer.py b/DNSServer.py index 1e0639e99..25e2da124 100755 --- a/DNSServer.py +++ b/DNSServer.py @@ -7,11 +7,12 @@ class DNSServer(): def __init__(self, param): cfg = param['CSettings'] + cfg_IP_self = param['IP_self'] if cfg.getSetting('use_custom_dns_bind_ip') == "True": - cfg_IP_self = cfg.getSetting('custom_dns_bind_ip') + intercept_ip = cfg.getSetting('custom_dns_bind_ip') else: - cfg_IP_self = param['IP_self'] + intercept_ip = cfg_IP_self cfg_Port_DNSServer = int(cfg.getSetting('port_dnsserver')) if not cfg_Port_DNSServer: @@ -31,13 +32,13 @@ def __init__(self, param): dprint(__name__, 0, "***") dprint(__name__, 0, f"DNSServer: Serving DNS on {cfg_IP_self} port {cfg_Port_DNSServer}.") - dprint(__name__, 1, f"intercept: {intercept} => {cfg_IP_self}") + 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 {cfg_IP_self}" for i in intercept] + intercept_records = [f"{i} 300 IN A {intercept_ip}" for i in intercept] resolver = InterceptResolver( address=cfg_IP_DNSMaster, From 5c0fe32205326e67e4ea9b6c96d39594aa1087dc Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 20:32:58 -0500 Subject: [PATCH 07/19] more python3 fixes --- Localize.py | 6 +++--- PlexAPI.py | 22 +++++++++++----------- WebServer.py | 6 ++++-- XMLConverter.py | 10 +++++----- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Localize.py b/Localize.py index 2214ae9f6..ec02017d5 100755 --- a/Localize.py +++ b/Localize.py @@ -51,7 +51,7 @@ def pickLanguage(languages): 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 @@ -61,10 +61,10 @@ def replaceTEXT(textcontent, language): 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')) diff --git a/PlexAPI.py b/PlexAPI.py index 3b021a2d8..3ff4bc93a 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -169,7 +169,7 @@ def PlexGDM(): try: # Send data to the multicast group dprint(__name__, 1, "Sending discovery message: {0}", Msg_PlexGDM) - GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM)) + GDM.sendto(bytes(Msg_PlexGDM, "utf8"), (IP_PlexGDM, Port_PlexGDM)) # Look for responses from all recipients while True: @@ -341,7 +341,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): if XML == False: pass # no data from MyPlex else: - queue = queue.Queue() + q = queue.Queue() threads = [] PMSsPoked = 0 @@ -377,7 +377,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): 'data': PMSInfo} dprint(__name__, 0, "poke {0} ({1}) at {2}", name, uuid, uri) - t = Thread(target=getXMLFromPMSToQueue, args=(PMS, queue)) + t = Thread(target=getXMLFromPMSToQueue, args=(PMS, q)) t.start() threads.append(t) @@ -394,8 +394,8 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): ThreadsAlive += 1 # analyse PMS/http response - declare new PMS - if not queue.empty(): - (PMSInfo, PMS) = queue.get() + if not q.empty(): + (PMSInfo, PMS) = q.get() if PMS == False: # communication error - skip this connection @@ -514,10 +514,10 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): return XML -def getXMLFromPMSToQueue(PMS, queue): +def getXMLFromPMSToQueue(PMS, q): XML = getXMLFromPMS(PMS['baseURL'], PMS['path'], PMS['options'], PMS['token']) - queue.put((PMS['data'], XML)) + q.put((PMS['data'], XML)) def getXArgsDeviceInfo(options={}): @@ -559,7 +559,7 @@ def getXArgsDeviceInfo(options={}): def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): - queue = queue.Queue() + q = queue.Queue() threads = [] root = etree.Element("MediaConverter") @@ -589,7 +589,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): # 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)) + t = Thread(target=getXMLFromPMSToQueue, args=(PMS, q)) t.start() threads.append(t) @@ -598,8 +598,8 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): t.join() # add new data to root XML, individual Server - while not queue.empty(): - (data, XML) = queue.get() + while not q.empty(): + (data, XML) = q.get() uuid = data['uuid'] Server = data['Server'] diff --git a/WebServer.py b/WebServer.py index 658cdba81..986da5566 100755 --- a/WebServer.py +++ b/WebServer.py @@ -88,7 +88,7 @@ def sendResponse(self, data, type, enableGzip): self.send_header('Content-type', type) try: accept_encoding = [x.strip() - for x in self.headers["accept-encoding"].split(",")] + for x in self.headers.get("accept-encoding", "").split(",")] except KeyError: accept_encoding = [] if enableGzip and \ @@ -99,7 +99,9 @@ def sendResponse(self, data, type, enableGzip): self.wfile.write(self.compress(data)) else: self.end_headers() - self.wfile.write(data.encode()) + if not isinstance(data, bytes): + data = bytes(data, 'utf8') + self.wfile.write(data) def do_GET(self): global g_param diff --git a/XMLConverter.py b/XMLConverter.py index 3953cc0d8..aa0dd59d0 100755 --- a/XMLConverter.py +++ b/XMLConverter.py @@ -841,7 +841,7 @@ def applyMath(self, val, math, frmt): return val def _(self, msgid): - return Localize.getTranslation(self.options['aTVLanguage']).ugettext(msgid) + return Localize.getTranslation(self.options['aTVLanguage']).gettext(msgid) class CCommandCollection(CCommandHelper): @@ -1463,12 +1463,12 @@ 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 + hour = int(min / 60) + min = int(min % 60) if hour == 0: - return self._("{0:d} Minutes").format(min) + return self._(f"{min} Minutes") else: - return self._("{0:d}hr {1:d}min").format(hour, min) + return self._(f"{hour}hr {min}min") if type == 'Audio': secs = int(duration)/1000 From d9dcd854e4ebc09e1eb5ea7cea42256090055953 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 20:39:11 -0500 Subject: [PATCH 08/19] dnslib will clean up automatically --- PlexConnect.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/PlexConnect.py b/PlexConnect.py index 60a1d4010..79cf2bf1d 100755 --- a/PlexConnect.py +++ b/PlexConnect.py @@ -160,9 +160,6 @@ def run(timeout=60): def shutdown(): for slave in procs: procs[slave].join() - if param['CATVSettings'].getSetting('enable_dnsserver') == 'True': - if dnsserver: - dnsserver.stop() param['CATVSettings'].saveSettings() dprint('PlexConnect', 0, "Shutdown") From 0545e09dad1548afdb696f1cf08240938c38dc2f Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 29 Sep 2021 20:48:36 -0500 Subject: [PATCH 09/19] getiterator removed in py3.9 --- PlexAPI.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 3ff4bc93a..3b6aa6a6c 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -345,7 +345,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): threads = [] PMSsPoked = 0 - for Dir in XML.getiterator('Device'): + 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') @@ -358,7 +358,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): 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') @@ -613,7 +613,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): Server.set('size', XML.getroot().get('size', '0')) # copy "Directory" content, add PMS to links - for Dir in XML.getiterator('Directory'): + 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 @@ -630,7 +630,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): Server.append(Dir) elif Dir.get('title') == 'Live TV & DVR': mp = None - for MediaProvider in XML.getiterator('MediaProvider'): + for MediaProvider in XML.iter('MediaProvider'): if MediaProvider.get('protocols') == 'livetv': mp = MediaProvider break @@ -646,7 +646,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): Server.append(Dir) # copy "Playlist" content, add PMS to links - for Playlist in XML.getiterator('Playlist'): + 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: @@ -655,7 +655,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): Server.append(Playlist) # copy "Video" content, add PMS to links - for Video in XML.getiterator('Video'): + 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: From 9055763187f8485d3c3e7282d3c714d6af0f01d2 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 08:08:27 -0500 Subject: [PATCH 10/19] fix std* redirects --- PlexConnect_daemon.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/PlexConnect_daemon.py b/PlexConnect_daemon.py index e42b7437d..42cbf7bf3 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -46,18 +46,19 @@ def daemonize(args): # 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()) + si = os.open('/dev/null', os.O_RDONLY) + so = os.open('/dev/null', os.O_APPEND) + se = os.open('/dev/null', os.O_APPEND, 0) + os.dup2(si, sys.stdin.fileno()) + os.dup2(so, sys.stdout.fileno()) + os.dup2(se, sys.stderr.fileno()) if args.pidfile: try: atexit.register(delpid) pid = str(os.getpid()) - file(args.pidfile, 'w').write("%s\n" % pid) + 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)) From 788c1130304fec9d50126889822c5fc17bf762b5 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 21:29:18 -0500 Subject: [PATCH 11/19] require pillow for fanart --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ced7d790..9b7648619 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -dnslib==0.9.16 \ No newline at end of file +dnslib==0.9.16 +pillow==8.3.2 \ No newline at end of file From 428f915f16973ad0fb54642fea41269ce9fca351 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 22:02:23 -0500 Subject: [PATCH 12/19] fix daemon for py3 --- Debug.py | 19 +++++++++---------- PlexConnect_daemon.py | 24 ++++++++---------------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/Debug.py b/Debug.py index 4a240fb28..b0c3d0ccf 100755 --- a/Debug.py +++ b/Debug.py @@ -63,21 +63,20 @@ def dprint(src, dlevel, *args): # 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")), end=' ') if len(asc_args) == 0: - print(src+":") + print(f"{src}:") elif len(asc_args) == 1: print(src+": "+str(asc_args[0])) else: diff --git a/PlexConnect_daemon.py b/PlexConnect_daemon.py index 42cbf7bf3..e8cc69d24 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -12,7 +12,7 @@ import argparse import atexit from PlexConnect import startup, shutdown, run, cmdShutdown - +from contextlib import redirect_stderr, redirect_stdout def daemonize(args): """ @@ -43,16 +43,6 @@ def daemonize(args): 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 = os.open('/dev/null', os.O_RDONLY) - so = os.open('/dev/null', os.O_APPEND) - se = os.open('/dev/null', os.O_APPEND, 0) - os.dup2(si, sys.stdin.fileno()) - os.dup2(so, sys.stdout.fileno()) - os.dup2(se, sys.stderr.fileno()) - if args.pidfile: try: atexit.register(delpid) @@ -82,11 +72,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() From f989a61a5a61971493d55e8110f6ec4cba071973 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 22:08:46 -0500 Subject: [PATCH 13/19] more py3 --- Debug.py | 8 ++++---- PlexConnect_daemon.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Debug.py b/Debug.py index b0c3d0ccf..6659c852e 100755 --- a/Debug.py +++ b/Debug.py @@ -78,9 +78,9 @@ def dprint(src, dlevel, *args): if len(asc_args) == 0: print(f"{src}:") elif len(asc_args) == 1: - print(src+": "+str(asc_args[0])) + 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:])}") """ @@ -90,14 +90,14 @@ def dprint(src, dlevel, *args): 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: diff --git a/PlexConnect_daemon.py b/PlexConnect_daemon.py index e8cc69d24..e90fd89e9 100755 --- a/PlexConnect_daemon.py +++ b/PlexConnect_daemon.py @@ -14,6 +14,7 @@ from PlexConnect import startup, shutdown, run, cmdShutdown from contextlib import redirect_stderr, redirect_stdout + def daemonize(args): """ do the UNIX double-fork magic, see Stevens' "Advanced From 173423c2cdd4e62a78d85aebb9e913d6e7ac76dd Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 22:31:45 -0500 Subject: [PATCH 14/19] pep8 + additional debugprint --- PlexAPI.py | 109 ++++++++++++++++++++++++----------------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 3b6aa6a6c..c00eadc16 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -74,11 +74,11 @@ 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 + address = f"{ip}:{port}" + baseURL = f"{scheme}://{address}" g_PMS[ATV_udid][uuid] = {'name': name, 'scheme': scheme, 'ip': ip, 'port': port, 'address': address, @@ -92,29 +92,21 @@ def declarePMS(ATV_udid, uuid, name, scheme, ip, port): 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: + if ATV_udid not 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): return uuid @@ -123,20 +115,18 @@ def getPMSFromAddress(ATV_udid, address): def getPMSAddress(ATV_udid, uuid): # get address 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 - return g_PMS[ATV_udid][uuid]['ip'] + ':' + g_PMS[ATV_udid][uuid]['port'] 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]) + if ATV_udid in g_PMS: + return len(g_PMS[ATV_udid]) + return 0 # no server known for this aTV """ @@ -197,7 +187,7 @@ def PlexGDM(): # decode response data update['discovery'] = "auto" # update['owned']='1' - #update['master']= 1 + # pdate['master']= 1 # update['role']='master' if "Content-Type:" in each: @@ -252,10 +242,10 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): 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 + # # 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') @@ -266,23 +256,22 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): # 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, "PlexAPI - Hostname " + - ip+" resolved to "+ip2) + dprint(__name__, 0, f"PlexAPI - Hostname {ip} resolved to {ip2}") ip = ip2 - except: - dprint(__name__, 0, "PlexAPI - ip_dns " + - ip+" could not be resolved") + except Exception: + dprint(__name__, 0, f"PlexAPI - ip_dns {ip} could not be resolved") port = CSettings.getSetting('port_pms') - XML = getXMLFromPMS('http://'+ip+':'+port, '/servers', None, '') + XML = getXMLFromPMS(f'http://{ip}:{port}', '/servers', None, '') - if XML == False: + if not XML: pass # no response from manual defined server (Settings.cfg) else: Server = XML.find('Server') @@ -295,9 +284,11 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): else: # PlexGDM + dprint(__name__, 0, f"PlexAPI - trying the PlexGDM()") + PMS_list = PlexGDM() - for uuid in PMS_list: - PMS = PMS_list[uuid] + for (uuid, PMS) in PMS_list.items(): + print(f"uuid:{uuid}\nPMS: {PMS}") # dflt: token='', local, owned declarePMS( ATV_udid, PMS['uuid'], PMS['serverName'], 'http', PMS['ip'], PMS['port']) @@ -316,7 +307,7 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): # debug print all servers dprint(__name__, 0, "Plex Media Servers found: {0}", len( - g_PMS[ATV_udid])-1) + g_PMS[ATV_udid]) - 1) for uuid in g_PMS[ATV_udid]: dprint(__name__, 1, str(g_PMS[ATV_udid][uuid])) @@ -338,7 +329,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): XML = getXMLFromPMS('https://plex.tv', '/api/resources?includeHttps=1', {}, authtoken) - if XML == False: + if not XML: pass # no data from MyPlex else: q = queue.Queue() @@ -352,7 +343,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): token = Dir.get('accessToken', authtoken) owned = Dir.get('owned', '0') - if Dir.find('Connection') == None: + if not Dir.find('Connection'): continue # no valid connection - skip PMSsPoked += 1 @@ -397,7 +388,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): if not q.empty(): (PMSInfo, PMS) = q.get() - if PMS == False: + if not PMS: # communication error - skip this connection continue @@ -418,7 +409,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): uri = PMSInfo['uri'] # PMS uuid not yet handled, so take it - if not uuid in g_PMS[ATV_udid]: + if uuid not in g_PMS[ATV_udid]: PMSsCnt += 1 dprint(__name__, 0, @@ -460,7 +451,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): xargs = {} - if not options == None: + if options is None: xargs = getXArgsDeviceInfo(options) if not authtoken == '': xargs['X-Plex-Token'] = authtoken @@ -473,7 +464,7 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): dprint(__name__, 1, 'Custom method ' + method) method = options['PlexConnectMethod'] - request = urllib.request.Request(baseURL+path, None, xargs) + request = urllib.request.Request(baseURL + path, None, xargs) request.add_header('User-agent', 'PlexConnect') request.get_method = lambda: method @@ -509,8 +500,6 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): dprint(__name__, 1, XML.getroot()) dprint(__name__, 1, "====== PMS-XML finished ======") - #XMLTree = etree.ElementTree(etree.fromstring(response)) - return XML @@ -563,7 +552,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): 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 \ @@ -572,12 +561,12 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): (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') @@ -607,24 +596,24 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): token = getPMSProperty(ATV_udid, uuid, 'accesstoken') PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')' - if XML == False: - Server.set('size', '0') + if not XML: + Server.set('size', '0') else: - Server.set('size', XML.getroot().get('size', '0')) + 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('key', PMS_mark + getURL('', path, key)) Dir.set('refreshKey', getURL( baseURL, path, key) + '/refresh') if 'thumb' in Dir.attrib: - Dir.set('thumb', PMS_mark + + Dir.set('thumb', PMS_mark + getURL('', path, Dir.get('thumb'))) if 'art' in Dir.attrib: - Dir.set('art', PMS_mark + + Dir.set('art', PMS_mark + getURL('', path, Dir.get('art'))) # print Dir.get('type') Server.append(Dir) @@ -648,7 +637,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): # 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)) + Playlist.set('key', PMS_mark + getURL('', path, key)) if 'composite' in Playlist.attrib: Playlist.set('composite', PMS_mark + getURL('', path, Playlist.get('composite'))) @@ -657,7 +646,7 @@ def getXMLFromMultiplePMS(ATV_udid, path, type, options={}): # 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)) + Video.set('key', PMS_mark + getURL('', path, key)) if 'thumb' in Video.attrib: Video.set('thumb', PMS_mark + getURL('', path, Video.get('thumb'))) @@ -736,7 +725,7 @@ def MyPlexSignIn(username, password, options): # provide credentials # optional... when 'realm' is unknown - ##passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() + # # passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm() # passmanager.add_password(None, address, username, password) # None: default "realm" passmanager = urllib.request.HTTPPasswordMgr() passmanager.add_password(MyPlexHost, MyPlexURL, @@ -1059,7 +1048,7 @@ def getDirectAudioPath(path, AuthToken): if testSectionXML: dprint('', 0, "*** local Server/Sections") PMS_list = PlexGDM() - XML = getSectionXML(PMS_list, {}, '') + XML = getXMLFromMultiplePMS(PMS_list, {}, '') # test XML from MyPlex if testMyPlexXML: From 6ab8c0d2fb8a58ef0e3bb52ad135ef89d34292c5 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Wed, 6 Oct 2021 23:18:22 -0500 Subject: [PATCH 15/19] fix some of the GDM code --- PlexAPI.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index c00eadc16..750fa3761 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -150,7 +150,6 @@ def PlexGDM(): # 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) @@ -158,7 +157,7 @@ def PlexGDM(): returnData = [] try: # Send data to the multicast group - dprint(__name__, 1, "Sending discovery message: {0}", Msg_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 @@ -171,6 +170,8 @@ def PlexGDM(): 'data': data}) except socket.timeout: break + except Exception as e: + print(str(e)) finally: GDM.close() @@ -182,8 +183,8 @@ def PlexGDM(): 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'): + 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' @@ -195,8 +196,7 @@ def PlexGDM(): 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: From 55a2246d7e6e7728f288c6411e55715d015c4ed2 Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Sat, 16 Oct 2021 15:49:00 -0500 Subject: [PATCH 16/19] fix querying for servers when logged in --- PlexAPI.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 750fa3761..86615deaa 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -42,10 +42,7 @@ 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.parse import urlencode, quote_plus @@ -328,14 +325,12 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): XML = getXMLFromPMS('https://plex.tv', '/api/resources?includeHttps=1', {}, authtoken) - if not XML: pass # no data from MyPlex else: q = queue.Queue() threads = [] PMSsPoked = 0 - for Dir in XML.iter('Device'): if Dir.get('product', '') == "Plex Media Server" and Dir.get('provides', '') == "server": uuid = Dir.get('clientIdentifier') @@ -343,7 +338,7 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): token = Dir.get('accessToken', authtoken) owned = Dir.get('owned', '0') - if not Dir.find('Connection'): + if not Dir.findall('Connection'): continue # no valid connection - skip PMSsPoked += 1 @@ -381,7 +376,7 @@ 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 @@ -487,20 +482,11 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): 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 = 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 ======") - - return XML + return etree.parse(file) + return etree.parse(response) def getXMLFromPMSToQueue(PMS, q): @@ -514,6 +500,8 @@ def getXArgsDeviceInfo(options={}): xargs['X-Plex-Device'] = 'AppleTV' xargs['X-Plex-Model'] = '2,3' # Base it on AppleTV model. # if not options is None: + if not options: + options = {} if 'PlexConnectUDID' in options: # UDID for MyPlex device identification xargs['X-Plex-Client-Identifier'] = options['PlexConnectUDID'] From e325925e1168a9496cf305272d1a42841375f40c Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Sat, 16 Oct 2021 19:53:28 -0500 Subject: [PATCH 17/19] fix a logic defect, tighten up some helpers --- PlexAPI.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 86615deaa..1337def11 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -38,7 +38,7 @@ import socket import io import gzip -from threading import Thread +from multiprocessing.dummy import Pool as ThreadPool import queue import traceback @@ -102,28 +102,23 @@ def getPMSProperty(ATV_udid, uuid, tag): def getPMSFromAddress(ATV_udid, address): # find PMS by IP, return UUID - if ATV_udid not 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 ATV_udid not in g_PMS: - return '' # no server known for this aTV - if uuid not 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 ATV_udid in g_PMS: - return len(g_PMS[ATV_udid]) - return 0 # no server known for this aTV + return len(g_PMS.get(ATV_udid, [])) """ @@ -342,7 +337,6 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): continue # no valid connection - skip PMSsPoked += 1 - # multiple connection possible - poke either one, fastest response wins for Con in Dir.iter('Connection'): protocol = Con.get('protocol') @@ -355,7 +349,6 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): 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} @@ -382,7 +375,6 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): # analyse PMS/http response - declare new PMS if not q.empty(): (PMSInfo, PMS) = q.get() - if not PMS: # communication error - skip this connection continue @@ -446,9 +438,9 @@ def getPMSListFromMyPlex(ATV_udid, authtoken): def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): xargs = {} - if options is None: + if options: xargs = getXArgsDeviceInfo(options) - if not authtoken == '': + if authtoken: xargs['X-Plex-Token'] = authtoken dprint(__name__, 1, "URL: {0}{1}", baseURL, path) @@ -499,9 +491,6 @@ 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: - if not options: - options = {} if 'PlexConnectUDID' in options: # UDID for MyPlex device identification xargs['X-Plex-Client-Identifier'] = options['PlexConnectUDID'] From 289994cf012d1b53ee6d79a55a99686a275e9cef Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Sat, 16 Oct 2021 19:59:30 -0500 Subject: [PATCH 18/19] didn't mean to move to multiprocessing.dummy, remove debug print --- PlexAPI.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 1337def11..7d19de6a3 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -38,7 +38,7 @@ import socket import io import gzip -from multiprocessing.dummy import Pool as ThreadPool +from threading import Thread import queue import traceback @@ -280,7 +280,6 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): PMS_list = PlexGDM() for (uuid, PMS) in PMS_list.items(): - print(f"uuid:{uuid}\nPMS: {PMS}") # dflt: token='', local, owned declarePMS( ATV_udid, PMS['uuid'], PMS['serverName'], 'http', PMS['ip'], PMS['port']) From a90c4a868b13356d9378a47ace89e7eef3d063eb Mon Sep 17 00:00:00 2001 From: david l goodrich Date: Sun, 17 Oct 2021 10:57:26 -0500 Subject: [PATCH 19/19] keep the XML debug output --- PlexAPI.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/PlexAPI.py b/PlexAPI.py index 7d19de6a3..097e53339 100755 --- a/PlexAPI.py +++ b/PlexAPI.py @@ -255,10 +255,12 @@ def discoverPMS(ATV_udid, CSettings, IP_self, tokenDict={}): try: ip2 = socket.gethostbyname(ip) if ip != ip2: - dprint(__name__, 0, f"PlexAPI - Hostname {ip} resolved to {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") + 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, '') @@ -476,8 +478,14 @@ def getXMLFromPMS(baseURL, path, options={}, authtoken='', enableGzip=False): if response.info().get('Content-Encoding') == 'gzip': buf = io.StringIO(response.read()) file = gzip.GzipFile(fileobj=buf) - return etree.parse(file) - return etree.parse(response) + XML = etree.parse(file) + else: + XML = etree.parse(response) + dprint(__name__, 1, "====== received PMS-XML ======") + dprint(__name__, 1, XML.getroot()) + dprint(__name__, 1, "====== PMS-XML finished ======") + return XML + def getXMLFromPMSToQueue(PMS, q): @@ -718,7 +726,6 @@ def MyPlexSignIn(username, password, options): return ('', '') else: raise - dprint(__name__, 1, "====== MyPlex sign in XML ======") dprint(__name__, 1, response) dprint(__name__, 1, "====== MyPlex sign in XML finished ======")