2016-03-21 04:26:02 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2014-12-29 03:21:58 +00:00
|
|
|
"""
|
|
|
|
PlexAPI Utils
|
|
|
|
"""
|
2016-03-23 03:38:06 +00:00
|
|
|
import re
|
2016-03-21 04:26:02 +00:00
|
|
|
from requests import put
|
2014-12-29 03:21:58 +00:00
|
|
|
from datetime import datetime
|
2016-03-23 03:38:06 +00:00
|
|
|
from plexapi.compat import quote, urlencode
|
|
|
|
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
2015-09-05 14:09:15 +00:00
|
|
|
|
2016-03-17 04:51:20 +00:00
|
|
|
|
|
|
|
# Registry of library types we may come across when parsing XML. This allows us to
|
|
|
|
# define a few helper functions to dynamically convery the XML into objects.
|
2016-03-21 04:26:02 +00:00
|
|
|
# see buildItem() below for an example.
|
2016-03-17 04:51:20 +00:00
|
|
|
LIBRARY_TYPES = {}
|
|
|
|
def register_libtype(cls):
|
|
|
|
LIBRARY_TYPES[cls.TYPE] = cls
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
2016-03-14 04:19:48 +00:00
|
|
|
# This used to be a simple variable equal to '__NA__'. However, there has been need to
|
|
|
|
# compare NA against None in some use cases. This object allows the internals of PlexAPI
|
|
|
|
# to distinguish between unfetched values and fetched, but non-existent values.
|
|
|
|
# (NA == None results to True; NA is None results to False)
|
2016-03-17 05:15:58 +00:00
|
|
|
class _NA(object):
|
2016-03-21 04:26:02 +00:00
|
|
|
def __bool__(self): return False
|
|
|
|
def __eq__(self, other): return isinstance(other, _NA) or other in [None, '__NA__']
|
|
|
|
def __nonzero__(self): return False
|
|
|
|
def __repr__(self): return '__NA__'
|
2016-03-17 05:15:58 +00:00
|
|
|
NA = _NA()
|
2014-12-29 03:21:58 +00:00
|
|
|
|
2016-03-17 04:51:20 +00:00
|
|
|
|
|
|
|
# Not all objects in the Plex listings return the complete list of elements for the object.
|
|
|
|
# This object will allow you to assume each object is complete, and if the specified value
|
|
|
|
# you request is None it will fetch the full object automatically and update itself.
|
2014-12-29 03:21:58 +00:00
|
|
|
class PlexPartialObject(object):
|
|
|
|
|
|
|
|
def __init__(self, server, data, initpath):
|
|
|
|
self.server = server
|
|
|
|
self.initpath = initpath
|
|
|
|
self._loadData(data)
|
2016-03-15 18:36:59 +00:00
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return other is not None and self.type == other.type and self.key == other.key
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
title = self.title.replace(' ','.')[0:20]
|
|
|
|
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
2014-12-29 03:21:58 +00:00
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
if self.isPartialObject():
|
|
|
|
self.reload()
|
2015-11-05 03:49:09 +00:00
|
|
|
return self.__dict__[attr]
|
2014-12-29 03:21:58 +00:00
|
|
|
|
|
|
|
def __setattr__(self, attr, value):
|
|
|
|
if value != NA:
|
|
|
|
super(PlexPartialObject, self).__setattr__(attr, value)
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
@property
|
|
|
|
def thumbUrl(self):
|
|
|
|
return self.server.url(self.thumb)
|
2016-03-23 03:38:06 +00:00
|
|
|
|
|
|
|
def _getStreamURL(self, **params):
|
|
|
|
if self.TYPE not in ('movie', 'episode', 'track'):
|
|
|
|
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
|
|
|
|
mvb = params.get('maxVideoBitrate')
|
|
|
|
vr = params.get('videoResolution', '')
|
|
|
|
params = {
|
|
|
|
'path': self.key,
|
|
|
|
'offset': params.get('offset', 0),
|
|
|
|
'copyts': params.get('copyts', 1),
|
|
|
|
'protocol': params.get('protocol'),
|
|
|
|
'mediaIndex': params.get('mediaIndex', 0),
|
|
|
|
'X-Plex-Platform': params.get('platform', 'Chrome'),
|
|
|
|
'maxVideoBitrate': max(mvb,64) if mvb else None,
|
|
|
|
'videoResolution': vr if re.match('^\d+x\d+$', vr) else None
|
|
|
|
}
|
|
|
|
params = {k:v for k,v in params.items() if v is not None} # remove None values
|
|
|
|
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
|
|
|
|
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params)))
|
2016-03-21 04:26:02 +00:00
|
|
|
|
|
|
|
def _findLocation(self, data):
|
|
|
|
elem = data.find('Location')
|
|
|
|
if elem is not None:
|
|
|
|
return elem.attrib.get('path')
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _findPlayer(self, data):
|
|
|
|
elem = data.find('Player')
|
|
|
|
if elem is not None:
|
|
|
|
from plexapi.client import Client
|
|
|
|
return Client(self.server, elem)
|
|
|
|
return None
|
2016-03-24 06:20:08 +00:00
|
|
|
|
|
|
|
def _findStreams(self, streamtype):
|
|
|
|
streams = []
|
|
|
|
for media in self.media:
|
|
|
|
for part in media.parts:
|
|
|
|
for stream in part.streams:
|
|
|
|
if stream.TYPE == streamtype:
|
|
|
|
streams.append(stream)
|
|
|
|
return streams
|
2016-03-21 04:26:02 +00:00
|
|
|
|
|
|
|
def _findTranscodeSession(self, data):
|
|
|
|
elem = data.find('TranscodeSession')
|
|
|
|
if elem is not None:
|
|
|
|
from plexapi import media
|
|
|
|
return media.TranscodeSession(self.server, elem)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _findUser(self, data):
|
|
|
|
elem = data.find('User')
|
|
|
|
if elem is not None:
|
|
|
|
from plexapi.myplex import MyPlexUser
|
|
|
|
return MyPlexUser(elem, self.initpath)
|
|
|
|
return None
|
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
def _loadData(self, data):
|
|
|
|
raise Exception('Abstract method not implemented.')
|
|
|
|
|
|
|
|
def isFullObject(self):
|
|
|
|
return self.initpath == self.key
|
|
|
|
|
|
|
|
def isPartialObject(self):
|
2016-03-21 04:26:02 +00:00
|
|
|
return not self.isFullObject()
|
|
|
|
|
|
|
|
def iterParts(self):
|
|
|
|
for item in self.media:
|
|
|
|
for part in item.parts:
|
|
|
|
yield part
|
|
|
|
|
|
|
|
def play(self, client):
|
|
|
|
client.playMedia(self)
|
|
|
|
|
|
|
|
def refresh(self):
|
|
|
|
self.server.query('%s/refresh' % self.key, method=put)
|
2014-12-29 03:21:58 +00:00
|
|
|
|
|
|
|
def reload(self):
|
2016-03-21 04:26:02 +00:00
|
|
|
""" Reload the data for this object from PlexServer XML. """
|
2014-12-29 03:21:58 +00:00
|
|
|
data = self.server.query(self.key)
|
|
|
|
self.initpath = self.key
|
|
|
|
self._loadData(data[0])
|
|
|
|
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
def buildItem(server, elem, initpath):
|
2016-03-17 04:51:20 +00:00
|
|
|
libtype = elem.attrib.get('type')
|
|
|
|
if libtype in LIBRARY_TYPES:
|
|
|
|
cls = LIBRARY_TYPES[libtype]
|
|
|
|
return cls(server, elem, initpath)
|
|
|
|
raise UnknownType('Unknown library type: %s' % libtype)
|
|
|
|
|
|
|
|
|
2016-03-17 05:14:31 +00:00
|
|
|
def cast(func, value):
|
|
|
|
if value not in [None, NA]:
|
|
|
|
if func == bool:
|
|
|
|
return bool(int(value))
|
|
|
|
elif func in [int, float]:
|
|
|
|
try:
|
|
|
|
return func(value)
|
|
|
|
except ValueError:
|
|
|
|
return float('nan')
|
|
|
|
return func(value)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
def findKey(server, key):
|
2016-03-17 04:51:20 +00:00
|
|
|
path = '/library/metadata/{0}'.format(key)
|
|
|
|
try:
|
|
|
|
# Item seems to be the first sub element
|
|
|
|
elem = server.query(path)[0]
|
2016-03-21 04:26:02 +00:00
|
|
|
return buildItem(server, elem, path)
|
2016-03-17 04:51:20 +00:00
|
|
|
except:
|
|
|
|
raise NotFound('Unable to find key: %s' % key)
|
|
|
|
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
def findItem(server, path, title):
|
2016-03-17 04:51:20 +00:00
|
|
|
for elem in server.query(path):
|
|
|
|
if elem.attrib.get('title').lower() == title.lower():
|
2016-03-21 04:26:02 +00:00
|
|
|
return buildItem(server, elem, path)
|
2016-03-17 04:51:20 +00:00
|
|
|
raise NotFound('Unable to find item: %s' % title)
|
|
|
|
|
|
|
|
|
2016-03-17 05:14:31 +00:00
|
|
|
def joinArgs(args):
|
|
|
|
if not args: return ''
|
|
|
|
arglist = []
|
|
|
|
for key in sorted(args, key=lambda x:x.lower()):
|
|
|
|
value = str(args[key])
|
|
|
|
arglist.append('%s=%s' % (key, quote(value)))
|
|
|
|
return '?%s' % '&'.join(arglist)
|
|
|
|
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
def listItems(server, path, libtype=None, watched=None):
|
2016-03-17 04:51:20 +00:00
|
|
|
items = []
|
|
|
|
for elem in server.query(path):
|
|
|
|
if libtype and elem.attrib.get('type') != libtype: continue
|
|
|
|
if watched is True and elem.attrib.get('viewCount', 0) == 0: continue
|
|
|
|
if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue
|
|
|
|
try:
|
2016-03-21 04:26:02 +00:00
|
|
|
items.append(buildItem(server, elem, path))
|
2016-03-17 04:51:20 +00:00
|
|
|
except UnknownType:
|
|
|
|
pass
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
2016-03-24 06:20:08 +00:00
|
|
|
def rget(obj, attrstr, default=None, delim='.'):
|
|
|
|
try:
|
|
|
|
parts = attrstr.split(delim, 1)
|
|
|
|
attr = parts[0]
|
|
|
|
attrstr = parts[1] if len(parts) == 2 else None
|
|
|
|
if isinstance(obj, dict): value = obj[attr]
|
|
|
|
elif isinstance(obj, list): value = obj[int(attr)]
|
|
|
|
elif isinstance(obj, tuple): value = obj[int(attr)]
|
|
|
|
elif isinstance(obj, object): value = getattr(obj, attr)
|
|
|
|
if attrstr: return rget(value, attrstr, default, delim)
|
|
|
|
return value
|
|
|
|
except:
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
2016-03-21 04:26:02 +00:00
|
|
|
def searchType(libtype):
|
2016-03-17 05:14:31 +00:00
|
|
|
if libtype == 'movie': return 1
|
|
|
|
elif libtype == 'show': return 2
|
|
|
|
elif libtype == 'season': return 3
|
|
|
|
elif libtype == 'episode': return 4
|
|
|
|
elif libtype == 'artist': return 8
|
|
|
|
elif libtype == 'album': return 9
|
|
|
|
elif libtype == 'track': return 10
|
2016-03-17 04:51:20 +00:00
|
|
|
raise NotFound('Unknown libtype: %s' % libtype)
|
|
|
|
|
|
|
|
|
2014-12-29 03:21:58 +00:00
|
|
|
def toDatetime(value, format=None):
|
|
|
|
if value and value != NA:
|
|
|
|
if format: value = datetime.strptime(value, format)
|
|
|
|
else: value = datetime.fromtimestamp(int(value))
|
|
|
|
return value
|