PlexKodiConnect/resources/lib/PlexAPI.py

2124 lines
73 KiB
Python
Raw Normal View History

"""
Taken from iBaa, https://github.com/iBaa/PlexConnect
Point of time: December 22, 2015
Collection of "connector functions" to Plex Media Server/MyPlex
PlexGDM:
loosely based on hippojay's plexGDM:
https://github.com/hippojay/script.plexbmc.helper... /resources/lib/plexgdm.py
Plex Media Server communication:
source (somewhat): https://github.com/hippojay/plugin.video.plexbmc
later converted from httplib to urllib2
Transcoder support:
PlexAPI_getTranscodePath() based on getTranscodeURL from pyplex/plexAPI
https://github.com/megawubs/pyplex/blob/master/plexAPI/info.py
MyPlex - Basic Authentication:
http://www.voidspace.org.uk/python/articles/urllib2.shtml
http://www.voidspace.org.uk/python/articles/authentication.shtml
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 time
import urllib2
import socket
from threading import Thread
import xml.etree.ElementTree as etree
2015-12-29 04:47:16 +11:00
import re
2016-01-02 00:40:40 +11:00
import json
from urllib import urlencode, quote_plus, unquote
2015-12-29 04:47:16 +11:00
import xbmcaddon
import xbmcgui
import xbmc
import xbmcvfs
2016-01-30 06:07:21 +11:00
import clientinfo
import utils
import downloadutils
from PlexFunctions import PlexToKodiTimefactor, PMSHttpsEnabled
2016-01-27 03:20:13 +11:00
@utils.logging
class PlexAPI():
# CONSTANTS
# Timeout for POST/GET commands, I guess in seconds
2016-03-11 02:02:46 +11:00
timeout = 10
def __init__(self):
2015-12-28 02:27:49 +11:00
self.__language__ = xbmcaddon.Addon().getLocalizedString
self.g_PMS = {}
self.doUtils = downloadutils.DownloadUtils().downloadUrl
2015-12-29 04:47:16 +11:00
2016-01-13 03:23:55 +11:00
def GetPlexLoginFromSettings(self):
"""
2016-03-04 23:34:30 +11:00
Returns a dict:
'plexLogin': utils.settings('plexLogin'),
'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'),
2016-03-10 01:37:27 +11:00
'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
2016-03-04 23:34:30 +11:00
2016-03-10 01:37:27 +11:00
Returns strings or unicode
2016-03-04 23:34:30 +11:00
Returns empty strings '' for a setting if not found.
2016-01-13 03:23:55 +11:00
2016-01-14 20:20:19 +11:00
myplexlogin is 'true' if user opted to log into plex.tv (the default)
2016-01-15 00:47:34 +11:00
plexhome is 'true' if plex home is used (the default)
2016-01-13 03:23:55 +11:00
"""
2016-03-04 23:34:30 +11:00
return {
2016-03-10 01:37:27 +11:00
'plexLogin': utils.settings('plexLogin'),
2016-03-04 23:34:30 +11:00
'plexToken': utils.settings('plexToken'),
'plexhome': utils.settings('plexhome'),
'plexid': utils.settings('plexid'),
2016-03-10 01:37:27 +11:00
'myplexlogin': utils.settings('myplexlogin'),
'plexAvatar': utils.settings('plexAvatar'),
'plexHomeSize': utils.settings('plexHomeSize')
2016-03-04 23:34:30 +11:00
}
2016-01-13 03:23:55 +11:00
def GetPlexLoginAndPassword(self):
"""
Signs in to plex.tv.
plexLogin, authtoken = GetPlexLoginAndPassword()
Input: nothing
Output:
plexLogin plex.tv username
authtoken token for plex.tv
Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file
If not logged in, empty strings are returned for both.
"""
string = self.__language__
retrievedPlexLogin = ''
plexLogin = 'dummy'
authtoken = ''
dialog = xbmcgui.Dialog()
while retrievedPlexLogin == '' and plexLogin != '':
# Enter plex.tv username. Or nothing to cancel.
plexLogin = dialog.input(self.addonName + string(39300),
type=xbmcgui.INPUT_ALPHANUM)
if plexLogin != "":
# Enter password for plex.tv user
plexPassword = dialog.input(
string(39301) + plexLogin,
type=xbmcgui.INPUT_ALPHANUM,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
retrievedPlexLogin, authtoken = self.MyPlexSignIn(
plexLogin,
plexPassword,
2016-04-06 02:23:00 +10:00
{'X-Plex-Client-Identifier':
clientinfo.ClientInfo().getDeviceId()})
2016-01-15 00:47:34 +11:00
self.logMsg("plex.tv username and token: %s, %s"
% (plexLogin, authtoken), 1)
if plexLogin == '':
# Could not sign in user
dialog.ok(self.addonName,
string(39302) + plexLogin)
# Write to Kodi settings file
2016-01-15 00:47:34 +11:00
utils.settings('plexLogin', value=retrievedPlexLogin)
utils.settings('plexToken', value=authtoken)
return (retrievedPlexLogin, authtoken)
2016-01-14 20:20:19 +11:00
def PlexTvSignInWithPin(self):
"""
Prompts user to sign in by visiting https://plex.tv/pin
2016-03-10 01:37:27 +11:00
Writes to Kodi settings file. Also returns:
2016-01-14 20:20:19 +11:00
{
2016-01-15 00:47:34 +11:00
'plexhome': 'true' if Plex Home, 'false' otherwise
2016-01-14 20:20:19 +11:00
'username':
2016-03-10 01:37:27 +11:00
'avatar': URL to user avator
2016-01-14 20:20:19 +11:00
'token':
2016-03-10 01:37:27 +11:00
'plexid': Plex user ID
'homesize': Number of Plex home users (defaults to '1')
2016-01-14 20:20:19 +11:00
}
Returns False if authentication did not work.
"""
string = self.__language__
2016-01-14 20:20:19 +11:00
code, identifier = self.GetPlexPin()
dialog = xbmcgui.Dialog()
if not code:
# Problems trying to contact plex.tv. Try again later
dialog.ok(self.addonName, string(39303))
2016-01-14 20:20:19 +11:00
return False
# Go to https://plex.tv/pin and enter the code:
2016-03-11 02:02:46 +11:00
# Or press No to cancel the sign in.
2016-01-14 20:20:19 +11:00
answer = dialog.yesno(self.addonName,
string(39304) + "\n\n",
2016-03-11 02:02:46 +11:00
code + "\n\n",
string(39311))
2016-01-14 20:20:19 +11:00
if not answer:
return False
count = 0
# Wait for approx 30 seconds (since the PIN is not visible anymore :-))
2016-03-04 23:34:30 +11:00
while count < 30:
2016-01-14 20:20:19 +11:00
xml = self.CheckPlexTvSignin(identifier)
if xml is not False:
2016-01-14 20:20:19 +11:00
break
2016-03-04 23:34:30 +11:00
# Wait for 1 seconds
xbmc.sleep(1000)
2016-01-14 20:20:19 +11:00
count += 1
if xml is False:
# Could not sign in to plex.tv Try again later
dialog.ok(self.addonName, string(39305))
2016-01-14 20:20:19 +11:00
return False
# Parse xml
2016-03-04 23:34:30 +11:00
userid = xml.attrib.get('id')
2016-01-14 20:20:19 +11:00
home = xml.get('home', '0')
2016-01-15 00:47:34 +11:00
if home == '1':
home = 'true'
else:
home = 'false'
2016-01-14 20:20:19 +11:00
username = xml.get('username', '')
2016-03-10 01:37:27 +11:00
avatar = xml.get('thumb', '')
2016-01-14 20:20:19 +11:00
token = xml.findtext('authentication-token')
2016-03-10 01:37:27 +11:00
homeSize = xml.get('homeSize', '1')
2016-01-14 20:20:19 +11:00
result = {
2016-01-15 00:47:34 +11:00
'plexhome': home,
2016-01-14 20:20:19 +11:00
'username': username,
'avatar': avatar,
2016-03-04 23:34:30 +11:00
'token': token,
2016-03-10 01:37:27 +11:00
'plexid': userid,
'homesize': homeSize
2016-01-14 20:20:19 +11:00
}
2016-01-30 06:07:21 +11:00
utils.settings('plexLogin', username)
utils.settings('plexToken', token)
utils.settings('plexhome', home)
2016-03-04 23:34:30 +11:00
utils.settings('plexid', userid)
2016-03-10 01:37:27 +11:00
utils.settings('plexAvatar', avatar)
utils.settings('plexHomeSize', homeSize)
2016-03-04 23:34:30 +11:00
# Let Kodi log into plex.tv on startup from now on
utils.settings('myplexlogin', 'true')
2016-01-14 20:20:19 +11:00
return result
def CheckPlexTvSignin(self, identifier):
"""
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
Returns False if not yet done so, or the XML response file as etree
"""
# Try to get a temporary token
xml = self.doUtils('https://plex.tv/pins/%s.xml' % identifier,
authenticate=False,
type="GET")
2016-01-14 20:20:19 +11:00
try:
temp_token = xml.find('auth_token').text
except:
self.logMsg("Could not find token in plex.tv answer.", -1)
2016-01-14 20:20:19 +11:00
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',
authenticate=False,
parameters={'X-Plex-Token': temp_token},
type="GET")
2016-01-14 20:20:19 +11:00
return xml
def GetPlexPin(self):
"""
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
"""
code = None
identifier = None
# Download
xml = self.doUtils('https://plex.tv/pins.xml',
authenticate=False,
type="POST")
2016-01-14 20:20:19 +11:00
try:
xml.attrib
2016-01-14 20:20:19 +11:00
except:
self.logMsg("Error, no PIN from plex.tv provided", -1)
return None, None
code = xml.find('code').text
identifier = xml.find('id').text
self.logMsg('Successfully retrieved code and id from plex.tv', 1)
return code, identifier
2016-01-14 20:20:19 +11:00
def CheckConnection(self, url, token=None, verifySSL=None):
"""
Checks connection to a Plex server, available at url. Can also be used
2016-01-15 00:47:34 +11:00
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)
2016-01-15 00:47:34 +11:00
token appropriate token to access server. If None is passed,
the current token is used
Output:
2016-01-15 00:47:34 +11:00
False if server could not be reached or timeout occured
200 if connection was successfull
2016-01-15 00:47:34 +11:00
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.
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'
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.attrib
except:
pass
else:
# 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):
"""
Returns a list of all keys that are saved for every entry in the
g_PMS variable.
"""
keylist = [
'address',
'baseURL',
'enableGzip',
'ip',
'local',
'name',
'owned',
'port',
'scheme'
]
return keylist
def setgPMSToSettings(self, g_PMS):
"""
PlexDB: takes an g_PMS list of Plex servers and saves them all to
the Kodi settings file. It does NOT save the ATV_udid as that id
seems to change with reboot. Settings are set using the Plex server
machineIdentifier.
Input:
g_PMS
Output:
Assumptions:
There is only one ATV_udid in g_PMS
Existing entries for servers with the same ID get overwritten.
New entries get added. Serverinfo already set in file are set to ''.
NOTE: it is currently not possible to delete entries in Kodi settings
file!
"""
addon = xbmcaddon.Addon()
# Get rid of uppermost level ATV_udid in g_PMS
ATV_udid = list(g_PMS.keys())[0]
g_PMS = g_PMS[ATV_udid]
serverlist = []
keylist = self.getgPMSKeylist()
for serverid, servervalues in g_PMS.items():
serverlist.append(serverid)
# Set values in Kodi settings file
for item in keylist:
# Append the server's ID first, then immediatelly the setting
addon.setSetting(
str(serverid) + str(item), # the key
str(g_PMS[serverid][item]) # the value
)
# Write a new or updated 'serverlist' string to settings
oldserverlist = addon.getSetting('serverlist')
# If no server has been saved yet, return
if oldserverlist == '':
serverlist = ','.join(serverlist)
addon.setSetting('serverlist', serverlist)
return
oldserverlist = oldserverlist.split(',')
for server in oldserverlist:
# Delete serverinfo that has NOT been passed in serverlist
if server not in serverlist:
# Set old value to '', because deleting is not possible
for item in keylist:
addon.setSetting(str(server) + str(item), '')
serverlist = ','.join(serverlist)
addon.setSetting('serverlist', serverlist)
return
def declarePMS(self, uuid, name, scheme, ip, port):
"""
Plex Media Server handling
parameters:
uuid - PMS ID
name, scheme, ip, port, type, owned, token
"""
address = ip + ':' + port
baseURL = scheme + '://' + ip + ':' + port
self.g_PMS[uuid] = {
'name': name,
'scheme': scheme,
'ip': ip,
'port': port,
'address': address,
'baseURL': baseURL,
'local': '1',
'owned': '1',
'accesstoken': '',
'enableGzip': False
}
def updatePMSProperty(self, uuid, tag, value):
# set property element of PMS by UUID
try:
self.g_PMS[uuid][tag] = value
except:
self.logMsg('%s has not yet been declared ' % uuid, -1)
return False
def getPMSProperty(self, uuid, tag):
# get name of PMS by UUID
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):
"""
PlexGDM
parameters:
none
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'
# setup socket for discovery -> multicast message
GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
GDM.settimeout(2.0)
# 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
GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM))
# Look for responses from all recipients
while True:
try:
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})
except socket.timeout:
break
finally:
GDM.close()
pmsList = {}
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'):
# decode response data
update['discovery'] = "auto"
# update['owned']='1'
# update['master']= 1
# update['role']='master'
if "Content-Type:" in each:
update['content-type'] = each.split(':')[1].strip()
elif "Resource-Identifier:" in each:
update['uuid'] = each.split(':')[1].strip()
elif "Name:" in each:
update['serverName'] = each.split(
':')[1].strip().decode('utf-8', 'replace')
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
return pmsList
def discoverPMS(self, IP_self, plexToken=None):
"""
parameters:
IP_self Own IP
optional:
plexToken token for plex.tv
result:
self.g_PMS dict set
"""
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)
# 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 = '%s:%s' % (PMS['ip'], PMS['port'])
https = PMSHttpsEnabled(url)
if https is None:
# Error contacting url. Skip for now
continue
elif https is True:
self.updatePMSProperty(PMS['uuid'], 'scheme', 'https')
self.updatePMSProperty(
PMS['uuid'],
'baseURL',
'https://%s:%s' % (PMS['ip'], PMS['port']))
2016-03-11 02:02:46 +11:00
else:
# Already declared with http
pass
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',
authenticate=False,
parameters={'includeHttps': 1},
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
self.logMsg('Could not get list of PMS from plex.tv', -1)
return
import Queue
queue = Queue.Queue()
threads = []
for Dir in xml.findall('Device'):
if "server" in Dir.get('provides'):
if Dir.find('Connection') is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
PMS = {}
PMS['name'] = Dir.get('name')
infoAge = time.time() - int(Dir.get('lastSeenAt'))
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', token)
PMS['owned'] = Dir.get('owned', '0')
PMS['local'] = Dir.get('publicAddressMatches')
PMS['ownername'] = Dir.get('sourceTitle', '')
PMS['path'] = '/'
PMS['options'] = None
# If PMS seems (!!) local, try a local connection first
# Backup to remote connection, if that failes
PMS['baseURL'] = ''
for Con in Dir.findall('Connection'):
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('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, queue))
t.start()
threads.append(t)
# wait for requests being answered
for t in threads:
t.join()
# declare new PMSs
while not queue.empty():
PMS = queue.get()
self.declarePMS(PMS['uuid'], PMS['name'],
PMS['protocol'], PMS['ip'], PMS['port'])
# dflt: token='', local, owned - updated later
self.updatePMSProperty(
PMS['uuid'], 'accesstoken', PMS['token'])
self.updatePMSProperty(
PMS['uuid'], 'owned', PMS['owned'])
self.updatePMSProperty(
PMS['uuid'], 'local', PMS['local'])
# set in declarePMS, overwrite for https encryption
self.updatePMSProperty(
PMS['uuid'], 'baseURL', PMS['baseURL'])
self.updatePMSProperty(
PMS['uuid'], 'ownername', PMS['ownername'])
queue.task_done()
def pokePMS(self, PMS, queue):
# Ignore SSL certificates for now
xml = self.doUtils(PMS['baseURL'],
authenticate=False,
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 MyPlexSignIn(self, username, password, options):
"""
MyPlex Sign In, Sign Out
parameters:
username - Plex forum name, MyPlex login, or email address
password
options - dict() of PlexConnect-options as received from aTV - necessary: PlexConnectUDID
result:
username
authtoken - token for subsequent communication with MyPlex
"""
# MyPlex web address
MyPlexHost = 'plex.tv'
MyPlexSignInPath = '/users/sign_in.xml'
MyPlexURL = 'https://' + MyPlexHost + MyPlexSignInPath
# create POST request
2016-04-06 02:23:00 +10:00
xargs = clientinfo.ClientInfo().getXArgsDeviceInfo(options)
self.logMsg("Header is: %s" % xargs, 1)
request = urllib2.Request(MyPlexURL, None, xargs)
request.get_method = lambda: 'POST'
# turn into 'POST'
# done automatically with data!=None. But we don't have data.
# no certificate, will fail with "401 - Authentification required"
"""
try:
f = urllib2.urlopen(request)
except urllib2.HTTPError, e:
print e.headers
print "has WWW_Authenticate:", e.headers.has_key('WWW-Authenticate')
print
"""
# provide credentials
# optional... when 'realm' is unknown
##passmanager = urllib2.HTTPPasswordMgrWithDefaultRealm()
# passmanager.add_password(None, address, username, password) # None:
# default "realm"
passmanager = urllib2.HTTPPasswordMgr()
passmanager.add_password(MyPlexHost, MyPlexURL, username, password)
authhandler = urllib2.HTTPBasicAuthHandler(passmanager)
urlopener = urllib2.build_opener(authhandler)
# sign in, get MyPlex response
try:
response = urlopener.open(request).read()
except urllib2.HTTPError as e:
if e.code == 401:
self.logMsg("Authentication failed", 0)
return ('', '')
else:
raise
# analyse response
XMLTree = etree.ElementTree(etree.fromstring(response))
el_username = XMLTree.find('username')
el_authtoken = XMLTree.find('authentication-token')
if el_username is None or \
el_authtoken is None:
username = ''
authtoken = ''
else:
username = el_username.text
authtoken = el_authtoken.text
return (username, authtoken)
def MyPlexSignOut(self, authtoken):
# MyPlex web address
MyPlexHost = 'plex.tv'
MyPlexSignOutPath = '/users/sign_out.xml'
MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath
# create POST request
xargs = {'X-Plex-Token': authtoken}
request = urllib2.Request(MyPlexURL, None, xargs)
# turn into 'POST' - done automatically with data!=None. But we don't
# have data.
request.get_method = lambda: 'POST'
response = urllib2.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')
2015-12-28 20:35:27 +11:00
def GetUserArtworkURL(self, username):
"""
Returns the URL for the user's Avatar. Or False if something went
wrong.
"""
plexToken = utils.settings('plexToken')
users = self.MyPlexListHomeUsers(plexToken)
url = ''
2015-12-28 20:35:27 +11:00
# If an error is encountered, set to False
if not users:
self.logMsg("Could not get userlist from plex.tv.", 1)
self.logMsg("No URL for user avatar.", 1)
return False
for user in users:
if username in user['title']:
url = user['thumb']
self.logMsg("Avatar url for user %s is: %s" % (username, url), 1)
return url
2016-03-10 01:37:27 +11:00
def ChoosePlexHomeUser(self, plexToken):
2015-12-28 02:27:49 +11:00
"""
Let's user choose from a list of Plex home users. Will switch to that
user accordingly.
2016-03-10 01:37:27 +11:00
Returns a dict:
{
'username': Unicode
'userid': '' Plex ID of the user
'token': '' User's token
'protected': True if PIN is needed, else False
}
2015-12-28 20:35:27 +11:00
2016-03-10 01:37:27 +11:00
Will return False if something went wrong (wrong PIN, no connection)
2015-12-28 02:27:49 +11:00
"""
string = self.__language__
dialog = xbmcgui.Dialog()
# Get list of Plex home users
2015-12-28 02:27:49 +11:00
users = self.MyPlexListHomeUsers(plexToken)
if not users:
2016-03-10 01:37:27 +11:00
self.logMsg("User download failed.", -1)
return False
2015-12-28 02:27:49 +11:00
userlist = []
userlistCoded = []
2015-12-28 02:27:49 +11:00
for user in users:
username = user['title']
userlist.append(username)
2016-03-10 01:37:27 +11:00
# To take care of non-ASCII usernames
userlistCoded.append(username.encode('utf-8'))
usernumber = len(userlist)
2016-03-10 01:37:27 +11:00
username = ''
2015-12-28 02:27:49 +11:00
usertoken = ''
2016-01-13 03:23:55 +11:00
trials = 0
while trials < 3:
if usernumber > 1:
# Select user
user_select = dialog.select(
self.addonName + string(39306),
userlistCoded)
2015-12-28 19:17:17 +11:00
if user_select == -1:
2016-03-10 01:37:27 +11:00
self.logMsg("No user selected.", 0)
utils.settings('username', value='')
2016-01-15 00:47:34 +11:00
xbmc.executebuiltin('Addon.OpenSettings(%s)'
2016-04-06 02:23:00 +10:00
% clientinfo.ClientInfo().getAddonId())
2016-03-10 01:37:27 +11:00
return False
# Only 1 user received, choose that one
else:
user_select = 0
2015-12-28 19:17:17 +11:00
selected_user = userlist[user_select]
2016-03-10 01:37:27 +11:00
self.logMsg("Selected user: %s" % selected_user, 0)
2015-12-28 19:17:17 +11:00
user = users[user_select]
2015-12-28 02:27:49 +11:00
# Ask for PIN, if protected:
2016-03-10 01:37:27 +11:00
pin = None
2015-12-28 02:27:49 +11:00
if user['protected'] == '1':
2016-03-04 23:34:30 +11:00
self.logMsg('Asking for users PIN', 1)
2015-12-28 02:27:49 +11:00
pin = dialog.input(
string(39307) + selected_user,
2015-12-28 02:27:49 +11:00
type=xbmcgui.INPUT_NUMERIC,
2016-03-04 23:34:30 +11:00
option=xbmcgui.ALPHANUM_HIDE_INPUT)
# User chose to cancel
2016-03-10 01:37:27 +11:00
# Plex bug: don't call url for protected user with empty PIN
if not pin:
trials += 1
continue
2015-12-28 02:27:49 +11:00
# Switch to this Plex Home user, if applicable
2016-03-11 02:02:46 +11:00
result = self.PlexSwitchHomeUser(
2015-12-28 03:12:46 +11:00
user['id'],
2015-12-28 02:27:49 +11:00
pin,
2016-01-15 00:47:34 +11:00
plexToken,
2016-03-10 01:37:27 +11:00
utils.settings('plex_machineIdentifier'))
2016-03-11 02:02:46 +11:00
if result:
# Successfully retrieved username: break out of while loop
username = result['username']
usertoken = result['usertoken']
break
# Couldn't get user auth
2016-03-11 02:02:46 +11:00
else:
2016-03-10 01:37:27 +11:00
trials += 1
# Could not login user, please try again
if not dialog.yesno(self.addonName,
string(39308) + selected_user,
string(39309)):
2016-03-04 23:34:30 +11:00
# User chose to cancel
break
2016-03-10 01:37:27 +11:00
if not username:
2016-03-10 01:37:27 +11:00
self.logMsg('Failed signing in a user to plex.tv', -1)
2016-04-06 02:23:00 +10:00
xbmc.executebuiltin('Addon.OpenSettings(%s)'
% clientinfo.ClientInfo().getAddonId())
2016-03-10 01:37:27 +11:00
return False
2015-12-28 02:27:49 +11:00
2016-03-10 01:37:27 +11:00
return {
'username': username,
'userid': user['id'],
'protected': True if user['protected'] == '1' else False,
'token': usertoken
}
def PlexSwitchHomeUser(self, userId, pin, token, machineIdentifier):
2015-12-28 02:27:49 +11:00
"""
Retrieves Plex home token for a Plex home user.
2016-03-11 02:02:46 +11:00
Returns False if unsuccessful
2015-12-28 02:27:49 +11:00
Input:
2016-01-15 00:47:34 +11:00
userId id of the Plex home user
2015-12-28 02:27:49 +11:00
pin PIN of the Plex home user, if protected
2016-01-15 00:47:34 +11:00
token token for plex.tv
2015-12-28 02:27:49 +11:00
Output:
2016-03-11 02:02:46 +11:00
{
'username'
'usertoken' Might be empty strings if no token found
for the machineIdentifier that was chosen
}
2015-12-28 02:27:49 +11:00
2016-03-11 02:02:46 +11:00
settings('userid') and settings('username') with new plex token
2015-12-28 02:27:49 +11:00
"""
2016-01-15 00:47:34 +11:00
url = 'https://plex.tv/api/home/users/' + userId + '/switch'
if pin:
2016-01-15 00:47:34 +11:00
url += '?pin=' + pin
self.logMsg('Switching to user %s' % userId, 0)
answer = self.doUtils(url,
authenticate=False,
type="POST",
headerOptions={'X-Plex-Token': token})
2016-03-10 01:37:27 +11:00
try:
answer.attrib
except:
2016-01-15 00:47:34 +11:00
self.logMsg('Error: plex.tv switch HomeUser change failed', -1)
2016-03-11 02:02:46 +11:00
return False
2015-12-28 02:27:49 +11:00
2016-01-15 00:47:34 +11:00
username = answer.attrib.get('title', '')
token = answer.attrib.get('authenticationToken', '')
2016-03-11 02:02:46 +11:00
userid = answer.attrib.get('id', '')
2015-12-28 02:27:49 +11:00
2016-03-11 02:02:46 +11:00
# Write to settings file
utils.settings('username', username)
utils.settings('userid', userid)
utils.settings('accessToken', token)
# Get final token to the PMS we've chosen
2016-03-10 01:37:27 +11:00
url = 'https://plex.tv/api/resources?includeHttps=1'
xml = self.doUtils(url,
authenticate=False,
type="GET",
headerOptions={'X-Plex-Token': token})
2016-03-10 01:37:27 +11:00
try:
xml.attrib
except:
2016-03-11 02:02:46 +11:00
self.logMsg('Answer from plex.tv not as excepted', -1)
# Set to empty iterable list for loop
xml = []
2016-01-15 00:47:34 +11:00
found = 0
2016-03-11 02:02:46 +11:00
self.logMsg('Our machineIdentifier is %s' % machineIdentifier, 0)
2016-03-10 01:37:27 +11:00
for device in xml:
2016-03-11 02:02:46 +11:00
identifier = device.attrib.get('clientIdentifier')
self.logMsg('Found a Plex machineIdentifier: %s' % identifier, 0)
if (identifier in machineIdentifier or
machineIdentifier in identifier):
2016-01-15 00:47:34 +11:00
found += 1
2016-03-10 01:37:27 +11:00
token = device.attrib.get('accessToken')
2016-03-11 02:02:46 +11:00
result = {
'username': username,
}
2016-01-15 00:47:34 +11:00
if found == 0:
2016-03-11 02:02:46 +11:00
self.logMsg('No tokens found for your server!', 0)
self.logMsg('Using empty string as token', 0)
result['usertoken'] = ''
else:
result['usertoken'] = token
2016-03-10 01:37:27 +11:00
2016-03-11 02:02:46 +11:00
self.logMsg('Plex.tv switch HomeUser change successfull for user %s'
% username, 0)
return result
def MyPlexListHomeUsers(self, token):
"""
2016-01-15 00:47:34 +11:00
Returns a list for myPlex home users for the current plex.tv account.
Input:
token for plex.tv
Output:
List of users, where one entry is of the form:
2016-01-15 00:47:34 +11:00
"id": userId,
"admin": '1'/'0',
"guest": '1'/'0',
"restricted": '1'/'0',
"protected": '1'/'0',
"email": email,
"title": title,
"username": username,
"thumb": thumb_url
}
2015-12-28 20:35:27 +11:00
If any value is missing, None is returned instead (or "" from plex.tv)
If an error is encountered, False is returned
"""
xml = self.doUtils('https://plex.tv/api/home/users/',
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
2016-03-10 01:37:27 +11:00
self.logMsg('Download of Plex home users failed.', -1)
return False
users = []
for user in xml:
users.append(user.attrib)
return users
def getDirectVideoPath(self, key, AuthToken):
"""
Direct Video Play support
parameters:
path
AuthToken
Indirect - media indirect specified, grab child XML to gain real path
options
result:
final path to media file
"""
if key.startswith('http://') or key.startswith('https://'): # external address - keep
path = key
else:
if AuthToken == '':
path = key
else:
xargs = dict()
xargs['X-Plex-Token'] = AuthToken
if key.find('?') == -1:
path = key + '?' + urlencode(xargs)
else:
path = key + '&' + urlencode(xargs)
return path
def getTranscodeImagePath(self, key, AuthToken, path, width, height):
"""
Transcode Image support
parameters:
key
AuthToken
path - source path of current XML: path[srcXML]
width
height
result:
final path to image file
"""
if key.startswith('http://') or key.startswith('https://'): # external address - can we get a transcoding request for external images?
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')
# This is bogus (note the extra path component) but ATV is stupid when it comes to caching images, it doesn't use querystrings.
# Fortunately PMS is lenient...
transcodePath = '/photo/:/transcode/' + \
str(width) + 'x' + str(height) + '/' + quote_plus(path)
args = dict()
args['width'] = width
args['height'] = height
args['url'] = path
if not AuthToken == '':
args['X-Plex-Token'] = AuthToken
return transcodePath + '?' + urlencode(args)
def getDirectImagePath(self, path, AuthToken):
"""
Direct Image support
parameters:
path
AuthToken
result:
final path to image file
"""
if not AuthToken == '':
xargs = dict()
xargs['X-Plex-Token'] = AuthToken
if path.find('?') == -1:
path = path + '?' + urlencode(xargs)
else:
path = path + '&' + urlencode(xargs)
return path
def getTranscodeAudioPath(self, path, AuthToken, options, maxAudioBitrate):
"""
Transcode Audio support
parameters:
path
AuthToken
options - dict() of PlexConnect-options as received from aTV
maxAudioBitrate - [kbps]
result:
final path to pull in PMS transcoder
"""
UDID = options['PlexConnectUDID']
transcodePath = '/music/:/transcode/universal/start.mp3?'
args = dict()
args['path'] = path
args['session'] = UDID
args['protocol'] = 'http'
args['maxAudioBitrate'] = maxAudioBitrate
2016-04-06 02:23:00 +10:00
xargs = clientinfo.ClientInfo().getXArgsDeviceInfo(options)
if not AuthToken == '':
xargs['X-Plex-Token'] = AuthToken
return transcodePath + urlencode(args) + '&' + urlencode(xargs)
def getDirectAudioPath(self, path, AuthToken):
"""
Direct Audio support
parameters:
path
AuthToken
result:
final path to audio file
"""
if not AuthToken == '':
xargs = dict()
xargs['X-Plex-Token'] = AuthToken
if path.find('?') == -1:
path = path + '?' + urlencode(xargs)
else:
path = path + '&' + urlencode(xargs)
return path
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:
data e.g. self.g_PMS
Output: List of all servers, with an entry of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'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.items():
serverlist.append({
'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.get('accesstoken'),
'baseURL': value.get('baseURL'),
'ownername': value.get('ownername')
})
return serverlist
2016-01-02 06:36:02 +11:00
2016-01-27 03:20:13 +11:00
@utils.logging
class API():
"""
API(item)
2016-01-02 06:36:02 +11:00
Processes a Plex media server's XML response
2015-12-29 04:47:16 +11:00
item: xml.etree.ElementTree element
"""
2015-12-29 04:47:16 +11:00
def __init__(self, item):
self.item = item
2016-01-04 05:17:59 +11:00
# which media part in the XML response shall we look at?
self.part = 0
2016-03-10 22:47:30 +11:00
self.__language__ = xbmcaddon.Addon().getLocalizedString
2016-03-11 02:02:46 +11:00
self.server = utils.window('pms_server')
2015-12-29 04:47:16 +11:00
2016-02-08 04:05:59 +11:00
def setPartNumber(self, number=None):
2016-01-04 05:17:59 +11:00
"""
Sets the part number to work with (used to deal with Movie with several
parts).
"""
2016-02-08 04:05:59 +11:00
self.part = number or 0
2016-01-04 05:17:59 +11:00
def getPartNumber(self):
"""
Returns the current media part number we're dealing with.
"""
return self.part
2016-01-02 19:28:31 +11:00
def getType(self):
"""
2016-01-30 06:07:21 +11:00
Returns the type of media, e.g. 'movie' or 'clip' for trailers
"""
return self.item.attrib.get('type')
2016-01-02 19:28:31 +11:00
2015-12-29 04:47:16 +11:00
def getChecksum(self):
"""
2016-03-28 01:57:35 +11:00
Returns a string, not int.
WATCH OUT - time in Plex, not Kodi ;-)
2015-12-29 04:47:16 +11:00
"""
# Include a letter to prohibit saving as an int!
2016-01-30 06:07:21 +11:00
checksum = "K%s%s" % (self.getRatingKey(),
self.item.attrib.get('updatedAt', ''))
2016-01-02 06:36:02 +11:00
return checksum
2015-12-29 04:47:16 +11:00
2016-01-30 06:07:21 +11:00
def getRatingKey(self):
2015-12-30 23:25:37 +11:00
"""
2016-01-30 06:07:21 +11:00
Returns the Plex key such as '246922' as a string
2015-12-30 23:25:37 +11:00
"""
return self.item.attrib.get('ratingKey')
2015-12-30 00:13:32 +11:00
2016-01-30 06:07:21 +11:00
def getKey(self):
"""
Returns the Plex key such as '/library/metadata/246922'
"""
return self.item.attrib.get('key')
def getFilePath(self):
"""
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
or None
"""
try:
2016-04-12 02:57:20 +10:00
res = self.item[0][self.part].attrib.get('file')
except:
res = None
if res is not None:
res = unquote(res).decode('utf-8')
return res
def getTVShowPath(self):
"""
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
or None
"""
res = None
for child in self.item:
if child.tag == 'Location':
res = child.attrib.get('path')
return res
2016-01-30 06:07:21 +11:00
2016-01-10 02:14:02 +11:00
def getIndex(self):
"""
Returns the 'index' of an PMS XML reply. Depicts e.g. season number.
"""
return self.item.attrib.get('index')
2016-01-10 02:14:02 +11:00
2015-12-29 04:47:16 +11:00
def getDateCreated(self):
"""
Returns the date when this library item was created or None
"""
res = self.item.attrib.get('addedAt')
if res is not None:
res = utils.DateToKodi(res)
return res
2015-12-29 04:47:16 +11:00
def getUserData(self):
"""
Returns a dict with None if a value is missing
{
'Favorite': favorite, # False, because n/a in Plex
'PlayCount': playcount,
'Played': played, # True/False
'LastPlayedDate': lastPlayedDate,
'Resume': resume, # Resume time in seconds
'Runtime': runtime,
'Rating': rating
}
"""
item = self.item.attrib
2016-04-02 19:31:21 +11:00
# Default - attributes not found with Plex
2015-12-29 04:47:16 +11:00
favorite = False
try:
playcount = int(item['viewCount'])
except:
playcount = None
2015-12-29 04:47:16 +11:00
if playcount:
played = True
2016-04-02 19:31:21 +11:00
else:
played = False
2015-12-29 04:47:16 +11:00
try:
2016-03-12 00:42:14 +11:00
lastPlayedDate = utils.DateToKodi(int(item['lastViewedAt']))
except:
2015-12-29 04:47:16 +11:00
lastPlayedDate = None
2016-04-02 19:31:21 +11:00
try:
userrating = float(item['userRating'])
except:
userrating = 0.0
try:
rating = float(item['audienceRating'])
except:
try:
rating = float(item['rating'])
except:
rating = 0.0
resume, runtime = self.getRuntime()
2015-12-29 04:47:16 +11:00
return {
'Favorite': favorite,
'PlayCount': playcount,
'Played': played,
'LastPlayedDate': lastPlayedDate,
'Resume': resume,
'Runtime': runtime,
'Rating': rating,
'UserRating': userrating
2015-12-29 04:47:16 +11:00
}
2016-03-02 02:18:12 +11:00
def getCollections(self):
"""
2016-03-02 02:28:48 +11:00
Returns a list of PMS collection tags or an empty list
2016-03-02 02:18:12 +11:00
"""
collections = []
for child in self.item:
if child.tag == 'Collection':
collections.append(child.attrib['tag'])
return collections
2015-12-29 04:47:16 +11:00
def getPeople(self):
"""
Returns a dict of lists of people found.
2015-12-29 04:47:16 +11:00
{
'Director': list,
'Writer': list,
'Cast': list,
'Producer': list
2015-12-29 04:47:16 +11:00
}
"""
director = []
writer = []
cast = []
producer = []
for child in self.item:
if child.tag == 'Director':
director.append(child.attrib['tag'])
elif child.tag == 'Writer':
writer.append(child.attrib['tag'])
elif child.tag == 'Role':
cast.append(child.attrib['tag'])
elif child.tag == 'Producer':
producer.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return {
'Director': director,
'Writer': writer,
'Cast': cast,
'Producer': producer
}
def getPeopleList(self):
"""
Returns a list of people from item, with a list item of the form
{
'Name': xxx,
'Type': xxx,
'Id': xxx
'imageurl': url to picture, None otherwise
('Role': xxx for cast/actors only, None if not found)
2015-12-29 04:47:16 +11:00
}
"""
people = []
# Key of library: Plex-identifier. Value represents the Kodi/emby side
people_of_interest = {
'Director': 'Director',
'Writer': 'Writer',
'Role': 'Actor',
'Producer': 'Producer'
}
for child in self.item:
if child.tag in people_of_interest.keys():
name = child.attrib['tag']
name_id = child.attrib['id']
Type = child.tag
2015-12-29 04:47:16 +11:00
Type = people_of_interest[Type]
2016-03-02 02:18:12 +11:00
url = child.attrib.get('thumb')
Role = child.attrib.get('role')
people.append({
'Name': name,
'Type': Type,
'Id': name_id,
'imageurl': url
})
if url:
people[-1].update({'imageurl': url})
if Role:
people[-1].update({'Role': Role})
2015-12-29 04:47:16 +11:00
return people
def getGenres(self):
"""
Returns a list of genres found. (Not a string)
2015-12-29 04:47:16 +11:00
"""
genre = []
for child in self.item:
if child.tag == 'Genre':
genre.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return genre
2016-02-07 22:38:50 +11:00
def getGuid(self):
return self.item.attrib.get('guid')
2016-01-10 02:14:02 +11:00
def getProvider(self, providername=None):
2015-12-29 04:47:16 +11:00
"""
providername: e.g. 'imdb', 'tvdb'
2015-12-29 04:47:16 +11:00
2016-02-03 23:01:13 +11:00
Return IMDB, e.g. "tt0903624". Returns None if not found
2015-12-29 04:47:16 +11:00
"""
item = self.item.attrib
2016-01-11 19:57:45 +11:00
try:
item = item['guid']
except KeyError:
return None
2016-01-10 02:14:02 +11:00
2016-02-03 23:01:13 +11:00
if providername == 'imdb':
regex = re.compile(r'''/(tt\d+)''')
elif providername == 'tvdb':
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
regex = re.compile(r'''tvdb://(\d+)''')
2016-02-03 23:01:13 +11:00
else:
return None
2016-01-11 19:57:45 +11:00
provider = regex.findall(item)
2015-12-29 04:47:16 +11:00
try:
2016-01-10 02:14:02 +11:00
provider = provider[0]
2016-01-11 19:57:45 +11:00
except IndexError:
2015-12-29 04:47:16 +11:00
provider = None
return provider
def getTitle(self):
"""
Returns an item's name/title or "Missing Title Name".
Output:
title, sorttitle
sorttitle = title, if no sorttitle is found
"""
title = self.item.attrib.get('title', 'Missing Title Name')
sorttitle = self.item.attrib.get('titleSort', title)
2015-12-29 04:47:16 +11:00
return title, sorttitle
def getPlot(self):
"""
Returns the plot or None.
"""
return self.item.attrib.get('summary', None)
def getTagline(self):
"""
Returns a shorter tagline or None
"""
return self.item.attrib.get('tagline', None)
def getAudienceRating(self):
"""
2016-04-02 19:31:21 +11:00
Returns the audience rating, 'rating' itself or 0.0
"""
2016-03-18 02:15:48 +11:00
res = self.item.attrib.get('audienceRating')
if res is None:
2016-04-02 19:31:21 +11:00
res = self.item.attrib.get('rating', 0.0)
res = float(res)
2016-03-18 02:15:48 +11:00
return res
def getYear(self):
"""
Returns the production(?) year ("year") or None
"""
return self.item.attrib.get('year', None)
2015-12-29 04:47:16 +11:00
def getRuntime(self):
"""
Resume point of time and runtime/totaltime in rounded to seconds.
2016-01-02 06:36:02 +11:00
Time from Plex server is measured in milliseconds.
Kodi: seconds
Output:
resume, runtime as ints. 0 if not found
2015-12-29 04:47:16 +11:00
"""
item = self.item.attrib
2016-01-04 05:17:59 +11:00
try:
runtime = float(item['duration'])
except KeyError:
runtime = 0.0
2015-12-29 04:47:16 +11:00
try:
2016-01-02 06:36:02 +11:00
resume = float(item['viewOffset'])
2015-12-29 04:47:16 +11:00
except KeyError:
2016-01-02 06:36:02 +11:00
resume = 0.0
runtime = int(runtime * PlexToKodiTimefactor())
resume = int(resume * PlexToKodiTimefactor())
2015-12-29 04:47:16 +11:00
return resume, runtime
def getMpaa(self):
"""
Get the content rating or None
"""
mpaa = self.item.attrib.get('contentRating', None)
2015-12-29 04:47:16 +11:00
# Convert more complex cases
if mpaa in ("NR", "UR"):
# Kodi seems to not like NR, but will accept Rated Not Rated
mpaa = "Rated Not Rated"
return mpaa
def getCountry(self):
"""
Returns a list of all countries found in item.
"""
country = []
for child in self.item:
if child.tag == 'Country':
country.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return country
2016-01-02 19:28:31 +11:00
def getPremiereDate(self):
"""
Returns the "originallyAvailableAt" or None
"""
return self.item.attrib.get('originallyAvailableAt', None)
2016-01-02 19:28:31 +11:00
2016-03-28 18:56:22 +11:00
def getMusicStudio(self):
return self.item.attrib.get('studio', '')
2015-12-29 04:47:16 +11:00
def getStudios(self):
"""
Returns a list with a single entry for the studio, or an empty list
"""
2015-12-29 04:47:16 +11:00
studio = []
try:
studio.append(self.getStudio(self.item.attrib['studio']))
2015-12-29 04:47:16 +11:00
except KeyError:
pass
return studio
def getStudio(self, studioName):
"""
Convert studio for Kodi to properly detect them
"""
2015-12-29 04:47:16 +11:00
studios = {
'abc (us)': "ABC",
'fox (us)': "FOX",
'mtv (us)': "MTV",
'showcase (ca)': "Showcase",
'wgn america': "WGN"
}
return studios.get(studioName.lower(), studioName)
def joinList(self, listobject):
"""
Smart-joins the listobject into a single string using a " / "
separator.
2015-12-29 04:47:16 +11:00
If the list is empty, smart_join returns an empty string.
"""
string = " / ".join(listobject)
return string
2016-02-03 23:01:13 +11:00
def getParentRatingKey(self):
return self.item.attrib.get('parentRatingKey', '')
2016-01-10 02:14:02 +11:00
def getEpisodeDetails(self):
"""
Call on a single episode.
Output: for the corresponding the TV show and season:
[
TV show key, Plex: 'grandparentRatingKey'
TV show title, Plex: 'grandparentTitle'
TV show season, Plex: 'parentIndex'
Episode number, Plex: 'index'
]
"""
item = self.item.attrib
2016-03-12 00:47:41 +11:00
key = item.get('grandparentRatingKey')
title = item.get('grandparentTitle')
season = item.get('parentIndex')
episode = item.get('index')
2016-02-03 23:01:13 +11:00
return key, title, season, episode
2015-12-29 04:47:16 +11:00
2016-02-20 02:10:19 +11:00
def addPlexHeadersToUrl(self, url, arguments={}):
"""
Takes an URL and optional arguments (also to be URL-encoded); returns
an extended URL with e.g. the Plex token included.
2016-01-30 06:07:21 +11:00
arguments overrule everything
"""
2016-04-06 02:23:00 +10:00
xargs = clientinfo.ClientInfo().getXArgsDeviceInfo()
xargs.update(arguments)
2016-01-30 06:07:21 +11:00
if '?' not in url:
url = "%s?%s" % (url, urlencode(xargs))
else:
url = "%s&%s" % (url, urlencode(xargs))
return url
2016-02-20 02:10:19 +11:00
def addPlexCredentialsToUrl(self, url):
"""
Returns an extended URL with the Plex token included as 'X-Plex-Token='
url may or may not already contain a '?'
"""
2016-04-06 02:23:00 +10:00
if utils.window('pms_token') == '':
return url
2016-02-20 02:10:19 +11:00
if '?' not in url:
2016-04-06 02:23:00 +10:00
url = "%s?X-Plex-Token=%s" % (url, utils.window('pms_token'))
2016-02-20 02:10:19 +11:00
else:
2016-04-06 02:23:00 +10:00
url = "%s&X-Plex-Token=%s" % (url, utils.window('pms_token'))
2016-02-20 02:10:19 +11:00
return url
2016-01-30 06:07:21 +11:00
def GetPlayQueueItemID(self):
"""
2016-01-30 06:07:21 +11:00
Returns current playQueueItemID for the item.
If not found, empty str is returned
"""
2016-02-03 23:01:13 +11:00
return self.item.attrib.get('playQueueItemID', '')
def getDataFromPartOrMedia(self, key):
"""
Retrieves XML data 'key' first from the active part. If unsuccessful,
tries to retrieve the data from the Media response part.
If all fails, None is returned.
"""
2016-02-03 23:01:13 +11:00
media = self.item[0].attrib
part = self.item[0][self.part].attrib
try:
try:
value = part[key]
except KeyError:
value = media[key]
except KeyError:
value = None
return value
def getVideoCodec(self):
"""
Returns the video codec and resolution for the child and part selected.
If any data is not found on a part-level, the Media-level data is
returned.
If that also fails (e.g. for old trailers, None is returned)
Output:
{
'videocodec': xxx, e.g. 'h264'
'resolution': xxx, e.g. '720' or '1080'
'height': xxx, e.g. '816'
'width': xxx, e.g. '1920'
'aspectratio': xxx, e.g. '1.78'
'bitrate': xxx, e.g. 10642 (an int!)
'container': xxx e.g. 'mkv'
}
"""
videocodec = self.getDataFromPartOrMedia('videoCodec')
resolution = self.getDataFromPartOrMedia('videoResolution')
height = self.getDataFromPartOrMedia('height')
width = self.getDataFromPartOrMedia('width')
aspectratio = self.getDataFromPartOrMedia('aspectratio')
bitrate = self.getDataFromPartOrMedia('bitrate')
container = self.getDataFromPartOrMedia('container')
videoCodec = {
'videocodec': videocodec,
'resolution': resolution,
'height': height,
'width': width,
'aspectratio': aspectratio,
'bitrate': bitrate,
'container': container
}
return videoCodec
2016-01-12 21:47:48 +11:00
def getExtras(self):
"""
2016-01-12 23:57:26 +11:00
Returns a list of trailer and extras from PMS XML. Returns [] if
2016-01-12 21:47:48 +11:00
no extras are found.
Extratypes:
'1': Trailer
'5': Behind the scenes
Output: list of dicts with one entry of the form:
'key': e.g. /library/metadata/xxxx
'title':
'thumb': artwork
'duration':
'extraType':
'originallyAvailableAt':
'year':
"""
elements = []
for extra in self.item.find('Extras'):
2016-01-12 21:47:48 +11:00
# Trailer:
key = extra.attrib.get('key', None)
title = extra.attrib.get('title', None)
thumb = extra.attrib.get('thumb', None)
duration = float(extra.attrib.get('duration', 0.0))
year = extra.attrib.get('year', None)
extraType = extra.attrib.get('extraType', None)
originallyAvailableAt = extra.attrib.get(
'originallyAvailableAt', None)
elements.append(
{'key': key,
'title': title,
'thumb': thumb,
'duration': int(duration * PlexToKodiTimefactor()),
'extraType': extraType,
'originallyAvailableAt': originallyAvailableAt,
'year': year})
2016-01-12 21:47:48 +11:00
return elements
2015-12-29 04:47:16 +11:00
def getMediaStreams(self):
"""
Returns the media streams for metadata purposes
Output: each track contains a dictionaries
{
2016-01-15 20:04:47 +11:00
'video': videotrack-list, 'codec', 'height', 'width',
'aspect', 'video3DFormat'
'audio': audiotrack-list, 'codec', 'channels',
'language'
'subtitle': list of subtitle languages (or "Unknown")
}
"""
2015-12-29 04:47:16 +11:00
videotracks = []
audiotracks = []
subtitlelanguages = []
# Sometimes, aspectratio is on the "toplevel"
aspectratio = self.item[0].attrib.get('aspectRatio', None)
2015-12-29 04:47:16 +11:00
# TODO: what if several Media tags exist?!?
# Loop over parts
for child in self.item[0]:
container = child.attrib.get('container', None)
# Loop over Streams
for grandchild in child:
mediaStream = grandchild.attrib
mediaType = int(mediaStream.get('streamType', 999))
if mediaType == 1: # Video streams
2015-12-29 04:47:16 +11:00
videotrack = {}
2016-01-15 20:04:47 +11:00
videotrack['codec'] = mediaStream['codec'].lower()
if "msmpeg4" in videotrack['codec']:
videotrack['codec'] = "divx"
elif "mpeg4" in videotrack['codec']:
2015-12-29 04:47:16 +11:00
# if "simple profile" in profile or profile == "":
2016-01-15 20:04:47 +11:00
# videotrack['codec'] = "xvid"
2015-12-29 04:47:16 +11:00
pass
2016-01-15 20:04:47 +11:00
elif "h264" in videotrack['codec']:
2015-12-29 04:47:16 +11:00
if container in ("mp4", "mov", "m4v"):
2016-01-15 20:04:47 +11:00
videotrack['codec'] = "avc1"
videotrack['height'] = mediaStream.get('height', None)
videotrack['width'] = mediaStream.get('width', None)
2015-12-29 04:47:16 +11:00
# TODO: 3d Movies?!?
# videotrack['Video3DFormat'] = item.get('Video3DFormat')
aspectratio = mediaStream.get('aspectRatio', aspectratio)
2016-01-15 20:04:47 +11:00
videotrack['aspect'] = aspectratio
2015-12-29 04:47:16 +11:00
# TODO: Video 3d format
videotrack['video3DFormat'] = None
videotracks.append(videotrack)
elif mediaType == 2: # Audio streams
2015-12-29 04:47:16 +11:00
audiotrack = {}
2016-01-15 20:04:47 +11:00
audiotrack['codec'] = mediaStream['codec'].lower()
2016-03-10 22:47:30 +11:00
profile = mediaStream.get('codecID', '').lower()
2016-01-15 20:04:47 +11:00
if "dca" in audiotrack['codec'] and "dts-hd ma" in profile:
audiotrack['codec'] = "dtshd_ma"
2015-12-29 04:47:16 +11:00
audiotrack['channels'] = mediaStream.get('channels')
2016-03-10 22:47:30 +11:00
# 'unknown' if we cannot get language
audiotrack['language'] = mediaStream.get(
'language', self.__language__(39310))
2015-12-29 04:47:16 +11:00
audiotracks.append(audiotrack)
elif mediaType == 3: # Subtitle streams
2016-03-10 22:47:30 +11:00
# 'unknown' if we cannot get language
subtitlelanguages.append(
mediaStream.get('language', self.__language__(39310)))
return {
2015-12-29 04:47:16 +11:00
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
2015-12-31 01:57:55 +11:00
def getAllArtwork(self, parentInfo=False):
"""
Gets the URLs to the Plex artwork, or empty string if not found.
2015-12-31 01:57:55 +11:00
Output:
{
'Primary': Plex key: "thumb". Only 1 pix
'Art':,
'Banner':,
'Logo':,
'Thumb':,
'Disc':,
'Backdrop': [] Plex key: "art". Only 1 pix
}
"""
item = self.item.attrib
2016-01-30 06:07:21 +11:00
2016-03-14 02:06:54 +11:00
# maxHeight = 10000
# maxWidth = 10000
# customquery = ""
2015-12-31 01:57:55 +11:00
2016-03-14 02:06:54 +11:00
# if utils.settings('compressArt') == "true":
# customquery = "&Quality=90"
2015-12-31 01:57:55 +11:00
2016-03-14 02:06:54 +11:00
# if utils.settings('enableCoverArt') == "false":
# customquery += "&EnableImageEnhancers=false"
2015-12-31 01:57:55 +11:00
allartworks = {
'Primary': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
}
# Process backdrops
# Get background artwork URL
try:
background = item['art']
background = "%s%s" % (self.server, background)
background = self.addPlexCredentialsToUrl(background)
2015-12-31 01:57:55 +11:00
except KeyError:
background = ""
allartworks['Backdrop'].append(background)
# Get primary "thumb" pictures:
try:
primary = item['thumb']
primary = "%s%s" % (self.server, primary)
primary = self.addPlexCredentialsToUrl(primary)
2015-12-31 01:57:55 +11:00
except KeyError:
primary = ""
allartworks['Primary'] = primary
# Process parent items if the main item is missing artwork
if parentInfo:
# Process parent backdrops
if not allartworks['Backdrop']:
2016-03-14 02:06:54 +11:00
background = item.get('parentArt')
if background:
background = "%s%s" % (self.server, background)
background = self.addPlexCredentialsToUrl(background)
allartworks['Backdrop'].append(background)
2015-12-31 01:57:55 +11:00
if not allartworks['Primary']:
2016-03-14 19:56:04 +11:00
primary = item.get('parentThumb')
2016-03-14 02:06:54 +11:00
if primary:
primary = "%s%s" % (self.server, primary)
primary = self.addPlexCredentialsToUrl(primary)
allartworks['Primary'] = primary
2015-12-31 01:57:55 +11:00
return allartworks
2016-01-02 00:40:40 +11:00
2016-02-07 22:38:50 +11:00
def getTranscodeVideoPath(self, action, quality={}):
2016-01-02 00:40:40 +11:00
"""
2016-02-03 23:01:13 +11:00
To be called on a VIDEO level of PMS xml response!
Transcode Video support; returns the URL to get a media started
2016-01-02 00:40:40 +11:00
Input:
2016-04-12 02:57:20 +10:00
action 'DirectStream' or 'Transcode'
2016-01-04 05:17:59 +11:00
quality: {
2016-02-05 06:23:04 +11:00
'videoResolution': e.g. '1024x768',
'videoQuality': e.g. '60',
'maxVideoBitrate': e.g. '2000' (in kbits)
}
(one or several of these options)
2016-01-02 00:40:40 +11:00
Output:
final URL to pull in PMS transcoder
2016-01-04 05:17:59 +11:00
TODO: mediaIndex
2016-01-02 00:40:40 +11:00
"""
2016-02-03 23:01:13 +11:00
2016-04-06 02:23:00 +10:00
xargs = clientinfo.ClientInfo().getXArgsDeviceInfo()
2016-02-05 06:23:04 +11:00
# For DirectPlay, path/key of PART is needed
2016-04-12 02:57:20 +10:00
if action == "DirectStream":
2016-02-05 06:23:04 +11:00
path = self.item[0][self.part].attrib['key']
2016-02-03 23:01:13 +11:00
url = self.server + path
2016-02-07 22:38:50 +11:00
# e.g. Trailers already feature an '?'!
2016-02-03 23:01:13 +11:00
if '?' in url:
url += '&' + urlencode(xargs)
else:
url += '?' + urlencode(xargs)
return url
# For Direct Streaming or Transcoding
from uuid import uuid4
2016-02-05 06:23:04 +11:00
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key']
2016-01-02 00:40:40 +11:00
transcodePath = self.server + \
2016-02-07 22:38:50 +11:00
'/video/:/transcode/universal/start.m3u8?'
2016-01-02 00:40:40 +11:00
args = {
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
'session': str(uuid4()),
'fastSeek': 1,
2016-01-02 00:40:40 +11:00
'path': path,
2016-01-04 05:17:59 +11:00
'mediaIndex': 0, # Probably refering to XML reply sheme
'partIndex': self.part,
# 'copyts': 1,
2016-02-07 22:38:50 +11:00
# 'offset': 0, # Resume point
2016-01-02 00:40:40 +11:00
}
# Seem like PHT to let the PMS use the transcoding profile
xargs['X-Plex-Device'] = 'Plex Home Theater'
2016-02-07 22:38:50 +11:00
# Currently not used!
if action == "DirectStream":
argsUpdate = {
'directPlay': '0',
'directStream': '1',
}
args.update(argsUpdate)
elif action == 'Transcode':
argsUpdate = {
'directPlay': '0',
'directStream': '0'
}
2016-01-02 00:40:40 +11:00
self.logMsg("Setting transcode quality to: %s" % quality, 1)
args.update(quality)
args.update(argsUpdate)
2016-01-02 00:40:40 +11:00
url = transcodePath + urlencode(xargs) + '&' + urlencode(args)
return url
2016-01-02 00:40:40 +11:00
def externalSubs(self, playurl):
externalsubs = []
mapping = {}
item = self.item
try:
mediastreams = item[0][self.part]
2016-01-02 00:40:40 +11:00
except (TypeError, KeyError, IndexError):
return
kodiindex = 0
for stream in mediastreams:
index = stream.attrib['id']
# Since Emby returns all possible tracks together, have to pull
# only external subtitles.
2016-02-07 22:38:50 +11:00
key = stream.attrib.get('key')
2016-01-02 00:40:40 +11:00
# IsTextSubtitleStream if true, is available to download from emby.
2016-02-07 22:38:50 +11:00
if stream.attrib.get('streamType') == "3" and key:
2016-01-02 00:40:40 +11:00
# Direct stream
2016-02-07 22:38:50 +11:00
url = ("%s%s" % (self.server, key))
url = self.addPlexCredentialsToUrl(url)
2016-01-02 00:40:40 +11:00
# map external subtitles for mapping
mapping[kodiindex] = index
externalsubs.append(url)
kodiindex += 1
mapping = json.dumps(mapping)
utils.window('emby_%s.indexMapping' % playurl, value=mapping)
self.logMsg('Found external subs: %s' % externalsubs)
2016-01-02 00:40:40 +11:00
return externalsubs
2016-03-15 03:47:05 +11:00
def CreateListItemFromPlexItem(self, listItem=None):
"""
Call on a child level of PMS xml response (e.g. in a for loop)
listItem: existing xbmcgui.ListItem to work with
otherwise, a new one is created
Returns XBMC listitem for this PMS library item
"""
people = self.getPeople()
userdata = self.getUserData()
title, sorttitle = self.getTitle()
if listItem is None:
listItem = xbmcgui.ListItem()
metadata = {
'genre': self.joinList(self.getGenres()),
'year': self.getYear(),
'rating': self.getAudienceRating(),
'playcount': userdata['PlayCount'],
'cast': people['Cast'],
'director': self.joinList(people.get('Director')),
'plot': self.getPlot(),
'title': title,
'sorttitle': sorttitle,
'duration': userdata['Runtime'],
'studio': self.joinList(self.getStudios()),
'tagline': self.getTagline(),
'writer': self.joinList(people.get('Writer')),
'premiered': self.getPremiereDate(),
'dateadded': self.getDateCreated(),
'lastplayed': userdata['LastPlayedDate'],
'mpaa': self.getMpaa(),
'aired': self.getPremiereDate()
}
if "episode" in self.getType():
# Only for tv shows
key, show, season, episode = self.getEpisodeDetails()
metadata['episode'] = episode
metadata['season'] = season
metadata['tvshowtitle'] = show
listItem.setProperty('IsPlayable', 'true')
listItem.setProperty('IsFolder', 'false')
listItem.setProperty('embyid', self.getRatingKey())
listItem.setLabel(title)
listItem.setInfo('video', infoLabels=metadata)
return listItem
def AddStreamInfo(self, listItem):
"""
Add media stream information to xbmcgui.ListItem
"""
mediastreams = self.getMediaStreams()
videostreamFound = False
if mediastreams:
for key, value in mediastreams.iteritems():
if key == "video" and value:
videostreamFound = True
if value:
listItem.addStreamInfo(key, value)
if not videostreamFound:
# just set empty streamdetails to prevent errors in the logs
listItem.addStreamInfo(
"video", {'duration': self.getRuntime()[1]})
2016-03-17 04:33:18 +11:00
def validatePlayurl(self, path, typus, forceCheck=False, folder=False):
2016-03-17 04:33:18 +11:00
"""
Returns a valid path for Kodi, e.g. with '\' substituted to '\\' in
Unicode. Returns None if this is not possible
path : Unicode
typus : Plex type from PMS xml
forceCheck : Will always try to check validity of path
Will also skip confirmation dialog if path not found
folder : Set to True if path is a folder
2016-03-17 04:33:18 +11:00
"""
if path is None:
return None
types = {
'movie': 'movie',
'show': 'tv',
'season': 'tv',
'episode': 'tv',
'artist': 'music',
'album': 'music',
'song': 'music',
'track': 'music',
}
typus = types[typus]
2016-03-17 04:33:18 +11:00
if utils.window('remapSMB') == 'true':
path = path.replace(utils.window('remapSMB%sOrg' % typus),
utils.window('remapSMB%sNew' % typus))
2016-03-17 04:33:18 +11:00
# There might be backslashes left over:
path = path.replace('\\', '/')
2016-03-17 04:33:18 +11:00
elif utils.window('replaceSMB') == 'true':
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
if utils.window('emby_pathverified') == 'true' and forceCheck is False:
return path
# exist() needs a / or \ at the end to work for directories
if folder is False:
# files
check = True if xbmcvfs.exists(path.encode('utf-8')) == 1 \
else False
else:
# directories
if "\\" in path:
# Add the missing backslash
check = True if \
xbmcvfs.exists((path + "\\").encode('utf-8')) == 1 \
else False
else:
check = True if \
xbmcvfs.exists((path + "/").encode('utf-8')) == 1 \
else False
if check is False:
if forceCheck is False:
# Validate the path is correct with user intervention
if self.askToValidate(path):
utils.window('emby_shouldStop', value="true")
path = None
utils.window('emby_pathverified', value='true')
utils.settings('emby_pathverified', value='true')
else:
path = None
elif forceCheck is False:
if utils.window('emby_pathverified') != 'true':
utils.window('emby_pathverified', value='true')
utils.settings('emby_pathverified', value='true')
return path
2016-03-17 04:33:18 +11:00
def askToValidate(self, url):
"""
Displays a YESNO dialog box:
Kodi can't locate file: <url>. Please verify the path.
You may need to verify your network credentials in the
add-on settings or use different Plex paths. Stop syncing?
Returns True if sync should stop, else False
"""
self.logMsg('Cannot access file: %s' % url, -1)
import xbmcaddon
string = xbmcaddon.Addon().getLocalizedString
resp = xbmcgui.Dialog().yesno(
heading=self.addonName,
line1=string(39031) + url,
line2=string(39032))
return resp