PlexKodiConnect/resources/lib/PlexAPI.py

2732 lines
96 KiB
Python
Raw Normal View History

2016-09-03 00:53:16 +10:00
# -*- coding: utf-8 -*-
"""
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...)
"""
2016-09-03 00:53:16 +10:00
2017-12-10 00:35:08 +11:00
from logging import getLogger
2017-01-25 04:00:35 +11:00
from time import time
import urllib2
import socket
from threading import Thread
import xml.etree.ElementTree as etree
2017-01-25 04:00:35 +11:00
from re import compile as re_compile, sub
from json import dumps
from urllib import urlencode, quote_plus, unquote
from os.path import basename, join
from os import makedirs
2015-12-29 04:47:16 +11:00
import xbmcgui
2017-01-25 04:00:35 +11:00
from xbmc import sleep, executebuiltin
from xbmcvfs import exists
2016-01-30 06:07:21 +11:00
2017-01-25 02:53:50 +11:00
import clientinfo as client
2017-01-25 04:00:35 +11:00
from downloadutils import DownloadUtils
2016-09-03 00:53:16 +10:00
from utils import window, settings, language as lang, tryDecode, tryEncode, \
DateToKodi, exists_dir, slugify
from PlexFunctions import PMSHttpsEnabled
import plexdb_functions as plexdb
import variables as v
import state
2016-09-03 00:53:16 +10:00
###############################################################################
2017-12-10 00:35:08 +11:00
log = getLogger("PLEX." + __name__)
2016-09-03 00:53:16 +10:00
2017-01-25 04:00:35 +11:00
REGEX_IMDB = re_compile(r'''/(tt\d+)''')
2017-02-19 00:45:31 +11:00
REGEX_TVDB = re_compile(r'''thetvdb:\/\/(.+?)\?''')
2016-09-03 00:53:16 +10:00
###############################################################################
class PlexAPI():
def __init__(self):
self.g_PMS = {}
2017-01-25 04:00:35 +11:00
self.doUtils = DownloadUtils().downloadUrl
2015-12-29 04:47:16 +11:00
2016-01-13 03:23:55 +11:00
def GetPlexLoginFromSettings(self):
"""
2016-03-04 23:34:30 +11:00
Returns a dict:
2016-09-03 00:53:16 +10:00
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
2016-03-04 23:34:30 +11:00
2016-03-10 01:37:27 +11:00
Returns strings or unicode
2016-03-04 23:34:30 +11:00
Returns empty strings '' for a setting if not found.
2016-01-13 03:23:55 +11:00
2016-01-14 20:20:19 +11:00
myplexlogin is 'true' if user opted to log into plex.tv (the default)
2016-01-15 00:47:34 +11:00
plexhome is 'true' if plex home is used (the default)
2016-01-13 03:23:55 +11:00
"""
2016-03-04 23:34:30 +11:00
return {
2016-09-03 00:53:16 +10:00
'plexLogin': settings('plexLogin'),
'plexToken': settings('plexToken'),
'plexhome': settings('plexhome'),
'plexid': settings('plexid'),
'myplexlogin': settings('myplexlogin'),
'plexAvatar': settings('plexAvatar'),
'plexHomeSize': settings('plexHomeSize')
2016-03-04 23:34:30 +11:00
}
2016-01-13 03:23:55 +11:00
def GetPlexLoginAndPassword(self):
"""
Signs in to plex.tv.
plexLogin, authtoken = GetPlexLoginAndPassword()
Input: nothing
Output:
plexLogin plex.tv username
authtoken token for plex.tv
Also writes 'plexLogin' and 'token_plex.tv' to Kodi settings file
If not logged in, empty strings are returned for both.
"""
retrievedPlexLogin = ''
plexLogin = 'dummy'
authtoken = ''
dialog = xbmcgui.Dialog()
while retrievedPlexLogin == '' and plexLogin != '':
# Enter plex.tv username. Or nothing to cancel.
2017-01-25 04:04:17 +11:00
plexLogin = dialog.input(lang(29999) + lang(39300),
type=xbmcgui.INPUT_ALPHANUM)
if plexLogin != "":
# Enter password for plex.tv user
plexPassword = dialog.input(
2016-09-03 00:53:16 +10:00
lang(39301) + plexLogin,
type=xbmcgui.INPUT_ALPHANUM,
option=xbmcgui.ALPHANUM_HIDE_INPUT)
retrievedPlexLogin, authtoken = self.MyPlexSignIn(
plexLogin,
plexPassword,
2017-01-25 02:53:50 +11:00
{'X-Plex-Client-Identifier': window('plex_client_Id')})
2016-09-03 00:53:16 +10:00
log.debug("plex.tv username and token: %s, %s"
% (plexLogin, authtoken))
if plexLogin == '':
# Could not sign in user
2017-01-25 04:04:17 +11:00
dialog.ok(lang(29999), lang(39302) + plexLogin)
# Write to Kodi settings file
2016-09-03 00:53:16 +10:00
settings('plexLogin', value=retrievedPlexLogin)
settings('plexToken', value=authtoken)
return (retrievedPlexLogin, authtoken)
2016-01-14 20:20:19 +11:00
def PlexTvSignInWithPin(self):
"""
Prompts user to sign in by visiting https://plex.tv/pin
2016-03-10 01:37:27 +11:00
Writes to Kodi settings file. Also returns:
2016-01-14 20:20:19 +11:00
{
2016-01-15 00:47:34 +11:00
'plexhome': 'true' if Plex Home, 'false' otherwise
2016-01-14 20:20:19 +11:00
'username':
2016-03-10 01:37:27 +11:00
'avatar': URL to user avator
2016-01-14 20:20:19 +11:00
'token':
2016-03-10 01:37:27 +11:00
'plexid': Plex user ID
'homesize': Number of Plex home users (defaults to '1')
2016-01-14 20:20:19 +11:00
}
Returns False if authentication did not work.
"""
code, identifier = self.GetPlexPin()
dialog = xbmcgui.Dialog()
if not code:
# Problems trying to contact plex.tv. Try again later
2017-01-25 04:04:17 +11:00
dialog.ok(lang(29999), lang(39303))
2016-01-14 20:20:19 +11:00
return False
# Go to https://plex.tv/pin and enter the code:
2016-03-11 02:02:46 +11:00
# Or press No to cancel the sign in.
2017-01-25 04:04:17 +11:00
answer = dialog.yesno(lang(29999),
2016-09-03 00:53:16 +10:00
lang(39304) + "\n\n",
2016-03-11 02:02:46 +11:00
code + "\n\n",
2016-09-03 00:53:16 +10:00
lang(39311))
2016-01-14 20:20:19 +11:00
if not answer:
return False
count = 0
# Wait for approx 30 seconds (since the PIN is not visible anymore :-))
2016-03-04 23:34:30 +11:00
while count < 30:
2016-01-14 20:20:19 +11:00
xml = self.CheckPlexTvSignin(identifier)
if xml is not False:
2016-01-14 20:20:19 +11:00
break
2016-03-04 23:34:30 +11:00
# Wait for 1 seconds
2017-01-25 04:00:35 +11:00
sleep(1000)
2016-01-14 20:20:19 +11:00
count += 1
if xml is False:
# Could not sign in to plex.tv Try again later
2017-01-25 04:04:17 +11:00
dialog.ok(lang(29999), lang(39305))
2016-01-14 20:20:19 +11:00
return False
# Parse xml
2016-03-04 23:34:30 +11:00
userid = xml.attrib.get('id')
2016-01-14 20:20:19 +11:00
home = xml.get('home', '0')
2016-01-15 00:47:34 +11:00
if home == '1':
home = 'true'
else:
home = 'false'
2016-01-14 20:20:19 +11:00
username = xml.get('username', '')
2016-03-10 01:37:27 +11:00
avatar = xml.get('thumb', '')
2016-01-14 20:20:19 +11:00
token = xml.findtext('authentication-token')
2016-03-10 01:37:27 +11:00
homeSize = xml.get('homeSize', '1')
2016-01-14 20:20:19 +11:00
result = {
2016-01-15 00:47:34 +11:00
'plexhome': home,
2016-01-14 20:20:19 +11:00
'username': username,
'avatar': avatar,
2016-03-04 23:34:30 +11:00
'token': token,
2016-03-10 01:37:27 +11:00
'plexid': userid,
'homesize': homeSize
2016-01-14 20:20:19 +11:00
}
2016-09-03 00:53:16 +10:00
settings('plexLogin', username)
settings('plexToken', token)
settings('plexhome', home)
settings('plexid', userid)
settings('plexAvatar', avatar)
settings('plexHomeSize', homeSize)
2016-03-04 23:34:30 +11:00
# Let Kodi log into plex.tv on startup from now on
2016-09-03 00:53:16 +10:00
settings('myplexlogin', 'true')
settings('plex_status', value=lang(39227))
2016-01-14 20:20:19 +11:00
return result
def CheckPlexTvSignin(self, identifier):
"""
Checks with plex.tv whether user entered the correct PIN on plex.tv/pin
Returns False if not yet done so, or the XML response file as etree
"""
# Try to get a temporary token
xml = self.doUtils('https://plex.tv/pins/%s.xml' % identifier,
2016-04-26 22:02:19 +10:00
authenticate=False)
2016-01-14 20:20:19 +11:00
try:
temp_token = xml.find('auth_token').text
except:
2016-09-03 00:53:16 +10:00
log.error("Could not find token in plex.tv answer")
2016-01-14 20:20:19 +11:00
return False
if not temp_token:
return False
# Use temp token to get the final plex credentials
xml = self.doUtils('https://plex.tv/users/account',
authenticate=False,
2016-04-26 22:02:19 +10:00
parameters={'X-Plex-Token': temp_token})
2016-01-14 20:20:19 +11:00
return xml
def GetPlexPin(self):
"""
For plex.tv sign-in: returns 4-digit code and identifier as 2 str
"""
code = None
identifier = None
# Download
xml = self.doUtils('https://plex.tv/pins.xml',
authenticate=False,
2016-04-26 22:02:19 +10:00
action_type="POST")
2016-01-14 20:20:19 +11:00
try:
xml.attrib
2016-01-14 20:20:19 +11:00
except:
2016-09-03 00:53:16 +10:00
log.error("Error, no PIN from plex.tv provided")
return None, None
code = xml.find('code').text
identifier = xml.find('id').text
2016-09-03 00:53:16 +10:00
log.info('Successfully retrieved code and id from plex.tv')
return code, identifier
2016-01-14 20:20:19 +11:00
def CheckConnection(self, url, token=None, verifySSL=None):
"""
Checks connection to a Plex server, available at url. Can also be used
2016-01-15 00:47:34 +11:00
to check for connection with plex.tv.
Override SSL to skip the check by setting verifySSL=False
if 'None', SSL will be checked (standard requests setting)
if 'True', SSL settings from file settings are used (False/True)
Input:
url URL to Plex server (e.g. https://192.168.1.1:32400)
2016-01-15 00:47:34 +11:00
token appropriate token to access server. If None is passed,
the current token is used
Output:
2016-01-15 00:47:34 +11:00
False if server could not be reached or timeout occured
200 if connection was successfull
2016-01-15 00:47:34 +11:00
int or other HTML status codes as received from the server
"""
# Add '/clients' to URL because then an authentication is necessary
# If a plex.tv URL was passed, this does not work.
headerOptions = None
if token is not None:
headerOptions = {'X-Plex-Token': token}
if verifySSL is True:
2016-09-03 00:53:16 +10:00
verifySSL = None if settings('sslverify') == 'true' \
else False
if 'plex.tv' in url:
url = 'https://plex.tv/api/home/users'
else:
url = url + '/library/onDeck'
2016-12-21 02:13:19 +11:00
log.debug("Checking connection to server %s with verifySSL=%s"
% (url, verifySSL))
count = 0
2016-12-21 02:13:19 +11:00
while count < 1:
answer = self.doUtils(url,
authenticate=False,
headerOptions=headerOptions,
2016-05-30 00:52:00 +10:00
verifySSL=verifySSL,
2017-05-01 01:45:45 +10:00
timeout=10)
2016-05-19 04:10:20 +10:00
if answer is None:
2016-12-21 02:13:19 +11:00
log.debug("Could not connect to %s" % url)
count += 1
2017-01-25 04:00:35 +11:00
sleep(500)
continue
try:
2016-05-19 04:10:20 +10:00
# xml received?
answer.attrib
except:
2016-05-19 04:10:20 +10:00
if answer is True:
# Maybe no xml but connection was successful nevertheless
answer = 200
else:
# Success - we downloaded an xml!
answer = 200
2016-05-19 04:10:20 +10:00
# We could connect but maybe were not authenticated. No worries
2016-09-03 00:53:16 +10:00
log.debug("Checking connection successfull. Answer: %s" % answer)
return answer
2016-12-21 02:13:19 +11:00
log.debug('Failed to connect to %s too many times. PMS is dead' % url)
return False
def GetgPMSKeylist(self):
"""
Returns a list of all keys that are saved for every entry in the
g_PMS variable.
"""
keylist = [
'address',
'baseURL',
'enableGzip',
'ip',
'local',
'name',
'owned',
'port',
'scheme'
]
return keylist
def declarePMS(self, uuid, name, scheme, ip, port):
"""
Plex Media Server handling
parameters:
uuid - PMS ID
name, scheme, ip, port, type, owned, token
"""
address = ip + ':' + port
baseURL = scheme + '://' + ip + ':' + port
self.g_PMS[uuid] = {
'name': name,
'scheme': scheme,
'ip': ip,
'port': port,
'address': address,
'baseURL': baseURL,
'local': '1',
'owned': '1',
'accesstoken': '',
'enableGzip': False
}
def updatePMSProperty(self, uuid, tag, value):
# set property element of PMS by UUID
try:
self.g_PMS[uuid][tag] = value
except:
2016-09-03 00:53:16 +10:00
log.error('%s has not yet been declared ' % uuid)
return False
def getPMSProperty(self, uuid, tag):
# get name of PMS by UUID
try:
answ = self.g_PMS[uuid].get(tag, '')
except:
2016-09-03 00:53:16 +10:00
log.error('%s not found in PMS catalogue' % uuid)
answ = False
return answ
def PlexGDM(self):
"""
PlexGDM
parameters:
none
result:
PMS_list - dict() of PMSs found
"""
import struct
IP_PlexGDM = '239.0.0.250' # multicast to PMS
Port_PlexGDM = 32414
Msg_PlexGDM = 'M-SEARCH * HTTP/1.0'
# setup socket for discovery -> multicast message
GDM = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
GDM.settimeout(2.0)
# Set the time-to-live for messages to 2 for local network
ttl = struct.pack('b', 2)
GDM.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
returnData = []
try:
# Send data to the multicast group
GDM.sendto(Msg_PlexGDM, (IP_PlexGDM, Port_PlexGDM))
# Look for responses from all recipients
while True:
try:
data, server = GDM.recvfrom(1024)
returnData.append({'from': server,
'data': data})
except socket.timeout:
break
except Exception as e:
# Probably error: (101, 'Network is unreachable')
log.error(e)
import traceback
log.error("Traceback:\n%s" % traceback.format_exc())
finally:
GDM.close()
pmsList = {}
for response in returnData:
update = {'ip': response.get('from')[0]}
# Check if we had a positive HTTP response
2016-05-30 00:52:00 +10:00
if "200 OK" not in response.get('data'):
continue
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:
2016-09-03 00:53:16 +10:00
update['serverName'] = tryDecode(
each.split(':')[1].strip())
2016-05-30 00:52:00 +10:00
elif "Port:" in each:
update['port'] = each.split(':')[1].strip()
elif "Updated-At:" in each:
update['updated'] = each.split(':')[1].strip()
elif "Version:" in each:
update['version'] = each.split(':')[1].strip()
pmsList[update['uuid']] = update
return pmsList
def discoverPMS(self, IP_self, plexToken=None):
"""
parameters:
IP_self Own IP
optional:
plexToken token for plex.tv
result:
self.g_PMS dict set
"""
self.g_PMS = {}
# Look first for local PMS in the LAN
pmsList = self.PlexGDM()
2016-09-03 00:53:16 +10:00
log.debug('PMS found in the local LAN via GDM: %s' % pmsList)
2016-05-25 03:00:39 +10:00
# Get PMS from plex.tv
if plexToken:
2016-09-03 00:53:16 +10:00
log.info('Checking with plex.tv for more PMS to connect to')
2016-05-25 03:00:39 +10:00
self.getPMSListFromMyPlex(plexToken)
else:
2016-09-03 00:53:16 +10:00
log.info('No plex token supplied, only checked LAN for PMS')
2016-05-25 03:00:39 +10:00
for uuid in pmsList:
PMS = pmsList[uuid]
2016-05-25 03:00:39 +10:00
if PMS['uuid'] in self.g_PMS:
2016-09-03 00:53:16 +10:00
log.debug('We already know of PMS %s from plex.tv'
% PMS['serverName'])
2017-02-06 02:11:31 +11:00
# Update with GDM data - potentially more reliable than plex.tv
self.updatePMSProperty(PMS['uuid'], 'ip', PMS['ip'])
self.updatePMSProperty(PMS['uuid'], 'port', PMS['port'])
self.updatePMSProperty(PMS['uuid'], 'local', '1')
self.updatePMSProperty(PMS['uuid'], 'scheme', 'http')
self.updatePMSProperty(PMS['uuid'],
'baseURL',
'http://%s:%s' % (PMS['ip'],
PMS['port']))
else:
self.declarePMS(PMS['uuid'], PMS['serverName'], 'http',
PMS['ip'], PMS['port'])
# Ping to check whether we need HTTPs or HTTP
2016-05-25 03:00:39 +10:00
https = PMSHttpsEnabled('%s:%s' % (PMS['ip'], PMS['port']))
if https is None:
# Error contacting url. Skip for now
continue
elif https is True:
self.updatePMSProperty(PMS['uuid'], 'scheme', 'https')
self.updatePMSProperty(
PMS['uuid'],
'baseURL',
'https://%s:%s' % (PMS['ip'], PMS['port']))
2016-03-11 02:02:46 +11:00
else:
# Already declared with http
pass
# install plex.tv "virtual" PMS - for myPlex, PlexHome
# self.declarePMS('plex.tv', 'plex.tv', 'https', 'plex.tv', '443')
# self.updatePMSProperty('plex.tv', 'local', '-')
# self.updatePMSProperty('plex.tv', 'owned', '-')
# self.updatePMSProperty(
# 'plex.tv', 'accesstoken', plexToken)
# (remote and local) servers from plex.tv
def getPMSListFromMyPlex(self, token):
"""
getPMSListFromMyPlex
get Plex media Server List from plex.tv/pms/resources
"""
xml = self.doUtils('https://plex.tv/api/resources',
authenticate=False,
parameters={'includeHttps': 1},
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
2016-05-25 03:00:39 +10:00
except AttributeError:
2016-09-03 00:53:16 +10:00
log.error('Could not get list of PMS from plex.tv')
return
import Queue
queue = Queue.Queue()
2016-05-30 00:52:00 +10:00
threadQueue = []
2016-05-25 03:00:39 +10:00
maxAgeSeconds = 2*60*60*24
for Dir in xml.findall('Device'):
2016-05-25 03:00:39 +10:00
if 'server' not in Dir.get('provides'):
# No PMS - skip
continue
if Dir.find('Connection') is None:
# no valid connection - skip
continue
# check MyPlex data age - skip if >2 days
PMS = {}
PMS['name'] = Dir.get('name')
2017-01-25 04:00:35 +11:00
infoAge = time() - int(Dir.get('lastSeenAt'))
2016-05-25 03:00:39 +10:00
if infoAge > maxAgeSeconds:
2016-09-03 00:53:16 +10:00
log.debug("Server %s not seen for 2 days - skipping."
% PMS['name'])
2016-05-25 03:00:39 +10:00
continue
PMS['uuid'] = Dir.get('clientIdentifier')
PMS['token'] = Dir.get('accessToken', token)
PMS['owned'] = Dir.get('owned', '1')
PMS['local'] = Dir.get('publicAddressMatches')
PMS['ownername'] = Dir.get('sourceTitle', '')
PMS['path'] = '/'
PMS['options'] = None
# Try a local connection first
# Backup to remote connection, if that failes
PMS['connections'] = []
for Con in Dir.findall('Connection'):
if Con.get('local') == '1':
PMS['connections'].append(Con)
# Append non-local
for Con in Dir.findall('Connection'):
if Con.get('local') != '1':
PMS['connections'].append(Con)
t = Thread(target=self.pokePMS,
args=(PMS, queue))
2016-05-30 00:52:00 +10:00
threadQueue.append(t)
maxThreads = 5
2016-05-30 00:52:00 +10:00
threads = []
# poke PMS, own thread for each PMS
while True:
2016-05-30 00:52:00 +10:00
# Remove finished threads
for t in threads:
if not t.isAlive():
threads.remove(t)
if len(threads) < maxThreads:
try:
t = threadQueue.pop()
except IndexError:
# We have done our work
break
else:
t.start()
threads.append(t)
else:
2017-01-25 04:00:35 +11:00
sleep(50)
2016-05-25 03:00:39 +10:00
# wait for requests being answered
for t in threads:
t.join()
# declare new PMSs
while not queue.empty():
PMS = queue.get()
self.declarePMS(PMS['uuid'], PMS['name'],
PMS['protocol'], PMS['ip'], PMS['port'])
self.updatePMSProperty(
PMS['uuid'], 'accesstoken', PMS['token'])
self.updatePMSProperty(
PMS['uuid'], 'owned', PMS['owned'])
self.updatePMSProperty(
PMS['uuid'], 'local', PMS['local'])
# set in declarePMS, overwrite for https encryption
self.updatePMSProperty(
PMS['uuid'], 'baseURL', PMS['baseURL'])
self.updatePMSProperty(
PMS['uuid'], 'ownername', PMS['ownername'])
2017-02-02 02:56:37 +11:00
log.debug('Found PMS %s: %s'
% (PMS['uuid'], self.g_PMS[PMS['uuid']]))
2016-05-25 03:00:39 +10:00
queue.task_done()
def pokePMS(self, PMS, queue):
2016-05-30 00:52:00 +10:00
data = PMS['connections'][0].attrib
if data['local'] == '1':
protocol = data['protocol']
address = data['address']
port = data['port']
2016-05-25 03:00:39 +10:00
url = '%s://%s:%s' % (protocol, address, port)
else:
2016-05-30 00:52:00 +10:00
url = data['uri']
2017-01-22 03:34:51 +11:00
if url.count(':') == 1:
url = '%s:%s' % (url, data['port'])
2017-01-13 05:37:31 +11:00
protocol, address, port = url.split(':', 2)
2016-05-25 03:00:39 +10:00
address = address.replace('/', '')
xml = self.doUtils('%s/identity' % url,
authenticate=False,
headerOptions={'X-Plex-Token': PMS['token']},
2016-05-25 03:00:39 +10:00
verifySSL=False,
timeout=10)
try:
2016-05-30 00:52:00 +10:00
xml.attrib['machineIdentifier']
except (AttributeError, KeyError):
2016-05-25 03:00:39 +10:00
# No connection, delete the one we just tested
del PMS['connections'][0]
if len(PMS['connections']) > 0:
# Still got connections left, try them
return self.pokePMS(PMS, queue)
return
else:
2016-05-25 03:00:39 +10:00
# Connection successful - correct PMS?
if xml.get('machineIdentifier') == PMS['uuid']:
# process later
PMS['baseURL'] = url
PMS['protocol'] = protocol
PMS['ip'] = address
PMS['port'] = port
queue.put(PMS)
return
2016-09-03 00:53:16 +10:00
log.info('Found a PMS at %s, but the expected machineIdentifier of '
'%s did not match the one we found: %s'
% (url, PMS['uuid'], xml.get('machineIdentifier')))
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
2017-01-25 02:53:50 +11:00
xargs = client.getXArgsDeviceInfo(options)
request = urllib2.Request(MyPlexURL, None, xargs)
request.get_method = lambda: 'POST'
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:
2016-09-03 00:53:16 +10:00
log.info("Authentication failed")
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):
2016-09-03 00:53:16 +10:00
"""
TO BE DONE!
"""
# MyPlex web address
MyPlexHost = 'plex.tv'
MyPlexSignOutPath = '/users/sign_out.xml'
MyPlexURL = 'http://' + MyPlexHost + MyPlexSignOutPath
# create POST request
xargs = {'X-Plex-Token': authtoken}
request = urllib2.Request(MyPlexURL, None, xargs)
# turn into 'POST' - done automatically with data!=None. But we don't
# have data.
request.get_method = lambda: 'POST'
response = urllib2.urlopen(request).read()
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.
"""
2016-09-03 00:53:16 +10:00
plexToken = settings('plexToken')
2015-12-28 20:35:27 +11:00
users = self.MyPlexListHomeUsers(plexToken)
url = ''
2015-12-28 20:35:27 +11:00
# If an error is encountered, set to False
if not users:
2016-09-03 00:53:16 +10:00
log.info("Couldnt get user from plex.tv. No URL for user avatar")
2015-12-28 20:35:27 +11:00
return False
for user in users:
if username in user['title']:
url = user['thumb']
2016-09-03 00:53:16 +10:00
log.debug("Avatar url for user %s is: %s" % (username, url))
2015-12-28 20:35:27 +11:00
return url
2016-03-10 01:37:27 +11:00
def ChoosePlexHomeUser(self, plexToken):
2015-12-28 02:27:49 +11:00
"""
Let's user choose from a list of Plex home users. Will switch to that
user accordingly.
2016-03-10 01:37:27 +11:00
Returns a dict:
{
'username': Unicode
'userid': '' Plex ID of the user
'token': '' User's token
'protected': True if PIN is needed, else False
}
2015-12-28 20:35:27 +11:00
2016-03-10 01:37:27 +11:00
Will return False if something went wrong (wrong PIN, no connection)
2015-12-28 02:27:49 +11:00
"""
dialog = xbmcgui.Dialog()
# Get list of Plex home users
2015-12-28 02:27:49 +11:00
users = self.MyPlexListHomeUsers(plexToken)
if not users:
2016-09-03 00:53:16 +10:00
log.error("User download failed.")
2016-03-10 01:37:27 +11:00
return False
2015-12-28 02:27:49 +11:00
userlist = []
userlistCoded = []
2015-12-28 02:27:49 +11:00
for user in users:
username = user['title']
userlist.append(username)
2016-03-10 01:37:27 +11:00
# To take care of non-ASCII usernames
2016-09-03 00:53:16 +10:00
userlistCoded.append(tryEncode(username))
usernumber = len(userlist)
2016-03-10 01:37:27 +11:00
username = ''
2015-12-28 02:27:49 +11:00
usertoken = ''
2016-01-13 03:23:55 +11:00
trials = 0
while trials < 3:
if usernumber > 1:
# Select user
user_select = dialog.select(
2017-01-25 04:04:17 +11:00
lang(29999) + lang(39306),
userlistCoded)
2015-12-28 19:17:17 +11:00
if user_select == -1:
2016-09-03 00:53:16 +10:00
log.info("No user selected.")
settings('username', value='')
2017-01-25 04:00:35 +11:00
executebuiltin('Addon.OpenSettings(%s)'
% v.ADDON_ID)
2016-03-10 01:37:27 +11:00
return False
# Only 1 user received, choose that one
else:
user_select = 0
2015-12-28 19:17:17 +11:00
selected_user = userlist[user_select]
2016-09-03 00:53:16 +10:00
log.info("Selected user: %s" % selected_user)
2015-12-28 19:17:17 +11:00
user = users[user_select]
2015-12-28 02:27:49 +11:00
# Ask for PIN, if protected:
2016-03-10 01:37:27 +11:00
pin = None
2015-12-28 02:27:49 +11:00
if user['protected'] == '1':
2016-09-03 00:53:16 +10:00
log.debug('Asking for users PIN')
2015-12-28 02:27:49 +11:00
pin = dialog.input(
2016-09-03 00:53:16 +10:00
lang(39307) + selected_user,
'',
xbmcgui.INPUT_NUMERIC,
xbmcgui.ALPHANUM_HIDE_INPUT)
2016-03-04 23:34:30 +11:00
# User chose to cancel
2016-03-10 01:37:27 +11:00
# Plex bug: don't call url for protected user with empty PIN
if not pin:
trials += 1
continue
2015-12-28 02:27:49 +11:00
# Switch to this Plex Home user, if applicable
2016-03-11 02:02:46 +11:00
result = self.PlexSwitchHomeUser(
2015-12-28 03:12:46 +11:00
user['id'],
2015-12-28 02:27:49 +11:00
pin,
2016-01-15 00:47:34 +11:00
plexToken,
2016-09-03 00:53:16 +10:00
settings('plex_machineIdentifier'))
2016-03-11 02:02:46 +11:00
if result:
# Successfully retrieved username: break out of while loop
username = result['username']
usertoken = result['usertoken']
break
# Couldn't get user auth
2016-03-11 02:02:46 +11:00
else:
2016-03-10 01:37:27 +11:00
trials += 1
# Could not login user, please try again
2017-01-25 04:04:17 +11:00
if not dialog.yesno(lang(29999),
2016-09-03 00:53:16 +10:00
lang(39308) + selected_user,
lang(39309)):
2016-03-04 23:34:30 +11:00
# User chose to cancel
break
if not username:
2016-09-03 00:53:16 +10:00
log.error('Failed signing in a user to plex.tv')
2017-01-25 04:00:35 +11:00
executebuiltin('Addon.OpenSettings(%s)' % v.ADDON_ID)
2016-03-10 01:37:27 +11:00
return False
return {
'username': username,
'userid': user['id'],
'protected': True if user['protected'] == '1' else False,
'token': usertoken
}
def PlexSwitchHomeUser(self, userId, pin, token, machineIdentifier):
2015-12-28 02:27:49 +11:00
"""
Retrieves Plex home token for a Plex home user.
2016-03-11 02:02:46 +11:00
Returns False if unsuccessful
2015-12-28 02:27:49 +11:00
Input:
2016-01-15 00:47:34 +11:00
userId id of the Plex home user
2015-12-28 02:27:49 +11:00
pin PIN of the Plex home user, if protected
2016-01-15 00:47:34 +11:00
token token for plex.tv
2015-12-28 02:27:49 +11:00
Output:
2016-03-11 02:02:46 +11:00
{
'username'
'usertoken' Might be empty strings if no token found
for the machineIdentifier that was chosen
}
2015-12-28 02:27:49 +11:00
2016-03-11 02:02:46 +11:00
settings('userid') and settings('username') with new plex token
2015-12-28 02:27:49 +11:00
"""
2016-09-03 00:53:16 +10:00
log.info('Switching to user %s' % userId)
2016-01-15 00:47:34 +11:00
url = 'https://plex.tv/api/home/users/' + userId + '/switch'
if pin:
2016-01-15 00:47:34 +11:00
url += '?pin=' + pin
answer = self.doUtils(url,
authenticate=False,
2016-04-26 22:02:19 +10:00
action_type="POST",
headerOptions={'X-Plex-Token': token})
2016-03-10 01:37:27 +11:00
try:
answer.attrib
except:
2016-09-03 00:53:16 +10:00
log.error('Error: plex.tv switch HomeUser change failed')
2016-03-11 02:02:46 +11:00
return False
2015-12-28 02:27:49 +11:00
2016-01-15 00:47:34 +11:00
username = answer.attrib.get('title', '')
token = answer.attrib.get('authenticationToken', '')
2015-12-28 02:27:49 +11:00
2016-03-11 02:02:46 +11:00
# Write to settings file
2016-09-03 00:53:16 +10:00
settings('username', username)
settings('accessToken', token)
settings('userid', answer.attrib.get('id', ''))
settings('plex_restricteduser',
'true' if answer.attrib.get('restricted', '0') == '1'
else 'false')
state.RESTRICTED_USER = True if \
answer.attrib.get('restricted', '0') == '1' else False
2016-03-11 02:02:46 +11:00
# Get final token to the PMS we've chosen
2016-03-10 01:37:27 +11:00
url = 'https://plex.tv/api/resources?includeHttps=1'
xml = self.doUtils(url,
authenticate=False,
headerOptions={'X-Plex-Token': token})
2016-03-10 01:37:27 +11:00
try:
xml.attrib
except:
2016-09-03 00:53:16 +10:00
log.error('Answer from plex.tv not as excepted')
2016-03-11 02:02:46 +11:00
# Set to empty iterable list for loop
xml = []
2016-01-15 00:47:34 +11:00
found = 0
2016-09-03 00:53:16 +10:00
log.debug('Our machineIdentifier is %s' % machineIdentifier)
2016-03-10 01:37:27 +11:00
for device in xml:
2016-03-11 02:02:46 +11:00
identifier = device.attrib.get('clientIdentifier')
2016-09-03 00:53:16 +10:00
log.debug('Found a Plex machineIdentifier: %s' % identifier)
2016-03-11 02:02:46 +11:00
if (identifier in machineIdentifier or
machineIdentifier in identifier):
2016-01-15 00:47:34 +11:00
found += 1
2016-03-10 01:37:27 +11:00
token = device.attrib.get('accessToken')
2016-03-11 02:02:46 +11:00
result = {
'username': username,
}
2016-01-15 00:47:34 +11:00
if found == 0:
2016-09-03 00:53:16 +10:00
log.info('No tokens found for your server! Using empty string')
2016-03-11 02:02:46 +11:00
result['usertoken'] = ''
else:
result['usertoken'] = token
2016-09-03 00:53:16 +10:00
log.info('Plex.tv switch HomeUser change successfull for user %s'
% username)
2016-03-11 02:02:46 +11:00
return result
def MyPlexListHomeUsers(self, token):
"""
2016-01-15 00:47:34 +11:00
Returns a list for myPlex home users for the current plex.tv account.
Input:
token for plex.tv
Output:
List of users, where one entry is of the form:
2016-01-15 00:47:34 +11:00
"id": userId,
"admin": '1'/'0',
"guest": '1'/'0',
"restricted": '1'/'0',
"protected": '1'/'0',
"email": email,
"title": title,
"username": username,
"thumb": thumb_url
}
2015-12-28 20:35:27 +11:00
If any value is missing, None is returned instead (or "" from plex.tv)
If an error is encountered, False is returned
"""
xml = self.doUtils('https://plex.tv/api/home/users/',
authenticate=False,
headerOptions={'X-Plex-Token': token})
try:
xml.attrib
except:
2016-09-03 00:53:16 +10:00
log.error('Download of Plex home users failed.')
return False
users = []
for user in xml:
users.append(user.attrib)
return users
def getDirectVideoPath(self, key, AuthToken):
"""
Direct Video Play support
parameters:
path
AuthToken
Indirect - media indirect specified, grab child XML to gain real path
options
result:
final path to media file
"""
if key.startswith('http://') or key.startswith('https://'): # external address - keep
path = key
else:
if AuthToken == '':
path = key
else:
xargs = dict()
xargs['X-Plex-Token'] = AuthToken
if key.find('?') == -1:
path = key + '?' + urlencode(xargs)
else:
path = key + '&' + urlencode(xargs)
return path
def getTranscodeImagePath(self, key, AuthToken, path, width, height):
"""
Transcode Image support
parameters:
key
AuthToken
path - source path of current XML: path[srcXML]
width
height
result:
final path to image file
"""
if key.startswith('http://') or key.startswith('https://'): # external address - can we get a transcoding request for external images?
path = key
elif key.startswith('/'): # internal full path.
path = 'http://127.0.0.1:32400' + key
else: # internal path, add-on
path = 'http://127.0.0.1:32400' + path + '/' + key
2016-09-03 00:53:16 +10:00
path = tryEncode(path)
# 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
2017-01-25 02:53:50 +11:00
xargs = client.getXArgsDeviceInfo(options)
if not AuthToken == '':
xargs['X-Plex-Token'] = AuthToken
return transcodePath + urlencode(args) + '&' + urlencode(xargs)
def getDirectAudioPath(self, path, AuthToken):
"""
Direct Audio support
parameters:
path
AuthToken
result:
final path to audio file
"""
if not AuthToken == '':
xargs = dict()
xargs['X-Plex-Token'] = AuthToken
if path.find('?') == -1:
path = path + '?' + urlencode(xargs)
else:
path = path + '&' + urlencode(xargs)
return path
def returnServerList(self, data):
"""
Returns a nicer list of all servers found in data, where data is in
g_PMS format, for the client device with unique ID ATV_udid
Input:
data e.g. self.g_PMS
Output: List of all servers, with an entry of the form:
{
'name': friendlyName, the Plex server's name
'address': ip:port
'ip': ip, without http/https
'port': port
'scheme': 'http'/'https', nice for checking for secure connections
'local': '1'/'0', Is the server a local server?
'owned': '1'/'0', Is the server owned by the user?
'machineIdentifier': id, Plex server machine identifier
'accesstoken': token Access token to this server
'baseURL': baseURL scheme://ip:port
'ownername' Plex username of PMS owner
}
"""
serverlist = []
for key, value in data.items():
serverlist.append({
'name': value.get('name'),
'address': value.get('address'),
'ip': value.get('ip'),
'port': value.get('port'),
'scheme': value.get('scheme'),
'local': value.get('local'),
'owned': value.get('owned'),
'machineIdentifier': key,
'accesstoken': value.get('accesstoken'),
'baseURL': value.get('baseURL'),
'ownername': value.get('ownername')
})
return serverlist
2016-01-02 06:36:02 +11:00
class API():
"""
API(item)
2016-01-02 06:36:02 +11:00
Processes a Plex media server's XML response
2015-12-29 04:47:16 +11:00
item: xml.etree.ElementTree element
"""
2015-12-29 04:47:16 +11:00
def __init__(self, item):
self.item = item
2016-01-04 05:17:59 +11:00
# which media part in the XML response shall we look at?
self.part = 0
self.mediastream = None
2016-09-03 00:53:16 +10:00
self.server = window('pms_server')
2015-12-29 04:47:16 +11:00
2016-02-08 04:05:59 +11:00
def setPartNumber(self, number=None):
2016-01-04 05:17:59 +11:00
"""
Sets the part number to work with (used to deal with Movie with several
parts).
"""
2016-02-08 04:05:59 +11:00
self.part = number or 0
2016-01-04 05:17:59 +11:00
def getPartNumber(self):
"""
Returns the current media part number we're dealing with.
"""
return self.part
2016-01-02 19:28:31 +11:00
def getType(self):
"""
2016-01-30 06:07:21 +11:00
Returns the type of media, e.g. 'movie' or 'clip' for trailers
"""
return self.item.attrib.get('type')
2016-01-02 19:28:31 +11:00
2015-12-29 04:47:16 +11:00
def getChecksum(self):
"""
2016-03-28 01:57:35 +11:00
Returns a string, not int.
WATCH OUT - time in Plex, not Kodi ;-)
2015-12-29 04:47:16 +11:00
"""
# Include a letter to prohibit saving as an int!
2016-01-30 06:07:21 +11:00
checksum = "K%s%s" % (self.getRatingKey(),
self.item.attrib.get('updatedAt', ''))
2016-01-02 06:36:02 +11:00
return checksum
2015-12-29 04:47:16 +11:00
2016-01-30 06:07:21 +11:00
def getRatingKey(self):
2015-12-30 23:25:37 +11:00
"""
2016-01-30 06:07:21 +11:00
Returns the Plex key such as '246922' as a string
2015-12-30 23:25:37 +11:00
"""
return self.item.attrib.get('ratingKey')
2015-12-30 00:13:32 +11:00
2016-01-30 06:07:21 +11:00
def getKey(self):
"""
Returns the Plex key such as '/library/metadata/246922' or empty string
2016-01-30 06:07:21 +11:00
"""
return self.item.attrib.get('key', '')
2018-01-08 03:50:30 +11:00
def plex_media_streams(self):
"""
Returns the media streams directly from the PMS xml.
Mind self.mediastream to be set before and self.part!
"""
return self.item[self.mediastream][self.part]
def getFilePath(self, forceFirstMediaStream=False):
"""
Returns the direct path to this item, e.g. '\\NAS\movies\movie.mkv'
or None
forceFirstMediaStream=True:
will always use 1st media stream, e.g. when several different
files are present for the same PMS item
"""
if self.mediastream is None and forceFirstMediaStream is False:
self.getMediastreamNumber()
try:
if forceFirstMediaStream is False:
ans = self.item[self.mediastream][self.part].attrib['file']
else:
ans = self.item[0][self.part].attrib['file']
except:
ans = None
if ans is not None:
2016-04-27 03:19:52 +10:00
try:
2016-09-03 00:53:16 +10:00
ans = tryDecode(unquote(ans))
except UnicodeDecodeError:
# Sometimes, Plex seems to have encoded in latin1
ans = unquote(ans).decode('latin1')
return ans
2017-03-19 22:14:16 +11:00
def get_picture_path(self):
"""
2017-03-26 23:09:43 +11:00
Returns the item's picture path (transcode, if necessary) as string.
Will always use addon paths, never direct paths
2017-03-19 22:14:16 +11:00
"""
extension = self.item[0][0].attrib['key'][self.item[0][0].attrib['key'].rfind('.'):].lower()
if (window('plex_force_transcode_pix') == 'true' or
extension not in v.KODI_SUPPORTED_IMAGES):
# Let Plex transcode
# max width/height supported by plex image transcoder is 1920x1080
path = self.server + PlexAPI().getTranscodeImagePath(
self.item[0][0].attrib.get('key'),
window('pms_token'),
"%s%s" % (self.server, self.item[0][0].attrib.get('key')),
1920,
1080)
else:
2017-03-26 23:09:43 +11:00
path = self.addPlexCredentialsToUrl(
'%s%s' % (window('pms_server'),
self.item[0][0].attrib['key']))
# Attach Plex id to url to let it be picked up by our playqueue agent
# later
return tryEncode('%s&plex_id=%s' % (path, self.getRatingKey()))
2017-03-19 22:14:16 +11:00
def getTVShowPath(self):
"""
Returns the direct path to the TV show, e.g. '\\NAS\tv\series'
or None
"""
res = None
for child in self.item:
if child.tag == 'Location':
res = child.attrib.get('path')
return res
2016-01-30 06:07:21 +11:00
2016-01-10 02:14:02 +11:00
def getIndex(self):
"""
Returns the 'index' of an PMS XML reply. Depicts e.g. season number.
"""
return self.item.attrib.get('index')
2016-01-10 02:14:02 +11:00
2015-12-29 04:47:16 +11:00
def getDateCreated(self):
"""
2016-04-15 17:32:04 +10:00
Returns the date when this library item was created.
If not found, returns 2000-01-01 10:00:00
"""
res = self.item.attrib.get('addedAt')
if res is not None:
2016-09-03 00:53:16 +10:00
res = DateToKodi(res)
2016-04-15 17:32:04 +10:00
else:
res = '2000-01-01 10:00:00'
return res
2015-12-29 04:47:16 +11:00
2018-01-22 04:31:49 +11:00
def getViewCount(self):
"""
Returns the play count for the item as an int or the int 0 if not found
"""
try:
return int(self.item.attrib['viewCount'])
except (KeyError, ValueError):
return 0
2015-12-29 04:47:16 +11:00
def getUserData(self):
"""
Returns a dict with None if a value is missing
{
'Favorite': favorite, # False, because n/a in Plex
'PlayCount': playcount,
'Played': played, # True/False
'LastPlayedDate': lastPlayedDate,
'Resume': resume, # Resume time in seconds
'Runtime': runtime,
'Rating': rating
}
"""
item = self.item.attrib
2016-04-02 19:31:21 +11:00
# Default - attributes not found with Plex
2015-12-29 04:47:16 +11:00
favorite = False
try:
playcount = int(item['viewCount'])
except (KeyError, ValueError):
playcount = None
2017-02-03 01:23:54 +11:00
played = True if playcount else False
2015-12-29 04:47:16 +11:00
try:
2016-09-03 00:53:16 +10:00
lastPlayedDate = DateToKodi(int(item['lastViewedAt']))
except (KeyError, ValueError):
2015-12-29 04:47:16 +11:00
lastPlayedDate = None
if state.INDICATE_MEDIA_VERSIONS is True:
2017-02-03 01:23:54 +11:00
userrating = 0
for entry in self.item.findall('./Media'):
userrating += 1
# Don't show a value of '1'
userrating = 0 if userrating == 1 else userrating
else:
try:
userrating = int(float(item['userRating']))
except (KeyError, ValueError):
userrating = 0
2016-04-02 19:31:21 +11:00
try:
rating = float(item['audienceRating'])
except (KeyError, ValueError):
2016-04-02 19:31:21 +11:00
try:
rating = float(item['rating'])
except (KeyError, ValueError):
2016-04-02 19:31:21 +11:00
rating = 0.0
resume, runtime = self.getRuntime()
2015-12-29 04:47:16 +11:00
return {
'Favorite': favorite,
'PlayCount': playcount,
'Played': played,
'LastPlayedDate': lastPlayedDate,
'Resume': resume,
'Runtime': runtime,
'Rating': rating,
'UserRating': userrating
2015-12-29 04:47:16 +11:00
}
2016-03-02 02:18:12 +11:00
def getCollections(self):
"""
2016-03-02 02:28:48 +11:00
Returns a list of PMS collection tags or an empty list
2016-03-02 02:18:12 +11:00
"""
collections = []
for child in self.item:
if child.tag == 'Collection':
if child.attrib['tag']:
collections.append(child.attrib['tag'])
2016-03-02 02:18:12 +11:00
return collections
2015-12-29 04:47:16 +11:00
def getPeople(self):
"""
Returns a dict of lists of people found.
2015-12-29 04:47:16 +11:00
{
'Director': list,
'Writer': list,
'Cast': list,
'Producer': list
2015-12-29 04:47:16 +11:00
}
"""
director = []
writer = []
cast = []
producer = []
for child in self.item:
try:
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'])
except KeyError:
log.warn('Malformed PMS answer for getPeople: %s: %s'
% (child.tag, child.attrib))
2015-12-29 04:47:16 +11:00
return {
'Director': director,
'Writer': writer,
'Cast': cast,
'Producer': producer
}
def getPeopleList(self):
"""
Returns a list of people from item, with a list item of the form
{
'Name': xxx,
'Type': xxx,
'Id': xxx
'imageurl': url to picture, None otherwise
('Role': xxx for cast/actors only, None if not found)
2015-12-29 04:47:16 +11:00
}
"""
people = []
# Key of library: Plex-identifier. Value represents the Kodi/emby side
people_of_interest = {
'Director': 'Director',
'Writer': 'Writer',
'Role': 'Actor',
'Producer': 'Producer'
}
for child in self.item:
if child.tag in people_of_interest.keys():
name = child.attrib['tag']
name_id = child.attrib['id']
Type = child.tag
2015-12-29 04:47:16 +11:00
Type = people_of_interest[Type]
2016-03-02 02:18:12 +11:00
url = child.attrib.get('thumb')
Role = child.attrib.get('role')
people.append({
'Name': name,
'Type': Type,
'Id': name_id,
'imageurl': url
})
if url:
people[-1].update({'imageurl': url})
if Role:
people[-1].update({'Role': Role})
2015-12-29 04:47:16 +11:00
return people
def getGenres(self):
"""
Returns a list of genres found. (Not a string)
2015-12-29 04:47:16 +11:00
"""
genre = []
for child in self.item:
if child.tag == 'Genre':
genre.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return genre
2016-02-07 22:38:50 +11:00
def getGuid(self):
return self.item.attrib.get('guid')
2016-01-10 02:14:02 +11:00
def getProvider(self, providername=None):
2015-12-29 04:47:16 +11:00
"""
providername: e.g. 'imdb', 'tvdb'
2015-12-29 04:47:16 +11:00
2016-02-03 23:01:13 +11:00
Return IMDB, e.g. "tt0903624". Returns None if not found
2015-12-29 04:47:16 +11:00
"""
item = self.item.attrib
2016-01-11 19:57:45 +11:00
try:
item = item['guid']
except KeyError:
return None
2016-01-10 02:14:02 +11:00
2016-02-03 23:01:13 +11:00
if providername == 'imdb':
2016-09-26 03:21:12 +10:00
regex = REGEX_IMDB
elif providername == 'tvdb':
# originally e.g. com.plexapp.agents.thetvdb://276564?lang=en
2016-09-26 03:21:12 +10:00
regex = REGEX_TVDB
2016-02-03 23:01:13 +11:00
else:
return None
2016-01-11 19:57:45 +11:00
provider = regex.findall(item)
2015-12-29 04:47:16 +11:00
try:
2016-01-10 02:14:02 +11:00
provider = provider[0]
2016-01-11 19:57:45 +11:00
except IndexError:
2015-12-29 04:47:16 +11:00
provider = None
return provider
def getTitle(self):
"""
Returns an item's name/title or "Missing Title Name".
Output:
title, sorttitle
sorttitle = title, if no sorttitle is found
"""
title = self.item.attrib.get('title', 'Missing Title Name')
sorttitle = self.item.attrib.get('titleSort', title)
2015-12-29 04:47:16 +11:00
return title, sorttitle
def getPlot(self):
"""
Returns the plot or None.
"""
return self.item.attrib.get('summary', None)
def getTagline(self):
"""
Returns a shorter tagline or None
"""
return self.item.attrib.get('tagline', None)
def getAudienceRating(self):
"""
2016-04-02 19:31:21 +11:00
Returns the audience rating, 'rating' itself or 0.0
"""
2016-03-18 02:15:48 +11:00
res = self.item.attrib.get('audienceRating')
if res is None:
2016-04-17 21:36:41 +10:00
res = self.item.attrib.get('rating')
try:
res = float(res)
except (ValueError, TypeError):
res = 0.0
2016-03-18 02:15:48 +11:00
return res
def getYear(self):
"""
Returns the production(?) year ("year") or None
"""
return self.item.attrib.get('year', None)
2018-01-22 21:20:37 +11:00
def getResume(self):
"""
Returns the resume point of time in seconds as int. 0 if not found
"""
try:
resume = float(self.item.attrib['viewOffset'])
except (KeyError, ValueError):
resume = 0.0
return int(resume * v.PLEX_TO_KODI_TIMEFACTOR)
2015-12-29 04:47:16 +11:00
def getRuntime(self):
"""
Resume point of time and runtime/totaltime in rounded to seconds.
2016-01-02 06:36:02 +11:00
Time from Plex server is measured in milliseconds.
Kodi: seconds
Output:
resume, runtime as ints. 0 if not found
2015-12-29 04:47:16 +11:00
"""
item = self.item.attrib
2016-01-04 05:17:59 +11:00
try:
runtime = float(item['duration'])
2016-04-17 21:36:41 +10:00
except (KeyError, ValueError):
runtime = 0.0
2015-12-29 04:47:16 +11:00
try:
2016-01-02 06:36:02 +11:00
resume = float(item['viewOffset'])
2016-04-17 21:36:41 +10:00
except (KeyError, ValueError):
2016-01-02 06:36:02 +11:00
resume = 0.0
runtime = int(runtime * v.PLEX_TO_KODI_TIMEFACTOR)
resume = int(resume * v.PLEX_TO_KODI_TIMEFACTOR)
2015-12-29 04:47:16 +11:00
return resume, runtime
def getMpaa(self):
"""
Get the content rating or None
"""
mpaa = self.item.attrib.get('contentRating', None)
2015-12-29 04:47:16 +11:00
# Convert more complex cases
if mpaa in ("NR", "UR"):
# Kodi seems to not like NR, but will accept Rated Not Rated
mpaa = "Rated Not Rated"
return mpaa
def getCountry(self):
"""
Returns a list of all countries found in item.
"""
country = []
for child in self.item:
if child.tag == 'Country':
country.append(child.attrib['tag'])
2015-12-29 04:47:16 +11:00
return country
2016-01-02 19:28:31 +11:00
def getPremiereDate(self):
"""
Returns the "originallyAvailableAt" or None
"""
2016-09-03 00:53:16 +10:00
return self.item.attrib.get('originallyAvailableAt')
2016-01-02 19:28:31 +11:00
2016-03-28 18:56:22 +11:00
def getMusicStudio(self):
return self.item.attrib.get('studio', '')
2015-12-29 04:47:16 +11:00
def getStudios(self):
"""
Returns a list with a single entry for the studio, or an empty list
"""
2015-12-29 04:47:16 +11:00
studio = []
try:
studio.append(self.getStudio(self.item.attrib['studio']))
2015-12-29 04:47:16 +11:00
except KeyError:
pass
return studio
def getStudio(self, studioName):
"""
Convert studio for Kodi to properly detect them
"""
2015-12-29 04:47:16 +11:00
studios = {
'abc (us)': "ABC",
'fox (us)': "FOX",
'mtv (us)': "MTV",
'showcase (ca)': "Showcase",
'wgn america': "WGN"
}
return studios.get(studioName.lower(), studioName)
def joinList(self, listobject):
"""
Smart-joins the listobject into a single string using a " / "
separator.
2015-12-29 04:47:16 +11:00
If the list is empty, smart_join returns an empty string.
"""
string = " / ".join(listobject)
return string
2016-02-03 23:01:13 +11:00
def getParentRatingKey(self):
return self.item.attrib.get('parentRatingKey', '')
2016-01-10 02:14:02 +11:00
def getEpisodeDetails(self):
"""
Call on a single episode.
Output: for the corresponding the TV show and season:
[
TV show key, Plex: 'grandparentRatingKey'
TV show title, Plex: 'grandparentTitle'
TV show season, Plex: 'parentIndex'
Episode number, Plex: 'index'
]
"""
item = self.item.attrib
2016-03-12 00:47:41 +11:00
key = item.get('grandparentRatingKey')
title = item.get('grandparentTitle')
season = item.get('parentIndex')
episode = item.get('index')
2016-02-03 23:01:13 +11:00
return key, title, season, episode
2015-12-29 04:47:16 +11:00
2016-02-20 02:10:19 +11:00
def addPlexHeadersToUrl(self, url, arguments={}):
"""
Takes an URL and optional arguments (also to be URL-encoded); returns
an extended URL with e.g. the Plex token included.
2016-01-30 06:07:21 +11:00
arguments overrule everything
"""
2017-01-25 02:53:50 +11:00
xargs = client.getXArgsDeviceInfo()
xargs.update(arguments)
2016-01-30 06:07:21 +11:00
if '?' not in url:
url = "%s?%s" % (url, urlencode(xargs))
else:
url = "%s&%s" % (url, urlencode(xargs))
return url
2016-02-20 02:10:19 +11:00
def addPlexCredentialsToUrl(self, url):
"""
Returns an extended URL with the Plex token included as 'X-Plex-Token='
url may or may not already contain a '?'
"""
2016-09-03 00:53:16 +10:00
if window('pms_token') == '':
2016-04-06 02:23:00 +10:00
return url
2016-02-20 02:10:19 +11:00
if '?' not in url:
2016-09-03 00:53:16 +10:00
url = "%s?X-Plex-Token=%s" % (url, window('pms_token'))
2016-02-20 02:10:19 +11:00
else:
2016-09-03 00:53:16 +10:00
url = "%s&X-Plex-Token=%s" % (url, window('pms_token'))
2016-02-20 02:10:19 +11:00
return url
2016-01-30 06:07:21 +11:00
def GetPlayQueueItemID(self):
"""
2016-01-30 06:07:21 +11:00
Returns current playQueueItemID for the item.
If not found, empty str is returned
"""
2016-12-28 23:14:21 +11:00
return self.item.attrib.get('playQueueItemID')
def getDataFromPartOrMedia(self, key):
"""
Retrieves XML data 'key' first from the active part. If unsuccessful,
tries to retrieve the data from the Media response part.
If all fails, None is returned.
"""
2016-02-03 23:01:13 +11:00
media = self.item[0].attrib
part = self.item[0][self.part].attrib
try:
try:
value = part[key]
except KeyError:
value = media[key]
except KeyError:
value = None
return value
def getVideoCodec(self):
"""
Returns the video codec and resolution for the child and part selected.
If any data is not found on a part-level, the Media-level data is
returned.
If that also fails (e.g. for old trailers, None is returned)
Output:
{
'videocodec': xxx, e.g. 'h264'
'resolution': xxx, e.g. '720' or '1080'
'height': xxx, e.g. '816'
'width': xxx, e.g. '1920'
'aspectratio': xxx, e.g. '1.78'
2016-05-16 00:30:09 +10:00
'bitrate': xxx, e.g. '10642'
'container': xxx e.g. 'mkv',
'bitDepth': xxx e.g. '8', '10'
}
"""
answ = {
'videocodec': self.getDataFromPartOrMedia('videoCodec'),
'resolution': self.getDataFromPartOrMedia('videoResolution'),
'height': self.getDataFromPartOrMedia('height'),
'width': self.getDataFromPartOrMedia('width'),
'aspectratio': self.getDataFromPartOrMedia('aspectratio'),
'bitrate': self.getDataFromPartOrMedia('bitrate'),
'container': self.getDataFromPartOrMedia('container'),
}
try:
answ['bitDepth'] = self.item[0][self.part][self.mediastream].attrib.get('bitDepth')
except:
answ['bitDepth'] = None
return answ
2016-01-12 21:47:48 +11:00
def getExtras(self):
"""
Currently ONLY returns the very first trailer found!
2016-01-12 23:57:26 +11:00
Returns a list of trailer and extras from PMS XML. Returns [] if
2016-01-12 21:47:48 +11:00
no extras are found.
Extratypes:
1: Trailer
5: Behind the scenes
2016-01-12 21:47:48 +11:00
Output: list of dicts with one entry of the form:
'key': e.g. /library/metadata/xxxx
'title':
'thumb': artwork
'duration':
'extraType':
'originallyAvailableAt':
'year':
"""
elements = []
extras = self.item.find('Extras')
if extras is None:
return elements
for extra in extras:
try:
extraType = int(extra.attrib['extraType'])
except:
extraType = None
if extraType != 1:
continue
key = extra.attrib.get('key', None)
title = extra.attrib.get('title', None)
thumb = extra.attrib.get('thumb', None)
duration = float(extra.attrib.get('duration', 0.0))
year = extra.attrib.get('year', None)
originallyAvailableAt = extra.attrib.get(
'originallyAvailableAt', None)
elements.append(
2016-09-03 00:53:16 +10:00
{
'key': key,
'title': title,
'thumb': thumb,
'duration': int(duration * v.PLEX_TO_KODI_TIMEFACTOR),
2016-09-03 00:53:16 +10:00
'extraType': extraType,
'originallyAvailableAt': originallyAvailableAt,
'year': year
})
break
2016-01-12 21:47:48 +11:00
return elements
2015-12-29 04:47:16 +11:00
def getMediaStreams(self):
"""
Returns the media streams for metadata purposes
Output: each track contains a dictionaries
{
2016-01-15 20:04:47 +11:00
'video': videotrack-list, 'codec', 'height', 'width',
'aspect', 'video3DFormat'
'audio': audiotrack-list, 'codec', 'channels',
'language'
'subtitle': list of subtitle languages (or "Unknown")
}
"""
2015-12-29 04:47:16 +11:00
videotracks = []
audiotracks = []
subtitlelanguages = []
try:
# Sometimes, aspectratio is on the "toplevel"
aspect = self.item[0].attrib.get('aspectRatio')
except IndexError:
# There is no stream info at all, returning empty
return {
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
# Loop over parts
for child in self.item[0]:
container = child.attrib.get('container')
# Loop over Streams
for grandchild in child:
stream = grandchild.attrib
media_type = int(stream.get('streamType', 999))
track = {}
if media_type == 1: # Video streams
if 'codec' in stream:
track['codec'] = stream['codec'].lower()
if "msmpeg4" in track['codec']:
track['codec'] = "divx"
elif "mpeg4" in track['codec']:
# if "simple profile" in profile or profile == "":
# track['codec'] = "xvid"
pass
elif "h264" in track['codec']:
if container in ("mp4", "mov", "m4v"):
track['codec'] = "avc1"
track['height'] = stream.get('height')
track['width'] = stream.get('width')
# track['Video3DFormat'] = item.get('Video3DFormat')
track['aspect'] = stream.get('aspectRatio', aspect)
track['duration'] = self.getRuntime()[1]
track['video3DFormat'] = None
videotracks.append(track)
elif media_type == 2: # Audio streams
if 'codec' in stream:
track['codec'] = stream['codec'].lower()
if ("dca" in track['codec'] and
"ma" in stream.get('profile', '').lower()):
track['codec'] = "dtshd_ma"
track['channels'] = stream.get('channels')
2016-03-10 22:47:30 +11:00
# 'unknown' if we cannot get language
track['language'] = stream.get(
2016-09-03 00:53:16 +10:00
'languageCode', lang(39310)).lower()
audiotracks.append(track)
elif media_type == 3: # Subtitle streams
2016-03-10 22:47:30 +11:00
# 'unknown' if we cannot get language
subtitlelanguages.append(
stream.get('languageCode', lang(39310)).lower())
return {
2015-12-29 04:47:16 +11:00
'video': videotracks,
'audio': audiotracks,
'subtitle': subtitlelanguages
}
2015-12-31 01:57:55 +11:00
def __getOneArtwork(self, entry):
if entry not in self.item.attrib:
return ''
artwork = self.item.attrib[entry]
if artwork.startswith('http'):
pass
else:
artwork = self.addPlexCredentialsToUrl(
"%s/photo/:/transcode?width=4000&height=4000&minSize=1&upscale=0&url=%s" % (self.server, artwork))
return artwork
def getAllArtwork(self, parentInfo=False):
"""
Gets the URLs to the Plex artwork, or empty string if not found.
parentInfo=True will check for parent's artwork if None is found
2015-12-31 01:57:55 +11:00
Output:
{
'Primary'
'Art'
'Banner'
'Logo'
'Thumb'
'Disc'
'Backdrop' : LIST with the first entry xml key "art"
}
"""
2015-12-31 01:57:55 +11:00
allartworks = {
'Primary': "", # corresponds to Plex poster ('thumb')
2015-12-31 01:57:55 +11:00
'Art': "",
'Banner': "", # corresponds to Plex banner ('banner') for series
2015-12-31 01:57:55 +11:00
'Logo': "",
'Thumb': "", # corresponds to Plex (grand)parent posters (thumb)
2015-12-31 01:57:55 +11:00
'Disc': "",
'Backdrop': [] # Corresponds to Plex fanart ('art')
2015-12-31 01:57:55 +11:00
}
# Process backdrops
# Get background artwork URL
allartworks['Backdrop'].append(self.__getOneArtwork('art'))
2015-12-31 01:57:55 +11:00
# Get primary "thumb" pictures:
allartworks['Primary'] = self.__getOneArtwork('thumb')
# Banner (usually only on tv series level)
allartworks['Banner'] = self.__getOneArtwork('banner')
# For e.g. TV shows, get series thumb
allartworks['Thumb'] = self.__getOneArtwork('grandparentThumb')
2015-12-31 01:57:55 +11:00
# Process parent items if the main item is missing artwork
if parentInfo:
# Process parent backdrops
if not allartworks['Backdrop']:
allartworks['Backdrop'].append(
self.__getOneArtwork('parentArt'))
2015-12-31 01:57:55 +11:00
if not allartworks['Primary']:
allartworks['Primary'] = self.__getOneArtwork('parentThumb')
return allartworks
2015-12-31 01:57:55 +11:00
def getFanartArtwork(self, allartworks, parentInfo=False):
"""
Downloads additional fanart from third party sources (well, link to
fanart only).
allartworks = {
'Primary': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
}
"""
externalId = self.getExternalItemId()
if externalId is not None:
allartworks = self.getFanartTVArt(externalId, allartworks)
return allartworks
def getExternalItemId(self, collection=False):
"""
Returns the item's IMDB id for movies or tvdb id for TV shows
If not found in item's Plex metadata, check themovidedb.org
collection=True will try to return the three-tuple:
collection ID, poster-path, background-path
None is returned if unsuccessful
"""
item = self.item.attrib
media_type = item.get('type')
mediaId = None
# Return the saved Plex id's, if applicable
# Always seek collection's ids since not provided by PMS
if collection is False:
if media_type == v.PLEX_TYPE_MOVIE:
mediaId = self.getProvider('imdb')
elif media_type == v.PLEX_TYPE_SHOW:
mediaId = self.getProvider('tvdb')
if mediaId is not None:
return mediaId
2016-09-03 00:53:16 +10:00
log.info('Plex did not provide ID for IMDB or TVDB. Start '
'lookup process')
else:
2017-05-31 17:56:23 +10:00
log.info('Start movie set/collection lookup on themoviedb using %s'
% item.get('title', ''))
2016-09-03 00:53:16 +10:00
apiKey = settings('themoviedbAPIKey')
if media_type == v.PLEX_TYPE_SHOW:
media_type = 'tv'
title = item.get('title', '')
# if the title has the year in remove it as tmdb cannot deal with it...
# replace e.g. 'The Americans (2015)' with 'The Americans'
2017-01-25 04:00:35 +11:00
title = sub(r'\s*\(\d{4}\)$', '', title, count=1)
2017-05-31 18:05:50 +10:00
url = 'https://api.themoviedb.org/3/search/%s' % media_type
parameters = {
'api_key': apiKey,
'language': v.KODILANGUAGE,
2016-09-03 00:53:16 +10:00
'query': tryEncode(title)
}
2017-01-25 04:00:35 +11:00
data = DownloadUtils().downloadUrl(
url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data.get('test')
except:
2016-09-03 00:53:16 +10:00
log.error('Could not download data from FanartTV')
return
if data.get('results') is None:
2016-09-03 00:53:16 +10:00
log.info('No match found on themoviedb for type: %s, title: %s'
% (media_type, title))
return
year = item.get('year')
matchFound = None
# find year match
if year is not None:
for entry in data["results"]:
if year in entry.get("first_air_date", ""):
matchFound = entry
break
elif year in entry.get("release_date", ""):
matchFound = entry
break
# find exact match based on title, if we haven't found a year match
if matchFound is None:
2016-09-03 00:53:16 +10:00
log.info('No themoviedb match found using year %s' % year)
replacements = (
' ',
'-',
'&',
',',
':',
';'
)
for entry in data["results"]:
name = entry.get("name", entry.get("title", ""))
original_name = entry.get("original_name", "")
title_alt = title.lower()
name_alt = name.lower()
org_name_alt = original_name.lower()
for replaceString in replacements:
title_alt = title_alt.replace(replaceString, '')
name_alt = name_alt.replace(replaceString, '')
org_name_alt = org_name_alt.replace(replaceString, '')
if name == title or original_name == title:
# match found for exact title name
matchFound = entry
break
elif (name.split(" (")[0] == title or title_alt == name_alt
or title_alt == org_name_alt):
# match found with substituting some stuff
matchFound = entry
break
# if a match was not found, we accept the closest match from TMDB
if matchFound is None and len(data.get("results")) > 0:
2016-09-03 00:53:16 +10:00
log.info('Using very first match from themoviedb')
matchFound = entry = data.get("results")[0]
if matchFound is None:
2016-09-03 00:53:16 +10:00
log.info('Still no themoviedb match for type: %s, title: %s, '
'year: %s' % (media_type, title, year))
log.debug('themoviedb answer was %s' % data['results'])
return
2016-09-03 00:53:16 +10:00
log.info('Found themoviedb match for %s: %s'
% (item.get('title'), matchFound))
tmdbId = str(entry.get("id", ""))
if tmdbId == '':
2016-09-03 00:53:16 +10:00
log.error('No themoviedb ID found, aborting')
return
if media_type == "multi" and entry.get("media_type"):
media_type = entry.get("media_type")
name = entry.get("name", entry.get("title"))
# lookup external tmdbId and perform artwork lookup on fanart.tv
parameters = {
'api_key': apiKey
}
for language in [v.KODILANGUAGE, "en"]:
parameters['language'] = language
if media_type == "movie":
2017-05-31 18:10:54 +10:00
url = 'https://api.themoviedb.org/3/movie/%s' % tmdbId
parameters['append_to_response'] = 'videos'
elif media_type == "tv":
2017-05-31 18:10:54 +10:00
url = 'https://api.themoviedb.org/3/tv/%s' % tmdbId
parameters['append_to_response'] = 'external_ids,videos'
2017-01-25 04:00:35 +11:00
data = DownloadUtils().downloadUrl(
url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data.get('test')
except:
2016-09-03 00:53:16 +10:00
log.error('Could not download %s with parameters %s'
% (url, parameters))
continue
2016-06-09 04:34:49 +10:00
if collection is False:
if data.get("imdb_id") is not None:
mediaId = str(data.get("imdb_id"))
2016-06-07 06:10:58 +10:00
break
if data.get("external_ids") is not None:
mediaId = str(data["external_ids"].get("tvdb_id"))
2016-06-07 06:10:58 +10:00
break
else:
if data.get("belongs_to_collection") is None:
continue
mediaId = str(data.get("belongs_to_collection").get("id"))
log.debug('Retrieved collections tmdb id %s for %s'
% (mediaId, title))
url = 'https://api.themoviedb.org/3/collection/%s' % mediaId
data = DownloadUtils().downloadUrl(
url,
authenticate=False,
parameters=parameters,
timeout=7)
try:
data.get('poster_path')
except AttributeError:
log.info('Could not find TheMovieDB poster paths for %s in'
'the language %s' % (title, language))
continue
else:
poster = 'https://image.tmdb.org/t/p/original%s' % data.get('poster_path')
background = 'https://image.tmdb.org/t/p/original%s' % data.get('backdrop_path')
mediaId = mediaId, poster, background
break
2016-06-07 06:10:58 +10:00
return mediaId
def getFanartTVArt(self, mediaId, allartworks, setInfo=False):
"""
perform artwork lookup on fanart.tv
mediaId: IMDB id for movies, tvdb id for TV shows
"""
item = self.item.attrib
2016-09-03 00:53:16 +10:00
api_key = settings('FanArtTVAPIKey')
typus = item.get('type')
if typus == 'show':
typus = 'tv'
if typus == "movie":
url = 'http://webservice.fanart.tv/v3/movies/%s?api_key=%s' \
% (mediaId, api_key)
elif typus == 'tv':
url = 'http://webservice.fanart.tv/v3/tv/%s?api_key=%s' \
% (mediaId, api_key)
else:
# Not supported artwork
return allartworks
2017-01-25 04:06:17 +11:00
data = DownloadUtils().downloadUrl(
url,
authenticate=False,
timeout=15)
try:
data.get('test')
except:
2016-09-03 00:53:16 +10:00
log.error('Could not download data from FanartTV')
return allartworks
# we need to use a little mapping between fanart.tv arttypes and kodi
# artttypes
fanartTVTypes = [
("logo", "Logo"),
("musiclogo", "clearlogo"),
("disc", "Disc"),
("clearart", "Art"),
("banner", "Banner"),
("clearlogo", "Logo"),
("background", "fanart"),
("showbackground", "fanart"),
("characterart", "characterart")
]
if typus == "artist":
fanartTVTypes.append(("thumb", "folder"))
else:
fanartTVTypes.append(("thumb", "Thumb"))
2016-06-07 06:10:58 +10:00
if setInfo:
fanartTVTypes.append(("poster", "Primary"))
prefixes = (
"hd" + typus,
"hd",
typus,
"",
)
for fanarttype in fanartTVTypes:
# Skip the ones we already have
if allartworks.get(fanarttype[1]):
continue
for prefix in prefixes:
fanarttvimage = prefix + fanarttype[0]
if fanarttvimage not in data:
continue
# select image in preferred language
for entry in data[fanarttvimage]:
if entry.get("lang") == v.KODILANGUAGE:
2016-11-17 22:21:15 +11:00
allartworks[fanarttype[1]] = entry.get("url", "").replace(' ', '%20')
break
# just grab the first english OR undefinded one as fallback
# (so we're actually grabbing the more popular one)
if not allartworks.get(fanarttype[1]):
for entry in data[fanarttvimage]:
if entry.get("lang") in ("en", "00"):
2016-11-17 22:21:15 +11:00
allartworks[fanarttype[1]] = entry.get("url", "").replace(' ', '%20')
break
# grab extrafanarts in list
maxfanarts = 10
fanartcount = 0
for prefix in prefixes:
fanarttvimage = prefix + 'background'
if fanarttvimage not in data:
continue
for entry in data[fanarttvimage]:
if entry.get("url") is None:
continue
if fanartcount > maxfanarts:
break
allartworks['Backdrop'].append(
entry['url'].replace(' ', '%20'))
fanartcount += 1
return allartworks
2016-01-02 00:40:40 +11:00
2016-06-07 06:10:58 +10:00
def getSetArtwork(self, parentInfo=False):
"""
Gets the URLs to the Plex artwork, or empty string if not found.
parentInfo=True will check for parent's artwork if None is found
Only call on movies
2016-06-07 06:10:58 +10:00
Output:
{
'Primary'
'Art'
'Banner'
'Logo'
'Thumb'
'Disc'
'Backdrop' : LIST with the first entry xml key "art"
}
"""
allartworks = {
'Primary': "",
'Art': "",
'Banner': "",
'Logo': "",
'Thumb': "",
'Disc': "",
'Backdrop': []
}
# Plex does not get much artwork - go ahead and get the rest from
# fanart tv only for movie or tv show
externalId = self.getExternalItemId(collection=True)
if externalId is not None:
try:
externalId, poster, background = externalId
except TypeError:
poster, background = None, None
if poster is not None:
allartworks['Primary'] = poster
if background is not None:
allartworks['Backdrop'].append(background)
allartworks = self.getFanartTVArt(externalId, allartworks, True)
else:
log.info('Did not find a set/collection ID on TheMovieDB using %s.'
2017-05-31 20:56:15 +10:00
' Artwork will be missing.' % self.getTitle()[0])
2017-05-31 20:57:53 +10:00
return allartworks
2016-06-07 06:10:58 +10:00
2016-04-17 21:36:41 +10:00
def shouldStream(self):
"""
Returns True if the item's 'optimizedForStreaming' is set, False other-
wise
"""
return self.item[0].attrib.get('optimizedForStreaming') == '1'
2016-04-17 21:36:41 +10:00
def getMediastreamNumber(self):
"""
Returns the Media stream as an int (mostly 0). Will let the user choose
if several media streams are present for a PMS item (if settings are
set accordingly)
"""
# How many streams do we have?
count = 0
for entry in self.item.findall('./Media'):
count += 1
if (count > 1 and (
(self.getType() != 'clip' and
2016-09-03 00:53:16 +10:00
settings('bestQuality') == 'false')
or
(self.getType() == 'clip' and
2016-09-03 00:53:16 +10:00
settings('bestTrailer') == 'false'))):
# Several streams/files available.
dialoglist = []
for entry in self.item.findall('./Media'):
# Get additional info (filename / languages)
2017-04-03 04:10:10 +10:00
filename = None
if 'file' in entry[0].attrib:
filename = basename(entry[0].attrib['file'])
2017-04-03 04:10:10 +10:00
# Languages of audio streams
languages = []
for stream in entry[0]:
if (stream.attrib['streamType'] == '1' and
'language' in stream.attrib):
languages.append(stream.attrib['language'])
languages = ', '.join(languages)
if filename:
option = tryEncode(filename)
if languages:
if option:
option = '%s (%s): ' % (option, tryEncode(languages))
else:
option = '%s: ' % tryEncode(languages)
if 'videoResolution' in entry.attrib:
option = '%s%sp ' % (option,
entry.attrib.get('videoResolution'))
if 'videoCodec' in entry.attrib:
option = '%s%s' % (option,
entry.attrib.get('videoCodec'))
option = option.strip() + ' - '
if 'audioProfile' in entry.attrib:
option = '%s%s ' % (option,
entry.attrib.get('audioProfile'))
if 'audioCodec' in entry.attrib:
option = '%s%s ' % (option,
entry.attrib.get('audioCodec'))
dialoglist.append(option)
media = xbmcgui.Dialog().select('Select stream', dialoglist)
else:
media = 0
self.mediastream = media
return media
def getTranscodeVideoPath(self, action, quality=None):
2016-01-02 00:40:40 +11:00
"""
2016-02-03 23:01:13 +11:00
To be called on a VIDEO level of PMS xml response!
Transcode Video support; returns the URL to get a media started
2016-01-02 00:40:40 +11:00
Input:
2016-04-12 02:57:20 +10:00
action 'DirectStream' or 'Transcode'
2016-01-04 05:17:59 +11:00
quality: {
2016-02-05 06:23:04 +11:00
'videoResolution': e.g. '1024x768',
'videoQuality': e.g. '60',
'maxVideoBitrate': e.g. '2000' (in kbits)
}
(one or several of these options)
2016-01-02 00:40:40 +11:00
Output:
final URL to pull in PMS transcoder
2016-01-04 05:17:59 +11:00
TODO: mediaIndex
2016-01-02 00:40:40 +11:00
"""
if self.mediastream is None:
self.getMediastreamNumber()
if quality is None:
quality = {}
2017-01-25 02:53:50 +11:00
xargs = client.getXArgsDeviceInfo()
2016-02-05 06:23:04 +11:00
# For DirectPlay, path/key of PART is needed
# trailers are 'clip' with PMS xmls
2016-04-12 02:57:20 +10:00
if action == "DirectStream":
path = self.item[self.mediastream][self.part].attrib['key']
2016-02-03 23:01:13 +11:00
url = self.server + path
2016-02-07 22:38:50 +11:00
# e.g. Trailers already feature an '?'!
2016-02-03 23:01:13 +11:00
if '?' in url:
url += '&' + urlencode(xargs)
else:
url += '?' + urlencode(xargs)
return url
2016-04-14 00:14:55 +10:00
# For Transcoding
headers = {
'X-Plex-Platform': 'Android',
'X-Plex-Platform-Version': '7.0',
'X-Plex-Product': 'Plex for Android',
'X-Plex-Version': '5.8.0.475'
}
2016-02-05 06:23:04 +11:00
# Path/key to VIDEO item of xml PMS response is needed, not part
path = self.item.attrib['key']
2016-01-02 00:40:40 +11:00
transcodePath = self.server + \
2016-02-07 22:38:50 +11:00
'/video/:/transcode/universal/start.m3u8?'
2016-01-02 00:40:40 +11:00
args = {
'audioBoost': settings('audioBoost'),
'autoAdjustQuality': 0,
'directPlay': 0,
'directStream': 1,
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
2017-01-25 02:53:50 +11:00
'session': window('plex_client_Id'),
'fastSeek': 1,
2016-01-02 00:40:40 +11:00
'path': path,
'mediaIndex': self.mediastream,
'partIndex': self.part,
'hasMDE': 1,
'location': 'lan',
2017-05-02 03:51:10 +10:00
'subtitleSize': settings('subtitleSize')
# 'copyts': 1,
2016-02-07 22:38:50 +11:00
# 'offset': 0, # Resume point
2016-01-02 00:40:40 +11:00
}
# Look like Android to let the PMS use the transcoding profile
xargs.update(headers)
log.debug("Setting transcode quality to: %s" % quality)
args.update(quality)
url = transcodePath + urlencode(xargs) + '&' + urlencode(args)
return url
2016-01-02 00:40:40 +11:00
2018-01-08 03:50:30 +11:00
def externalSubs(self):
2016-01-02 00:40:40 +11:00
externalsubs = []
try:
2016-11-06 02:38:56 +11:00
mediastreams = self.item[0][self.part]
2016-01-02 00:40:40 +11:00
except (TypeError, KeyError, IndexError):
return
kodiindex = 0
fileindex = 0
2016-01-02 00:40:40 +11:00
for stream in mediastreams:
# Since plex returns all possible tracks together, have to pull
# only external subtitles - only for these a 'key' exists
if stream.attrib.get('streamType') != "3":
# Not a subtitle
continue
# Only set for additional external subtitles NOT lying beside video
2016-02-07 22:38:50 +11:00
key = stream.attrib.get('key')
# Only set for dedicated subtitle files lying beside video
# ext = stream.attrib.get('format')
if key:
# We do know the language - temporarily download
if stream.attrib.get('languageCode') is not None:
2017-05-02 03:51:10 +10:00
path = self.download_external_subtitles(
"{server}%s" % key,
"subtitle%02d.%s.%s" % (fileindex,
stream.attrib['languageCode'],
stream.attrib['codec']))
fileindex += 1
# We don't know the language - no need to download
else:
path = self.addPlexCredentialsToUrl(
"%s%s" % (self.server, key))
externalsubs.append(path)
2016-01-02 00:40:40 +11:00
kodiindex += 1
2018-01-08 03:50:30 +11:00
log.info('Found external subs: %s', externalsubs)
2016-01-02 00:40:40 +11:00
return externalsubs
2016-03-15 03:47:05 +11:00
@staticmethod
2017-05-02 03:51:10 +10:00
def download_external_subtitles(url, filename):
"""
One cannot pass the subtitle language for ListItems. Workaround; will
download the subtitle at url to the Kodi PKC directory in a temp dir
Returns the path to the downloaded subtitle or None
"""
if not exists_dir(v.EXTERNAL_SUBTITLE_TEMP_PATH):
makedirs(v.EXTERNAL_SUBTITLE_TEMP_PATH)
path = join(v.EXTERNAL_SUBTITLE_TEMP_PATH, filename)
r = DownloadUtils().downloadUrl(url, return_response=True)
try:
r.status_code
except AttributeError:
log.error('Could not temporarily download subtitle %s' % url)
return
else:
2018-01-08 03:50:30 +11:00
log.debug('Writing temp subtitle to %s', path)
try:
with open(path, 'wb') as f:
f.write(r.content)
except UnicodeEncodeError:
2018-01-08 03:50:30 +11:00
log.debug('Need to slugify the filename %s', path)
path = slugify(path)
with open(path, 'wb') as f:
f.write(r.content)
return path
def GetKodiPremierDate(self):
2016-06-13 01:22:22 +10:00
"""
Takes Plex' originallyAvailableAt of the form "yyyy-mm-dd" and returns
Kodi's "dd.mm.yyyy"
2016-06-13 01:22:22 +10:00
"""
date = self.getPremiereDate()
if date is None:
return
2016-06-13 01:22:22 +10:00
try:
2017-01-25 04:00:35 +11:00
date = sub(r'(\d+)-(\d+)-(\d+)', r'\3.\2.\1', date)
2016-06-13 01:22:22 +10:00
except:
date = None
return date
2016-06-13 01:22:22 +10:00
2017-03-19 22:14:16 +11:00
def CreateListItemFromPlexItem(self,
listItem=None,
appendShowTitle=False,
appendSxxExx=False):
if self.getType() == v.PLEX_TYPE_PHOTO:
listItem = self.__createPhotoListItem(listItem)
# Only set the bare minimum of artwork
listItem.setArt({'icon': 'DefaultPicture.png',
'fanart': self.__getOneArtwork('thumb')})
else:
listItem = self.__createVideoListItem(listItem,
appendShowTitle,
appendSxxExx)
self.add_video_streams(listItem)
self.set_listitem_artwork(listItem)
return listItem
def __createPhotoListItem(self, listItem=None):
"""
Use for photo items only
2016-03-15 03:47:05 +11:00
"""
title, _ = self.getTitle()
if listItem is None:
listItem = xbmcgui.ListItem(title)
else:
listItem.setLabel(title)
metadata = {
'date': self.GetKodiPremierDate(),
'size': long(self.item[0][0].attrib.get('size', 0)),
'exif:width': self.item[0].attrib.get('width', ''),
'exif:height': self.item[0].attrib.get('height', ''),
}
2017-03-19 22:14:16 +11:00
listItem.setInfo(type='image', infoLabels=metadata)
listItem.setProperty('plot', self.getPlot())
listItem.setProperty('plexid', self.getRatingKey())
return listItem
2017-03-19 22:14:16 +11:00
def __createVideoListItem(self,
listItem=None,
appendShowTitle=False,
appendSxxExx=False):
"""
Use for video items only
2016-03-15 03:47:05 +11:00
Call on a child level of PMS xml response (e.g. in a for loop)
listItem : existing xbmcgui.ListItem to work with
otherwise, a new one is created
appendShowTitle : True to append TV show title to episode title
appendSxxExx : True to append SxxExx to episode title
2016-03-15 03:47:05 +11:00
Returns XBMC listitem for this PMS library item
"""
title, sorttitle = self.getTitle()
2016-06-13 01:22:22 +10:00
typus = self.getType()
2016-03-15 03:47:05 +11:00
if listItem is None:
listItem = xbmcgui.ListItem(title)
2017-03-19 22:14:16 +11:00
else:
listItem.setLabel(title)
2017-03-19 22:54:29 +11:00
# Necessary; Kodi won't start video otherwise!
listItem.setProperty('IsPlayable', 'true')
# Video items, e.g. movies and episodes or clips
people = self.getPeople()
userdata = self.getUserData()
metadata = {
'genre': self.joinList(self.getGenres()),
'year': self.getYear(),
'rating': self.getAudienceRating(),
'playcount': userdata['PlayCount'],
'cast': people['Cast'],
'director': self.joinList(people.get('Director')),
'plot': self.getPlot(),
'sorttitle': sorttitle,
'duration': userdata['Runtime'],
'studio': self.joinList(self.getStudios()),
'tagline': self.getTagline(),
'writer': self.joinList(people.get('Writer')),
'premiered': self.getPremiereDate(),
'dateadded': self.getDateCreated(),
'lastplayed': userdata['LastPlayedDate'],
'mpaa': self.getMpaa(),
'aired': self.getPremiereDate()
}
2018-01-22 21:20:37 +11:00
# Do NOT set resumetime - otherwise Kodi always resumes at that time
# even if the user chose to start element from the beginning
# listItem.setProperty('resumetime', str(userdata['Resume']))
listItem.setProperty('totaltime', str(userdata['Runtime']))
2016-03-15 03:47:05 +11:00
2017-03-19 22:14:16 +11:00
if typus == v.PLEX_TYPE_EPISODE:
2016-03-15 03:47:05 +11:00
key, show, season, episode = self.getEpisodeDetails()
season = -1 if season is None else int(season)
episode = -1 if episode is None else int(episode)
metadata['episode'] = episode
metadata['season'] = season
2016-03-15 03:47:05 +11:00
metadata['tvshowtitle'] = show
if season and episode:
listItem.setProperty('episodeno',
"s%.2de%.2d" % (season, episode))
if appendSxxExx is True:
title = "S%.2dE%.2d - %s" % (season, episode, title)
2017-01-03 00:07:24 +11:00
listItem.setArt({'icon': 'DefaultTVShows.png'})
if appendShowTitle is True:
title = "%s - %s " % (show, title)
2017-03-19 22:14:16 +11:00
if appendShowTitle or appendSxxExx:
listItem.setLabel(title)
elif typus == v.PLEX_TYPE_MOVIE:
2017-01-03 00:07:24 +11:00
listItem.setArt({'icon': 'DefaultMovies.png'})
else:
2016-06-13 01:22:22 +10:00
# E.g. clips, trailers, ...
2017-01-03 00:07:24 +11:00
listItem.setArt({'icon': 'DefaultVideo.png'})
plexId = self.getRatingKey()
2016-05-31 16:06:42 +10:00
listItem.setProperty('plexid', plexId)
with plexdb.Get_Plex_DB() as plex_db:
try:
listItem.setProperty('dbid',
str(plex_db.getItem_byId(plexId)[0]))
except TypeError:
pass
# Expensive operation
metadata['title'] = title
listItem.setInfo('video', infoLabels=metadata)
2017-08-03 02:54:05 +10:00
try:
# Add context menu entry for information screen
2017-08-03 04:01:13 +10:00
listItem.addContextMenuItems([(lang(30032), 'XBMC.Action(Info)',)])
2017-08-03 02:54:05 +10:00
except TypeError:
# Kodi fuck-up
pass
2016-03-15 03:47:05 +11:00
return listItem
2017-03-19 22:14:16 +11:00
def add_video_streams(self, listItem):
2016-03-15 03:47:05 +11:00
"""
Add media stream information to xbmcgui.ListItem
"""
2017-10-10 07:12:30 +11:00
for key, value in self.getMediaStreams().iteritems():
if value:
listItem.addStreamInfo(key, value)
2016-03-17 04:33:18 +11:00
2017-05-07 01:22:29 +10:00
def validatePlayurl(self, path, typus, forceCheck=False, folder=False,
omitCheck=False):
2016-03-17 04:33:18 +11:00
"""
Returns a valid path for Kodi, e.g. with '\' substituted to '\\' in
Unicode. Returns None if this is not possible
path : Unicode
typus : Plex type from PMS xml
forceCheck : Will always try to check validity of path
Will also skip confirmation dialog if path not found
folder : Set to True if path is a folder
2017-05-07 01:22:29 +10:00
omitCheck : Will entirely omit validity check if True
2016-03-17 04:33:18 +11:00
"""
if path is None:
return None
typus = v.REMAP_TYPE_FROM_PLEXTYPE[typus]
if state.REMAP_PATH is True:
path = path.replace(getattr(state, 'remapSMB%sOrg' % typus),
getattr(state, 'remapSMB%sNew' % typus),
1)
2016-03-17 04:33:18 +11:00
# There might be backslashes left over:
path = path.replace('\\', '/')
elif state.REPLACE_SMB_PATH is True:
if path.startswith('\\\\'):
path = 'smb:' + path.replace('\\', '/')
2017-08-19 23:03:19 +10:00
if ((state.PATH_VERIFIED and forceCheck is False) or
2017-05-07 01:22:29 +10:00
omitCheck is True):
return path
# exist() needs a / or \ at the end to work for directories
if folder is False:
# files
check = exists(tryEncode(path))
else:
# directories
if "\\" in path:
if not path.endswith('\\'):
# Add the missing backslash
check = exists_dir(path + "\\")
else:
check = exists_dir(path)
else:
if not path.endswith('/'):
check = exists_dir(path + "/")
else:
check = exists_dir(path)
if not check:
if forceCheck is False:
# Validate the path is correct with user intervention
if self.askToValidate(path):
state.STOP_SYNC = True
path = None
2017-08-19 23:03:19 +10:00
state.PATH_VERIFIED = True
else:
path = None
elif forceCheck is False:
2017-08-19 23:03:19 +10:00
# Only set the flag if we were not force-checking the path
state.PATH_VERIFIED = True
return path
2016-03-17 04:33:18 +11:00
def askToValidate(self, url):
"""
Displays a YESNO dialog box:
Kodi can't locate file: <url>. Please verify the path.
You may need to verify your network credentials in the
add-on settings or use different Plex paths. Stop syncing?
Returns True if sync should stop, else False
"""
2016-09-03 00:53:16 +10:00
log.warn('Cannot access file: %s' % url)
2016-03-17 04:33:18 +11:00
resp = xbmcgui.Dialog().yesno(
2017-01-25 04:04:17 +11:00
heading=lang(29999),
2016-09-03 00:53:16 +10:00
line1=lang(39031) + url,
line2=lang(39032))
2016-03-17 04:33:18 +11:00
return resp
2017-01-09 01:03:41 +11:00
def set_listitem_artwork(self, listitem):
"""
Set all artwork to the listitem
"""
allartwork = self.getAllArtwork(parentInfo=True)
arttypes = {
'poster': "Primary",
'tvshow.poster': "Thumb",
'clearart': "Art",
'tvshow.clearart': "Art",
'clearart': "Primary",
'tvshow.clearart': "Primary",
'clearlogo': "Logo",
'tvshow.clearlogo': "Logo",
'discart': "Disc",
'fanart_image': "Backdrop",
'landscape': "Backdrop",
"banner": "Banner"
}
for arttype in arttypes:
art = arttypes[arttype]
if art == "Backdrop":
try:
# Backdrop is a list, grab the first backdrop
self._set_listitem_artprop(listitem,
arttype,
allartwork[art][0])
except:
pass
else:
self._set_listitem_artprop(listitem, arttype, allartwork[art])
def _set_listitem_artprop(self, listitem, arttype, path):
if arttype in (
'thumb', 'fanart_image', 'small_poster', 'tiny_poster',
'medium_landscape', 'medium_poster', 'small_fanartimage',
'medium_fanartimage', 'fanart_noindicators'):
listitem.setProperty(arttype, path)
else:
listitem.setArt({arttype: path})
def set_playback_win_props(self, playurl, listitem):
"""
Set all properties necessary for plugin path playback for listitem
"""
itemtype = self.getType()
userdata = self.getUserData()
plexitem = "plex_%s" % playurl
window('%s.runtime' % plexitem, value=str(userdata['Runtime']))
window('%s.type' % plexitem, value=itemtype)
2017-12-14 06:14:27 +11:00
state.PLEX_IDS[tryDecode(playurl)] = self.getRatingKey()
# window('%s.itemid' % plexitem, value=self.getRatingKey())
2017-01-09 01:03:41 +11:00
window('%s.playcount' % plexitem, value=str(userdata['PlayCount']))
if itemtype == v.PLEX_TYPE_EPISODE:
2017-01-09 01:03:41 +11:00
window('%s.refreshid' % plexitem, value=self.getParentRatingKey())
else:
window('%s.refreshid' % plexitem, value=self.getRatingKey())
# Append external subtitles to stream
playmethod = window('%s.playmethod' % plexitem)
# Direct play automatically appends external
2017-05-01 21:10:28 +10:00
# BUT: Plex may add additional subtitles NOT lying right next to video
2017-01-09 01:03:41 +11:00
if playmethod in ("DirectStream", "DirectPlay"):
listitem.setSubtitles(self.externalSubs(playurl))