Rewired download and PMS connection

- Look for PMS in the LAN, even if plex.tv is available
This commit is contained in:
tomkat83 2016-04-06 16:24:03 +02:00
parent 8bad79413c
commit 260fc7adf8
9 changed files with 468 additions and 664 deletions

View file

@ -363,7 +363,7 @@
<string id="39019">[COLOR red]Partial or full reset of Database and PKC[/COLOR]</string>
<string id="39020">[COLOR yellow]Cache all images to Kodi texture cache[/COLOR]</string>
<string id="39021">[COLOR yellow]Sync Emby Theme Media to Kodi[/COLOR]</string>
<string id="39022"> (local)</string>
<string id="39022">local</string>
<string id="39023">Failed to authenticate. Did you login to plex.tv?</string>
<string id="39024">[COLOR yellow]Reset PMS and plex.tv connections to re-login[/COLOR]</string>
<string id="39025">Automatically log into plex.tv on startup</string>
@ -396,6 +396,8 @@
<string id="39051">Wait before sync new/changed PMS item [s]</string>
<string id="39052">Background Sync</string>
<string id="39053">Do a full library sync every x minutes</string>
<string id="39054">remote</string>
<string id="39055">Searching for Plex Server</string>

View file

@ -301,7 +301,7 @@
<string id="39019">[COLOR red]Datenbank und auf Wunsch PKC zurücksetzen[/COLOR]</string>
<string id="39020">[COLOR yellow]Alle Plex Bilder in Kodi zwischenspeichern[/COLOR]</string>
<string id="39021">[COLOR yellow]Plex Themes zu Kodi synchronisieren[/COLOR]</string>
<string id="39022"> (lokal)</string>
<string id="39022">lokal</string>
<string id="39023">Plex Media Server Authentifizierung fehlgeschlagen. Haben Sie sich bei plex.tv eingeloggt?</string>
<string id="39024">[COLOR yellow]PMS und plex.tv Verbindungen zurücksetzen für erneuten Login[/COLOR]</string>
<string id="39025">Automatisch beim Starten bei plex.tv einloggen</string>
@ -334,6 +334,8 @@
<string id="39051">Warten bevor neue/geänderte PMS Einträge gesynct werden [s]</string>
<string id="39052">Hintergrund-Synchronisation</string>
<string id="39053">Kompletten Scan aller Bibliotheken alle x Minuten durchführen</string>
<string id="39054">remote</string>
<string id="39055">Suche Plex Server</string>
<!-- Plex Entrypoint.py -->

View file

