mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-22 11:43:13 +00:00
Much more stability from yesterday; Easier to use fetchItem funtions; Common __repr__ for all plexobjects; Fix all uses if listItems
This commit is contained in:
parent
4624512356
commit
8212ca9c46
13 changed files with 309 additions and 495 deletions
|
@ -13,7 +13,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
|
|||
|
||||
# Core Settings
|
||||
PROJECT = 'PlexAPI' # name provided to plex server
|
||||
VERSION = '2.0.2' # version of this api
|
||||
VERSION = '2.9.0' # version of this api
|
||||
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int) # request timeout
|
||||
X_PLEX_CONTAINER_SIZE = 50 # max results to return in a single search page
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class Audio(PlexPartialObject):
|
|||
|
||||
def refresh(self):
|
||||
""" Tells Plex to refresh the metadata for this and all subitems. """
|
||||
self._root.query('%s/refresh' % self.key, method=self._root.session.put)
|
||||
self._root._query('%s/refresh' % self.key, method=self._root.session.put)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
|
@ -109,7 +109,7 @@ class Artist(Audio):
|
|||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
|
||||
key = '%s/children' % self.key
|
||||
return self._fetchItems(key, Album.TYPE)
|
||||
return self.fetchItems(key, tag=Album.TYPE)
|
||||
|
||||
def track(self, title):
|
||||
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
|
||||
|
@ -125,7 +125,7 @@ class Artist(Audio):
|
|||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
|
||||
key = '%s/allLeaves' % self.key
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Artist.track`. """
|
||||
|
@ -202,7 +202,7 @@ class Album(Audio):
|
|||
def tracks(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
|
||||
key = '%s/children' % self.key
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def get(self, title):
|
||||
""" Alias of :func:`~plexapi.audio.Album.track`. """
|
||||
|
@ -210,7 +210,7 @@ class Album(Audio):
|
|||
|
||||
def artist(self):
|
||||
""" Return :func:`~plexapi.audio.Artist` of this album. """
|
||||
return self._fetchItems(self.parentKey)[0]
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
""" Downloads all tracks for this artist to the specified location.
|
||||
|
@ -313,8 +313,8 @@ class Track(Audio, Playable):
|
|||
|
||||
def album(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Album`. """
|
||||
return self._fetchItems(self.parentKey)[0]
|
||||
return self.fetchItems(self.parentKey)[0]
|
||||
|
||||
def artist(self):
|
||||
""" Return this track's :class:`~plexapi.audio.Artist`. """
|
||||
return self._fetchItems(self.grandparentKey)[0]
|
||||
return self.fetchItems(self.grandparentKey)[0]
|
||||
|
|
284
plexapi/base.py
284
plexapi/base.py
|
@ -5,6 +5,151 @@ from plexapi.compat import urlencode
|
|||
from plexapi.exceptions import NotFound, UnknownType, Unsupported
|
||||
|
||||
|
||||
class PlexObject(object):
|
||||
""" Base class for all Plex objects.
|
||||
TODO: Finish documenting this.
|
||||
"""
|
||||
key = None
|
||||
|
||||
def __init__(self, root, data, initpath=None):
|
||||
self._root = root # Root MyPlexAccount or PlexServer
|
||||
self._data = data # XML data needed to build object
|
||||
self._initpath = initpath or self.key # Request path used to fetch data
|
||||
self._loadData(data)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.__firstattr('_baseurl', 'key', 'id', 'uri'),
|
||||
self.__firstattr('title', 'name', 'username', 'librarySectionTitle', 'product')
|
||||
] if p])
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if value is not None or attr.startswith('_'):
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def __firstattr(self, *attrs):
|
||||
for attr in attrs:
|
||||
value = str(self.__dict__.get(attr,'')).replace(' ','-')
|
||||
value = value.replace('/library/metadata/','').replace('/children','')
|
||||
if value: return value[:20]
|
||||
|
||||
def _buildItem(self, elem, initpath, cls=None, bytag=False):
|
||||
""" Factory function to build objects based on registered LIBRARY_TYPES. """
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if libtype in utils.LIBRARY_TYPES:
|
||||
cls = cls or utils.LIBRARY_TYPES[libtype]
|
||||
return cls(self._root, elem, initpath)
|
||||
raise UnknownType("Unknown library type <%s type='%s'../>" % (elem.tag, libtype))
|
||||
|
||||
def _buildItemOrNone(self, elem, initpath, cls=None, bytag=False):
|
||||
""" Calls :func:`~plexapi.base.PlexObject._buildItem()` but returns
|
||||
None if elem is an unknown type.
|
||||
"""
|
||||
try:
|
||||
return self._buildItem(elem, initpath, cls, bytag)
|
||||
except UnknownType:
|
||||
return None
|
||||
|
||||
def _buildItems(self, data, cls=None):
|
||||
""" Build and return a list of items (optionally filtered by tag).
|
||||
|
||||
Parameters:
|
||||
data (ElementTree): XML data to search for items.
|
||||
cls (:class:`plexapi.base.PlexObject`): Optionally specify the PlexObject
|
||||
to be built. If not specified _buildItem will be called and the best
|
||||
guess item will be built.
|
||||
"""
|
||||
items = []
|
||||
tag = cls.TYPE if cls else None
|
||||
for elem in data:
|
||||
if elem.tag == tag:
|
||||
items.append(self._buildItemOrNone(elem, self._initpath, cls))
|
||||
return [item for item in items if item]
|
||||
|
||||
def fetchItem(self, key, cls=None, bytag=False, tag=None, **attrs):
|
||||
""" Load the specified key to find and build the first item with the
|
||||
specified tag and attrs. If no tag or attrs are specified then
|
||||
the first item in the result set is returned.
|
||||
"""
|
||||
for elem in self._root._query(key):
|
||||
if tag and elem.tag != tag:
|
||||
continue
|
||||
if not all(elem.attrib.get(a,'').lower() == str(v).lower() for a,v in attrs.items()):
|
||||
continue
|
||||
return self._buildItem(elem, key, cls, bytag)
|
||||
raise NotFound('Unable to find elem: tag=%s, attrs=%s' % (tag, attrs))
|
||||
|
||||
def fetchItems(self, key, cls=None, bytag=False, tag=None, **attrs):
|
||||
""" Load the specified key to find and build all items with the
|
||||
specified tag and attrs.
|
||||
"""
|
||||
items = []
|
||||
for elem in self._root._query(key):
|
||||
if tag and elem.tag != tag:
|
||||
continue
|
||||
if not all(elem.attrib.get(a,'').lower() == str(v).lower() for a,v in attrs.items()):
|
||||
continue
|
||||
items.append(self._buildItemOrNone(elem, key, cls, bytag))
|
||||
return [item for item in items if item]
|
||||
|
||||
def _loadData(self, data):
|
||||
raise NotImplemented('Abstract method not implemented.')
|
||||
|
||||
def reload(self, safe=False):
|
||||
""" Reload the data for this object from self.key. """
|
||||
if not self.key:
|
||||
if safe: return None
|
||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||
self._initpath = self.key
|
||||
data = self._root._query(self.key)
|
||||
self._loadData(data[0])
|
||||
|
||||
|
||||
class PlexPartialObject(PlexObject):
|
||||
""" 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 (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.
|
||||
"""
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.key == other.key
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
# Check a few cases where we dont want to reload
|
||||
value = super(PlexPartialObject, self).__getattribute__(attr)
|
||||
if attr == 'key' or attr.startswith('_'): return value
|
||||
if value not in (None, []): return value
|
||||
if self.isFullObject(): return value
|
||||
# Log warning that were reloading the object
|
||||
clsname = self.__class__.__name__
|
||||
title = self.__dict__.get('title', self.__dict__.get('name'))
|
||||
objname = "%s '%s'" % (clsname, title) if title else clsname
|
||||
log.warn("Reloading %s for attr '%s'" % (objname, attr))
|
||||
# Reload and return the value
|
||||
self.reload()
|
||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||
|
||||
def isFullObject(self):
|
||||
""" 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.
|
||||
"""
|
||||
return not self.key or self.key == self._initpath
|
||||
|
||||
def isPartialObject(self):
|
||||
""" Returns True if this is not a full object. """
|
||||
return not self.isFullObject()
|
||||
|
||||
|
||||
class Playable(object):
|
||||
""" 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,
|
||||
|
@ -107,142 +252,3 @@ class Playable(object):
|
|||
if filepath:
|
||||
filepaths.append(filepath)
|
||||
return filepaths
|
||||
|
||||
|
||||
class PlexObject(object):
|
||||
""" Base class for all Plex objects.
|
||||
TODO: Finish documenting this.
|
||||
"""
|
||||
key = None
|
||||
|
||||
def __init__(self, root, data, initpath=None):
|
||||
self._root = root # Root MyPlexAccount or PlexServer
|
||||
self._data = data # XML data needed to build object
|
||||
self._initpath = initpath or self.key # Request path used to fetch data
|
||||
self._loadData(data)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
if value is not None or attr.startswith('_'):
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def _buildItem(self, elem, initpath, bytag=False):
|
||||
""" Factory function to build objects based on registered LIBRARY_TYPES. """
|
||||
libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
if libtype == 'photo' and elem.tag == 'Directory':
|
||||
libtype = 'photoalbum'
|
||||
if libtype in utils.LIBRARY_TYPES:
|
||||
cls = utils.LIBRARY_TYPES[libtype]
|
||||
return cls(self._root, elem, initpath)
|
||||
raise UnknownType('Unknown library type: %s' % libtype)
|
||||
|
||||
def _buildItems(self, data, cls=None, tag=None, attrs=None, safe=False):
|
||||
""" Build and return a list of items (optionally filtered by tag).
|
||||
|
||||
Parameters:
|
||||
data (ElementTree): XML data to search for items.
|
||||
cls (PlexObject): Optionally specify the PlexObject to be built. If not specified
|
||||
_buildItem will be called and the best guess item will be built.
|
||||
tag (str): Only build items with the specified tag. If not specified and
|
||||
cls is specified, tag will be set to cls.TYPE.
|
||||
attrs (dict): Dict containing attributes to filter the elements by. If not
|
||||
specified, all elements will be considered.
|
||||
safe (bool): If True, dont raise an exception when unable to build an object.
|
||||
"""
|
||||
items = []
|
||||
tag = cls.TYPE if not tag and cls else tag
|
||||
attrs = attrs or {}
|
||||
for elem in data:
|
||||
try:
|
||||
if not tag or elem.tag == tag:
|
||||
for attr, value in attrs.items():
|
||||
if elem.attrib.get(attr) != str(value):
|
||||
continue
|
||||
if cls is not None:
|
||||
items.append(cls(self._root, elem, self._initpath))
|
||||
else:
|
||||
items.append(self._buildItem(elem, self._initpath))
|
||||
except Exception as err:
|
||||
if safe:
|
||||
log.warn('Failed to build %s (type=%s); %s' % (elem.tag, elem.attrib.get('type', 'NA'), err))
|
||||
continue
|
||||
raise
|
||||
return items
|
||||
|
||||
def _fetchItem(self, key, title=None, name=None):
|
||||
for elem in self._root._query(key):
|
||||
if title and elem.attrib.get('title').lower() == title.lower():
|
||||
return self._buildItem(elem, key)
|
||||
if name and elem.attrib.get('name').lower() == name.lower():
|
||||
return self._buildItem(elem, key)
|
||||
raise NotFound('Unable to find item: %s' % (title or name))
|
||||
|
||||
def _fetchItems(self, key, libtype=None, bytag=False):
|
||||
""" Fetch and build items from the specified key. """
|
||||
items = []
|
||||
for elem in self._root._query(key):
|
||||
try:
|
||||
if not libtype or elem.attrib.get('type') == libtype:
|
||||
items.append(self._buildItem(elem, key, bytag))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
|
||||
def _loadData(self, data):
|
||||
raise NotImplemented('Abstract method not implemented.')
|
||||
|
||||
def reload(self, safe=False):
|
||||
""" Reload the data for this object from self.key. """
|
||||
if not self.key:
|
||||
if safe: return None
|
||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||
self._initpath = self.key
|
||||
data = self._root._query(self.key)
|
||||
self._loadData(data[0])
|
||||
|
||||
|
||||
class PlexPartialObject(PlexObject):
|
||||
""" 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 (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.
|
||||
"""
|
||||
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'
|
||||
title = self.title.replace(' ', '.')[0:20].encode('utf8')
|
||||
return '<%s:%s:%s>' % (clsname, key, title)
|
||||
|
||||
def __getattribute__(self, attr):
|
||||
# Check a few cases where we dont want to reload
|
||||
value = super(PlexPartialObject, self).__getattribute__(attr)
|
||||
if attr == 'key' or attr.startswith('_'): return value
|
||||
if value not in (None, []): return value
|
||||
if self.isFullObject(): return value
|
||||
# Log warning that were reloading the object
|
||||
clsname = self.__class__.__name__
|
||||
title = self.__dict__.get('title', self.__dict__.get('name'))
|
||||
objname = "%s '%s'" % (clsname, title) if title else clsname
|
||||
log.warn("Reloading %s for attr '%s'" % (objname, attr))
|
||||
# Reload and return the value
|
||||
self.reload()
|
||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||
|
||||
def isFullObject(self):
|
||||
""" 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.
|
||||
"""
|
||||
return not self.key or self.key == self._initpath
|
||||
|
||||
def isPartialObject(self):
|
||||
""" Returns True if this is not a full object. """
|
||||
return not self.isFullObject()
|
||||
|
|
|
@ -89,9 +89,6 @@ class PlexClient(PlexObject):
|
|||
self.vendor = data.attrib.get('vendor')
|
||||
self.version = data.attrib.get('version')
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
|
||||
|
||||
def _query(self, path, method=None, headers=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex client. This method helps
|
||||
by encoding the response to utf-8 and parsing the returned XML into and
|
||||
|
@ -134,12 +131,12 @@ class PlexClient(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
|
||||
"""
|
||||
if value is True and not self.server:
|
||||
if value is True and not self._server:
|
||||
raise Unsupported('Cannot use client proxy with unknown server.')
|
||||
self._proxyThroughServer = value
|
||||
|
||||
def sendCommand(self, command, proxy=None, **params):
|
||||
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily
|
||||
""" Convenience wrapper around :func:`~plexapi.client.PlexClient._query()` to more easily
|
||||
send simple commands to the client. Returns an ElementTree object containing
|
||||
the response.
|
||||
|
||||
|
@ -149,22 +146,21 @@ class PlexClient(PlexObject):
|
|||
**params (dict): Additional GET parameters to include with the command.
|
||||
|
||||
Raises:
|
||||
:class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
|
||||
:class:`~plexapi.exceptions.Unsupported`: When we detect the client
|
||||
doesn't support this capability.
|
||||
"""
|
||||
command = command.strip('/')
|
||||
controller = command.split('/')[0]
|
||||
if controller not in self.protocolCapabilities:
|
||||
raise Unsupported('Client %s does not support the %s controller.' %
|
||||
(self.title, controller))
|
||||
path = '/player/%s%s' % (command, utils.joinArgs(params))
|
||||
raise Unsupported('Client %s doesnt support %s controller.' % (self.title, controller))
|
||||
key = '/player/%s%s' % (command, utils.joinArgs(params))
|
||||
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||
self._commandId += 1
|
||||
params['commandID'] = self._commandId
|
||||
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||
if proxy:
|
||||
return self.server.query(path, headers=headers)
|
||||
path = '/player/%s%s' % (command, utils.joinArgs(params))
|
||||
return self._query(path, headers=headers)
|
||||
return self._root._query(key, headers=headers)
|
||||
return self._query(key, headers=headers)
|
||||
|
||||
#---------------------
|
||||
# Navigation Commands
|
||||
|
@ -235,11 +231,11 @@ class PlexClient(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
||||
"""
|
||||
if not self.server:
|
||||
if not self._server:
|
||||
raise Unsupported('A server must be specified before using this command.')
|
||||
server_url = media.server._baseurl.split(':')
|
||||
server_url = media._server._baseurl.split(':')
|
||||
self.sendCommand('mirror/details', **dict({
|
||||
'machineIdentifier': self.server.machineIdentifier,
|
||||
'machineIdentifier': self._server.machineIdentifier,
|
||||
'address': server_url[1].strip('/'),
|
||||
'port': server_url[-1],
|
||||
'key': media.key,
|
||||
|
@ -402,12 +398,12 @@ class PlexClient(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
||||
"""
|
||||
if not self.server:
|
||||
if not self._server:
|
||||
raise Unsupported('A server must be specified before using this command.')
|
||||
server_url = media.server._baseurl.split(':')
|
||||
playqueue = self.server.createPlayQueue(media)
|
||||
server_url = media._server._baseurl.split(':')
|
||||
playqueue = self._server.createPlayQueue(media)
|
||||
self.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier': self.server.machineIdentifier,
|
||||
'machineIdentifier': self._server.machineIdentifier,
|
||||
'address': server_url[1].strip('/'),
|
||||
'port': server_url[-1],
|
||||
'offset': offset,
|
||||
|
|
|
@ -27,8 +27,8 @@ class Library(PlexObject):
|
|||
self.title1 = data.attrib.get('title1')
|
||||
self.title2 = data.attrib.get('title2')
|
||||
|
||||
def __repr__(self):
|
||||
return '<Library:%s>' % self.title1.encode('utf8')
|
||||
def __len__(self):
|
||||
return len(self.sections())
|
||||
|
||||
def sections(self):
|
||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||
|
@ -57,9 +57,9 @@ class Library(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: Invalid library section title.
|
||||
"""
|
||||
for item in self.sections():
|
||||
if item.title == title:
|
||||
return item
|
||||
for section in self.sections():
|
||||
if section.title == title:
|
||||
return section
|
||||
raise NotFound('Invalid library section: %s' % title)
|
||||
|
||||
def sectionByID(self, sectionID):
|
||||
|
@ -80,29 +80,11 @@ class Library(PlexObject):
|
|||
|
||||
def onDeck(self):
|
||||
""" Returns a list of all media items on deck. """
|
||||
return self._fetchItems('/library/onDeck')
|
||||
return self.fetchItems('/library/onDeck')
|
||||
|
||||
def recentlyAdded(self):
|
||||
""" Returns a list of all media items recently added. """
|
||||
return self._fetchItems('/library/recentlyAdded')
|
||||
|
||||
def get(self, title): # this should use hub search when its merged
|
||||
""" Return the first item from all items with the specified title.
|
||||
|
||||
Parameters:
|
||||
title (str): Title of the item to return.
|
||||
"""
|
||||
for i in self.all():
|
||||
if i.title.lower() == title.lower():
|
||||
return i
|
||||
|
||||
def getByKey(self, key):
|
||||
""" Return the first item from all items with the specified key.
|
||||
|
||||
Parameters:
|
||||
key (str): Key of the item to return.
|
||||
"""
|
||||
return utils.findKey(self.server, key)
|
||||
return self.fetchItems('/library/recentlyAdded')
|
||||
|
||||
def search(self, title=None, libtype=None, **kwargs):
|
||||
""" Searching within a library section is much more powerful. It seems certain
|
||||
|
@ -121,7 +103,7 @@ class Library(PlexObject):
|
|||
for attr, value in kwargs.items():
|
||||
args[attr] = value
|
||||
key = '/library/all%s' % utils.joinArgs(args)
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def cleanBundles(self):
|
||||
""" Poster images and other metadata for items in your library are kept in "bundle"
|
||||
|
@ -130,7 +112,7 @@ class Library(PlexObject):
|
|||
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
|
||||
"""
|
||||
# TODO: Should this check the response for success or the correct mediaprefix?
|
||||
self.server.query('/library/clean/bundles')
|
||||
self._server._query('/library/clean/bundles')
|
||||
|
||||
def emptyTrash(self):
|
||||
""" If a library has items in the Library Trash, use this option to empty the Trash. """
|
||||
|
@ -142,16 +124,13 @@ class Library(PlexObject):
|
|||
For example, if you have deleted or added an entire library or many items in a
|
||||
library, you may like to optimize the database.
|
||||
"""
|
||||
self.server.query('/library/optimize')
|
||||
self._root._query('/library/optimize')
|
||||
|
||||
def refresh(self):
|
||||
""" Refresh the metadata for the entire library. This will fetch fresh metadata for
|
||||
all contents in the library, including items that already have metadata.
|
||||
"""
|
||||
self.server.query('/library/sections/all/refresh')
|
||||
|
||||
def __len__(self):
|
||||
return len(self.sections())
|
||||
self._root._query('/library/sections/all/refresh')
|
||||
|
||||
|
||||
class LibrarySection(PlexObject):
|
||||
|
@ -205,10 +184,6 @@ class LibrarySection(PlexObject):
|
|||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
self.uuid = data.attrib.get('uuid')
|
||||
|
||||
def __repr__(self):
|
||||
title = self.title.replace(' ', '.')[0:20]
|
||||
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
||||
|
||||
def get(self, title):
|
||||
""" Returns the media item with the specified title.
|
||||
|
||||
|
@ -216,16 +191,17 @@ class LibrarySection(PlexObject):
|
|||
title (str): Title of the item to return.
|
||||
"""
|
||||
key = '/library/sections/%s/all' % self.key
|
||||
return utils.findItem(self.server, key, title)
|
||||
return self.fetchItem(key, title=title)
|
||||
|
||||
def all(self):
|
||||
""" Returns a list of media from this library section. """
|
||||
key = '/library/sections/%s/all' % self.key
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def onDeck(self):
|
||||
""" Returns a list of media items on deck from this library section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/onDeck' % self.key)
|
||||
key = '/library/sections/%s/onDeck' % self.key
|
||||
return self.fetchItems(key)
|
||||
|
||||
def recentlyAdded(self, maxresults=50):
|
||||
""" Returns a list of media items recently added from this library section.
|
||||
|
@ -237,17 +213,20 @@ class LibrarySection(PlexObject):
|
|||
|
||||
def analyze(self):
|
||||
""" Run an analysis on all of the items in this library section. """
|
||||
self.server.query('/library/sections/%s/analyze' % self.key, method=self.server.session.put)
|
||||
key = '/library/sections/%s/analyze' % self.key
|
||||
self._server._query(key, method=self.server.session.put)
|
||||
|
||||
def emptyTrash(self):
|
||||
""" If a section has items in the Trash, use this option to empty the Trash. """
|
||||
self.server.query('/library/sections/%s/emptyTrash' % self.key)
|
||||
key = '/library/sections/%s/emptyTrash' % self.key
|
||||
self._server._query(key)
|
||||
|
||||
def refresh(self):
|
||||
""" Refresh the metadata for this library section. This will fetch fresh metadata for
|
||||
all contents in the section, including items that already have metadata.
|
||||
"""
|
||||
self.server.query('/library/sections/%s/refresh' % self.key)
|
||||
key = '/library/sections/%s/refresh' % self.key
|
||||
self._server._query(key)
|
||||
|
||||
def listChoices(self, category, libtype=None, **kwargs):
|
||||
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
|
||||
|
@ -263,6 +242,7 @@ class LibrarySection(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
|
||||
"""
|
||||
# TODO: Should this be moved to base?
|
||||
if category in kwargs:
|
||||
raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
|
||||
args = {}
|
||||
|
@ -270,8 +250,8 @@ class LibrarySection(PlexObject):
|
|||
args[category] = self._cleanSearchFilter(subcategory, value)
|
||||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
|
||||
return utils.listItems(self.server, query, bytag=True)
|
||||
key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
|
||||
return self.fetchItems(key, bytag=True)
|
||||
|
||||
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
|
||||
""" Search the library. If there are many results, they will be fetched from the server
|
||||
|
@ -303,8 +283,7 @@ class LibrarySection(PlexObject):
|
|||
* studio: List of studios to search within ([studio_or_key, ...]). [music]
|
||||
* year: List of years to search within ([yyyy, ...]). [all]
|
||||
"""
|
||||
# Cleanup the core arguments
|
||||
# TODO: maxresults is raising a 500 error here.
|
||||
# cleanup the core arguments
|
||||
args = {}
|
||||
for category, value in kwargs.items():
|
||||
args[category] = self._cleanSearchFilter(category, value, libtype)
|
||||
|
@ -314,14 +293,13 @@ class LibrarySection(PlexObject):
|
|||
args['sort'] = self._cleanSearchSort(sort)
|
||||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
# Iterate over the results
|
||||
# iterate over the results
|
||||
results, subresults = [], '_init'
|
||||
args['X-Plex-Container-Start'] = 0
|
||||
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
|
||||
while subresults and maxresults > len(results):
|
||||
query = '/library/sections/%s/all%s' % (
|
||||
self.key, utils.joinArgs(args))
|
||||
subresults = utils.listItems(self.server, query)
|
||||
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
|
||||
subresults = self.fetchItems(key)
|
||||
results += subresults[:maxresults - len(results)]
|
||||
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
|
||||
return results
|
||||
|
@ -342,16 +320,10 @@ class LibrarySection(PlexObject):
|
|||
for item in value:
|
||||
item = str(item.id if isinstance(item, MediaTag) else item).lower()
|
||||
# find most logical choice(s) to use in url
|
||||
if item in allowed:
|
||||
result.add(item)
|
||||
continue
|
||||
if item in lookup:
|
||||
result.add(lookup[item])
|
||||
continue
|
||||
if item in allowed: result.add(item); continue
|
||||
if item in lookup: result.add(lookup[item]); continue
|
||||
matches = [k for t, k in lookup.items() if item in t]
|
||||
if matches:
|
||||
map(result.add, matches)
|
||||
continue
|
||||
if matches: map(result.add, matches); continue
|
||||
# nothing matched; use raw item value
|
||||
log.warning('Filter value not listed, using raw item value: %s' % item)
|
||||
result.add(item)
|
||||
|
@ -435,7 +407,8 @@ class MusicSection(LibrarySection):
|
|||
|
||||
def albums(self):
|
||||
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
|
||||
return utils.listItems(self.server, '/library/sections/%s/albums' % self.key)
|
||||
key = '/library/sections/%s/albums' % self.key
|
||||
return self.fetchItems(key)
|
||||
|
||||
def searchArtists(self, **kwargs):
|
||||
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
|
@ -459,48 +432,22 @@ class PhotoSection(LibrarySection):
|
|||
TYPE (str): 'photo'
|
||||
"""
|
||||
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
|
||||
ALLOWED_SORT = ()
|
||||
ALLOWED_SORT = ('addedAt')
|
||||
TYPE = 'photo'
|
||||
|
||||
def searchAlbums(self, title, **kwargs): # lets use this for now.
|
||||
def searchAlbums(self, title, **kwargs):
|
||||
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
albums = utils.listItems(self.server, '/library/sections/%s/all?type=14' % self.key)
|
||||
return [i for i in albums if i.title.lower() == title.lower()]
|
||||
key = '/library/sections/%s/all?type=14' % self.key
|
||||
return self.fetchItems(key, title=title)
|
||||
|
||||
def searchPhotos(self, title, **kwargs):
|
||||
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
|
||||
photos = utils.listItems(self.server, '/library/sections/%s/all?type=13' % self.key)
|
||||
return [i for i in photos if i.title.lower() == title.lower()]
|
||||
key = '/library/sections/%s/all?type=13' % self.key
|
||||
return self.fetchItems(key, title=title)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class Hub(PlexObject):
|
||||
TYPE = 'Hub'
|
||||
FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role}
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.hubIdentifier = data.attrib.get('hubIdentifier')
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.items = self._buildItems(data)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Hub:%s>' % self.title.encode('utf8')
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def _buildItems(self, data):
|
||||
if self.type in self.FILTERTYPES:
|
||||
cls = self.FILTERTYPES[self.type]
|
||||
return [cls(self._root, elem, self._initpath) for elem in data]
|
||||
return super(Hub, self)._buildItems(data, safe=True)
|
||||
|
||||
|
||||
@utils.register_libtype
|
||||
class FilterChoice(object):
|
||||
class FilterChoice(PlexObject):
|
||||
""" Represents a single filter choice. These objects are gathered when using filters
|
||||
while searching for library items and is the object returned in the result set of
|
||||
:func:`~plexapi.library.LibrarySection.listChoices()`.
|
||||
|
@ -517,16 +464,33 @@ class FilterChoice(object):
|
|||
"""
|
||||
TYPE = 'Directory'
|
||||
|
||||
def __init__(self, server, data, initpath):
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.server = server
|
||||
self.initpath = initpath
|
||||
self.fastKey = data.attrib.get('fastKey')
|
||||
self.key = data.attrib.get('key')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
|
||||
def __repr__(self):
|
||||
title = self.title.replace(' ', '.')[0:20]
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.key, title)
|
||||
|
||||
@utils.register_libtype
|
||||
class Hub(PlexObject):
|
||||
FILTERTYPES = {'genre':Genre, 'director':Director, 'actor':Role}
|
||||
TYPE = 'Hub'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.hubIdentifier = data.attrib.get('hubIdentifier')
|
||||
self.size = utils.cast(int, data.attrib.get('size'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.type = data.attrib.get('type')
|
||||
self.items = self._buildItems(data)
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def _buildItems(self, data):
|
||||
if self.type in self.FILTERTYPES:
|
||||
cls = self.FILTERTYPES[self.type]
|
||||
return [cls(self._root, elem, self._initpath) for elem in data]
|
||||
return super(Hub, self)._buildItems(data)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.utils import cast, listItems
|
||||
from plexapi.utils import cast
|
||||
|
||||
|
||||
class Media(PlexObject):
|
||||
|
@ -54,10 +54,6 @@ class Media(PlexObject):
|
|||
self.width = cast(int, data.attrib.get('width'))
|
||||
self.parts = self._buildItems(data, MediaPart)
|
||||
|
||||
def __repr__(self):
|
||||
title = self.video.title.replace(' ','.')[0:20]
|
||||
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
|
||||
|
||||
|
||||
class MediaPart(PlexObject):
|
||||
""" Represents a single media part (often a single file) for the media this belongs to.
|
||||
|
@ -85,9 +81,6 @@ class MediaPart(PlexObject):
|
|||
self.key = data.attrib.get('key')
|
||||
self.size = cast(int, data.attrib.get('size'))
|
||||
self.streams = self._buildStreams(data)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
def _buildStreams(self, data):
|
||||
streams = []
|
||||
|
@ -151,9 +144,6 @@ class MediaPartStream(PlexObject):
|
|||
cls = STREAMCLS.get(stype, MediaPartStream)
|
||||
return cls(server, data, initpath)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
|
||||
class VideoStream(MediaPartStream):
|
||||
""" Respresents a video stream within a :class:`~plexapi.media.MediaPart`.
|
||||
|
@ -314,19 +304,13 @@ class MediaTag(PlexObject):
|
|||
self.tagType = cast(int, data.attrib.get('tagType'))
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
|
||||
def __repr__(self):
|
||||
tag = self.tag.replace(' ', '.')[0:20].encode('utf-8')
|
||||
if self.librarySectionTitle:
|
||||
return u'<%s:%s:%s:%s>' % (self.__class__.__name__, self.id, tag, self.librarySectionTitle)
|
||||
return u'<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
|
||||
|
||||
def items(self, *args, **kwargs):
|
||||
""" Return the list of items within this tag. This function is only applicable
|
||||
in search results from PlexServer :func:`~plexapi.server.PlexServer.search()`.
|
||||
"""
|
||||
if not self.key:
|
||||
raise BadRequest('Key is not defined for this tag: %s' % self.tag)
|
||||
return listItems(self.server, self.key)
|
||||
return self.fetchItems(self.key)
|
||||
|
||||
|
||||
class Collection(MediaTag):
|
||||
|
@ -373,7 +357,3 @@ class Field(PlexObject):
|
|||
self._data = data
|
||||
self.name = data.attrib.get('name')
|
||||
self.locked = cast(bool, data.attrib.get('locked'))
|
||||
|
||||
def __repr__(self):
|
||||
name = self.name.replace(' ', '.')[0:20]
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, name, self.locked)
|
||||
|
|
|
@ -55,9 +55,6 @@ class MyPlexAccount(PlexObject):
|
|||
data = self._query(self.SIGNIN, method=self._session.post, auth=(username, password))
|
||||
super(MyPlexAccount, self).__init__(self, data, self.SIGNIN)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self._token = data.attrib.get('authenticationToken')
|
||||
|
@ -202,9 +199,6 @@ class MyPlexUser(PlexObject):
|
|||
self.title = data.attrib.get('title')
|
||||
self.username = data.attrib.get('username')
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
|
||||
|
||||
|
||||
class MyPlexResource(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that can provide
|
||||
|
@ -254,9 +248,6 @@ class MyPlexResource(PlexObject):
|
|||
self.presence = utils.cast(bool, data.attrib.get('presence'))
|
||||
self.connections = self._buildItems(data, ResourceConnection)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
|
||||
|
||||
def connect(self, ssl=None, safe=False):
|
||||
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
|
||||
one address specified for a server or client. This function will prioritize local
|
||||
|
@ -332,9 +323,6 @@ class ResourceConnection(PlexObject):
|
|||
self.local = utils.cast(bool, data.attrib.get('local'))
|
||||
self.httpuri = 'http://%s:%s' % (self.address, self.port)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
|
||||
|
||||
|
||||
class MyPlexDevice(PlexObject):
|
||||
""" This object represents resources connected to your Plex server that provide
|
||||
|
@ -386,9 +374,6 @@ class MyPlexDevice(PlexObject):
|
|||
self.screenDensity = data.attrib.get('screenDensity')
|
||||
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
|
||||
|
||||
def connect(self, safe=False):
|
||||
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
|
||||
one address specified for a server or client. After trying to connect to all
|
||||
|
|
|
@ -51,7 +51,7 @@ class Photoalbum(PlexPartialObject):
|
|||
def photos(self):
|
||||
""" Returns a list of :class:`~plexapi.photo.Photo` objects in this album. """
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def photo(self, title):
|
||||
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """
|
||||
|
@ -114,7 +114,7 @@ class Photo(PlexPartialObject):
|
|||
|
||||
def photoalbum(self):
|
||||
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def section(self):
|
||||
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
|
||||
|
|
|
@ -30,7 +30,7 @@ class Playlist(PlexPartialObject, Playable):
|
|||
def items(self):
|
||||
""" Returns a list of all items in the playlist. """
|
||||
key = '%s/items' % self.key
|
||||
return self._fetchItems(key)
|
||||
return self.fetchItems(key)
|
||||
|
||||
def addItems(self, items):
|
||||
"""Add items to a playlist."""
|
||||
|
@ -43,27 +43,27 @@ class Playlist(PlexPartialObject, Playable):
|
|||
ratingKeys.append(str(item.ratingKey))
|
||||
uuid = items[0].section().uuid
|
||||
ratingKeys = ','.join(ratingKeys)
|
||||
path = '%s/items%s' % (self.key, utils.joinArgs({
|
||||
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
|
||||
key = '%s/items%s' % (self.key, utils.joinArgs({
|
||||
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys)
|
||||
}))
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
return self._root._query(key, method=self._root._session.put)
|
||||
|
||||
def removeItem(self, item):
|
||||
"""Remove a file from a playlist."""
|
||||
path = '%s/items/%s' % (self.key, item.playlistItemID)
|
||||
return self._root._query(path, method=self._root._session.delete)
|
||||
key = '%s/items/%s' % (self.key, item.playlistItemID)
|
||||
return self._root._query(key, method=self._root._session.delete)
|
||||
|
||||
def moveItem(self, item, after=None):
|
||||
"""Move a to a new position in playlist."""
|
||||
path = '%s/items/%s/move' % (self.key, item.playlistItemID)
|
||||
key = '%s/items/%s/move' % (self.key, item.playlistItemID)
|
||||
if after:
|
||||
path += '?after=%s' % after.playlistItemID
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
key += '?after=%s' % after.playlistItemID
|
||||
return self._root._query(key, method=self._root._session.put)
|
||||
|
||||
def edit(self, title=None, summary=None):
|
||||
"""Edit playlist."""
|
||||
path = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
|
||||
return self._root._query(path, method=self._root._session.put)
|
||||
key = '/library/metadata/%s%s' % (self.ratingKey, utils.joinArgs({'title':title, 'summary':summary}))
|
||||
return self._root._query(key, method=self._root._session.put)
|
||||
|
||||
def delete(self):
|
||||
"""Delete playlist."""
|
||||
|
@ -81,11 +81,11 @@ class Playlist(PlexPartialObject, Playable):
|
|||
ratingKeys.append(str(item.ratingKey))
|
||||
ratingKeys = ','.join(ratingKeys)
|
||||
uuid = items[0].section().uuid
|
||||
path = '/playlists%s' % utils.joinArgs({
|
||||
key = '/playlists%s' % utils.joinArgs({
|
||||
'uri': 'library://%s/directory//library/metadata/%s' % (uuid, ratingKeys),
|
||||
'type': items[0].listType,
|
||||
'title': title,
|
||||
'smart': 0
|
||||
})
|
||||
data = server._query(path, method=server._session.post)[0]
|
||||
return cls(server, data, initpath=path)
|
||||
data = server._query(key, method=server._session.post)[0]
|
||||
return cls(server, data, initpath=key)
|
||||
|
|
|
@ -140,9 +140,6 @@ class PlexServer(PlexObject):
|
|||
self.version = data.attrib.get('version')
|
||||
self.voiceSearch = cast(bool, data.attrib.get('voiceSearch'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self._baseurl)
|
||||
|
||||
def _query(self, key, method=None, headers=None, **kwargs):
|
||||
""" Main method used to handle HTTPS requests to the Plex server. This method helps
|
||||
by encoding the response to utf-8 and parsing the returned XML into and
|
||||
|
@ -233,14 +230,13 @@ class PlexServer(PlexObject):
|
|||
|
||||
def history(self):
|
||||
""" Returns a list of media items from watched history. """
|
||||
#return utils.listItems(self, '/status/sessions/history/all')
|
||||
return self._fetchItems('/status/sessions/history/all')
|
||||
return self.fetchItems('/status/sessions/history/all')
|
||||
|
||||
def playlists(self):
|
||||
""" Returns a list of all :class:`~plexapi.playlist.Playlist` objects saved on the server. """
|
||||
# TODO: Add sort and type options?
|
||||
# /playlists/all?type=15&sort=titleSort%3Aasc&playlistType=video&smart=0
|
||||
return self._fetchItems('/playlists')
|
||||
return self.fetchItems('/playlists')
|
||||
|
||||
def playlist(self, title):
|
||||
""" Returns the :class:`~plexapi.client.Playlist` that matches the specified title.
|
||||
|
@ -251,10 +247,7 @@ class PlexServer(PlexObject):
|
|||
Raises:
|
||||
:class:`~plexapi.exceptions.NotFound`: Invalid playlist title
|
||||
"""
|
||||
for item in self.playlists():
|
||||
if item.title == title:
|
||||
return item
|
||||
raise NotFound('Invalid playlist title: %s' % title)
|
||||
return self.fetchItem('/playlists', title=title)
|
||||
|
||||
def search(self, query, mediatype=None, limit=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
|
@ -280,14 +273,13 @@ class PlexServer(PlexObject):
|
|||
if limit:
|
||||
params['limit'] = limit
|
||||
key = '/hubs/search?%s' % urlencode(params)
|
||||
for hub in self._fetchItems(key, bytag=True):
|
||||
for hub in self.fetchItems(key, bytag=True):
|
||||
results += hub.items
|
||||
return results
|
||||
|
||||
def sessions(self):
|
||||
""" Returns a list of all active session (currently playing) media objects. """
|
||||
return self._fetchItems('/status/sessions')
|
||||
#return utils.listItems(self, '/status/sessions')
|
||||
return self.fetchItems('/status/sessions')
|
||||
|
||||
def transcodeImage(self, media, height, width, opacity=100, saturation=100):
|
||||
""" Returns the URL for a transcoded image from the specified media object.
|
||||
|
@ -300,6 +292,7 @@ class PlexServer(PlexObject):
|
|||
opacity (int): Opacity of the resulting image (possibly deprecated).
|
||||
saturation (int): Saturating of the resulting image.
|
||||
"""
|
||||
# TODO: Does this function really belong here?
|
||||
if media:
|
||||
transcode_url = '/photo/:/transcode?height=%s&width=%s&opacity=%s&saturation=%s&url=%s' % (
|
||||
height, width, opacity, saturation, media)
|
||||
|
|
|
@ -39,9 +39,6 @@ class SyncItem(object):
|
|||
self.policy = data.find('Policy').attrib.copy()
|
||||
self.location = data.find('Location').attrib.copy()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s:%s>' % (self.__class__.__name__, self.id)
|
||||
|
||||
def server(self):
|
||||
server = list(filter(lambda x: x.machineIdentifier == self.machineIdentifier, self._servers))
|
||||
if 0 == len(server):
|
||||
|
@ -50,11 +47,11 @@ class SyncItem(object):
|
|||
|
||||
def getMedia(self):
|
||||
server = self.server().connect()
|
||||
items = utils.listItems(server, '/sync/items/%s' % self.id)
|
||||
return items
|
||||
key = '/sync/items/%s' % self.id
|
||||
return server.fetchItems(key)
|
||||
|
||||
def markAsDone(self, sync_id):
|
||||
server = self.server().connect()
|
||||
url = '/sync/%s/%s/files/%s/downloaded' % (
|
||||
self._device.clientIdentifier, server.machineIdentifier, sync_id)
|
||||
server.query(url, method=requests.put)
|
||||
server._query(url, method=requests.put)
|
||||
|
|
|
@ -39,28 +39,6 @@ def register_libtype(cls):
|
|||
return cls
|
||||
|
||||
|
||||
# def buildItem(server, elem, initpath, bytag=False):
|
||||
# """ Factory function to build the objects used within the PlexAPI.
|
||||
|
||||
# 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' />
|
||||
|
||||
# Raises:
|
||||
# UnknownType: Unknown library type.
|
||||
# """
|
||||
# libtype = elem.tag if bytag else elem.attrib.get('type')
|
||||
# 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):
|
||||
""" Cast the specified value to the specified type (returned by func). Currently this
|
||||
only support int, float, bool. Should be extended if needed.
|
||||
|
@ -81,42 +59,6 @@ def cast(func, value):
|
|||
return value
|
||||
|
||||
|
||||
def findKey(server, key):
|
||||
""" Finds and builds a object based on ratingKey.
|
||||
|
||||
Parameters:
|
||||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
key (int): ratingKey to find and return.
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to find 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):
|
||||
""" Finds and builds a object based on title.
|
||||
|
||||
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.
|
||||
|
||||
Raises:
|
||||
NotFound: Unable to find item.
|
||||
"""
|
||||
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):
|
||||
""" Returns a list of filepaths from a location tag.
|
||||
|
||||
|
@ -225,33 +167,7 @@ def listChoices(server, path):
|
|||
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
|
||||
path (str): Relative path to request XML data from.
|
||||
"""
|
||||
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
|
||||
|
||||
|
||||
def listItems(server, path, libtype=None, watched=None, bytag=False):
|
||||
""" 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).
|
||||
"""
|
||||
items = []
|
||||
for elem in server.query(path):
|
||||
if libtype and elem.attrib.get('type') != libtype:
|
||||
continue
|
||||
if watched is True and int(elem.attrib.get('viewCount', 0)) == 0:
|
||||
continue
|
||||
if watched is False and int(elem.attrib.get('viewCount', 0)) >= 1:
|
||||
continue
|
||||
try:
|
||||
items.append(buildItem(server, elem, path, bytag))
|
||||
except UnknownType:
|
||||
pass
|
||||
return items
|
||||
return {c.attrib['title']: c.attrib['key'] for c in server._query(path)}
|
||||
|
||||
|
||||
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
|
||||
|
|
123
plexapi/video.py
123
plexapi/video.py
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from plexapi import media, utils
|
||||
from plexapi.exceptions import NotFound
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
|
||||
|
||||
|
@ -36,24 +36,25 @@ class Video(PlexPartialObject):
|
|||
that are useful to know–whether it's a video file,
|
||||
a music track, or one of your photos.
|
||||
"""
|
||||
self._root.query('/%s/analyze' % self.key.lstrip('/'), method=self._root._session.put)
|
||||
key = '/%s/analyze' % self.key.lstrip('/')
|
||||
self._root._query(key, method=self._root._session.put)
|
||||
|
||||
def markWatched(self):
|
||||
"""Mark a items as watched."""
|
||||
path = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._root.query(path)
|
||||
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._root._query(key)
|
||||
self.reload()
|
||||
|
||||
def markUnwatched(self):
|
||||
"""Mark a item as unwatched."""
|
||||
path = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._root.query(path)
|
||||
key = '/:/unscrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._root._query(key)
|
||||
self.reload()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh a item."""
|
||||
self._root.query('%s/refresh' %
|
||||
self.key, method=self._root._session.put)
|
||||
key = '%s/refresh' % self.key
|
||||
self._root._query(key, method=self._root._session.put)
|
||||
|
||||
def section(self):
|
||||
"""Library section."""
|
||||
|
@ -69,13 +70,12 @@ class Movie(Video, Playable):
|
|||
|
||||
Args:
|
||||
data (Element): XML reponse from PMS as Element
|
||||
normally built from server.query
|
||||
normally built from server._query
|
||||
"""
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
self.art = data.attrib.get('art')
|
||||
self.audienceRating = utils.cast(
|
||||
float, data.attrib.get('audienceRating'))
|
||||
self.audienceRating = utils.cast(float, data.attrib.get('audienceRating'))
|
||||
self.audienceRatingImage = data.attrib.get('audienceRatingImage')
|
||||
self.chapterSource = data.attrib.get('chapterSource')
|
||||
self.contentRating = data.attrib.get('contentRating')
|
||||
|
@ -101,9 +101,6 @@ class Movie(Video, Playable):
|
|||
self.producers = self._buildItems(data, media.Producer)
|
||||
self.roles = self._buildItems(data, media.Role)
|
||||
self.writers = self._buildItems(data, media.Writer)
|
||||
# self.videoStreams = utils.findStreams(self.media, 'videostream') # these dont go here
|
||||
# self.audioStreams = utils.findStreams(self.media, 'audiostream') # these dont go here
|
||||
# self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') # these dont go here
|
||||
|
||||
@property
|
||||
def actors(self):
|
||||
|
@ -181,7 +178,7 @@ class Show(Video):
|
|||
def seasons(self):
|
||||
"""Returns a list of Season."""
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key, Season.TYPE)
|
||||
return self.fetchItems(key, type=Season.TYPE)
|
||||
|
||||
def season(self, title=None):
|
||||
""" Returns the season with the specified title or number.
|
||||
|
@ -192,16 +189,12 @@ class Show(Video):
|
|||
if isinstance(title, int):
|
||||
title = 'Season %s' % title
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItem(key, title)
|
||||
return self.fetchItem(key, tag='Directory', title=title)
|
||||
|
||||
def episodes(self, watched=None):
|
||||
"""Returs a list of Episode
|
||||
|
||||
Args:
|
||||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
"""
|
||||
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return utils.listItems(self._root, leavesKey, watched=watched)
|
||||
def episodes(self):
|
||||
""" Returs a list of Episode """
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return self.fetchItems(key)
|
||||
|
||||
def episode(self, title=None, season=None, episode=None):
|
||||
"""Find a episode using a title or season and episode.
|
||||
|
@ -231,8 +224,8 @@ class Show(Video):
|
|||
if not title and (not season or not episode):
|
||||
raise TypeError('Missing argument: title or season and episode are required')
|
||||
if title:
|
||||
path = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return utils.findItem(self._root, path, title)
|
||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
||||
return self._findItem(key, title)
|
||||
elif season and episode:
|
||||
results = [i for i in self.episodes() if i.seasonNumber == season and i.index == episode]
|
||||
if results:
|
||||
|
@ -261,7 +254,7 @@ class Show(Video):
|
|||
|
||||
def refresh(self):
|
||||
"""Refresh the metadata."""
|
||||
self._root.query('/library/metadata/%s/refresh' % self.ratingKey, method=self._root._session.put)
|
||||
self._root._query('/library/metadata/%s/refresh' % self.ratingKey, method=self._root._session.put)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
downloaded = []
|
||||
|
@ -280,7 +273,7 @@ class Season(Video):
|
|||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
data (Element): Usually built from server._query
|
||||
"""
|
||||
Video._loadData(self, data)
|
||||
self.key = self.key.replace('/children', '')
|
||||
|
@ -291,6 +284,13 @@ class Season(Video):
|
|||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount'))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
'%s-s%s' % (self.parentTitle.replace(' ','-')[:20], self.seasonNumber),
|
||||
] if p])
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
@ -307,27 +307,19 @@ class Season(Video):
|
|||
watched (bool): Defaults to None. Exclude watched episodes
|
||||
"""
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
return self._fetchItems(key, Episode.TYPE)
|
||||
# childrenKey = '/library/metadata/%s/children' % self.ratingKey
|
||||
# return utils.listItems(self._root, childrenKey, watched=watched)
|
||||
return self.fetchItems(key, type=Episode.TYPE)
|
||||
|
||||
def episode(self, title=None, episode=None):
|
||||
"""Find a episode using a title or season and episode.
|
||||
def episode(self, title=None, num=None):
|
||||
""" Returns the episode with the given title or number.
|
||||
|
||||
Note:
|
||||
episode is required if title is missing.
|
||||
|
||||
Args:
|
||||
title (str): Default None
|
||||
episode (int): Episode number, default None
|
||||
Parameters:
|
||||
title (str): Title of the episode to return.
|
||||
num (int): Number of the episode to return (if title not specified).
|
||||
|
||||
Raises:
|
||||
TypeError: If title and episode is missing.
|
||||
NotFound: If that episode cant be found.
|
||||
|
||||
Returns:
|
||||
Episode
|
||||
|
||||
Examples:
|
||||
>>> plex.search('The blacklist').season(1).episode(episode=1)
|
||||
<Episode:116263:The.Freelancer>
|
||||
|
@ -335,31 +327,21 @@ class Season(Video):
|
|||
<Episode:116263:The.Freelancer>
|
||||
|
||||
"""
|
||||
if not title and not episode:
|
||||
raise TypeError('Missing argument, you need to use title or episode.')
|
||||
if not title and not num:
|
||||
raise BadRequest('Missing argument, you need to use title or episode.')
|
||||
|
||||
key = '/library/metadata/%s/children' % self.ratingKey
|
||||
if title:
|
||||
path = '/library/metadata/%s/children' % self.ratingKey
|
||||
return utils.findItem(self._root, path, title)
|
||||
elif episode:
|
||||
results = [i for i in self.episodes() if i.seasonNumber == self.index and i.index == episode]
|
||||
if results:
|
||||
return results[0]
|
||||
raise NotFound('Couldnt find %s.Season %s Episode %s.' % (self.grandparentTitle, self.index. episode))
|
||||
return self._findItem(key, title=title)
|
||||
return self._findItem(key, seasonNumber=self.index, index=num)
|
||||
|
||||
def get(self, title):
|
||||
"""Get a episode with a matching title.
|
||||
|
||||
Args:
|
||||
title (str): fx Secret santa
|
||||
|
||||
Returns:
|
||||
Episode
|
||||
"""
|
||||
""" Alias for self.episode. """
|
||||
return self.episode(title)
|
||||
|
||||
def show(self):
|
||||
"""Return this seasons show."""
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def watched(self):
|
||||
"""Returns a list of watched Episode"""
|
||||
|
@ -369,12 +351,6 @@ class Season(Video):
|
|||
"""Returns a list of unwatched Episode"""
|
||||
return self.episodes(watched=False)
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
key = self.key.replace('/library/metadata/', '').replace('/children', '') if self.key else 'NA'
|
||||
title = self.title.replace(' ', '.')[0:20].encode('utf8')
|
||||
return '<%s:%s:%s:%s>' % (clsname, key, self.parentTitle, title)
|
||||
|
||||
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
|
||||
downloaded = []
|
||||
for ep in self.episodes():
|
||||
|
@ -392,7 +368,7 @@ class Episode(Video, Playable):
|
|||
"""Used to set the attributes
|
||||
|
||||
Args:
|
||||
data (Element): Usually built from server.query
|
||||
data (Element): Usually built from server._query
|
||||
"""
|
||||
Video._loadData(self, data)
|
||||
Playable._loadData(self, data)
|
||||
|
@ -427,10 +403,11 @@ class Episode(Video, Playable):
|
|||
self.transcodeSession = utils.findTranscodeSession(self._root, data)
|
||||
|
||||
def __repr__(self):
|
||||
clsname = self.__class__.__name__
|
||||
key = self.key.replace('/library/metadata/', '').replace('/children', '') if self.key else 'NA'
|
||||
title = self.title.replace(' ', '.')[0:20].encode('utf8')
|
||||
return '<%s:%s:%s:S%s:E%s:%s>' % (clsname, key, self.grandparentTitle, self.seasonNumber, self.index, title)
|
||||
return '<%s>' % ':'.join([p for p in [
|
||||
self.__class__.__name__,
|
||||
self.key.replace('/library/metadata/', '').replace('/children', ''),
|
||||
'%s-s%se%s' % (self.grandparentTitle.replace(' ','-')[:20], self.seasonNumber, self.index),
|
||||
] if p])
|
||||
|
||||
@property
|
||||
def isWatched(self):
|
||||
|
@ -452,11 +429,11 @@ class Episode(Video, Playable):
|
|||
|
||||
def season(self):
|
||||
"""Return this episode Season"""
|
||||
return utils.listItems(self._root, self.parentKey)[0]
|
||||
return self.fetchItem(self.parentKey)
|
||||
|
||||
def show(self):
|
||||
"""Return this episodes Show"""
|
||||
return utils.listItems(self._root, self.grandparentKey)[0]
|
||||
return self.fetchItem(self.grandparentKey)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
|
|
Loading…
Reference in a new issue