python-plexapi/plexapi/utils.py

534 lines
15 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2017-01-02 21:06:40 +00:00
import re
2014-12-29 03:21:58 +00:00
from datetime import datetime
from plexapi.compat import quote, urlencode
from plexapi.exceptions import NotFound, UnknownType, Unsupported
from threading import Thread
# Search Types - Plex uses these to filter specific media types when searching.
2016-12-16 00:17:02 +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):
2016-12-16 00:17:02 +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.
"""
LIBRARY_TYPES[cls.TYPE] = cls
return cls
2016-12-16 00:22:18 +00:00
2016-03-17 05:15:58 +00:00
class _NA(object):
2016-12-16 00:17:02 +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)
"""
def __bool__(self):
2017-01-02 21:06:40 +00:00
"""Make sure Na always is False.
2016-12-16 23:38:08 +00:00
Returns:
2017-01-02 21:06:40 +00:00
bool: False
2016-12-16 23:38:08 +00:00
"""
2016-12-16 00:17:02 +00:00
return False
def __eq__(self, other):
2017-01-02 21:06:40 +00:00
"""Check eq.
2016-12-16 23:38:08 +00:00
Args:
2017-01-02 21:06:40 +00:00
other (str): Description
2016-12-16 23:38:08 +00:00
Returns:
2017-01-02 21:06:40 +00:00
bool: True is equal
2016-12-16 23:38:08 +00:00
"""
2016-12-16 00:17:02 +00:00
return isinstance(other, _NA) or other in [None, '__NA__']
def __nonzero__(self):
return False
def __repr__(self):
2017-01-02 21:06:40 +00:00
"""Pretty print."""
2016-12-16 00:17:02 +00:00
return '__NA__'
2016-03-17 05:15:58 +00:00
NA = _NA()
2014-12-29 03:21:58 +00:00
2014-12-29 03:21:58 +00:00
class PlexPartialObject(object):
2016-12-16 00:17:02 +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:
2017-01-02 21:06:40 +00:00
initpath (str): Relative url to PMS
server (): Description
2016-12-16 00:17:02 +00:00
"""
2014-12-29 03:21:58 +00:00
def __init__(self, data, initpath, server=None):
2016-12-16 23:38:08 +00:00
"""
Args:
data (xml.etree.ElementTree.Element): passed from server.query
2017-01-02 21:06:40 +00:00
initpath (str): Relative path
2016-12-16 23:38:08 +00:00
server (None or Plexserver, optional): PMS class your connected to
"""
self.server = server
2014-12-29 03:21:58 +00:00
self.initpath = initpath
self._loadData(data)
def __eq__(self, other):
2016-12-16 23:38:08 +00:00
"""Summary
Args:
other (TYPE): Description
Returns:
TYPE: Description
"""
return other is not None and self.key == other.key
def __repr__(self):
2016-12-16 00:22:18 +00:00
"""Pretty repr."""
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):
2016-12-16 23:38:08 +00:00
"""Auto reload self, if the attribute is NA
Args:
2017-01-02 21:06:40 +00:00
attr (str): fx key
2016-12-16 23:38:08 +00:00
"""
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-12-16 23:38:08 +00:00
"""Set attribute
Args:
2017-01-02 21:06:40 +00:00
attr (str): fx key
2016-12-16 23:38:08 +00:00
value (TYPE): Description
"""
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):
2016-12-16 23:38:08 +00:00
"""Uses a element to set a attrs.
Args:
data (Element): Used by attrs
"""
raise Exception('Abstract method not implemented.')
def isFullObject(self):
2016-04-13 03:26:04 +00:00
return not self.key or self.key == self.initpath
def isPartialObject(self):
return not self.isFullObject()
def reload(self):
2016-12-16 23:38:08 +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):
2016-12-16 23:38:08 +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.
2016-12-16 00:17:02 +00:00
2016-12-16 23:38:08 +00:00
Attributes: # todo
2017-01-02 21:06:40 +00:00
player (Plexclient): Player
playlistItemID (int): Playlist item id
sessionKey (int): 1223
transcodeSession (str): 12312312
username (str): Fx Hellowlol
viewedAt (datetime): viewed at.
2016-12-16 00:17:02 +00:00
"""
def _loadData(self, data):
2016-12-16 23:38:08 +00:00
"""Set the class attributes
Args:
data (xml.etree.ElementTree.Element): usually from server.query
"""
# 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)
# data for history details (/status/sessions/history/all)
self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA))
2016-04-11 03:49:23 +00:00
# data for playlist items
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
2016-12-16 00:17:02 +00:00
def getStreamURL(self, **params):
2016-12-16 23:38:08 +00:00
"""Make a stream url that can be used by vlc.
Args:
**params (dict): Description
Returns:
string: ''
Raises:
Unsupported: Raises a error is the type is wrong.
"""
if self.TYPE not in ('movie', 'episode', 'track'):
2016-12-16 00:17:02 +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):
2016-12-16 23:38:08 +00:00
"""Yield parts."""
for item in self.media:
for part in item.parts:
yield part
2016-12-16 00:17:02 +00:00
def play(self, client):
2016-12-16 23:38:08 +00:00
"""Start playback on a client.
Args:
client (PlexClient): The client to start playing on.
"""
client.playMedia(self)
2014-12-29 03:21:58 +00:00
def buildItem(server, elem, initpath, bytag=False):
2016-12-16 00:17:02 +00:00
"""Build classes used by the plexapi.
2016-12-16 23:38:08 +00:00
Args:
server (Plexserver): Your connected to.
elem (xml.etree.ElementTree.Element): xml from PMS
2017-01-02 21:06:40 +00:00
initpath (str): Relative path
2016-12-16 23:38:08 +00:00
bytag (bool, optional): Description # figure out what this do
2016-12-16 00:17:02 +00:00
2016-12-16 23:38:08 +00:00
Raises:
UnknownType: Unknown library type libtype
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):
2016-12-16 23:38:08 +00:00
"""Helper to change to the correct type
Args:
func (function): function to used [int, bool float]
value (string, int, float): value to cast
"""
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):
2016-12-16 23:38:08 +00:00
"""Finds and builds a object based on ratingKey.
Args:
server (Plexserver): PMS your connected to
key (int): key to look for
Raises:
NotFound: Unable to find key. Key
"""
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):
2016-12-16 23:38:08 +00:00
"""Finds and builds a object based on title.
Args:
server (Plexserver): Description
2017-01-02 21:06:40 +00:00
path (str): Relative path
title (str): Fx 16 blocks
2016-12-16 23:38:08 +00:00
Raises:
NotFound: Unable to find item: title
"""
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):
2016-12-16 00:17:02 +00:00
"""Extract the path from a location tag
2016-12-16 23:38:08 +00:00
Args:
data (xml.etree.ElementTree.Element): xml from PMS as Element
single (bool, optional): Only return one
2016-12-16 00:17:02 +00:00
2016-12-16 23:38:08 +00:00
Returns:
filepath string if single is True else list of filepaths
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):
2016-12-16 00:17:02 +00:00
"""Find a player in a elementthee
2016-12-16 23:38:08 +00:00
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): xml from pms as a element
2016-12-16 00:17:02 +00:00
2016-12-16 23:38:08 +00:00
Returns:
PlexClient or None
2016-12-16 00:17:02 +00:00
"""
elem = data.find('Player')
if elem is not None:
from plexapi.client import PlexClient
2016-12-16 00:17:02 +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):
2016-12-16 23:38:08 +00:00
"""Find streams.
Args:
media (Show, Movie, Episode): A item where find streams
2017-01-02 21:06:40 +00:00
streamtype (str): Possible options [movie, show, episode] # is this correct?
2016-12-16 23:38:08 +00:00
Returns:
list: of streams
"""
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):
2016-12-16 23:38:08 +00:00
"""Find transcode session.
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): XML response from PMS as Element
Returns:
media.TranscodeSession or None
"""
elem = data.find('TranscodeSession')
if elem is not None:
from plexapi import media
return media.TranscodeSession(server, elem)
return None
def findUsername(data):
2016-12-16 23:38:08 +00:00
"""Find a username in a Element
Args:
data (xml.etree.ElementTree.Element): XML from PMS as a Element
Returns:
username or None
"""
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):
2016-12-16 23:38:08 +00:00
"""Check of a string is a 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):
2016-12-16 23:38:08 +00:00
"""Builds a query string where only
the value is quoted.
Args:
args (dict): ex {'genre': 'action', 'type': 1337}
Returns:
string: ?genre=action&type=1337
"""
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):
2016-12-16 23:38:08 +00:00
"""ListChoices is by _cleanSort etc.
Args:
server (Plexserver): Server your connected to
2017-01-02 21:06:40 +00:00
path (str): Relative path to PMS
2016-12-16 23:38:08 +00:00
Returns:
dict: title:key
"""
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):
2016-12-16 23:38:08 +00:00
"""Return a list buildItem. See buildItem doc.
Args:
server (Plexserver): PMS your connected to.
2017-01-02 21:06:40 +00:00
path (str): Relative path to PMS
2016-12-16 23:38:08 +00:00
libtype (None or string, optional): [movie, show, episode, music] # check me
watched (None, True, False, optional): Skip or include watched items
bytag (bool, optional): Dunno wtf this is used for # todo
Returns:
list: of buildItem
"""
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='.'):
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):
2016-12-16 23:38:08 +00:00
"""Map search type name to int using SEACHTYPES
Used when querying PMS.
Args:
2017-01-02 21:06:40 +00:00
libtype (str): Possible options see SEARCHTYPES
2016-12-16 23:38:08 +00:00
Returns:
int: fx 1
Raises:
NotFound: Unknown libtype: libtype
"""
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):
2016-12-16 23:38:08 +00:00
"""Run some function in threads.
Args:
callback (function): funcion to run in thread
listargs (list): args parssed to the callback
"""
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-02 21:06:40 +00:00
"""Helper for datetime.
2016-12-16 23:38:08 +00:00
Args:
2017-01-02 21:06:40 +00:00
value (str): value to use to make datetime
2016-12-16 23:38:08 +00:00
format (None, optional): string as strptime.
Returns:
datetime
"""
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