@ -29,18 +29,12 @@ http://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
http://stackoverflow.com/questions/111945/is-there-any-way-to-do-http-put-in-python
(and others...)
"""
import struct
import time
import urllib2
import httplib
import socket
import StringIO
import gzip
from threading import Thread
import traceback
import requests
import xml.etree.ElementTree as etree
from uuid import uuid4
import re
import json
@ -232,15 +226,15 @@ class PlexAPI():
try:
temp_token = xml.find('auth_token').text
except:
self.logMsg("Error: Could not find token in plex.tv answer.", -1)
self.logMsg("Could not find token in plex.tv answer.", -1)
return False
self.logMsg("temp token from plex.tv is: %s" % temp_token, 2)
if not temp_token:
return False
# Use temp token to get the final plex credentials
xml = self.doUtils('https://plex.tv/users/account?X-Plex-Token=%s'
% temp_token,
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
parameters={'X-Plex-Token': temp_token},
type="GET")
return xml
@ -320,66 +314,63 @@ class PlexAPI():
return False
return xml
def CheckConnection(self, url, token=None):
def CheckConnection(self, url, token=None, verifySSL=None):
"""
Checks connection to a Plex server, available at url. Can also be used
to check for connection with plex.tv.
Will check up to 3x until reply with False
Override SSL to skip the check by setting verifySSL=False
if 'None', SSL will be checked (standard requests setting)
if 'True', SSL settings from file settings are used (False/True)
Input:
url URL to Plex server (e.g. https://192.168.1.1:32400)
token appropriate token to access server. If None is passed,
the current token is used
Output:
False if server could not be reached or timeout occured
e.g. 200 if connection was successfull
200 if connection was successfull
int or other HTML status codes as received from the server
"""
# Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work.
header = clientinfo.ClientInfo().getXArgsDeviceInfo()
if token:
header['X-Plex-Token'] = token
sslverify = utils.settings('sslverify')
if sslverify == "true":
sslverify = True
else:
sslverify = False
self.logMsg("Checking connection to server %s with sslverify=%s"
% (url, sslverify), 1)
timeout = (3, 10)
headerOptions = None
if token is not None:
headerOptions = {'X-Plex-Token': token}
if verifySSL is True:
verifySSL = None if utils.settings('sslverify') == 'true' \
else False
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
else:
url = url + '/library/onDeck'
# Check up to 3 times before giving up - this sometimes happens when
# PKC was just started
self.logMsg("Checking connection to server %s with verifySSL=%s"
% (url, verifySSL), 1)
# Check up to 3 times before giving up
count = 0
while count < 3:
answer = self.doUtils(url,
authenticate=False,
headerOptions=headerOptions,
verifySSL=verifySSL)
if answer is False:
self.logMsg("Could not connect to %s" % url, 0)
count += 1
xbmc.sleep(500)
continue
try:
answer = requests.get(url,
headers={},
params=header,
verify=sslverify,
timeout=timeout)
except requests.exceptions.ConnectionError as e:
self.logMsg("Server is offline or cannot be reached. Url: %s "
"Header: %s Error message: %s"
% (url, header, e), 0)
count += 1
xbmc.sleep(1000)
continue
except requests.exceptions.ReadTimeout:
self.logMsg("Server timeout reached for Url %s with header %s"
% (url, header), 0)
count += 1
xbmc.sleep(1000)
continue
answer.attrib
except:
pass
else:
result = answer.status_code
self.logMsg("Result was: %s" % result, 1)
return result
self.logMsg('Failed to connect to %s too many times.' % url, -1)
# Success - we downloaded an xml!
answer = 200
self.logMsg("Checking connection successfull. Answer: %s"
% answer, 1)
return answer
self.logMsg('Failed to connect to %s too many times. PMS is dead'
% url, 0)
return False
def GetgPMSKeylist(self):
@ -455,23 +446,21 @@ class PlexAPI():
addon.setSetting('serverlist', serverlist)
return
def declarePMS(self, ATV_udid, uuid, name, scheme, ip, port):
def declarePMS(self, uuid, name, scheme, ip, port):
"""
Plex Media Server handling
parameters:
ATV_udid
uuid - PMS ID
name, scheme, ip, port, type, owned, token
"""
# store PMS information in g_PMS database
if ATV_udid not in self.g_PMS:
self.g_PMS[ATV_udid] = {}
address = ip + ':' + port
baseURL = scheme+'://'+ip+':'+port
self.g_PMS[ATV_udid][uuid] = { 'name': name,
'scheme':scheme, 'ip': ip , 'port': port,
self.g_PMS[uuid] = {
'name': name,
'scheme': scheme,
'ip': ip,
'port': port,
'address': address,
'baseURL': baseURL,
'local': '1',
@ -480,48 +469,22 @@ class PlexAPI():
'enableGzip': False
}
def updatePMSProperty(self, ATV_udid, uuid, tag, value):
def updatePMSProperty(self, uuid, tag, value):
# set property element of PMS by UUID
if not ATV_udid in self.g_PMS:
return '' # no server known for this aTV
if not uuid in self.g_PMS[ATV_udid]:
return '' # requested PMS not available
try:
self.g_PMS[uuid][tag] = value
except:
self.logMsg('%s has not yet been declared ' % uuid, -1)
return False
self.g_PMS[ATV_udid][uuid][tag] = value
def getPMSProperty(self, ATV_udid, uuid, tag):
def getPMSProperty(self, uuid, tag):
# get name of PMS by UUID
if not ATV_udid in self.g_PMS:
return '' # no server known for this aTV
if not uuid in self.g_PMS[ATV_udid]:
return '' # requested PMS not available
return self.g_PMS[ATV_udid][uuid].get(tag, '')
def getPMSFromAddress(self, ATV_udid, address):
# find PMS by IP, return UUID
if not ATV_udid in self.g_PMS:
return '' # no server known for this aTV
for uuid in self.g_PMS[ATV_udid]:
if address in self.g_PMS[ATV_udid][uuid].get('address', None):
return uuid
return '' # IP not found
def getPMSAddress(self, ATV_udid, uuid, data):
# get address of PMS by UUID
if not ATV_udid in data:
return '' # no server known for this aTV
if not uuid in data[ATV_udid]:
return '' # requested PMS not available
return data[ATV_udid][uuid]['ip'] + ':' + data[ATV_udid][uuid]['port']
def getPMSCount(self, ATV_udid):
# get count of discovered PMS by UUID
if not ATV_udid in self.g_PMS:
return 0 # no server known for this aTV
return len(self.g_PMS[ATV_udid])
try:
answ = self.g_PMS[uuid].get(tag, '')
except:
self.logMsg('%s not found in PMS catalogue' % uuid, -1)
answ = False
return answ
def PlexGDM(self):
"""
@ -532,25 +495,23 @@ class PlexAPI():
result:
PMS_list - dict() of PMSs found
"""
import struct
IP_PlexGDM = '239.0.0.250' # multicast to PMS
Port_PlexGDM = 32414
Msg_PlexGDM = 'M-SEARCH * HTTP/1.0'
# 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)
GDM.settimeout(2.0)
# Set the time-to-live for messages to 1 for local network
ttl = struct.pack('b', 1)
# Set the time-to-live for messages to 2 for local network
ttl = struct.pack('b', 2)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
# dprint(__name__, 1, "Sending discovery message: {0}", Msg_PlexGDM)
GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM))
# Look for responses from all recipients
@ -566,13 +527,11 @@ class PlexAPI():
finally:
GDM.close()
discovery_complete = True
pmsList = {}
PMS_list = {}
if returnData:
self.logMsg('returndata is: %s' % returnData)
for response in returnData:
update = {'ip': response.get('from')[0]}
# Check if we had a positive HTTP response
if "200 OK" in response.get('data'):
for each in response.get('data').split('\n'):
@ -587,98 +546,83 @@ class PlexAPI():
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')
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()
pmsList[update['uuid']] = update
PMS_list[update['uuid']] = update
return pmsList
# 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
def discoverPMS(self, ATV_udid, CSettings, IP_self, tokenDict={}):
def discoverPMS(self, IP_self, plexToken=None):
"""
discoverPMS
parameters:
ATV_udid
CSettings - for manual PMS configuration. this one looks strange.
IP_self
IP_self Own IP
optional:
tokenDict - dictionary of tokens for MyPlex, PlexHome
plexToken token for plex.tv
result:
self.g_PMS dictionary for ATV_udid
self.g_PMS dict set
"""
self.g_PMS[ATV_udid] = {}
self.g_PMS = {}
xbmcgui.Dialog().notification(
heading=self.addonName,
message=self.__language__(39055),
icon="special://home/addons/plugin.video.plexkodiconnect/icon.png",
time=3000,
sound=False)
# install plex.tv "virtual" PMS - for myPlex, PlexHome
self.declarePMS(ATV_udid, 'plex.tv', 'plex.tv', 'https', 'plex.tv', '443')
self.updatePMSProperty(ATV_udid, 'plex.tv', 'local', '-')
self.updatePMSProperty(ATV_udid, 'plex.tv', 'owned', '-')
self.updatePMSProperty(ATV_udid, 'plex.tv', 'accesstoken', tokenDict.get('MyPlexToken', ''))
if 'PlexHomeToken' in tokenDict:
authtoken = tokenDict.get('PlexHomeToken')
else:
authtoken = tokenDict.get('MyPlexToken', '')
if authtoken == '':
# not logged into myPlex
# local PMS
# PlexGDM
PMS_list = self.PlexGDM()
for uuid_id in PMS_list:
PMS = PMS_list[uuid_id]
self.declarePMS(ATV_udid, PMS['uuid'], PMS['serverName'], 'http', PMS['ip'], PMS['port']) # dflt: token='', local, owned
else:
# MyPlex servers
self.getPMSListFromMyPlex(ATV_udid, authtoken)
# Delete plex.tv again
del self.g_PMS[ATV_udid]['plex.tv']
# all servers - update enableGzip
for uuid_id in self.g_PMS.get(ATV_udid, {}):
# Look first for local PMS in the LAN
pmsList = self.PlexGDM()
self.logMsg('pmslist: %s' % pmsList, 1)
for uuid in pmsList:
PMS = pmsList[uuid]
self.declarePMS(PMS['uuid'], PMS['serverName'], 'http',
PMS['ip'], PMS['port'])
self.updatePMSProperty(PMS['uuid'], 'owned', '-')
# Ping to check whether we need HTTPs or HTTP
url = (self.getPMSProperty(ATV_udid, uuid_id, 'ip') + ':'
+ self.getPMSProperty(ATV_udid, uuid_id, 'port'))
url = '%s:%s' % (PMS['ip'], PMS['port'])
https = PMSHttpsEnabled(url)
if https is None:
# Error contacting url
# Error contacting url. Skip for now
continue
elif https:
self.updatePMSProperty(ATV_udid, uuid_id, 'scheme', 'https')
elif https is True:
self.updatePMSProperty(PMS['uuid'], 'scheme', 'https')
self.updatePMSProperty(
PMS['uuid'],
'baseURL',
'https://%s:%s' % (PMS['ip'], PMS['port']))
else:
self.updatePMSProperty(ATV_udid, uuid_id, 'scheme', 'http')
# enable Gzip if not on same host, local&remote PMS depending
# on setting
enableGzip = (not self.getPMSProperty(ATV_udid, uuid_id, 'ip') == IP_self) \
and (
(self.getPMSProperty(ATV_udid, uuid_id, 'local') == '1'
and False)
or
(self.getPMSProperty(ATV_udid, uuid_id, 'local') == '0'
and True) == 'True'
)
self.updatePMSProperty(ATV_udid, uuid_id, 'enableGzip', enableGzip)
# Already declared with http
pass
def getPMSListFromMyPlex(self, ATV_udid, authtoken):
if not plexToken:
self.logMsg('No plex.tv token supplied, checked LAN for PMS', 0)
return
# install plex.tv "virtual" PMS - for myPlex, PlexHome
# self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443')
# self.updatePMSProperty('plex.tv', 'local', '-')
# self.updatePMSProperty('plex.tv', 'owned', '-')
# self.updatePMSProperty(
# 'plex.tv', 'accesstoken', plexToken)
# (remote and local) servers from plex.tv
# Get PMS from plex.tv. This will overwrite any PMS we already found
self.getPMSListFromMyPlex(plexToken)
def getPMSListFromMyPlex(self, token):
"""
getPMSListFromMyPlex
get Plex media Server List from plex.tv/pms/resources
"""
xml = self.doUtils('https://plex.tv/api/resources?includeHttps=1',
xml = self.doUtils('https://plex.tv/api/resources',
authenticate=False,
headerOptions={'X-Plex-Token': authtoken})
parameters={'includeHttps': 1},
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
@ -699,36 +643,43 @@ class PlexAPI():
PMS = {}
PMS['name'] = Dir.get('name')
infoAge = time.time() - int(Dir.get('lastSeenAt'))
oneDayInSec = 2*60*60*24
if infoAge > 1*oneDayInSec:
self.logMsg("Server %s not seen for 1 day - "
oneDayInSec = 60*60*24
if infoAge > 2*oneDayInSec:
self.logMsg("Server %s not seen for 2 days - "
"skipping." % PMS['name'], 0)
continue
PMS['uuid'] = Dir.get('clientIdentifier')
PMS['token'] = Dir.get('accessToken', authtoken)
PMS['token'] = Dir.get('accessToken', token)
PMS['owned'] = Dir.get('owned', '0')
PMS['local'] = Dir.get('publicAddressMatches')
PMS['ownername'] = Dir.get('sourceTitle', '')
PMS['path'] = '/'
PMS['options'] = None
# flag to set first connection, possibly overwrite later with
# more suitable
PMS['baseURL'] = ""
# If PMS seems (!!) local, try a local connection first
# Backup to remote connection, if that failes
PMS['baseURL'] = ''
for Con in Dir.iter(tag='Connection'):
if (PMS['baseURL'] == "" or
Con.get('local') == PMS['local']):
localConn = Con.get('local')
if ((PMS['local'] == '1' and localConn == '1') or
(PMS['local'] == '0' and localConn == '0')):
# Either both local or both remote
PMS['protocol'] = Con.get('protocol')
PMS['ip'] = Con.get('address')
PMS['port'] = Con.get('port')
PMS['baseURL'] = Con.get('baseURL')
# todo: handle unforeseen - like we get multiple suitable
# connections. how to choose one?
PMS['baseURL'] = Con.get('uri')
elif PMS['local'] == '1' and localConn == '0':
# Backup connection if local one did not work
PMS['backup'] = {}
PMS['backup']['protocol'] = Con.get('protocol')
PMS['backup']['ip'] = Con.get('address')
PMS['backup']['port'] = Con.get('port')
PMS['backup']['baseURL'] = Con.get('uri')
# poke PMS, own thread for each poke
t = Thread(target=self.pokePMS,
args=(PMS['baseURL'], PMS['token'], PMS, queue))
args=(PMS, queue))
t.start()
threads.append(t)
@ -739,45 +690,49 @@ class PlexAPI():
# declare new PMSs
while not queue.empty():
PMS = queue.get()
self.declarePMS(ATV_udid, PMS['uuid'], PMS['name'],
self.declarePMS(PMS['uuid'], PMS['name'],
PMS['protocol'], PMS['ip'], PMS['port'])
# dflt: token='', local, owned - updated later
self.updatePMSProperty(
ATV_udid, PMS['uuid'], 'accesstoken', PMS['token'])
PMS['uuid'], 'accesstoken', PMS['token'])
self.updatePMSProperty(
ATV_udid, PMS['uuid'], 'owned', PMS['owned'])
PMS['uuid'], 'owned', PMS['owned'])
self.updatePMSProperty(
ATV_udid, PMS['uuid'], 'local', PMS['local'])
PMS['uuid'], 'local', PMS['local'])
# set in declarePMS, overwrite for https encryption
self.updatePMSProperty(
ATV_udid, PMS['uuid'], 'baseURL', PMS['baseURL'])
PMS['uuid'], 'baseURL', PMS['baseURL'])
self.updatePMSProperty(
ATV_udid, PMS['uuid'], 'ownername', PMS['ownername'])
PMS['uuid'], 'ownername', PMS['ownername'])
queue.task_done()
def pokePMS(self, url, token, PMS, queue):
xml = self.doUtils(url,
def pokePMS(self, PMS, queue):
# Ignore SSL certificates for now
xml = self.doUtils(PMS['baseURL'],
authenticate=False,
headerOptions={'X-Plex-Token': token})
headerOptions={'X-Plex-Token': PMS['token']},
verifySSL=False)
try:
xml.attrib
except:
# Connection failed
# retry with remote connection if we just tested local one.
if PMS['local'] == '1' and PMS.get('backup'):
self.logMsg('Couldnt talk to local PMS locally.'
'Trying again remotely.', 0)
PMS['protocol'] = PMS['backup']['protocol']
PMS['ip'] = PMS['backup']['ip']
PMS['port'] = PMS['backup']['port']
PMS['baseURL'] = PMS['backup']['baseURL']
PMS['local'] = '0'
# Try again
self.pokePMS(PMS, queue)
else:
return
else:
# Connection successful, process later
queue.put(PMS)
def getURL(self, baseURL, path, key):
if key.startswith('http://') or key.startswith('https://'): # external server
URL = key
elif key.startswith('/'): # internal full path.
URL = baseURL + key
elif key == '': # internal path
URL = baseURL + path
else: # internal path, add-on
URL = baseURL + path + '/' + key
return URL
def MyPlexSignIn(self, username, password, options):
"""
MyPlex Sign In, Sign Out
@ -1226,13 +1181,12 @@ class PlexAPI():
return path
def returnServerList(self, ATV_udid, data):
def returnServerList(self, data):
"""
Returns a nicer list of all servers found in data, where data is in
g_PMS format, for the client device with unique ID ATV_udid
Input:
ATV_udid Unique client ID
data e.g. self.g_PMS
Output: List of all servers, with an entry of the form:
@ -1247,21 +1201,23 @@ class PlexAPI():
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
serverlist = []
for key, value in data[ATV_udid].items():
for key, value in data.items():
serverlist.append({
'name': value['name'],
'address': value['address'],
'ip': value['ip'],
'port': value['port'],
'scheme': value['scheme'],
'local': value['local'],
'owned': value['owned'],
'name': value.get('name'),
'address': value.get('address'),
'ip': value.get('ip'),
'port': value.get('port'),
'scheme': value.get('scheme'),
'local': value.get('local'),
'owned': value.get('owned'),
'machineIdentifier': key,
'accesstoken': value['accesstoken'],
'baseURL': value['baseURL']
'accesstoken': value.get('accesstoken'),
'baseURL': value.get('baseURL'),
'ownername': value.get('ownername')
})
return serverlist
@ -2001,6 +1957,7 @@ class API():
return url
# For Direct Streaming or Transcoding
from uuid import uuid4
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key']
transcodePath = self.server + \

View file

@ -410,47 +410,38 @@ def getPlexRepeat(kodiRepeat):
def PMSHttpsEnabled(url):
"""
Returns True if the PMS wants to talk https, False otherwise. None if error
occured, e.g. the connection timed out
Returns True if the PMS can talk https, False otherwise.
None if error occured, e.g. the connection timed out
With with e.g. url=192.168.0.1:32400 (NO http/https)
Call with e.g. url='192.168.0.1:32400' (NO http/https)
This is done by GET /identity (returns an error if https is enabled and we
are trying to use http)
Prefers HTTPS over HTTP
"""
# True if https, False if http
answer = True
doUtils = downloadutils.DownloadUtils().downloadUrl
res = doUtils('https://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
# Don't use downloadutils here, otherwise we may get un-authorized!
res = requests.get('https://%s/identity' % url,
headers={},
verify=False,
timeout=(3, 10))
# Don't verify SSL since we can connect for sure then!
except requests.exceptions.ConnectionError as e:
res.attrib
except:
# Might have SSL deactivated. Try with http
res = doUtils('http://%s/identity' % url,
authenticate=False,
verifySSL=False)
try:
res = requests.get('http://%s/identity' % url,
headers={},
timeout=(3, 10))
except requests.exceptions.ConnectionError as e:
logMsg(title, "Server is offline or cannot be reached. Url: %s"
", Error message: %s" % (url, e), -1)
return None
except requests.exceptions.ReadTimeout:
logMsg(title, "Server timeout reached for Url %s" % url, -1)
res.attrib
except:
logMsg(title, "Could not contact PMS %s" % url, -1)
return None
else:
answer = False
except requests.exceptions.ReadTimeout:
logMsg(title, "Server timeout reached for Url %s" % url, -1)
return None
if res.status_code == requests.codes.ok:
return answer
# Received a valid XML. Server wants to talk HTTP
return False
else:
return None
# Received a valid XML. Server wants to talk HTTPS
return True
def GetMachineIdentifier(url):

View file

@ -5,9 +5,7 @@
import requests
import xml.etree.ElementTree as etree
import xbmcgui
import utils
from utils import logging, settings, window
import clientinfo
###############################################################################
@ -20,382 +18,258 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
###############################################################################
@utils.logging
@logging
class DownloadUtils():
"""
Manages any up/downloads with PKC. Careful to initiate correctly
Use startSession() to initiate.
If not initiated, e.g. SSL check will fallback to False
"""
# Borg - multiple instances, shared state
_shared_state = {}
# Requests session
s = None
timeout = 30
def __init__(self):
self.__dict__ = self._shared_state
def setUsername(self, username):
# Reserved for userclient only
"""
Reserved for userclient only
"""
self.username = username
self.logMsg("Set username: %s" % username, 2)
self.logMsg("Set username: %s" % username, 0)
def setUserId(self, userId):
# Reserved for userclient only
"""
Reserved for userclient only
"""
self.userId = userId
self.logMsg("Set userId: %s" % userId, 2)
self.logMsg("Set userId: %s" % userId, 0)
def setServer(self, server):
# Reserved for userclient only
"""
Reserved for userclient only
"""
self.server = server
self.logMsg("Set server: %s" % server, 2)
self.logMsg("Set server: %s" % server, 0)
def setToken(self, token):
# Reserved for userclient only
"""
Reserved for userclient only
"""
self.token = token
self.logMsg("Set token: xxxxxxx", 2)
def setSSL(self, ssl, sslclient):
# Reserved for userclient only
self.sslverify = ssl
self.sslclient = sslclient
self.logMsg("Verify SSL host certificate: %s" % ssl, 2)
self.logMsg("SSL client side certificate: %s" % sslclient, 2)
def postCapabilities(self, deviceId):
# Post settings to session
url = "{server}/emby/Sessions/Capabilities/Full?format=json"
data = {
'PlayableMediaTypes': "Audio,Video",
'SupportsMediaControl': True,
'SupportedCommands': (
"MoveUp,MoveDown,MoveLeft,MoveRight,Select,"
"Back,ToggleContextMenu,ToggleFullscreen,ToggleOsdMenu,"
"GoHome,PageUp,NextLetter,GoToSearch,"
"GoToSettings,PageDown,PreviousLetter,TakeScreenshot,"
"VolumeUp,VolumeDown,ToggleMute,SendString,DisplayMessage,"
"SetAudioStreamIndex,SetSubtitleStreamIndex,"
"Mute,Unmute,SetVolume,"
"Play,Playstate,PlayNext"
)
}
self.logMsg("Capabilities URL: %s" % url, 2)
self.logMsg("Postdata: %s" % data, 2)
self.downloadUrl(url, postBody=data, type="POST")
self.logMsg("Posted capabilities to %s" % self.server, 2)
# Attempt at getting sessionId
url = "{server}/emby/Sessions?DeviceId=%s&format=json" % deviceId
result = self.downloadUrl(url)
try:
sessionId = result[0]['Id']
except (KeyError, TypeError):
self.logMsg("Failed to retrieve sessionId.", 1)
if token == '':
self.logMsg('Set token: empty token!', 0)
else:
self.logMsg("Session: %s" % result, 2)
self.logMsg("SessionId: %s" % sessionId, 1)
utils.window('emby_sessionId', value=sessionId)
self.logMsg("Set token: xxxxxxx", 0)
# Post any permanent additional users
# additionalUsers = utils.settings('additionalUsers')
# if additionalUsers:
def setSSL(self, verifySSL=None, certificate=None):
"""
Reserved for userclient only
# additionalUsers = additionalUsers.split(',')
# self.logMsg(
# "List of permanent users added to the session: %s"
# % additionalUsers, 1)
verifySSL must be 'true' to enable certificate validation
# # Get the user list from server to get the userId
# url = "{server}/emby/Users?format=json"
# result = self.downloadUrl(url)
# for additional in additionalUsers:
# addUser = additional.decode('utf-8').lower()
# # Compare to server users to list of permanent additional users
# for user in result:
# username = user['Name'].lower()
# if username in addUser:
# userId = user['Id']
# url = (
# "{server}/emby/Sessions/%s/Users/%s?format=json"
# % (sessionId, userId)
# )
# self.downloadUrl(url, postBody={}, type="POST")
certificate must be path to certificate or 'None'
"""
if verifySSL is None:
verifySSL = settings('sslverify')
if certificate is None:
certificate = settings('sslcert')
self.logMsg("Verify SSL certificates set to: %s" % verifySSL, 0)
self.logMsg("SSL client side certificate set to: %s" % certificate, 0)
if verifySSL != 'true':
self.s.verify = False
if certificate != 'None':
self.s.cert = certificate
def startSession(self):
# User should be authenticated when this method is called
client = clientinfo.ClientInfo()
self.deviceId = client.getDeviceId()
verify = False
# If user enabled host certificate verification
try:
verify = self.sslverify
if self.sslclient is not None:
verify = self.sslclient
except:
self.logMsg("Could not load SSL settings.", -1)
"""
User should be authenticated when this method is called (via
userclient)
"""
# Start session
self.s = requests.Session()
client = clientinfo.ClientInfo()
self.deviceId = client.getDeviceId()
# Attach authenticated header to the session
self.s.headers = client.getXArgsDeviceInfo()
self.s.verify = verify
self.s.encoding = 'utf-8'
# Set SSL settings
self.setSSL()
# Set other stuff
self.setServer(window('pms_server'))
self.setToken(window('pms_token'))
self.setUserId(window('currUserId'))
self.setUsername(window('plex_username'))
# Retry connections to the server
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
self.logMsg("Requests session started on: %s" % self.server, 1)
self.logMsg("Requests session started on: %s" % self.server, 0)
def stopSession(self):
try:
self.s.close()
except:
self.logMsg("Requests session could not be terminated.", 1)
self.logMsg("Requests session could not be terminated.", 0)
try:
del self.s
except:
pass
self.logMsg('Request session stopped', 0)
def getHeader(self, options=None):
try:
header = self.s.headers.copy()
except:
header = clientinfo.ClientInfo().getXArgsDeviceInfo()
if options is not None:
header.update(options)
return header
def downloadUrl(self, url, postBody=None, type="GET", parameters=None,
authenticate=True, headerOptions=None):
timeout = self.timeout
default_link = ""
def __doDownload(self, s, type, **kwargs):
if type == "GET":
r = s.get(**kwargs)
elif type == "POST":
r = s.post(**kwargs)
elif type == "DELETE":
r = s.delete(**kwargs)
elif type == "OPTIONS":
r = s.options(**kwargs)
elif type == "PUT":
r = s.put(**kwargs)
return r
try:
# If user is authenticated
if (authenticate):
def downloadUrl(self, url, type="GET", postBody=None, parameters=None,
authenticate=True, headerOptions=None, verifySSL=True):
"""
Override SSL check with verifySSL=False
If authenticate=True, existing request session will be used/started
Otherwise, 'empty' request will be made
Returns:
False If an error occured
True If connection worked but no body was received
401, ... integer if PMS answered with HTTP error 401
(unauthorized) or other http error codes
xml xml etree root object, if applicable
JSON json() object, if applicable
"""
kwargs = {}
if authenticate:
# Get requests session
try:
s = self.s
except AttributeError:
self.logMsg("Request session does not exist: start one", 0)
self.startSession()
s = self.s
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId)
header = self.getHeader(options=headerOptions)
# Prepare request
if type == "GET":
r = s.get(url, json=postBody, params=parameters, timeout=timeout, headers=header)
elif type == "POST":
r = s.post(url, json=postBody, timeout=timeout, headers=header)
elif type == "DELETE":
r = s.delete(url, json=postBody, timeout=timeout, headers=header)
elif type == "OPTIONS":
r = s.options(url, json=postBody, timeout=timeout, headers=header)
# For Plex Companion
elif type == "POSTXML":
r = s.post(url, postBody, timeout=timeout, headers=header)
elif type == "PUT":
r = s.put(url, timeout=timeout, headers=header)
except AttributeError:
# request session does not exists
self.logMsg("Request session does not exist: start one", 1)
# Get user information
self.userId = utils.window('currUserId')
self.server = utils.window('pms_server')
self.token = utils.window('pms_token')
header = self.getHeader(options=headerOptions)
verifyssl = False
cert = None
# IF user enables ssl verification
if utils.settings('sslverify') == "true":
verifyssl = True
if utils.settings('sslcert') != "None":
verifyssl = utils.settings('sslcert')
# Replace for the real values
url = url.replace("{server}", self.server)
url = url.replace("{UserId}", self.userId)
# Prepare request
if type == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
elif type == "POST":
r = requests.post(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
elif type == "DELETE":
r = requests.delete(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
elif type == "OPTIONS":
r = requests.options(url,
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
elif type == "PUT":
r = requests.put(url,
json=postBody,
headers=header,
timeout=timeout,
cert=cert,
verify=verifyssl)
# If user is not authenticated
elif not authenticate:
header = self.getHeader(options=headerOptions)
# If user enables ssl verification
try:
verifyssl = self.sslverify
if self.sslclient is not None:
verifyssl = self.sslclient
except AttributeError:
if utils.settings('sslverify') == "true":
verifyssl = True
else:
verifyssl = False
self.logMsg("Set SSL verification to: %s" % verifyssl, 2)
# Prepare request
if type == "GET":
r = requests.get(url,
json=postBody,
params=parameters,
headers=header,
timeout=timeout,
verify=verifyssl)
# User is not (yet) authenticated. Used to communicate with
# plex.tv and to check for PMS servers
s = requests
headerOptions = self.getHeader(options=headerOptions)
kwargs['timeout'] = self.timeout
if settings('sslcert') != 'None':
kwargs['cert'] = settings('sslcert')
elif type == "POST":
r = requests.post(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
elif type == "PUT":
r = requests.put(url,
json=postBody,
headers=header,
timeout=timeout,
verify=verifyssl)
##### THE RESPONSE #####
# self.logMsg(r.url, 2)
if r.status_code == 204:
# No body in the response
# self.logMsg("====== 204 Success ======", 2)
pass
elif r.status_code == requests.codes.ok:
# Set the variables we were passed (fallback to request session
# otherwise - faster)
kwargs['url'] = url
if verifySSL is False:
kwargs['verify'] = False
if headerOptions is not None:
kwargs['headers'] = headerOptions
if postBody is not None:
kwargs['data'] = postBody
if parameters is not None:
kwargs['params'] = parameters
# ACTUAL DOWNLOAD HAPPENING HERE
try:
# Allow for xml responses
r = etree.fromstring(r.content)
# self.logMsg("====== 200 Success ======", 2)
# self.logMsg("Received an XML response for: %s" % url, 2)
r = self.__doDownload(s, type, **kwargs)
# THE EXCEPTIONS
except requests.exceptions.ConnectionError as e:
# Connection error
self.logMsg("Server unreachable at: %s" % url, -1)
self.logMsg(e, 2)
# Make the addon aware of status
window('emby_online', value="false")
return False
except requests.exceptions.ConnectTimeout as e:
self.logMsg("Server timeout at: %s" % url, -1)
self.logMsg(e, 2)
return False
except requests.exceptions.HTTPError as e:
r = r.status_code
if r == 401:
# Unauthorized
self.logMsg('Error 401 contacting %s' % url, -1)
elif r in (301, 302):
# Redirects
self.logMsg('HTTP redirect error %s at %s' % (r, url), -1)
elif r == 400:
# Bad requests
self.logMsg('Bad request at %s' % url, -1)
else:
self.logMsg('HTTP Error %s at %s' % (r, url), -1)
self.logMsg(e, 2)
return r
except requests.exceptions.SSLError as e:
self.logMsg("Invalid SSL certificate for: %s" % url, -1)
self.logMsg(e, 2)
return False
except requests.exceptions.RequestException as e:
self.logMsg("Unknown error connecting to: %s" % url, -1)
self.logMsg("Error message: %s" % e, 2)
return False
except:
self.logMsg('Unknown requests error', -1)
import traceback
self.logMsg(traceback.format_exc(), 0)
return False
# THE RESPONSE #####
if r.status_code == 204:
# No body in the response
return True
elif r.status_code in (200, 201):
# 200: OK
# 201: Created
try:
# xml response
r = etree.fromstring(r.content)
return r
except:
r.encoding = 'utf-8'
if r.text == '':
# Answer does not contain a body (even though it should)
return True
try:
# UNICODE - JSON object
r = r.json()
# self.logMsg("====== 200 Success ======", 2)
# self.logMsg("Response: %s" % r, 2)
return r
except:
try:
if r.text == '' and r.status_code == 200:
# self.logMsg("====== 200 Success ======", 2)
# self.logMsg("Answer from PMS does not contain a body", 2)
pass
# self.logMsg("Unable to convert the response for: %s" % url, 2)
# self.logMsg("Content-type was: %s" % r.headers['content-type'], 2)
except:
self.logMsg("Unable to convert the response for: %s" % url, 2)
self.logMsg("Content-type was: %s" % r.headers['content-type'], 2)
else:
r.raise_for_status()
##### EXCEPTIONS #####
except requests.exceptions.ConnectionError as e:
# Make the addon aware of status
if utils.window('emby_online') != "false":
self.logMsg("Server unreachable at: %s" % url, -1)
self.logMsg(e, 2)
utils.window('emby_online', value="false")
except requests.exceptions.ConnectTimeout as e:
self.logMsg("Server timeout at: %s" % url, 0)
self.logMsg(e, 1)
except requests.exceptions.HTTPError as e:
if r.status_code == 401:
# Unauthorized
status = utils.window('emby_serverStatus')
if 'X-Application-Error-Code' in r.headers:
# Emby server errors
if r.headers['X-Application-Error-Code'] == "ParentalControl":
# Parental control - access restricted
self.logMsg('Setting emby_serverStatus to restricted')
utils.window('emby_serverStatus', value="restricted")
xbmcgui.Dialog().notification(
heading=self.addonName,
message="Access restricted.",
icon=xbmcgui.NOTIFICATION_ERROR,
time=5000)
self.logMsg("Unable to convert the response for: %s"
% url, -1)
self.logMsg("Received headers were: %s" % r.headers, -1)
return False
elif r.headers['X-Application-Error-Code'] == "UnauthorizedAccessException":
# User tried to do something his emby account doesn't allow
pass
elif status not in ("401", "Auth"):
# Tell userclient token has been revoked.
self.logMsg('Error 401 contacting %s' % url, 0)
self.logMsg('Setting emby_serverStatus to 401', 0)
utils.window('emby_serverStatus', value="401")
self.logMsg("HTTP Error: %s" % e, 0)
xbmcgui.Dialog().notification(
heading=self.addonName,
message="Error connecting: Unauthorized.",
icon=xbmcgui.NOTIFICATION_ERROR)
return 401
elif r.status_code in (301, 302):
# Redirects
pass
elif r.status_code == 400:
# Bad requests
pass
except requests.exceptions.SSLError as e:
self.logMsg("Invalid SSL certificate for: %s" % url, 0)
self.logMsg(e, 1)
except requests.exceptions.RequestException as e:
self.logMsg("Unknown error connecting to: %s" % url, 0)
self.logMsg(e, 1)
return default_link
else:
self.logMsg('Unknown answer from PMS %s with status code %s. '
'Message:' % (url, r.status_code), -1)
r.encoding = 'utf-8'
self.logMsg(r.text, -1)
return True

View file

@ -36,7 +36,6 @@ class InitialSetup():
# SERVER INFO #####
self.logMsg("Initial setup called.", 0)
server = self.userClient.getServer()
clientId = self.clientInfo.getDeviceId()
serverid = utils.settings('plex_machineIdentifier')
# Get Plex credentials from settings file, if they exist
plexdict = self.plx.GetPlexLoginFromSettings()
@ -54,8 +53,15 @@ class InitialSetup():
if (plexToken and myplexlogin == 'true' and forcePlexTV is False
and chooseServer is False):
chk = self.plx.CheckConnection('plex.tv', plexToken)
try:
chk.attrib
except:
pass
else:
# Success - we downloaded an xml!
chk = 200
# HTTP Error: unauthorized. Token is no longer valid
if chk == 401 or chk == 403:
if chk in (401, 403):
self.logMsg('plex.tv connection returned HTTP %s' % chk, 0)
# Delete token in the settings
utils.settings('plexToken', value='')
@ -113,18 +119,13 @@ class InitialSetup():
httpsUpdated = False
while True:
if httpsUpdated is False:
tokenDict = {'MyPlexToken': plexToken} if plexToken else {}
# Populate g_PMS variable with the found Plex servers
self.plx.discoverPMS(clientId,
None,
xbmc.getIPAddress(),
tokenDict=tokenDict)
self.logMsg("Result of setting g_PMS variable: %s"
% self.plx.g_PMS, 1)
self.plx.discoverPMS(xbmc.getIPAddress(),
plexToken=plexToken)
isconnected = False
serverlist = self.plx.returnServerList(clientId,
self.plx.g_PMS)
self.logMsg('PMS serverlist: %s' % serverlist)
self.logMsg('g_PMS: %s' % self.plx.g_PMS, 1)
serverlist = self.plx.returnServerList(self.plx.g_PMS)
self.logMsg('PMS serverlist: %s' % serverlist, 2)
# Let user pick server from a list
# Get a nicer list
dialoglist = []
@ -138,11 +139,20 @@ class InitialSetup():
for server in serverlist:
if server['local'] == '1':
# server is in the same network as client. Add "local"
dialoglist.append(
server['name']
+ string(39022))
msg = string(39022)
else:
dialoglist.append(server['name'])
# Add 'remote'
msg = string(39054)
if server.get('ownername'):
# Display username if its not our PMS
dialoglist.append('%s (%s, %s)'
% (server['name'],
server['ownername'],
msg))
else:
dialoglist.append('%s (%s)'
% (server['name'],
msg))
resp = dialog.select(string(39012), dialoglist)
server = serverlist[resp]
activeServer = server['machineIdentifier']
@ -154,15 +164,20 @@ class InitialSetup():
else:
url = server['baseURL']
# Deactive SSL verification if the server is local!
# Watch out - settings is cached by Kodi - use dedicated var!
if server['local'] == '1':
utils.settings('sslverify', 'false')
self.logMsg("Setting SSL verify to false, because server is "
"local", 1)
verifySSL = False
else:
utils.settings('sslverify', 'true')
self.logMsg("Setting SSL verify to true, because server is "
"not local", 1)
chk = self.plx.CheckConnection(url, server['accesstoken'])
verifySSL = None
chk = self.plx.CheckConnection(url,
server['accesstoken'],
verifySSL=verifySSL)
if chk == 504 and httpsUpdated is False:
# Not able to use HTTP, try HTTPs for now
serverlist[resp]['scheme'] = 'https'
@ -221,19 +236,6 @@ class InitialSetup():
% (activeServer, server['ip'], server['port'],
server['scheme']), 0)
# ADDITIONAL PROMPTS #####
# directPaths = dialog.yesno(
# heading="%s: Playback Mode" % self.addonName,
# line1=(
# "Caution! If you choose Native mode, you "
# "will probably lose access to certain Plex "
# "features."),
# nolabel="Addon (Default)",
# yeslabel="Native (Direct Paths)")
# if directPaths:
# self.logMsg("User opted to use direct paths.", 1)
# utils.settings('useDirectPaths', value="1")
if forcePlexTV is True or chooseServer is True:
return

View file

@ -278,9 +278,8 @@ class Subscriber:
Threaded POST request, because they stall due to PMS response missing
the Content-Length header :-(
"""
response = self.download.downloadUrl(
url,
response = self.download.downloadUrl(url,
postBody=msg,
type="POSTXML")
type="POST")
if response in [False, None, 401]:
self.subMgr.removeSubscriber(self.uuid)

View file

@ -106,29 +106,12 @@ class UserClient(threading.Thread):
def getSSLverify(self):
# Verify host certificate
settings = utils.settings
s_sslverify = settings('sslverify')
if settings('altip') == "true":
s_sslverify = settings('secondsslverify')
if s_sslverify == "true":
return True
else:
return False
return None if utils.settings('sslverify') == 'true' else False
def getSSL(self):
# Client side certificate
settings = utils.settings
s_cert = settings('sslcert')
if settings('altip') == "true":
s_cert = settings('secondsslcert')
if s_cert == "None":
return None
else:
return s_cert
return None if utils.settings('sslcert') == 'None' \
else utils.settings('sslcert')
def setUserPref(self):
self.logMsg('Setting user preferences', 0)
@ -183,8 +166,9 @@ class UserClient(threading.Thread):
if authenticated is False:
self.logMsg('Testing validity of current token', 0)
res = PlexAPI.PlexAPI().CheckConnection(
self.currServer, self.currToken)
res = PlexAPI.PlexAPI().CheckConnection(self.currServer,
token=self.currToken,
verifySSL=self.ssl)
if res is False:
self.logMsg('Answer from PMS is not as expected. Retrying', -1)
return False
@ -227,13 +211,6 @@ class UserClient(threading.Thread):
window('remapSMB%sOrg' % item, value=org)
window('remapSMB%sNew' % item, value=new)
# Set DownloadUtils values
doUtils.setUsername(username)
doUtils.setUserId(self.currUserId)
doUtils.setServer(self.currServer)
doUtils.setToken(self.currToken)
doUtils.setSSL(self.ssl, self.sslcert)
# Start DownloadUtils session
doUtils.startSession()
# self.getAdditionalUsers()

View file

@ -233,7 +233,7 @@ class Service():
if server is False:
# No server info set in add-on settings
pass
elif plx.CheckConnection(server) is False:
elif plx.CheckConnection(server, verifySSL=True) is False:
# Server is offline or cannot be reached
# Alert the user and suppress future warning
if self.server_online: