2016-01-28 02:33:02 +11:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from urllib import urlencode
|
2016-01-30 06:07:21 +11:00
|
|
|
from ast import literal_eval
|
|
|
|
from urlparse import urlparse, parse_qs
|
|
|
|
import re
|
2016-03-15 00:51:49 +11:00
|
|
|
from copy import deepcopy
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
from xbmcaddon import Addon
|
|
|
|
|
2016-01-30 06:07:21 +11:00
|
|
|
import downloadutils
|
2016-02-03 23:01:13 +11:00
|
|
|
from utils import logMsg, settings
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
|
|
|
|
addonName = Addon().getAddonInfo('name')
|
|
|
|
title = "%s %s" % (addonName, __name__)
|
|
|
|
|
|
|
|
|
2016-01-30 06:07:21 +11:00
|
|
|
def PlexToKodiTimefactor():
|
|
|
|
"""
|
|
|
|
Kodi measures time in seconds, but Plex in milliseconds
|
|
|
|
"""
|
|
|
|
return 1.0 / 1000.0
|
|
|
|
|
|
|
|
|
2016-02-01 02:13:40 +11:00
|
|
|
def ConvertPlexToKodiTime(plexTime):
|
|
|
|
"""
|
|
|
|
Converts Plextime to Koditime. Returns an int (in seconds).
|
|
|
|
"""
|
|
|
|
return int(float(plexTime) * PlexToKodiTimefactor())
|
|
|
|
|
|
|
|
|
2016-01-28 06:41:28 +11:00
|
|
|
def GetItemClassFromType(itemType):
|
|
|
|
classes = {
|
|
|
|
'movie': 'Movies',
|
2016-03-02 02:18:12 +11:00
|
|
|
'season': 'TVShows',
|
2016-01-28 06:41:28 +11:00
|
|
|
'episode': 'TVShows',
|
2016-02-13 02:53:49 +11:00
|
|
|
'show': 'TVShows',
|
|
|
|
'artist': 'Music',
|
|
|
|
'album': 'Music',
|
|
|
|
'track': 'Music'
|
2016-01-28 06:41:28 +11:00
|
|
|
}
|
|
|
|
return classes[itemType]
|
|
|
|
|
|
|
|
|
2016-02-05 01:57:24 +11:00
|
|
|
def GetKodiTypeFromPlex(plexItemType):
|
2016-02-19 22:31:55 +11:00
|
|
|
"""
|
|
|
|
As used in playlist.item here: http://kodi.wiki/view/JSON-RPC_API
|
|
|
|
"""
|
2016-02-09 05:40:58 +11:00
|
|
|
classes = {
|
|
|
|
'movie': 'movie',
|
|
|
|
'episode': 'episode',
|
2016-02-19 22:31:55 +11:00
|
|
|
'track': 'song',
|
|
|
|
'artist': 'artist',
|
|
|
|
'album': 'album',
|
|
|
|
'XXXXXX': 'musicvideo',
|
|
|
|
'XXXXXXX': 'genre'
|
2016-02-09 05:40:58 +11:00
|
|
|
}
|
|
|
|
return classes[plexItemType.lower()]
|
|
|
|
|
|
|
|
|
2016-01-30 06:07:21 +11:00
|
|
|
def GetPlexKeyNumber(plexKey):
|
|
|
|
"""
|
|
|
|
Deconstructs e.g. '/library/metadata/xxxx' to the tuple
|
|
|
|
|
|
|
|
('library/metadata', 'xxxx')
|
|
|
|
|
|
|
|
Returns ('','') if nothing is found
|
|
|
|
"""
|
|
|
|
regex = re.compile(r'''/(.+)/(\d+)$''')
|
|
|
|
try:
|
|
|
|
result = regex.findall(plexKey)[0]
|
|
|
|
except IndexError:
|
|
|
|
result = ('', '')
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def ParseContainerKey(containerKey):
|
|
|
|
"""
|
|
|
|
Parses e.g. /playQueues/3045?own=1&repeat=0&window=200 to:
|
|
|
|
'playQueues', '3045', {'window': ['200'], 'own': ['1'], 'repeat': ['0']}
|
|
|
|
|
|
|
|
Output hence: library, key, query (query as a special dict)
|
|
|
|
"""
|
|
|
|
result = urlparse(containerKey)
|
|
|
|
library, key = GetPlexKeyNumber(result.path)
|
|
|
|
query = parse_qs(result.query)
|
|
|
|
return library, key, query
|
|
|
|
|
|
|
|
|
|
|
|
def LiteralEval(string):
|
|
|
|
"""
|
|
|
|
Turns a string e.g. in a dict, safely :-)
|
|
|
|
"""
|
|
|
|
return literal_eval(string)
|
|
|
|
|
|
|
|
|
2016-01-28 06:41:28 +11:00
|
|
|
def GetMethodFromPlexType(plexType):
|
|
|
|
methods = {
|
|
|
|
'movie': 'add_update',
|
2016-02-07 22:38:50 +11:00
|
|
|
'episode': 'add_updateEpisode',
|
|
|
|
'show': 'add_update',
|
2016-02-13 02:53:49 +11:00
|
|
|
'season': 'add_updateSeason',
|
|
|
|
'track': 'add_updateSong',
|
|
|
|
'album': 'add_updateAlbum',
|
|
|
|
'artist': 'add_updateArtist'
|
2016-01-28 06:41:28 +11:00
|
|
|
}
|
|
|
|
return methods[plexType]
|
|
|
|
|
|
|
|
|
2016-01-28 02:33:02 +11:00
|
|
|
def XbmcItemtypes():
|
|
|
|
return ['photo', 'video', 'audio']
|
|
|
|
|
|
|
|
|
|
|
|
def PlexItemtypes():
|
|
|
|
return ['photo', 'video', 'audio']
|
|
|
|
|
|
|
|
|
|
|
|
def PlexLibraryItemtypes():
|
|
|
|
return ['movie', 'show']
|
|
|
|
# later add: 'artist', 'photo'
|
|
|
|
|
|
|
|
|
|
|
|
def EmbyItemtypes():
|
|
|
|
return ['Movie', 'Series', 'Season', 'Episode']
|
|
|
|
|
|
|
|
|
2016-02-07 22:38:50 +11:00
|
|
|
def SelectStreams(url, args):
|
|
|
|
"""
|
|
|
|
Does a PUT request to tell the PMS what audio and subtitle streams we have
|
|
|
|
chosen.
|
|
|
|
"""
|
|
|
|
downloadutils.DownloadUtils().downloadUrl(
|
|
|
|
url + '?' + urlencode(args), type='PUT')
|
|
|
|
|
|
|
|
|
2016-01-30 06:07:21 +11:00
|
|
|
def GetPlayQueue(playQueueID):
|
|
|
|
"""
|
2016-02-01 02:13:40 +11:00
|
|
|
Fetches the PMS playqueue with the playQueueID as an XML
|
2016-01-30 06:07:21 +11:00
|
|
|
|
2016-02-10 23:50:04 +11:00
|
|
|
Returns None if something went wrong
|
2016-01-30 06:07:21 +11:00
|
|
|
"""
|
|
|
|
url = "{server}/playQueues/%s" % playQueueID
|
2016-02-01 02:13:40 +11:00
|
|
|
args = {'Accept': 'application/xml'}
|
|
|
|
xml = downloadutils.DownloadUtils().downloadUrl(url, headerOptions=args)
|
2016-01-30 06:07:21 +11:00
|
|
|
try:
|
2016-02-01 02:13:40 +11:00
|
|
|
xml.attrib['playQueueID']
|
|
|
|
except (AttributeError, KeyError):
|
2016-02-10 23:50:04 +11:00
|
|
|
return None
|
2016-02-01 02:13:40 +11:00
|
|
|
return xml
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
|
2016-02-01 20:33:33 +11:00
|
|
|
def GetPlexMetadata(key):
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
|
|
|
Returns raw API metadata for key as an etree XML.
|
|
|
|
|
|
|
|
Can be called with either Plex key '/library/metadata/xxxx'metadata
|
|
|
|
OR with the digits 'xxxx' only.
|
|
|
|
|
2016-02-01 20:33:33 +11:00
|
|
|
Returns None if something went wrong
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
|
|
|
key = str(key)
|
|
|
|
if '/library/metadata/' in key:
|
|
|
|
url = "{server}" + key
|
|
|
|
else:
|
|
|
|
url = "{server}/library/metadata/" + key
|
|
|
|
arguments = {
|
|
|
|
'checkFiles': 1, # No idea
|
|
|
|
'includeExtras': 1, # Trailers and Extras => Extras
|
2016-02-01 20:33:33 +11:00
|
|
|
# 'includeRelated': 1, # Similar movies => Video -> Related
|
|
|
|
# 'includeRelatedCount': 5,
|
|
|
|
# 'includeOnDeck': 1,
|
2016-01-28 02:33:02 +11:00
|
|
|
'includeChapters': 1,
|
|
|
|
'includePopularLeaves': 1,
|
|
|
|
'includeConcerts': 1
|
|
|
|
}
|
|
|
|
url = url + '?' + urlencode(arguments)
|
2016-02-01 20:33:33 +11:00
|
|
|
xml = downloadutils.DownloadUtils().downloadUrl(url)
|
2016-01-28 02:33:02 +11:00
|
|
|
# Did we receive a valid XML?
|
|
|
|
try:
|
2016-02-01 20:33:33 +11:00
|
|
|
xml.attrib
|
2016-01-28 02:33:02 +11:00
|
|
|
# Nope we did not receive a valid XML
|
|
|
|
except AttributeError:
|
|
|
|
logMsg(title, "Error retrieving metadata for %s" % url, -1)
|
2016-02-01 20:33:33 +11:00
|
|
|
xml = None
|
2016-01-28 02:33:02 +11:00
|
|
|
return xml
|
|
|
|
|
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
def GetAllPlexChildren(key, containerSize=None):
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
2016-02-01 20:33:33 +11:00
|
|
|
Returns a list (raw xml API dump) of all Plex children for the key.
|
2016-01-28 02:33:02 +11:00
|
|
|
(e.g. /library/metadata/194853/children pointing to a season)
|
|
|
|
|
|
|
|
Input:
|
|
|
|
key Key to a Plex item, e.g. 12345
|
|
|
|
"""
|
2016-03-15 00:51:49 +11:00
|
|
|
url = "{server}/library/metadata/%s/children?" % key
|
|
|
|
return DownloadChunks(url, containerSize)
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
def GetPlexSectionResults(viewId, args=None, containerSize=None):
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
2016-02-01 20:33:33 +11:00
|
|
|
Returns a list (XML API dump) of all Plex items in the Plex
|
2016-01-28 02:33:02 +11:00
|
|
|
section with key = viewId.
|
2016-02-01 20:33:33 +11:00
|
|
|
|
2016-02-13 02:53:49 +11:00
|
|
|
Input:
|
|
|
|
args: optional dict to be urlencoded
|
|
|
|
|
2016-02-01 20:33:33 +11:00
|
|
|
Returns None if something went wrong
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
2016-03-15 00:51:49 +11:00
|
|
|
url = "{server}/library/sections/%s/all?" % viewId
|
2016-02-13 02:53:49 +11:00
|
|
|
if args:
|
2016-03-15 00:51:49 +11:00
|
|
|
url += urlencode(args) + '&'
|
|
|
|
return DownloadChunks(url, containerSize)
|
2016-02-13 02:53:49 +11:00
|
|
|
|
2016-02-01 20:33:33 +11:00
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
def DownloadChunks(url, containerSize):
|
|
|
|
"""
|
|
|
|
Downloads PMS url in chunks of containerSize (int).
|
|
|
|
If containerSize is None: ONE xml is fetched directly
|
2016-02-01 20:33:33 +11:00
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
url MUST end with '?' (if no other url encoded args are present) or '&'
|
|
|
|
|
|
|
|
Returns a stitched-together xml or None.
|
|
|
|
"""
|
|
|
|
if containerSize is None:
|
|
|
|
# Get rid of '?' or '&' at the end of url
|
|
|
|
xml = downloadutils.DownloadUtils().downloadUrl(url[:-1])
|
|
|
|
try:
|
|
|
|
xml.attrib
|
|
|
|
except AttributeError:
|
|
|
|
# Nope, not an XML, abort
|
|
|
|
logMsg(title, "Error getting url %s" % url[:-1], -1)
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return xml
|
|
|
|
|
|
|
|
xml = None
|
|
|
|
pos = 0
|
|
|
|
errorCounter = 0
|
|
|
|
while errorCounter < 10:
|
|
|
|
args = {
|
|
|
|
'X-Plex-Container-Size': containerSize,
|
|
|
|
'X-Plex-Container-Start': pos
|
|
|
|
}
|
|
|
|
xmlpart = downloadutils.DownloadUtils().downloadUrl(
|
|
|
|
url + urlencode(args))
|
|
|
|
# If something went wrong - skip in the hope that it works next time
|
|
|
|
try:
|
|
|
|
xmlpart.attrib
|
|
|
|
except AttributeError:
|
|
|
|
logMsg(title, 'Error while downloading chunks: %s'
|
|
|
|
% (url + urlencode(args)), -1)
|
|
|
|
pos += containerSize
|
|
|
|
errorCounter += 1
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Very first run: starting xml (to retain data in xml's root!)
|
|
|
|
if xml is None:
|
|
|
|
xml = deepcopy(xmlpart)
|
|
|
|
if len(xmlpart) < containerSize:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
pos += containerSize
|
|
|
|
continue
|
|
|
|
# Build answer xml - containing the entire library
|
|
|
|
for child in xmlpart:
|
|
|
|
xml.append(child)
|
|
|
|
# Done as soon as we don't receive a full complement of items
|
|
|
|
if len(xmlpart) < containerSize:
|
|
|
|
break
|
|
|
|
pos += containerSize
|
|
|
|
if errorCounter == 10:
|
|
|
|
logMsg(title, 'Fatal error while downloading chunks for %s' % url, -1)
|
|
|
|
return None
|
|
|
|
return xml
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
def GetAllPlexLeaves(viewId, lastViewedAt=None, updatedAt=None,
|
|
|
|
containerSize=None):
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
2016-02-01 20:33:33 +11:00
|
|
|
Returns a list (raw XML API dump) of all Plex subitems for the key.
|
2016-01-28 02:33:02 +11:00
|
|
|
(e.g. /library/sections/2/allLeaves pointing to all TV shows)
|
|
|
|
|
|
|
|
Input:
|
2016-01-30 06:07:21 +11:00
|
|
|
viewId Id of Plex library, e.g. '2'
|
|
|
|
lastViewedAt Unix timestamp; only retrieves PMS items viewed
|
|
|
|
since that point of time until now.
|
|
|
|
updatedAt Unix timestamp; only retrieves PMS items updated
|
|
|
|
by the PMS since that point of time until now.
|
2016-03-15 00:51:49 +11:00
|
|
|
containerSize Number of items simultaneously fetched from PMS
|
2016-01-30 06:07:21 +11:00
|
|
|
|
|
|
|
If lastViewedAt and updatedAt=None, ALL PMS items are returned.
|
|
|
|
|
|
|
|
Warning: lastViewedAt and updatedAt are combined with AND by the PMS!
|
|
|
|
|
|
|
|
Relevant "master time": PMS server. I guess this COULD lead to problems,
|
|
|
|
e.g. when server and client are in different time zones.
|
2016-01-28 02:33:02 +11:00
|
|
|
"""
|
2016-01-30 06:07:21 +11:00
|
|
|
args = []
|
2016-02-04 00:44:11 +11:00
|
|
|
url = "{server}/library/sections/%s/allLeaves" % viewId
|
|
|
|
|
2016-01-30 06:07:21 +11:00
|
|
|
if lastViewedAt:
|
|
|
|
args.append('lastViewedAt>=%s' % lastViewedAt)
|
|
|
|
if updatedAt:
|
|
|
|
args.append('updatedAt>=%s' % updatedAt)
|
2016-02-04 00:44:11 +11:00
|
|
|
if args:
|
2016-03-15 00:51:49 +11:00
|
|
|
url += '?' + '&'.join(args) + '&'
|
|
|
|
else:
|
|
|
|
url += '?'
|
2016-02-01 20:33:33 +11:00
|
|
|
|
2016-03-15 00:51:49 +11:00
|
|
|
return DownloadChunks(url, containerSize)
|
2016-01-28 02:33:02 +11:00
|
|
|
|
|
|
|
|
|
|
|
def GetPlexCollections(mediatype):
|
|
|
|
"""
|
|
|
|
Input:
|
|
|
|
mediatype String or list of strings with possible values
|
|
|
|
'movie', 'show', 'artist', 'photo'
|
|
|
|
Output:
|
|
|
|
List with an 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 (1, 2, 3...)
|
|
|
|
'uuid': xxx Other unique Plex key, e.g.
|
|
|
|
74aec9f2-a312-4723-9436-de2ea43843c1
|
|
|
|
}
|
|
|
|
Returns an empty list if nothing is found.
|
|
|
|
"""
|
|
|
|
collections = []
|
|
|
|
url = "{server}/library/sections"
|
2016-01-30 06:07:21 +11:00
|
|
|
jsondata = downloadutils.DownloadUtils().downloadUrl(url)
|
2016-01-28 02:33:02 +11:00
|
|
|
try:
|
|
|
|
result = jsondata['_children']
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
for item in result:
|
|
|
|
contentType = item['type']
|
|
|
|
if contentType in mediatype:
|
|
|
|
name = item['title']
|
|
|
|
contentId = item['key']
|
|
|
|
uuid = item['uuid']
|
|
|
|
collections.append({
|
|
|
|
'name': name,
|
|
|
|
'type': contentType,
|
|
|
|
'id': str(contentId),
|
|
|
|
'uuid': uuid
|
|
|
|
})
|
|
|
|
return collections
|
2016-02-03 23:01:13 +11:00
|
|
|
|
|
|
|
|
|
|
|
def GetPlexPlaylist(itemid, librarySectionUUID, mediatype='movie'):
|
|
|
|
"""
|
|
|
|
Returns raw API metadata XML dump for a playlist with e.g. trailers.
|
|
|
|
"""
|
|
|
|
trailerNumber = settings('trailerNumber')
|
|
|
|
if not trailerNumber:
|
|
|
|
trailerNumber = '3'
|
|
|
|
url = "{server}/playQueues"
|
|
|
|
args = {
|
|
|
|
'type': mediatype,
|
|
|
|
'uri': 'library://' + librarySectionUUID +
|
|
|
|
'/item/%2Flibrary%2Fmetadata%2F' + itemid,
|
|
|
|
'includeChapters': '1',
|
|
|
|
'extrasPrefixCount': trailerNumber,
|
|
|
|
'shuffle': '0',
|
|
|
|
'repeat': '0'
|
|
|
|
}
|
|
|
|
xml = downloadutils.DownloadUtils().downloadUrl(
|
|
|
|
url + '?' + urlencode(args), type="POST")
|
|
|
|
try:
|
|
|
|
xml[0].tag
|
|
|
|
except (IndexError, TypeError, AttributeError):
|
|
|
|
logMsg("Error retrieving metadata for %s" % url, -1)
|
|
|
|
return None
|
|
|
|
return xml
|
2016-02-07 22:38:50 +11:00
|
|
|
|
|
|
|
|
|
|
|
def getPlexRepeat(kodiRepeat):
|
|
|
|
plexRepeat = {
|
|
|
|
'off': '0',
|
|
|
|
'one': '1',
|
|
|
|
'all': '2' # does this work?!?
|
|
|
|
}
|
|
|
|
return plexRepeat.get(kodiRepeat)
|
2016-03-09 03:41:07 +11:00
|
|
|
|
|
|
|
|
|
|
|
def PMSHttpsEnabled(url):
|
|
|
|
"""
|
|
|
|
Returns True if the PMS wants to talk https, False otherwise
|
|
|
|
|
|
|
|
With with e.g. url=192.168.0.1:32400 (NO http/https)
|
|
|
|
|
|
|
|
This is done by GET /identity (returns an error if https is enabled and we
|
|
|
|
are trying to use http)
|
2016-03-11 02:02:46 +11:00
|
|
|
|
|
|
|
Prefers HTTPS over HTTP
|
2016-03-09 03:41:07 +11:00
|
|
|
"""
|
2016-03-11 02:02:46 +11:00
|
|
|
xml = downloadutils.DownloadUtils().downloadUrl(
|
|
|
|
'https://%s/identity' % url)
|
2016-03-09 03:41:07 +11:00
|
|
|
try:
|
2016-03-11 02:02:46 +11:00
|
|
|
# received a valid XML - https connection is possible
|
2016-03-09 03:41:07 +11:00
|
|
|
xml.attrib
|
|
|
|
logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1)
|
|
|
|
return True
|
2016-03-11 02:02:46 +11:00
|
|
|
except:
|
|
|
|
# couldn't get an xml - switch to http traffic
|
|
|
|
logMsg('PMSHttpsEnabled', 'PMS on %s talks HTTPS' % url, 1)
|
|
|
|
return False
|
2016-03-12 00:42:14 +11:00
|
|
|
|
|
|
|
|
|
|
|
def scrobble(ratingKey, state):
|
|
|
|
"""
|
|
|
|
Tells the PMS to set an item's watched state to state="watched" or
|
|
|
|
state="unwatched"
|
|
|
|
"""
|
|
|
|
args = {
|
|
|
|
'key': ratingKey,
|
|
|
|
'identifier': 'com.plexapp.plugins.library'
|
|
|
|
}
|
|
|
|
if state == "watched":
|
|
|
|
url = "{server}/:/scrobble?" + urlencode(args)
|
|
|
|
elif state == "unwatched":
|
|
|
|
url = "{server}/:/unscrobble?" + urlencode(args)
|
|
|
|
else:
|
|
|
|
return
|
|
|
|
downloadutils.DownloadUtils().downloadUrl(url, type="GET")
|
|
|
|
logMsg("Toggled watched state for Plex item %s" % ratingKey, 1)
|