python-plexapi/plexapi/utils.py

486 lines
18 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2017-01-02 21:06:40 +00:00
import logging, re
2014-12-29 03:21:58 +00:00
from datetime import datetime
2017-01-26 06:44:55 +00:00
from plexapi.compat import quote, urlencode, string_type
from plexapi.exceptions import NotFound, UnknownType, Unsupported
from threading import Thread
# Search Types - Plex uses these to filter specific media types when searching.
2017-01-26 05:25:13 +00:00
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4,
'artist': 8, 'album': 9, 'track': 10}
LIBRARY_TYPES = {}
2016-12-16 00:17:02 +00:00
def register_libtype(cls):
2017-01-26 04:21:13 +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. See
buildItem() below for an example.
2016-12-16 00:17:02 +00:00
"""
LIBRARY_TYPES[cls.TYPE] = cls
return cls
2016-12-16 00:22:18 +00:00
class NA(object):
2017-01-26 04:21:13 +00:00
""" This used to be a simple variable equal to '__NA__'. 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-12-16 00:17:02 +00:00
"""
def __bool__(self):
return False
def __eq__(self, other):
return isinstance(other, NA) or other in [None, '__NA__']
2016-12-16 00:17:02 +00:00
def __nonzero__(self):
return False
def __repr__(self):
return '__NA__'
class SecretsFilter(logging.Filter):
""" Logging filter to hide secrets. """
def __init__(self, secrets=None):
self.secrets = secrets or set()
def add_secret(self, secret):
self.secrets.add(secret)
def filter(self, record):
cleanargs = list(record.args)
for i in range(len(cleanargs)):
2017-01-26 06:44:55 +00:00
if isinstance(cleanargs[i], string_type):
for secret in self.secrets:
cleanargs[i] = cleanargs[i].replace(secret, '<hidden>')
record.args = tuple(cleanargs)
return True
2014-12-29 03:21:58 +00:00
2014-12-29 03:21:58 +00:00
class PlexPartialObject(object):
2017-01-26 05:25:13 +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.
Attributes:
data (:class:`ElementTree`): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
2016-12-16 00:17:02 +00:00
"""
def __init__(self, data, initpath, server=None):
self.server = server
2014-12-29 03:21:58 +00:00
self.initpath = initpath
self._loadData(data)
def __eq__(self, other):
return other is not None and self.key == other.key
def __repr__(self):
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '') if self.key else 'NA'
2016-12-16 00:17:02 +00:00
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s>' % (clsname, key, title)
2014-12-29 03:21:58 +00:00
def __getattr__(self, attr):
2017-01-26 05:25:13 +00:00
# Auto reload self, from the full key (path) when needed.
2016-04-13 03:26:04 +00:00
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
return self.__dict__.get(attr, NA)
self.reload()
return self.__dict__.get(attr, NA)
2016-12-16 00:17:02 +00:00
def __setattr__(self, attr, value):
2016-04-13 03:26:04 +00:00
if value != NA or self.isFullObject():
2016-12-16 23:38:08 +00:00
self.__dict__[attr] = value
2014-12-29 03:21:58 +00:00
def _loadData(self, data):
raise Exception('Abstract method not implemented.')
def isFullObject(self):
2017-01-26 05:25:13 +00:00
""" Retruns True if this is already a full object. A full object means all attributes
were populated from the api path representing only this item. For example, the
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie contain.
"""
2016-04-13 03:26:04 +00:00
return not self.key or self.key == self.initpath
def isPartialObject(self):
2017-01-26 05:25:13 +00:00
""" Returns True if this is NOT a full object. """
return not self.isFullObject()
def reload(self):
2017-01-26 05:25:13 +00:00
""" Reload the data for this object from PlexServer XML. """
data = self.server.query(self.key)
self.initpath = self.key
self._loadData(data[0])
class Playable(object):
2017-01-26 05:25:13 +00:00
""" This is a general place to store functions specific to media that is Playable.
Things were getting mixed up a bit when dealing with Shows, Season, Artists,
Albums which are all not playable.
Attributes:
player (:class:`~plexapi.client.PlexClient`): Client object playing this item (for active sessions).
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
sessionKey (int): Active session key.
transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
if item is being transcoded (None otherwise).
username (str): Username of the person playing this item (for active sessions).
viewedAt (datetime): Datetime item was last viewed (history).
2016-12-16 00:17:02 +00:00
"""
def _loadData(self, data):
2017-01-26 05:25:13 +00:00
# Load data for active sessions (/status/sessions)
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
self.username = findUsername(data)
self.player = findPlayer(self.server, data)
self.transcodeSession = findTranscodeSession(self.server, data)
2017-01-26 05:25:13 +00:00
# Load data for history details (/status/sessions/history/all)
self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA))
2017-01-26 05:25:13 +00:00
# Load data for playlist items
2016-04-11 03:49:23 +00:00
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
2016-12-16 00:17:02 +00:00
def getStreamURL(self, **params):
2017-01-26 05:25:13 +00:00
""" Returns a stream url that may be used by external applications such as VLC.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
**params (dict): optional parameters to manipulate the playback when accessing
the stream. A few known parameters include: maxVideoBitrate, videoResolution
offset, copyts, protocol, mediaIndex, platform.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Raises:
Unsupported: When the item doesn't support fetching a stream URL.
2016-12-16 23:38:08 +00:00
"""
if self.TYPE not in ('movie', 'episode', 'track'):
2017-01-26 05:25:13 +00:00
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'),
2016-12-16 00:17:02 +00:00
'maxVideoBitrate': max(mvb, 64) if mvb else None,
'videoResolution': vr if re.match('^\d+x\d+$', vr) else None
}
2016-12-16 00:17:02 +00:00
# remove None values
params = {k: v for k, v in params.items() if v is not None}
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params)))
2016-12-16 00:17:02 +00:00
def iterParts(self):
2017-01-26 05:25:13 +00:00
""" Iterates over the parts of this media item. """
for item in self.media:
for part in item.parts:
yield part
2016-12-16 00:17:02 +00:00
def play(self, client):
2017-01-26 05:25:13 +00:00
""" Start playback on the specified client.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
client (:class:`~plexapi.client.PlexClient`): Client to start playing on.
2016-12-16 23:38:08 +00:00
"""
client.playMedia(self)
2014-12-29 03:21:58 +00:00
def buildItem(server, elem, initpath, bytag=False):
2017-01-26 05:25:13 +00:00
""" Factory function to build the objects used within the PlexAPI.
2016-12-16 00:17:02 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
elem (ElementTree): XML data needed to build the object.
initpath (str): Relative path requested when retrieving specified `data` (optional).
bytag (bool): Creates the object from the name specified by the tag instead of the
default which builds the object specified by the type attribute. <tag type='foo' />
2016-12-16 00:17:02 +00:00
2017-01-26 05:25:13 +00:00
Raises:
UnknownType: Unknown library type.
2016-12-16 00:17:02 +00:00
"""
libtype = elem.tag if bytag else elem.attrib.get('type')
2016-04-10 03:59:47 +00:00
if libtype == 'photo' and elem.tag == 'Directory':
libtype = 'photoalbum'
if libtype in LIBRARY_TYPES:
cls = LIBRARY_TYPES[libtype]
return cls(server, elem, initpath)
raise UnknownType('Unknown library type: %s' % libtype)
def cast(func, value):
2017-01-26 05:25:13 +00:00
""" Cast the specified value to the specified type (returned by func).
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
func (func): Calback function to used cast to type (int, bool, float, etc).
value (any): value to be cast and returned.
2016-12-16 23:38:08 +00:00
"""
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
def findKey(server, key):
2017-01-26 05:25:13 +00:00
""" Finds and builds a object based on ratingKey.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
key (int): ratingKey to find and return.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Raises:
NotFound: Unable to find key
2016-12-16 23:38:08 +00:00
"""
path = '/library/metadata/{0}'.format(key)
try:
# Item seems to be the first sub element
elem = server.query(path)[0]
return buildItem(server, elem, path)
except:
raise NotFound('Unable to find key: %s' % key)
def findItem(server, path, title):
2017-01-26 05:25:13 +00:00
""" Finds and builds a object based on title.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): API path that returns item to search title for.
title (str): Title of the item to find and return.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Raises:
NotFound: Unable to find item.
2016-12-16 23:38:08 +00:00
"""
for elem in server.query(path):
if elem.attrib.get('title').lower() == title.lower():
return buildItem(server, elem, path)
raise NotFound('Unable to find item: %s' % title)
def findLocations(data, single=False):
2017-01-26 05:25:13 +00:00
""" Returns a list of filepaths from a location tag.
2016-12-16 00:17:02 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
data (ElementTree): XML object to search for locations in.
single (bool): Set True to only return the first location found.
Return type will be a string if this is set to True.
2016-12-16 00:17:02 +00:00
"""
locations = []
for elem in data:
if elem.tag == 'Location':
locations.append(elem.attrib.get('path'))
if single:
return locations[0] if locations else None
return locations
2016-12-16 00:17:02 +00:00
def findPlayer(server, data):
2017-01-26 05:25:13 +00:00
""" Returns the :class:`~plexapi.client.PlexClient` object found in the specified data.
2016-12-16 00:17:02 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (:class:`ElementTree`): XML data to find Player in.
2016-12-16 00:17:02 +00:00
"""
elem = data.find('Player')
if elem is not None:
from plexapi.client import PlexClient
2017-01-26 05:25:13 +00:00
baseurl = 'http://%s:%s' % (elem.attrib.get('address'), elem.attrib.get('port'))
return PlexClient(baseurl, server=server, data=elem)
return None
def findStreams(media, streamtype):
2017-01-26 05:25:13 +00:00
""" Returns a list of streams (str) found in media that match the specified streamtype.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode).
streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream).
2016-12-16 23:38:08 +00:00
"""
streams = []
for mediaitem in media:
for part in mediaitem.parts:
for stream in part.streams:
if stream.TYPE == streamtype:
streams.append(stream)
return streams
def findTranscodeSession(server, data):
2017-01-26 05:25:13 +00:00
""" Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified
XML data.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (:class:`ElementTree`): XML data to find TranscodeSession in.
2016-12-16 23:38:08 +00:00
"""
elem = data.find('TranscodeSession')
if elem is not None:
from plexapi import media
return media.TranscodeSession(server, elem)
return None
def findUsername(data):
2017-01-26 05:25:13 +00:00
""" Returns the username if found in the specified XML data. Returns None if not found.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
data (:class:`ElementTree`): XML data to find username in.
2016-12-16 23:38:08 +00:00
"""
elem = data.find('User')
if elem is not None:
return elem.attrib.get('title')
return None
2017-01-02 21:06:40 +00:00
def isInt(str):
2017-01-26 05:25:13 +00:00
""" Returns True if the specified string passes as an int. """
2016-12-16 00:17:02 +00:00
try:
2017-01-02 21:06:40 +00:00
int(str)
return True
except ValueError:
return False
def joinArgs(args):
2017-01-26 05:25:13 +00:00
""" Returns a query string (uses for HTTP URLs) where only the value is URL encoded.
Example return value: '?genre=action&type=1337'.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
args (dict): Arguments to include in query string.
2016-12-16 23:38:08 +00:00
"""
2016-12-16 00:17:02 +00:00
if not args:
return ''
arglist = []
2016-12-16 00:17:02 +00:00
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)
def listChoices(server, path):
2017-01-26 05:25:13 +00:00
""" Returns a dict of {title:key} for all simple choices in a search filter.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
2016-12-16 23:38:08 +00:00
"""
2016-12-16 00:17:02 +00:00
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None, bytag=False):
2017-01-26 05:25:13 +00:00
""" Returns a list of object built from :func:`~plexapi.utils.buildItem()` found
within the specified path.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
libtype (str): Optionally return only the specified library type.
watched (bool): Optionally return only watched or unwatched items.
bytag (bool): Set true if libtype is found in the XML tag (and not the 'type' attribute).
2016-12-16 23:38:08 +00:00
"""
items = []
for elem in server.query(path):
2016-12-16 00:17:02 +00:00
if libtype and elem.attrib.get('type') != libtype:
continue
2016-12-31 23:16:31 +00:00
if watched is True and int(elem.attrib.get('viewCount', 0)) == 0:
2016-12-16 00:17:02 +00:00
continue
2016-12-31 23:16:31 +00:00
if watched is False and int(elem.attrib.get('viewCount', 0)) >= 1:
2016-12-16 00:17:02 +00:00
continue
try:
items.append(buildItem(server, elem, path, bytag))
except UnknownType:
pass
return items
2016-12-16 00:17:02 +00:00
def rget(obj, attrstr, default=None, delim='.'):
2017-01-26 05:25:13 +00:00
""" Returns the value at the specified attrstr location within a nexted tree of
dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley
for each key in attrstr (split by by the delimiter) This function is heavily
influenced by the lookups used in Django templates.
Parameters:
obj (any): Object to start the lookup in (dict, obj, list, tuple, etc).
attrstr (str): String to lookup (ex: 'foo.bar.baz.value')
default (any): Default value to return if not found.
delim (str): Delimiter separating keys in attrstr.
"""
try:
parts = attrstr.split(delim, 1)
attr = parts[0]
attrstr = parts[1] if len(parts) == 2 else None
2016-12-16 00:17:02 +00:00
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-12-16 00:17:02 +00:00
def searchType(libtype):
2017-01-26 05:25:13 +00:00
""" Returns the integer value of the library string type.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
libtype (str): Library type to lookup (movie, show, season, episode,
artist, album, track)
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Raises:
NotFound: Unknown libtype
2016-12-16 23:38:08 +00:00
"""
2016-04-13 02:55:45 +00:00
libtype = str(libtype)
if libtype in [str(v) for v in SEARCHTYPES.values()]:
return libtype
2016-04-13 02:55:45 +00:00
if SEARCHTYPES.get(libtype) is not None:
return SEARCHTYPES[libtype]
raise NotFound('Unknown libtype: %s' % libtype)
def threaded(callback, listargs):
2017-01-26 05:25:13 +00:00
""" Returns the result of <callback> for each set of *args in listargs. Each call
to <callback. is called concurrently in their own separate threads.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
callback (func): Callback function to apply to each set of *args.
listargs (list): List of lists; *args to pass each thread.
2016-12-16 23:38:08 +00:00
"""
threads, results = [], []
for args in listargs:
args += [results, len(results)]
results.append(None)
threads.append(Thread(target=callback, args=args))
threads[-1].start()
for thread in threads:
thread.join()
return results
2014-12-29 03:21:58 +00:00
def toDatetime(value, format=None):
2017-01-26 05:25:13 +00:00
""" Returns a datetime object from the specified value.
2016-12-16 23:38:08 +00:00
2017-01-26 05:25:13 +00:00
Parameters:
value (str): value to return as a datetime
format (str): Format to pass strftime (optional; if value is a str).
2016-12-16 23:38:08 +00:00
"""
2014-12-29 03:21:58 +00:00
if value and value != NA:
2016-12-16 00:17:02 +00:00
if format:
value = datetime.strptime(value, format)
else:
value = datetime.fromtimestamp(int(value))
2014-12-29 03:21:58 +00:00
return value