2015-12-27 22:09:07 +11:00
|
|
|
|
|
|
|
"""
|
|
|
|
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
|
2015-12-27 22:09:07 +11:00
|
|
|
|
|
|
|
import struct
|
|
|
|
import time
|
2015-12-31 02:37:30 +11:00
|
|
|
import urllib
|
2015-12-27 22:09:07 +11:00
|
|
|
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
|
|
|
|
|
2015-12-27 22:09:07 +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
|
2015-12-27 22:09:07 +11:00
|
|
|
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()
|
|
|
|
|
2015-12-29 04:47:16 +11:00
|
|
|
self.doUtils = downloadutils.DownloadUtils()
|
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
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-27 22:09:07 +11:00
|
|
|
|
2015-12-29 04:47:16 +11:00
|
|
|
r = self.doUtils.downloadUrl(
|
2015-12-27 22:09:07 +11:00
|
|
|
url,
|
|
|
|
authenticate=False,
|
|
|
|
headerOptions={'X-Plex-Token': token}
|
|
|
|
)
|
2015-12-28 00:42:54 +11:00
|
|
|
self.logMsg("Response was: %s" % r, 2)
|
2015-12-27 22:09:07 +11:00
|
|
|
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:
|
|
|
|
options: dictionary of options that will override the
|
|
|
|
standard header options otherwise set.
|
|
|
|
JSON=True will enforce a JSON answer, never mind any options
|
|
|
|
Output:
|
|
|
|
header dictionary
|
|
|
|
"""
|
|
|
|
# Get addon infos
|
|
|
|
xargs = dict()
|
2015-12-31 01:57:55 +11:00
|
|
|
xargs['User-agent'] = self.addonName
|
2015-12-27 22:09:07 +11:00
|
|
|
xargs['X-Plex-Device'] = self.deviceName
|
|
|
|
# xargs['X-Plex-Model'] = ''
|
|
|
|
xargs['X-Plex-Platform'] = self.platform
|
|
|
|
xargs['X-Plex-Client-Platform'] = self.platform
|
2015-12-31 01:57:55 +11:00
|
|
|
xargs['X-Plex-Product'] = self.addonName
|
2015-12-27 22:09:07 +11:00
|
|
|
xargs['X-Plex-Version'] = self.plexversion
|
|
|
|
xargs['X-Plex-Client-Identifier'] = self.clientId
|
|
|
|
if JSON:
|
|
|
|
xargs['Accept'] = 'application/json'
|
2015-12-31 20:53:22 +11:00
|
|
|
if options:
|
|
|
|
xargs.update(options)
|
2015-12-27 22:09:07 +11:00
|
|
|
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 UserAccessRestricted(self, username):
|
|
|
|
"""
|
|
|
|
Returns True if the user's access is restricted (parental restrictions)
|
|
|
|
False otherwise.
|
|
|
|
|
|
|
|
Returns False also if access cannot be checked because plex.tv cannot
|
|
|
|
be reached.
|
|
|
|
"""
|
|
|
|
plexToken = utils.settings('plexToken')
|
|
|
|
users = self.MyPlexListHomeUsers(plexToken)
|
|
|
|
# If an error is encountered, set to False
|
|
|
|
if not users:
|
|
|
|
self.logMsg("Could not check user access restrictions.", 1)
|
|
|
|
self.logMsg("Setting restrictions to False.", 1)
|
|
|
|
return False
|
|
|
|
for user in users:
|
|
|
|
if username in user['title']:
|
|
|
|
restricted = user['restricted']
|
|
|
|
if restricted == '1':
|
|
|
|
restricted = True
|
|
|
|
else:
|
|
|
|
restricted = False
|
|
|
|
self.logMsg("Successfully checked user parental access for %s: restricted access is set to %s" % (username, restricted), 1)
|
|
|
|
return restricted
|
|
|
|
|
|
|
|
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__
|
|
|
|
plexToken = utils.settings('plexToken')
|
|
|
|
plexLogin = utils.settings('plexLogin')
|
|
|
|
self.logMsg("Getting user list.", 1)
|
2015-12-28 19:00:43 +11:00
|
|
|
# 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)
|
2015-12-28 19:00:43 +11:00
|
|
|
usernumber = len(userlist)
|
2015-12-28 02:27:49 +11:00
|
|
|
usertoken = ''
|
2015-12-28 19:00:43 +11:00
|
|
|
# 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
|
2015-12-28 19:00:43 +11:00
|
|
|
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
|
|
|
|
)
|
2015-12-28 19:00:43 +11:00
|
|
|
# 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.'
|
|
|
|
)
|
2015-12-28 19:00:43 +11:00
|
|
|
# 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
|
|
|
|
"""
|
2015-12-27 22:09:07 +11:00
|
|
|
MyPlexHost = 'https://plex.tv'
|
|
|
|
MyPlexURL = MyPlexHost + '/api/home/users/' + id + '/switch'
|
2015-12-28 02:27:49 +11:00
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
if pin:
|
|
|
|
MyPlexURL += '?pin=' + pin
|
2015-12-28 02:27:49 +11:00
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
xargs = {}
|
2015-12-28 02:27:49 +11:00
|
|
|
xargs = self.getXArgsDeviceInfo(options)
|
2015-12-27 22:09:07 +11:00
|
|
|
xargs['X-Plex-Token'] = authtoken
|
2015-12-28 02:27:49 +11:00
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
request = urllib2.Request(MyPlexURL, None, xargs)
|
2015-12-28 02:27:49 +11:00
|
|
|
request.get_method = lambda: 'POST'
|
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
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)
|
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
# analyse response
|
|
|
|
XMLTree = etree.ElementTree(etree.fromstring(response))
|
2015-12-28 02:27:49 +11:00
|
|
|
|
2015-12-27 22:09:07 +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
|
|
|
|
2015-12-27 22:09:07 +11:00
|
|
|
if username and authtoken:
|
2015-12-28 02:27:49 +11:00
|
|
|
self.logMsg('MyPlex switch HomeUser change successfull', 0)
|
2015-12-27 22:09:07 +11:00
|
|
|
else:
|
2015-12-28 02:27:49 +11:00
|
|
|
self.logMsg('MyPlex switch HomeUser change failed', 0)
|
2015-12-27 22:09:07 +11:00
|
|
|
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',
|
2015-12-27 22:09:07 +11:00
|
|
|
"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
|
2015-12-27 22:09:07 +11:00
|
|
|
"""
|
|
|
|
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 getTranscodeVideoPath(self, path, AuthToken, options, action, quality, subtitle, audio, partIndex):
|
|
|
|
"""
|
|
|
|
Transcode Video support
|
|
|
|
|
|
|
|
parameters:
|
|
|
|
path
|
|
|
|
AuthToken
|
|
|
|
options - dict() of PlexConnect-options as received from aTV
|
|
|
|
action - transcoder action: Auto, Directplay, Transcode
|
|
|
|
quality - (resolution, quality, bitrate)
|
|
|
|
subtitle - {'selected', 'dontBurnIn', 'size'}
|
|
|
|
audio - {'boost'}
|
|
|
|
result:
|
|
|
|
final path to pull in PMS transcoder
|
|
|
|
"""
|
|
|
|
UDID = options['PlexConnectUDID']
|
|
|
|
|
|
|
|
transcodePath = '/video/:/transcode/universal/start.m3u8?'
|
|
|
|
|
|
|
|
vRes = quality[0]
|
|
|
|
vQ = quality[1]
|
|
|
|
mVB = quality[2]
|
|
|
|
dprint(__name__, 1, "Setting transcode quality Res:{0} Q:{1} {2}Mbps", vRes, vQ, mVB)
|
|
|
|
dprint(__name__, 1, "Subtitle: selected {0}, dontBurnIn {1}, size {2}", subtitle['selected'], subtitle['dontBurnIn'], subtitle['size'])
|
|
|
|
dprint(__name__, 1, "Audio: boost {0}", audio['boost'])
|
|
|
|
|
|
|
|
args = dict()
|
|
|
|
args['session'] = UDID
|
|
|
|
args['protocol'] = 'hls'
|
|
|
|
args['videoResolution'] = vRes
|
|
|
|
args['maxVideoBitrate'] = mVB
|
|
|
|
args['videoQuality'] = vQ
|
|
|
|
args['directStream'] = '0' if action=='Transcode' else '1'
|
|
|
|
# 'directPlay' - handled by the client in MEDIARUL()
|
|
|
|
args['subtitleSize'] = subtitle['size']
|
|
|
|
args['skipSubtitles'] = subtitle['dontBurnIn'] #'1' # shut off PMS subtitles. Todo: skip only for aTV native/SRT (or other supported)
|
|
|
|
args['audioBoost'] = audio['boost']
|
|
|
|
args['fastSeek'] = '1'
|
|
|
|
args['path'] = path
|
|
|
|
args['partIndex'] = partIndex
|
|
|
|
|
|
|
|
xargs = getXArgsDeviceInfo(options)
|
|
|
|
xargs['X-Plex-Client-Capabilities'] = "protocols=http-live-streaming,http-mp4-streaming,http-streaming-video,http-streaming-video-720p,http-mp4-video,http-mp4-video-720p;videoDecoders=h264{profile:high&resolution:1080&level:41};audioDecoders=mp3,aac{bitrate:160000}"
|
|
|
|
if not AuthToken=='':
|
|
|
|
xargs['X-Plex-Token'] = AuthToken
|
|
|
|
|
|
|
|
return transcodePath + urlencode(args) + '&' + urlencode(xargs)
|
|
|
|
|
|
|
|
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
|
2015-12-27 22:09:07 +11:00
|
|
|
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
|
|
|
"""
|
2015-12-31 20:53:22 +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
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
|
|
|
}
|
|
|
|
url = url + '?' + urllib.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)
|
2015-12-31 20:53:22 +11:00
|
|
|
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()
|
2015-12-31 02:37:30 +11:00
|
|
|
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')
|
2015-12-31 20:53:22 +11:00
|
|
|
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!
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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+)$')
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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
|
|
|
|
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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):
|
|
|
|
"""
|
2015-12-31 20:53:22 +11:00
|
|
|
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 = []
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
2015-12-31 20:53:22 +11:00
|
|
|
'imageurl': url to picture
|
2015-12-29 04:47:16 +11:00
|
|
|
('Role': xxx for cast/actors only)
|
|
|
|
}
|
|
|
|
"""
|
2015-12-31 20:53:22 +11:00
|
|
|
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'
|
|
|
|
}
|
2015-12-31 20:53:22 +11:00
|
|
|
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]
|
2015-12-31 20:53:22 +11:00
|
|
|
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 = []
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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
|
|
|
|
|
2015-12-31 20:53:22 +11:00
|
|
|
def getTitle(self):
|
2015-12-29 04:47:16 +11:00
|
|
|
item = self.item
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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
|
|
|
|
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
2015-12-31 20:53:22 +11:00
|
|
|
# xml
|
|
|
|
try:
|
|
|
|
item = item[0].attrib
|
|
|
|
# json
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2015-12-29 04:47:16 +11:00
|
|
|
time_factor = 1/1000
|
2015-12-31 20:53:22 +11:00
|
|
|
runtime = int(item['duration']) * time_factor
|
2015-12-29 04:47:16 +11:00
|
|
|
try:
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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 = []
|
2015-12-31 20:53:22 +11:00
|
|
|
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 = []
|
2015-12-31 20:53:22 +11:00
|
|
|
# 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
|
2015-12-31 20:53:22 +11:00
|
|
|
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
|
|
|
|
|
2015-12-31 02:37:30 +11:00
|
|
|
def addPlexCredentialsToUrl(self, url, arguments={}):
|
|
|
|
token = {'X-Plex-Token': self.token}
|
|
|
|
xargs = PlexAPI().getXArgsDeviceInfo(options=token)
|
|
|
|
xargs.update(arguments)
|
|
|
|
url = "%s?%s" % (url, urllib.urlencode(xargs))
|
|
|
|
return url
|
|
|
|
|
2015-12-29 04:47:16 +11:00
|
|
|
def getMediaStreams(self):
|
|
|
|
item = self.item
|
|
|
|
videotracks = []
|
|
|
|
audiotracks = []
|
|
|
|
subtitlelanguages = []
|
|
|
|
aspectratio = None
|
2015-12-31 20:53:22 +11:00
|
|
|
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?!?
|
2015-12-31 20:53:22 +11:00
|
|
|
# Loop over parts
|
|
|
|
for child in item[0][0]:
|
|
|
|
part = child.attrib
|
2015-12-29 04:47:16 +11:00
|
|
|
container = part['container'].lower()
|
2015-12-31 20:53:22 +11:00
|
|
|
# Loop over Streams
|
|
|
|
for grandchild in child:
|
|
|
|
mediaStream = grandchild.attrib
|
2015-12-29 04:47:16 +11:00
|
|
|
try:
|
|
|
|
type = mediaStream['streamType']
|
|
|
|
except KeyError:
|
|
|
|
type = None
|
|
|
|
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")
|
|
|
|
return {
|
|
|
|
'video': videotracks,
|
|
|
|
'audio': audiotracks,
|
|
|
|
'subtitle': subtitlelanguages
|
|
|
|
}
|
2015-12-31 01:57:55 +11:00
|
|
|
|
2015-12-31 02:37:30 +11:00
|
|
|
def getAllArtwork(self, parentInfo=False):
|
2015-12-31 01:57:55 +11:00
|
|
|
|
|
|
|
server = self.server
|
2015-12-31 02:37:30 +11:00
|
|
|
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
|
2015-12-31 20:53:22 +11:00
|
|
|
item = item[0].attrib
|
2015-12-31 01:57:55 +11:00
|
|
|
try:
|
|
|
|
background = item['art']
|
|
|
|
background = "%s%s" % (server, background)
|
2015-12-31 02:37:30 +11:00
|
|
|
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)
|
2015-12-31 02:37:30 +11:00
|
|
|
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
|
|
|
|
|
2015-12-31 02:37:30 +11:00
|
|
|
return allartworks
|