PlexKodiConnect/resources/lib/PlexAPI.py

2023 lines
72 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...)
"""
# Specific to PlexDB:
import clientinfo
import utils
import downloadutils
import xbmcaddon
import xbmcgui
2015-12-28 02:27:49 +11:00
import xbmc
import struct
import time
import urllib2
import httplib
import socket
import StringIO
import gzip
from threading import Thread
import Queue
import traceback
2015-12-29 04:47:16 +11:00
import re
2016-01-02 00:40:40 +11:00
import json
2015-12-29 04:47:16 +11:00
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
from urllib import urlencode, quote_plus
# from Version import __VERSION__
# from Debug import * # dprint(), prettyXML()
"""
storage for PMS addresses and additional information - now per aTV! (replaces global PMS_list)
syntax: PMS[<ATV_UDID>][PMS_UUID][<data>]
data: name, ip, ...type (local, myplex)
"""
class PlexAPI():
# CONSTANTS
# Timeout for POST/GET commands, I guess in seconds
timeout = 60
# VARIABLES
def __init__(self):
2015-12-28 02:27:49 +11:00
self.__language__ = xbmcaddon.Addon().getLocalizedString
self.g_PMS = {}
client = clientinfo.ClientInfo()
self.addonName = client.getAddonName()
self.addonId = client.getAddonId()
self.clientId = client.getDeviceId()
self.deviceName = client.getDeviceName()
self.plexversion = client.getVersion()
self.platform = client.getPlatform()
2016-01-02 00:40:40 +11:00
self.userId = utils.window('emby_currUser')
self.token = utils.window('emby_accessToken%s' % self.userId)
self.server = utils.window('emby_server%s' % self.userId)
self.plexLogin = utils.settings('plexLogin')
self.plexToken = utils.settings('plexToken')
self.machineIdentifier = utils.window('plex_machineIdentifier')
2015-12-29 04:47:16 +11:00
self.doUtils = downloadutils.DownloadUtils()
def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
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.
"""
retrievedPlexLogin = ''
plexLogin = 'dummy'
authtoken = ''
while retrievedPlexLogin == '' and plexLogin != '':
dialog = xbmcgui.Dialog()
plexLogin = dialog.input(
self.addonName +
': Enter plex.tv username. Or nothing to cancel.',
type=xbmcgui.INPUT_ALPHANUM,
)
if plexLogin != "":
dialog = xbmcgui.Dialog()
plexPassword = dialog.input(
'Enter password for plex.tv user %s' % plexLogin,
type=xbmcgui.INPUT_ALPHANUM,
option=xbmcgui.ALPHANUM_HIDE_INPUT
)
retrievedPlexLogin, authtoken = self.MyPlexSignIn(
plexLogin,
plexPassword,
{'X-Plex-Client-Identifier': self.clientId}
)
self.logMsg("plex.tv username and token: %s, %s" % (plexLogin, authtoken), 1)
if plexLogin == '':
dialog = xbmcgui.Dialog()
dialog.ok(self.addonName, 'Could not sign in user %s' % plexLogin)
# Write to Kodi settings file
addon = xbmcaddon.Addon()
addon.setSetting('plexLogin', retrievedPlexLogin)
addon.setSetting('plexToken', authtoken)
return (retrievedPlexLogin, authtoken)
def CheckConnection(self, url, token):
"""
Checks connection to a Plex server, available at url. Can also be used
to check for connection with plex.tv!
Input:
url URL to Plex server (e.g. https://192.168.1.1:32400)
token appropriate token to access server
Output:
200 if the connection was successfull
'' empty string if connection failed for whatever reason
401 integer if token has been revoked
"""
# Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work.
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
else:
url = url + '/clients'
2015-12-28 00:42:54 +11:00
self.logMsg("CheckConnection called for url %s with a token" % url, 2)
2015-12-29 04:47:16 +11:00
r = self.doUtils.downloadUrl(
url,
authenticate=False,
headerOptions={'X-Plex-Token': token}
)
2015-12-28 00:42:54 +11:00
self.logMsg("Response was: %s" % r, 2)
2016-01-02 00:40:40 +11:00
# List of exception returns, when connection failed
exceptionlist = [
'',
401
]
# To get rid of the stuff that was downloaded :-)
if r not in exceptionlist:
r = 200
return r
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, ATV_udid, 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,
'address': address,
'baseURL': baseURL,
'local': '1',
'owned': '1',
'accesstoken': '',
'enableGzip': False
}
def updatePMSProperty(self, ATV_udid, 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
self.g_PMS[ATV_udid][uuid][tag] = value
def getPMSProperty(self, ATV_udid, 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])
def PlexGDM(self):
"""
PlexGDM
parameters:
none
result:
PMS_list - dict() of PMSs found
"""
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)
# Set the time-to-live for messages to 1 for local network
ttl = struct.pack('b', 1)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
# dprint(__name__, 1, "Sending discovery message: {0}", Msg_PlexGDM)
GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM))
# 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()
discovery_complete = True
PMS_list = {}
if returnData:
for response in returnData:
update = { 'ip' : response.get('from')[0] }
# Check if we had a positive HTTP response
if "200 OK" in response.get('data'):
for each in response.get('data').split('\n'):
# 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') # store in utf-8
elif "Port:" in each:
update['port'] = each.split(':')[1].strip()
elif "Updated-At:" in each:
update['updated'] = each.split(':')[1].strip()
elif "Version:" in each:
update['version'] = each.split(':')[1].strip()
PMS_list[update['uuid']] = update
# if PMS_list=={}:
# 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={}):
"""
discoverPMS
parameters:
ATV_udid
CSettings - for manual PMS configuration. this one looks strange.
IP_self
optional:
tokenDict - dictionary of tokens for MyPlex, PlexHome
result:
self.g_PMS dictionary for ATV_udid
"""
# Plex: changed CSettings to new function getServerFromSettings()
self.g_PMS[ATV_udid] = {}
# 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 in PMS_list:
PMS = PMS_list[uuid]
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)
# all servers - update enableGzip
for uuid in self.g_PMS.get(ATV_udid, {}):
# enable Gzip if not on same host, local&remote PMS depending
# on setting
enableGzip = (not self.getPMSProperty(ATV_udid, uuid, 'ip') == IP_self) \
and (
(self.getPMSProperty(ATV_udid, uuid, 'local') == '1'
and False)
or
(self.getPMSProperty(ATV_udid, uuid, 'local') == '0'
and True) == 'True'
)
self.updatePMSProperty(ATV_udid, uuid, 'enableGzip', enableGzip)
def getPMSListFromMyPlex(self, ATV_udid, authtoken):
"""
getPMSListFromMyPlex
get Plex media Server List from plex.tv/pms/resources
"""
# dprint(__name__, 0, "***")
# dprint(__name__, 0, "poke plex.tv - request Plex Media Server list")
# dprint(__name__, 0, "***")
XML = self.getXMLFromPMS('https://plex.tv', '/api/resources?includeHttps=1', {}, authtoken)
if XML==False:
pass # no data from MyPlex
else:
queue = Queue.Queue()
threads = []
for Dir in XML.getiterator('Device'):
if Dir.get('product','') == "Plex Media Server" and Dir.get('provides','') == "server":
uuid = Dir.get('clientIdentifier')
name = Dir.get('name')
token = Dir.get('accessToken', authtoken)
owned = Dir.get('owned', '0')
local = Dir.get('publicAddressMatches')
if Dir.find('Connection') == None:
continue # no valid connection - skip
uri = "" # flag to set first connection, possibly overwrite later with more suitable
for Con in Dir.getiterator('Connection'):
if uri=="" or Con.get('local','') == local:
protocol = Con.get('protocol')
ip = Con.get('address')
port = Con.get('port')
uri = Con.get('uri')
# todo: handle unforeseen - like we get multiple suitable connections. how to choose one?
# check MyPlex data age - skip if >2 days
infoAge = time.time() - int(Dir.get('lastSeenAt'))
oneDayInSec = 60*60*24
if infoAge > 2*oneDayInSec: # two days in seconds -> expiration in setting?
dprint(__name__, 1, "Server {0} not updated for {1} days - skipping.", name, infoAge/oneDayInSec)
continue
# poke PMS, own thread for each poke
PMSInfo = { 'uuid': uuid, 'name': name, 'token': token, 'owned': owned, 'local': local, \
'protocol': protocol, 'ip': ip, 'port': port, 'uri': uri }
PMS = { 'baseURL': uri, 'path': '/', 'options': None, 'token': token, \
'data': PMSInfo }
t = Thread(target=self.getXMLFromPMSToQueue, 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():
(PMSInfo, PMS) = queue.get()
if PMS==False:
continue
uuid = PMSInfo['uuid']
name = PMSInfo['name']
token = PMSInfo['token']
owned = PMSInfo['owned']
local = PMSInfo['local']
protocol = PMSInfo['protocol']
ip = PMSInfo['ip']
port = PMSInfo['port']
uri = PMSInfo['uri']
self.declarePMS(ATV_udid, uuid, name, protocol, ip, port) # dflt: token='', local, owned - updated later
self.updatePMSProperty(ATV_udid, uuid, 'accesstoken', token)
self.updatePMSProperty(ATV_udid, uuid, 'owned', owned)
self.updatePMSProperty(ATV_udid, uuid, 'local', local)
self.updatePMSProperty(ATV_udid, uuid, 'baseURL', uri) # set in declarePMS, overwrite for https encryption
def getXMLFromPMS(self, baseURL, path, options={}, authtoken='', enableGzip=False):
"""
Plex Media Server communication
parameters:
host
path
options - dict() of PlexConnect-options as received from aTV
None for no
std. X-Plex-Args
authtoken - authentication answer from MyPlex Sign In
result:
returned XML or 'False' in case of error
"""
xargs = {}
if options is not None:
xargs = self.getXArgsDeviceInfo(options)
if not authtoken == '':
xargs['X-Plex-Token'] = authtoken
self.logMsg("URL for XML download: %s%s" % (baseURL, path), 1)
self.logMsg("xargs: %s" % xargs, 1)
request = urllib2.Request(baseURL+path, None, xargs)
request.add_header('User-agent', 'PlexDB')
if enableGzip:
request.add_header('Accept-encoding', 'gzip')
try:
response = urllib2.urlopen(request, timeout=20)
except (urllib2.URLError, httplib.HTTPException) as e:
self.logMsg("No Response from Plex Media Server", 0)
if hasattr(e, 'reason'):
self.logMsg("We failed to reach a server. Reason: %s" % e.reason, 0)
elif hasattr(e, 'code'):
self.logMsg("The server couldn't fulfill the request. Error code: %s" % e.code, 0)
self.logMsg("Traceback:\n%s" % traceback.format_exc(), 0)
return False
except IOError:
self.logMsg("Error loading response XML from Plex Media Server:\n%s" % traceback.format_exc(), 0)
return False
if response.info().get('Content-Encoding') == 'gzip':
buf = StringIO.StringIO(response.read())
file = gzip.GzipFile(fileobj=buf)
XML = etree.parse(file)
else:
# parse into etree
XML = etree.parse(response)
# Log received XML if debugging enabled.
self.logMsg("====== received PMS-XML ======", 1)
self.logMsg(XML.getroot(), 1)
self.logMsg("====== PMS-XML finished ======", 1)
return XML
def getXMLFromPMSToQueue(self, PMS, queue):
XML = self.getXMLFromPMS(PMS['baseURL'],PMS['path'],PMS['options'],PMS['token'])
queue.put( (PMS['data'], XML) )
def getXArgsDeviceInfo(self, options={}, JSON=False):
"""
Returns a dictionary that can be used as headers for GET and POST
requests. An authentication option is NOT yet added.
Inputs:
2016-01-02 00:40:40 +11:00
JSON=True will enforce a JSON answer
options: dictionary of options that will override the
standard header options otherwise set.
Output:
header dictionary
"""
# Get addon infos
2016-01-02 00:40:40 +11:00
xargs = {
'User-agent': self.addonName,
'X-Plex-Device': self.deviceName,
'X-Plex-Platform': self.platform,
'X-Plex-Client-Platform': self.platform,
'X-Plex-Product': self.addonName,
'X-Plex-Version': self.plexversion,
'X-Plex-Client-Identifier': self.clientId,
'machineIdentifier': self.machineIdentifier,
'Accept': 'application/xml'
}
try:
xargs['X-Plex-Token'] = self.token
except NameError:
# no token needed/saved yet
pass
if JSON:
xargs['Accept'] = 'application/json'
if options:
xargs.update(options)
return xargs
def getXMLFromMultiplePMS(self, ATV_udid, path, type, options={}):
"""
provide combined XML representation of local servers' XMLs, eg. /library/section
parameters:
ATV_udid
path
type - owned <> shared (previously: local, myplex)
options
result:
XML
"""
queue = Queue.Queue()
threads = []
root = etree.Element("MediaConverter")
root.set('friendlyName', type+' Servers')
for uuid in g_PMS.get(ATV_udid, {}):
if (type=='all' and getPMSProperty(ATV_udid, uuid, 'name')!='plex.tv') or \
(type=='owned' and getPMSProperty(ATV_udid, uuid, 'owned')=='1') or \
(type=='shared' and getPMSProperty(ATV_udid, uuid, 'owned')=='0') or \
(type=='local' and getPMSProperty(ATV_udid, uuid, 'local')=='1') or \
(type=='remote' and getPMSProperty(ATV_udid, uuid, 'local')=='0'):
Server = etree.SubElement(root, 'Server') # create "Server" node
Server.set('name', getPMSProperty(ATV_udid, uuid, 'name'))
Server.set('address', getPMSProperty(ATV_udid, uuid, 'ip'))
Server.set('port', getPMSProperty(ATV_udid, uuid, 'port'))
Server.set('baseURL', getPMSProperty(ATV_udid, uuid, 'baseURL'))
Server.set('local', getPMSProperty(ATV_udid, uuid, 'local'))
Server.set('owned', getPMSProperty(ATV_udid, uuid, 'owned'))
baseURL = getPMSProperty(ATV_udid, uuid, 'baseURL')
token = getPMSProperty(ATV_udid, uuid, 'accesstoken')
PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')'
Server.set('searchKey', PMS_mark + getURL('', '', '/Search/Entry.xml'))
# request XMLs, one thread for each
PMS = { 'baseURL':baseURL, 'path':path, 'options':options, 'token':token, \
'data': {'uuid': uuid, 'Server': Server} }
t = Thread(target=getXMLFromPMSToQueue, args=(PMS, queue))
t.start()
threads.append(t)
# wait for requests being answered
for t in threads:
t.join()
# add new data to root XML, individual Server
while not queue.empty():
(data, XML) = queue.get()
uuid = data['uuid']
Server = data['Server']
baseURL = getPMSProperty(ATV_udid, uuid, 'baseURL')
token = getPMSProperty(ATV_udid, uuid, 'accesstoken')
PMS_mark = 'PMS(' + getPMSProperty(ATV_udid, uuid, 'address') + ')'
if XML==False:
Server.set('size', '0')
else:
Server.set('size', XML.getroot().get('size', '0'))
for Dir in XML.getiterator('Directory'): # copy "Directory" content, add PMS to links
key = Dir.get('key') # absolute path
Dir.set('key', PMS_mark + getURL('', path, key))
Dir.set('refreshKey', getURL(baseURL, path, key) + '/refresh')
if 'thumb' in Dir.attrib:
Dir.set('thumb', PMS_mark + getURL('', path, Dir.get('thumb')))
if 'art' in Dir.attrib:
Dir.set('art', PMS_mark + getURL('', path, Dir.get('art')))
Server.append(Dir)
for Playlist in XML.getiterator('Playlist'): # copy "Playlist" content, add PMS to links
key = Playlist.get('key') # absolute path
Playlist.set('key', PMS_mark + getURL('', path, key))
if 'composite' in Playlist.attrib:
Playlist.set('composite', PMS_mark + getURL('', path, Playlist.get('composite')))
Server.append(Playlist)
for Video in XML.getiterator('Video'): # copy "Video" content, add PMS to links
key = Video.get('key') # absolute path
Video.set('key', PMS_mark + getURL('', path, key))
if 'thumb' in Video.attrib:
Video.set('thumb', PMS_mark + getURL('', path, Video.get('thumb')))
if 'parentKey' in Video.attrib:
Video.set('parentKey', PMS_mark + getURL('', path, Video.get('parentKey')))
if 'parentThumb' in Video.attrib:
Video.set('parentThumb', PMS_mark + getURL('', path, Video.get('parentThumb')))
if 'grandparentKey' in Video.attrib:
Video.set('grandparentKey', PMS_mark + getURL('', path, Video.get('grandparentKey')))
if 'grandparentThumb' in Video.attrib:
Video.set('grandparentThumb', PMS_mark + getURL('', path, Video.get('grandparentThumb')))
Server.append(Video)
root.set('size', str(len(root.findall('Server'))))
XML = etree.ElementTree(root)
dprint(__name__, 1, "====== Local Server/Sections XML ======")
dprint(__name__, 1, XML.getroot())
dprint(__name__, 1, "====== Local Server/Sections XML finished ======")
return XML # XML representation - created "just in time". Do we need to cache it?
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
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
xargs = self.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)
request.get_method = lambda: 'POST' # turn into 'POST' - done automatically with data!=None. But we don't have data.
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)
# 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
2015-12-28 02:27:49 +11:00
def ChoosePlexHomeUser(self):
"""
Let's user choose from a list of Plex home users. Will switch to that
user accordingly.
Output:
username
2015-12-28 03:12:46 +11:00
userid
2015-12-28 02:27:49 +11:00
authtoken
2015-12-28 20:35:27 +11:00
Will return empty strings if failed.
2015-12-28 02:27:49 +11:00
"""
string = self.__language__
2016-01-02 00:40:40 +11:00
plexLogin = self.plexLogin
plexToken = self.plexToken
2015-12-28 02:27:49 +11:00
self.logMsg("Getting user list.", 1)
# Get list of Plex home users
2015-12-28 02:27:49 +11:00
users = self.MyPlexListHomeUsers(plexToken)
# Download users failed. Set username to Plex login
if not users:
utils.settings('username', value=plexLogin)
self.logMsg("User download failed. Set username = plexlogin", 1)
2015-12-28 03:12:46 +11:00
return ('', '', '')
2015-12-28 02:27:49 +11:00
userlist = []
for user in users:
username = user['title']
userlist.append(username)
usernumber = len(userlist)
2015-12-28 02:27:49 +11:00
usertoken = ''
# Plex home not in use: only 1 user returned
trials = 1
while trials < 4:
if usernumber > 1:
dialog = xbmcgui.Dialog()
user_select = dialog.select(string(30200), userlist)
2015-12-28 19:17:17 +11:00
if user_select == -1:
self.logMsg("No user selected.", 1)
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
return ('', '', '')
# No Plex home in use - only 1 user
else:
user_select = 0
2015-12-28 19:17:17 +11:00
selected_user = userlist[user_select]
self.logMsg("Selected user: %s" % selected_user, 1)
utils.settings('username', value=selected_user)
user = users[user_select]
2015-12-28 02:27:49 +11:00
# Ask for PIN, if protected:
if user['protected'] == '1':
dialog = xbmcgui.Dialog()
pin = dialog.input(
'Enter PIN for user %s' % selected_user,
type=xbmcgui.INPUT_NUMERIC,
option=xbmcgui.ALPHANUM_HIDE_INPUT
)
2015-12-28 03:12:46 +11:00
else:
pin = None
2015-12-28 02:27:49 +11:00
# Switch to this Plex Home user, if applicable
username, usertoken = self.MyPlexSwitchHomeUser(
2015-12-28 03:12:46 +11:00
user['id'],
2015-12-28 02:27:49 +11:00
pin,
plexToken
)
# Couldn't get user auth
2015-12-28 02:27:49 +11:00
if not username:
dialog = xbmcgui.Dialog()
dialog.ok(
self.addonName,
'Could not log in user %s' % selected_user,
'Please try again.'
)
# Successfully retrieved: break out of while loop
else:
break
trials += trials
if not username:
xbmc.executebuiltin('Addon.OpenSettings(%s)' % self.addonId)
2015-12-28 20:35:27 +11:00
return ('', '', '', '')
2015-12-28 03:12:46 +11:00
return (username, user['id'], usertoken)
2015-12-28 02:27:49 +11:00
def MyPlexSwitchHomeUser(self, id, pin, authtoken, options={}):
"""
Retrieves Plex home token for a Plex home user.
Input:
id id of the Plex home user
pin PIN of the Plex home user, if protected
authtoken token for plex.tv
options={} optional additional header options
Output:
username Plex home username
authtoken token for Plex home user
Returns empty strings if unsuccessful
"""
MyPlexHost = 'https://plex.tv'
MyPlexURL = MyPlexHost + '/api/home/users/' + id + '/switch'
2015-12-28 02:27:49 +11:00
if pin:
MyPlexURL += '?pin=' + pin
2015-12-28 02:27:49 +11:00
xargs = {}
2015-12-28 02:27:49 +11:00
xargs = self.getXArgsDeviceInfo(options)
xargs['X-Plex-Token'] = authtoken
2015-12-28 02:27:49 +11:00
request = urllib2.Request(MyPlexURL, None, xargs)
2015-12-28 02:27:49 +11:00
request.get_method = lambda: 'POST'
response = urllib2.urlopen(request).read()
2015-12-28 02:27:49 +11:00
self.logMsg("====== MyPlexHomeUser XML ======", 1)
self.logMsg(response, 1)
self.logMsg("====== MyPlexHomeUser XML finished ======", 1)
# analyse response
XMLTree = etree.ElementTree(etree.fromstring(response))
2015-12-28 02:27:49 +11:00
el_user = XMLTree.getroot() # root=<user>. double check?
username = el_user.attrib.get('title', '')
authtoken = el_user.attrib.get('authenticationToken', '')
2015-12-28 02:27:49 +11:00
if username and authtoken:
2015-12-28 02:27:49 +11:00
self.logMsg('MyPlex switch HomeUser change successfull', 0)
else:
2015-12-28 02:27:49 +11:00
self.logMsg('MyPlex switch HomeUser change failed', 0)
return (username, authtoken)
def MyPlexListHomeUsers(self, authtoken):
"""
Returns all myPlex home users for the currently signed in account.
Input:
authtoken for plex.tv
options, optional
Output:
List of users, where one entry is of the form:
{
2015-12-28 03:12:46 +11:00
"id": userId, "admin": '1'/'0', "guest": '1'/'0',
2015-12-28 00:42:54 +11:00
"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.getXMLFromPMS('https://plex.tv', '/api/home/users/', {}, authtoken)
if not XML:
# Download failed; quitting with False
return False
# analyse response
root = XML.getroot()
users = []
for user in root:
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
xargs = 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, ATV_udid, 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:
{
'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
}
"""
serverlist = []
for key, value in data[ATV_udid].items():
serverlist.append({
'name': value['name'],
'address': value['address'],
'ip': value['ip'],
'port': value['port'],
'scheme': value['scheme'],
'local': value['local'],
'owned': value['owned'],
'machineIdentifier': key,
'accesstoken': value['accesstoken'],
'baseURL': value['baseURL']
})
return serverlist
2015-12-29 04:47:16 +11:00
def GetPlexCollections(self, mediatype):
"""
Input:
mediatype String or list of strings with possible values
'movie', 'show', 'artist', 'photo'
Output:
Collection containing only mediatype. List with entry of the form:
{
'name': xxx Plex title for the media section
'type': xxx Plex type: 'movie', 'show', 'artist', 'photo'
'id': xxx Plex unique key for the section
}
"""
2015-12-28 23:10:05 +11:00
collections = []
url = "{server}/library/sections"
jsondata = self.doUtils.downloadUrl(url)
try:
result = jsondata['_children']
2015-12-29 04:47:16 +11:00
except KeyError:
2015-12-28 23:10:05 +11:00
pass
else:
2015-12-28 23:10:05 +11:00
for item in result:
contentType = item['type']
2015-12-29 04:47:16 +11:00
if contentType in mediatype:
name = item['title']
contentId = item['key']
2015-12-28 23:10:05 +11:00
collections.append({
2015-12-29 04:47:16 +11:00
'name': name,
'type': contentType,
'id': str(contentId)
2015-12-28 23:10:05 +11:00
})
return collections
2015-12-29 04:47:16 +11:00
def GetPlexSectionResults(self, viewId):
"""
Returns a list (raw API dump) of all Plex movies in the Plex section
with key = viewId.
"""
result = []
url = "{server}/library/sections/%s/all" % viewId
jsondata = self.doUtils.downloadUrl(url)
try:
result = jsondata['_children']
except KeyError:
2015-12-30 23:25:37 +11:00
self.logMsg("Error retrieving all movies for section %s" % viewId, 1)
2015-12-29 04:47:16 +11:00
pass
return result
def GetPlexMetadata(self, key):
"""
Returns raw API metadata for key.
2015-12-30 23:25:37 +11:00
Can be called with either Plex key '/library/metadata/xxxx'metadata
OR with the digits 'xxxx' only.
2015-12-29 04:47:16 +11:00
"""
xml = ''
2015-12-30 23:25:37 +11:00
key = str(key)
if '/library/metadata/' in key:
url = "{server}" + key
else:
url = "{server}/library/metadata/" + key
arguments = {
'checkFiles': 1, # No idea
'includeExtras': 1, # Trailers and Extras => Extras
'includeRelated': 1, # Similar movies => Video -> Related
'includeRelatedCount': 5,
'includeOnDeck': 1,
'includeChapters': 1,
'includePopularLeaves': 1,
'includeConcerts': 1
}
2016-01-02 00:40:40 +11:00
url = url + '?' + urlencode(arguments)
headerOptions = {'Accept': 'application/xml'}
xml = self.doUtils.downloadUrl(url, headerOptions=headerOptions)
if not xml:
2015-12-29 04:47:16 +11:00
self.logMsg("Error retrieving metadata for %s" % url, 1)
return xml
2015-12-29 04:47:16 +11:00
class API():
def __init__(self, item):
self.item = item
self.clientinfo = clientinfo.ClientInfo()
self.addonName = self.clientinfo.getAddonName()
2016-01-02 00:40:40 +11:00
self.clientId = self.clientinfo.getDeviceId()
self.userId = utils.window('emby_currUser')
self.server = utils.window('emby_server%s' % self.userId)
self.token = utils.window('emby_accessToken%s' % self.userId)
2015-12-29 04:47:16 +11:00
def logMsg(self, msg, lvl=1):
className = self.__class__.__name__
utils.logMsg("%s %s" % (self.addonName, className), msg, lvl)
def convert_date(self, stamp):
"""
convert_date(stamp) converts a Unix time stamp (seconds passed since
January 1 1970) to a propper, human-readable time stamp
"""
# DATEFORMAT = xbmc.getRegion('dateshort')
# TIMEFORMAT = xbmc.getRegion('meridiem')
# date_time = time.localtime(stamp)
# if DATEFORMAT[1] == 'd':
# localdate = time.strftime('%d-%m-%Y', date_time)
# elif DATEFORMAT[1] == 'm':
# localdate = time.strftime('%m-%d-%Y', date_time)
# else:
# localdate = time.strftime('%Y-%m-%d', date_time)
# if TIMEFORMAT != '/':
# localtime = time.strftime('%I:%M%p', date_time)
# else:
# localtime = time.strftime('%H:%M', date_time)
# return localtime + ' ' + localdate
DATEFORMAT = xbmc.getRegion('dateshort')
TIMEFORMAT = xbmc.getRegion('meridiem')
date_time = time.localtime(float(stamp))
2015-12-29 04:47:16 +11:00
localdate = time.strftime('%Y-%m-%d', date_time)
return localdate
def getChecksum(self):
"""
2015-12-30 23:25:37 +11:00
Returns a string, not int!
2015-12-29 04:47:16 +11:00
Maybe get rid of viewOffset = (resume point)?!?
"""
item = self.item
2015-12-30 23:25:37 +11:00
# Include a letter to prohibit saving as an int!
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-30 23:25:37 +11:00
checksum = "K%s%s%s%s%s" % (
self.getKey(),
2015-12-29 04:47:16 +11:00
item['updatedAt'],
item.get('viewCount', ""),
item.get('lastViewedAt', ""),
item.get('viewOffset', "")
)
2015-12-30 23:25:37 +11:00
return str(checksum)
2015-12-29 04:47:16 +11:00
2015-12-30 00:13:32 +11:00
def getKey(self):
2015-12-30 23:25:37 +11:00
"""
Returns the Plex unique movie id as a str, not int
"""
2015-12-30 00:13:32 +11:00
item = self.item
key_regex = re.compile(r'/(\d+)$')
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
key = item['key']
key = key_regex.findall(key)[0]
2015-12-30 23:25:37 +11:00
return str(key)
2015-12-30 00:13:32 +11:00
2015-12-29 04:47:16 +11:00
def getDateCreated(self):
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
try:
dateadded = item['addedAt']
dateadded = self.convert_date(dateadded)
except KeyError:
dateadded = None
return dateadded
def getUserData(self):
item = self.item
# Default
favorite = False
playcount = None
played = False
lastPlayedDate = None
resume = 0
rating = 0
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
try:
playcount = int(item['viewCount'])
except KeyError:
playcount = None
if playcount:
played = True
try:
lastPlayedDate = int(item['lastViewedAt'])
lastPlayedDate = self.convert_date(lastPlayedDate)
except KeyError:
lastPlayedDate = None
try:
resume = int(item['viewOffset'])
except KeyError:
resume = 0
return {
'Favorite': favorite,
'PlayCount': playcount,
'Played': played,
'LastPlayedDate': lastPlayedDate,
'Resume': resume,
'Rating': rating
}
def getPeople(self):
"""
Input is Plex' XMl
Returns a dictionary of lists of people found in item.
2015-12-29 04:47:16 +11:00
{
'Director': list,
'Writer': list,
'Cast': list,
'Producer': list
}
"""
item = self.item
director = []
writer = []
cast = []
producer = []
for child in item[0]:
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
2015-12-29 04:47:16 +11:00
('Role': xxx for cast/actors only)
}
"""
item = self.item
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 item[0]:
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]
try:
url = child.attrib['thumb']
except KeyError:
url = None
try:
Role = child.attrib['role']
except KeyError:
Role = None
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 in item. (Not a string!!)
"""
item = self.item
genre = []
for child in item[0]:
if child.tag == 'Genre':
genre.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return genre
def getProvider(self, providername):
"""
provider = getProvider(self, item, providername)
providername: imdb, tvdb, musicBrainzArtist, musicBrainzAlbum,
musicBrainzTrackId
Return IMDB: "tt1234567"
"""
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
imdb_regex = re.compile(r'''(
imdb:// # imdb tag, which will be followed be tt1234567
(tt\d{7}) # actual IMDB ID, e.g. tt1234567
\?? # zero or one ?
(.*) # rest, e.g. language setting
)''', re.VERBOSE)
try:
if "Imdb" in providername:
provider = imdb_regex.findall(item['guid'])
provider = provider[0][1]
elif "tvdb" in providername:
provider = item['ProviderIds']['Tvdb']
elif "musicBrainzArtist" in providername:
provider = item['ProviderIds']['MusicBrainzArtist']
elif "musicBrainzAlbum" in providername:
provider = item['ProviderIds']['MusicBrainzAlbum']
elif "musicBrainzTrackId" in providername:
provider = item['ProviderIds']['MusicBrainzTrackId']
except:
provider = None
return provider
def getTitle(self):
2015-12-29 04:47:16 +11:00
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
title = item['title']
try:
sorttitle = item['titleSort']
except KeyError:
sorttitle = title
return title, sorttitle
def getPlot(self):
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
plot = item['summary']
return plot
def getTagline(self):
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
try:
tagline = item['tagline']
except KeyError:
tagline = None
return tagline
def getAudienceRating(self):
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
try:
rating = item['audienceRating']
except:
rating = None
return rating
def getYear(self):
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
year = item['year']
return year
2015-12-29 04:47:16 +11:00
def getRuntime(self):
"""
Resume point of time and runtime/totaltime. Rounded to 6th decimal.
Assumption: time for both resume and runtime is measured in
milliseconds on the Plex side and in seconds on the Kodi side.
"""
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
time_factor = 1/1000
runtime = int(item['duration']) * time_factor
2015-12-29 04:47:16 +11:00
try:
resume = int(item['viewOffset']) * time_factor
2015-12-29 04:47:16 +11:00
except KeyError:
resume = 0
resume = round(float(resume), 6)
runtime = round(float(runtime), 6)
return resume, runtime
def getMpaa(self):
# Convert more complex cases
item = self.item
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
try:
mpaa = item['contentRating']
except KeyError:
mpaa = None
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.
"""
item = self.item
country = []
for child in item[0]:
if child.tag == 'Country':
country.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return country
def getStudios(self):
item = self.item
studio = []
# xml
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
try:
studio.append(self.getStudio(item['studio']))
except KeyError:
pass
return studio
def getStudio(self, studioName):
# Convert studio for Kodi to properly detect them
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 list into a single string using a " / " separator.
If the list is empty, smart_join returns an empty string.
"""
string = " / ".join(listobject)
return string
def getFilePath(self):
item = self.item
try:
item = item[0].attrib
# json
except KeyError:
pass
2015-12-29 04:47:16 +11:00
try:
filepath = item['key']
except KeyError:
filepath = ""
else:
if "\\\\" in filepath:
# append smb protocol
filepath = filepath.replace("\\\\", "smb://")
filepath = filepath.replace("\\", "/")
if item.get('VideoType'):
videotype = item['VideoType']
# Specific format modification
if 'Dvd'in videotype:
filepath = "%s/VIDEO_TS/VIDEO_TS.IFO" % filepath
elif 'Bluray' in videotype:
filepath = "%s/BDMV/index.bdmv" % filepath
if "\\" in filepath:
# Local path scenario, with special videotype
filepath = filepath.replace("/", "\\")
return filepath
def addPlexCredentialsToUrl(self, url, arguments={}):
token = {'X-Plex-Token': self.token}
xargs = PlexAPI().getXArgsDeviceInfo(options=token)
xargs.update(arguments)
2016-01-02 00:40:40 +11:00
url = "%s?%s" % (url, urlencode(xargs))
return url
2015-12-29 04:47:16 +11:00
def getMediaStreams(self):
item = self.item
videotracks = []
audiotracks = []
subtitlelanguages = []
aspectratio = None
try:
aspectratio = item[0][0].attrib['aspectRatio']
except KeyError:
pass
2015-12-29 04:47:16 +11:00
# Loop over parts:
# TODO: what if several Media tags exist?!?
# Loop over parts
for child in item[0][0]:
2015-12-31 22:46:41 +11:00
container = child.attrib['container'].lower()
# Loop over Streams
for grandchild in child:
mediaStream = grandchild.attrib
2015-12-31 22:46:41 +11:00
type = int(mediaStream['streamType'])
2015-12-29 04:47:16 +11:00
if type == 1: # Video streams
videotrack = {}
videotrack['videocodec'] = mediaStream['codec'].lower()
if "msmpeg4" in videotrack['videocodec']:
videotrack['videocodec'] = "divx"
elif "mpeg4" in videotrack['videocodec']:
# if "simple profile" in profile or profile == "":
# videotrack['videocodec'] = "xvid"
pass
elif "h264" in videotrack['videocodec']:
if container in ("mp4", "mov", "m4v"):
videotrack['videocodec'] = "avc1"
videotrack['height'] = mediaStream.get('height')
videotrack['width'] = mediaStream.get('width')
# TODO: 3d Movies?!?
# videotrack['Video3DFormat'] = item.get('Video3DFormat')
try:
aspectratio = mediaStream['aspectRatio']
except KeyError:
if not aspectratio:
aspectratio = round(float(videotrack['width'] / videotrack['height']), 6)
videotrack['aspectratio'] = aspectratio
# TODO: Video 3d format
videotrack['video3DFormat'] = None
videotracks.append(videotrack)
elif type == 2: # Audio streams
audiotrack = {}
audiotrack['audiocodec'] = mediaStream['codec'].lower()
profile = mediaStream['codecID'].lower()
if "dca" in audiotrack['audiocodec'] and "dts-hd ma" in profile:
audiotrack['audiocodec'] = "dtshd_ma"
audiotrack['channels'] = mediaStream.get('channels')
try:
audiotrack['audiolanguage'] = mediaStream.get('language')
except KeyError:
audiotrack['audiolanguage'] = 'unknown'
audiotracks.append(audiotrack)
elif type == 3: # Subtitle streams
try:
subtitlelanguages.append(mediaStream['language'])
except:
subtitlelanguages.append("Unknown")
2015-12-31 22:46:41 +11:00
media = {
2015-12-29 04:47:16 +11:00
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
2015-12-31 22:46:41 +11:00
return media
2015-12-31 01:57:55 +11:00
def getAllArtwork(self, parentInfo=False):
2015-12-31 01:57:55 +11:00
server = self.server
item = self.item
2015-12-31 01:57:55 +11:00
maxHeight = 10000
maxWidth = 10000
customquery = ""
if utils.settings('compressArt') == "true":
customquery = "&Quality=90"
if utils.settings('enableCoverArt') == "false":
customquery += "&EnableImageEnhancers=false"
allartworks = {
'Primary': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
}
# Process backdrops
# Get background artwork URL
item = item[0].attrib
2015-12-31 01:57:55 +11:00
try:
background = item['art']
background = "%s%s" % (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" % (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']:
parentId = item.get('ParentBackdropItemId')
if parentId:
# If there is a parentId, go through the parent backdrop list
parentbackdrops = item['ParentBackdropImageTags']
backdropIndex = 0
for parentbackdroptag in parentbackdrops:
artwork = (
"%s/emby/Items/%s/Images/Backdrop/%s?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, backdropIndex,
maxWidth, maxHeight, parentbackdroptag, customquery))
allartworks['Backdrop'].append(artwork)
backdropIndex += 1
# Process the rest of the artwork
parentartwork = ['Logo', 'Art', 'Thumb']
for parentart in parentartwork:
if not allartworks[parentart]:
parentId = item.get('Parent%sItemId' % parentart)
if parentId:
parentTag = item['Parent%sImageTag' % parentart]
artwork = (
"%s/emby/Items/%s/Images/%s/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, parentart,
maxWidth, maxHeight, parentTag, customquery))
allartworks[parentart] = artwork
# Parent album works a bit differently
if not allartworks['Primary']:
parentId = item.get('AlbumId')
if parentId and item.get('AlbumPrimaryImageTag'):
parentTag = item['AlbumPrimaryImageTag']
artwork = (
"%s/emby/Items/%s/Images/Primary/0?"
"MaxWidth=%s&MaxHeight=%s&Format=original&Tag=%s%s"
% (server, parentId, maxWidth, maxHeight, parentTag, customquery))
allartworks['Primary'] = artwork
return allartworks
2016-01-02 00:40:40 +11:00
def getTranscodeVideoPath(self, action, quality={}, subtitle={}, audioboost=None, partIndex=None, options={}):
"""
Transcode Video support
Input:
action 'Transcode' OR any other string
quality: {'videoResolution': 'resolution',
'videoQuality': 'quality',
'maxVideoBitrate': 'bitrate'}
subtitle {'selected', 'dontBurnIn', 'size'}
audioboost Guess this takes an int as str
partIndex No idea
options dict() of PlexConnect-options as received from aTV
Output:
final path to pull in PMS transcoder
"""
# path to item
transcodePath = self.server + \
'/video/:/transcode/universal/start.m3u8?'
ID = self.getKey()
path = self.server + '/library/metadata/' + ID
args = {
'session': self.clientId,
'protocol': 'hls',
'fastSeek': '1',
'path': path,
'X-Plex-Client-Capabilities': "protocols=http-live-streaming,"
"http-streaming-video,"
"http-streaming-video-720p,"
"http-streaming-video-1080p,"
"http-mp4-streaming,"
"http-mp4-video,"
"http-mp4-video-720p,"
"http-mp4-video-1080p;"
"videoDecoders="
"h264{profile:high&resolution:1080&level:51};"
"audioDecoders="
"mp3,"
"aac{bitrate:160000},"
"ac3{channels:6},"
"dts{channels:6}"
# 'partIndex': partIndex What do we do with this?!?
}
# All the settings
if subtitle:
args_update = {
'subtitleSize': subtitle['size'],
'skipSubtitles': subtitle['dontBurnIn'] # '1': shut off PMS
}
args.update(args_update)
self.logMsg(
"Subtitle: selected %s, dontBurnIn %s, size %s"
% (subtitle['selected'], subtitle['dontBurnIn'],
subtitle['size']),
1
)
if audioboost:
args_update = {
'audioBoost': audioboost
}
args.update(args_update)
self.logMsg("audioboost: %s" % audioboost, 1)
if action == 'Transcode':
# Possible Plex settings:
# 'videoResolution': vRes,
# 'maxVideoBitrate': mVB,
# : vQ
self.logMsg("Setting transcode quality to: %s" % quality, 1)
args['directStream'] = '0'
args.update(quality)
else:
args['directStream'] = '1'
xargs = PlexAPI().getXArgsDeviceInfo(options=options)
return transcodePath + urlencode(args) + '&' + urlencode(xargs)
def adjustResume(self, resume_seconds):
resume = 0
if resume_seconds:
resume = round(float(resume_seconds), 6)
jumpback = int(utils.settings('resumeJumpBack'))
if resume > jumpback:
# To avoid negative bookmark
resume = resume - jumpback
return resume
def returnParts(self):
"""
TODO
"""
item = self.item
PartCount = 0
# Run through parts
for child in item[0][0]:
if child.tag == 'Part':
PartCount += PartCount
def externalSubs(self, playurl):
externalsubs = []
mapping = {}
item = self.item
itemid = self.getKey()
try:
mediastreams = item[0][0][0]
except (TypeError, KeyError, IndexError):
return
kodiindex = 0
for stream in mediastreams:
# index = stream['Index']
index = stream.attrib['id']
# Since Emby returns all possible tracks together, have to pull only external subtitles.
# IsTextSubtitleStream if true, is available to download from emby.
if (stream.attrib['streamType'] == "3" and
stream.attrib['format']):
# Direct stream
# PLEX: TODO!!
url = ("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, index))
# 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)
return externalsubs