mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-07 13:41:15 -07:00
Update PlexAPI to 4.5.2
This commit is contained in:
parent
f6ca1dfa28
commit
74b4e45915
12 changed files with 947 additions and 362 deletions
|
@ -15,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
|
|||
|
||||
# PlexAPI Settings
|
||||
PROJECT = 'PlexAPI'
|
||||
VERSION = '4.4.1'
|
||||
VERSION = '4.5.2'
|
||||
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
||||
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
|
||||
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
||||
|
|
|
@ -4,7 +4,7 @@ from urllib.parse import quote_plus
|
|||
from plexapi import library, media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
||||
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||
|
||||
|
@ -52,7 +52,7 @@ class Audio(PlexPartialObject):
|
|||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'audio'
|
||||
|
@ -114,7 +114,7 @@ class Audio(PlexPartialObject):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Artist(Audio, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
||||
""" Represents a single Artist.
|
||||
|
||||
|
|
|
@ -144,34 +144,9 @@ class PlexObject(object):
|
|||
it only returns those items. By default we convert the xml elements
|
||||
with the best guess PlexObjects based on tag and type attrs.
|
||||
etag (str): Only fetch items with the specified tag.
|
||||
**kwargs (dict): Optionally add attribute filters on the items to fetch. For
|
||||
example, passing in viewCount=0 will only return matching items. Filtering
|
||||
is done before the Python objects are built to help keep things speedy.
|
||||
Note: Because some attribute names are already used as arguments to this
|
||||
function, such as 'tag', you may still reference the attr tag byappending
|
||||
an underscore. For example, passing in _tag='foobar' will return all items
|
||||
where tag='foobar'. Also Note: Case very much matters when specifying kwargs
|
||||
-- Optionally, operators can be specified by append it
|
||||
to the end of the attribute name for more complex lookups. For example,
|
||||
passing in viewCount__gte=0 will return all items where viewCount >= 0.
|
||||
Available operations include:
|
||||
|
||||
* __contains: Value contains specified arg.
|
||||
* __endswith: Value ends with specified arg.
|
||||
* __exact: Value matches specified arg.
|
||||
* __exists (bool): Value is or is not present in the attrs.
|
||||
* __gt: Value is greater than specified arg.
|
||||
* __gte: Value is greater than or equal to specified arg.
|
||||
* __icontains: Case insensative value contains specified arg.
|
||||
* __iendswith: Case insensative value ends with specified arg.
|
||||
* __iexact: Case insensative value matches specified arg.
|
||||
* __in: Value is in a specified list or tuple.
|
||||
* __iregex: Case insensative value matches the specified regular expression.
|
||||
* __istartswith: Case insensative value starts with specified arg.
|
||||
* __lt: Value is less than specified arg.
|
||||
* __lte: Value is less than or equal to specified arg.
|
||||
* __regex: Value matches the specified regular expression.
|
||||
* __startswith: Value starts with specified arg.
|
||||
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||
See :func:`~plexapi.base.PlexObject.fetchItems` for more details
|
||||
on how this is used.
|
||||
"""
|
||||
if ekey is None:
|
||||
raise BadRequest('ekey was not provided')
|
||||
|
@ -185,12 +160,76 @@ class PlexObject(object):
|
|||
|
||||
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
|
||||
""" Load the specified key to find and build all items with the specified tag
|
||||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
on how this is used.
|
||||
and attrs.
|
||||
|
||||
Parameters:
|
||||
ekey (str): API URL path in Plex to fetch items from.
|
||||
cls (:class:`~plexapi.base.PlexObject`): If you know the class of the
|
||||
items to be fetched, passing this in will help the parser ensure
|
||||
it only returns those items. By default we convert the xml elements
|
||||
with the best guess PlexObjects based on tag and type attrs.
|
||||
etag (str): Only fetch items with the specified tag.
|
||||
container_start (None, int): offset to get a subset of the data
|
||||
container_size (None, int): How many items in data
|
||||
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||
See the details below for more info.
|
||||
|
||||
**Filtering XML Attributes**
|
||||
|
||||
Any XML attribute can be filtered when fetching results. Filtering is done before
|
||||
the Python objects are built to help keep things speedy. For example, passing in
|
||||
``viewCount=0`` will only return matching items where the view count is ``0``.
|
||||
Note that case matters when specifying attributes. Attributes futher down in the XML
|
||||
tree can be filtered by *prepending* the attribute with each element tag ``Tag__``.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fetchItem(ekey, viewCount=0)
|
||||
fetchItem(ekey, contentRating="PG")
|
||||
fetchItem(ekey, Genre__tag="Animation")
|
||||
fetchItem(ekey, Media__videoCodec="h265")
|
||||
fetchItem(ekey, Media__Part__container="mp4)
|
||||
|
||||
Note that because some attribute names are already used as arguments to this
|
||||
function, such as ``tag``, you may still reference the attr tag by prepending an
|
||||
underscore. For example, passing in ``_tag='foobar'`` will return all items where
|
||||
``tag='foobar'``.
|
||||
|
||||
**Using PlexAPI Operators**
|
||||
|
||||
Optionally, PlexAPI operators can be specified by *appending* it to the end of the
|
||||
attribute for more complex lookups. For example, passing in ``viewCount__gte=0``
|
||||
will return all items where ``viewCount >= 0``.
|
||||
|
||||
List of Available Operators:
|
||||
|
||||
* ``__contains``: Value contains specified arg.
|
||||
* ``__endswith``: Value ends with specified arg.
|
||||
* ``__exact``: Value matches specified arg.
|
||||
* ``__exists`` (*bool*): Value is or is not present in the attrs.
|
||||
* ``__gt``: Value is greater than specified arg.
|
||||
* ``__gte``: Value is greater than or equal to specified arg.
|
||||
* ``__icontains``: Case insensative value contains specified arg.
|
||||
* ``__iendswith``: Case insensative value ends with specified arg.
|
||||
* ``__iexact``: Case insensative value matches specified arg.
|
||||
* ``__in``: Value is in a specified list or tuple.
|
||||
* ``__iregex``: Case insensative value matches the specified regular expression.
|
||||
* ``__istartswith``: Case insensative value starts with specified arg.
|
||||
* ``__lt``: Value is less than specified arg.
|
||||
* ``__lte``: Value is less than or equal to specified arg.
|
||||
* ``__regex``: Value matches the specified regular expression.
|
||||
* ``__startswith``: Value starts with specified arg.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fetchItem(ekey, viewCount__gte=0)
|
||||
fetchItem(ekey, Media__container__in=["mp4", "mkv"])
|
||||
fetchItem(ekey, guid__iregex=r"(imdb:\/\/|themoviedb:\/\/)")
|
||||
fetchItem(ekey, Media__Part__file__startswith="D:\\Movies")
|
||||
|
||||
"""
|
||||
url_kw = {}
|
||||
|
@ -204,7 +243,7 @@ class PlexObject(object):
|
|||
data = self._server.query(ekey, params=url_kw)
|
||||
items = self.findItems(data, cls, ekey, **kwargs)
|
||||
|
||||
librarySectionID = data.attrib.get('librarySectionID')
|
||||
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
if librarySectionID:
|
||||
for item in items:
|
||||
item.librarySectionID = librarySectionID
|
||||
|
@ -526,6 +565,8 @@ class Playable(object):
|
|||
transcodeSessions (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
|
||||
if item is being transcoded (None otherwise).
|
||||
viewedAt (datetime): Datetime item was last viewed (history).
|
||||
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
|
||||
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
|
||||
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
|
||||
playQueueItemID (int): PlayQueue item ID (only populated for :class:`~plexapi.playlist.PlayQueue` items).
|
||||
"""
|
||||
|
@ -538,6 +579,7 @@ class Playable(object):
|
|||
self.session = self.findItems(data, etag='Session') # session
|
||||
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
|
||||
self.accountID = utils.cast(int, data.attrib.get('accountID')) # history
|
||||
self.deviceID = utils.cast(int, data.attrib.get('deviceID')) # history
|
||||
self.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
|
||||
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ class PlexClient(PlexObject):
|
|||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Path used to generate data.
|
||||
baseurl (str): HTTP URL to connect dirrectly to this client.
|
||||
identifier (str): The resource/machine identifier for the desired client.
|
||||
May be necessary when connecting to a specific proxied client (optional).
|
||||
token (str): X-Plex-Token used for authenication (optional).
|
||||
session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
|
||||
timeout (int): timeout in seconds on initial connect to client (default config.TIMEOUT).
|
||||
|
@ -59,9 +61,10 @@ class PlexClient(PlexObject):
|
|||
key = '/resources'
|
||||
|
||||
def __init__(self, server=None, data=None, initpath=None, baseurl=None,
|
||||
token=None, connect=True, session=None, timeout=None):
|
||||
identifier=None, token=None, connect=True, session=None, timeout=None):
|
||||
super(PlexClient, self).__init__(server, data, initpath)
|
||||
self._baseurl = baseurl.strip('/') if baseurl else None
|
||||
self._clientIdentifier = identifier
|
||||
self._token = logfilter.add_secret(token)
|
||||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||
server_session = server._session if server else None
|
||||
|
@ -90,7 +93,25 @@ class PlexClient(PlexObject):
|
|||
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||
self._initpath = self.key
|
||||
data = self.query(self.key, timeout=timeout)
|
||||
self._loadData(data[0])
|
||||
if not data:
|
||||
raise NotFound("Client not found at %s" % self._baseurl)
|
||||
if self._clientIdentifier:
|
||||
client = next(
|
||||
(
|
||||
x
|
||||
for x in data
|
||||
if x.attrib.get("machineIdentifier") == self._clientIdentifier
|
||||
),
|
||||
None,
|
||||
)
|
||||
if client is None:
|
||||
raise NotFound(
|
||||
"Client with identifier %s not found at %s"
|
||||
% (self._clientIdentifier, self._baseurl)
|
||||
)
|
||||
else:
|
||||
client = data[0]
|
||||
self._loadData(client)
|
||||
return self
|
||||
|
||||
def reload(self):
|
||||
|
|
|
@ -59,7 +59,7 @@ class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
|
|||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi import media, settings, utils
|
||||
from plexapi.exceptions import NotFound
|
||||
|
||||
|
||||
class AdvancedSettingsMixin(object):
|
||||
""" Mixin for Plex objects that can have advanced settings. """
|
||||
|
||||
def preferences(self):
|
||||
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
|
||||
items = []
|
||||
data = self._server.query(self._details_key)
|
||||
for item in data.iter('Preferences'):
|
||||
for elem in item:
|
||||
setting = settings.Preferences(data=elem, server=self._server)
|
||||
setting._initpath = self.key
|
||||
items.append(setting)
|
||||
|
||||
return items
|
||||
|
||||
def preference(self, pref):
|
||||
""" Returns a :class:`~plexapi.settings.Preferences` object for the specified pref.
|
||||
|
||||
Parameters:
|
||||
pref (str): The id of the preference to return.
|
||||
"""
|
||||
prefs = self.preferences()
|
||||
try:
|
||||
return next(p for p in prefs if p.id == pref)
|
||||
except StopIteration:
|
||||
availablePrefs = [p.id for p in prefs]
|
||||
raise NotFound('Unknown preference "%s" for %s. '
|
||||
'Available preferences: %s'
|
||||
% (pref, self.TYPE, availablePrefs)) from None
|
||||
|
||||
def editAdvanced(self, **kwargs):
|
||||
""" Edit a Plex object's advanced settings. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
preferences = {pref.id: list(pref.enumValues.keys()) for pref in self.preferences()}
|
||||
for settingID, value in kwargs.items():
|
||||
enumValues = preferences.get(settingID)
|
||||
if value in enumValues:
|
||||
data[settingID] = value
|
||||
else:
|
||||
raise NotFound('%s not found in %s' % (value, enumValues))
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
def defaultAdvanced(self):
|
||||
""" Edit all of a Plex object's advanced settings to default. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
for preference in self.preferences():
|
||||
data[preference.id] = preference.default
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
|
||||
class ArtUrlMixin(object):
|
||||
""" Mixin for Plex objects that can have a background artwork url. """
|
||||
|
||||
|
|
|
@ -499,15 +499,18 @@ class MyPlexAccount(PlexObject):
|
|||
url = self.PLEXSERVERS.replace('{machineId}', machineIdentifier)
|
||||
data = self.query(url, self._session.get)
|
||||
for elem in data[0]:
|
||||
allSectionIds[elem.attrib.get('id', '').lower()] = elem.attrib.get('id')
|
||||
allSectionIds[elem.attrib.get('title', '').lower()] = elem.attrib.get('id')
|
||||
allSectionIds[elem.attrib.get('key', '').lower()] = elem.attrib.get('id')
|
||||
_id = utils.cast(int, elem.attrib.get('id'))
|
||||
_key = utils.cast(int, elem.attrib.get('key'))
|
||||
_title = elem.attrib.get('title', '').lower()
|
||||
allSectionIds[_id] = _id
|
||||
allSectionIds[_key] = _id
|
||||
allSectionIds[_title] = _id
|
||||
log.debug(allSectionIds)
|
||||
# Convert passed in section items to section ids from above lookup
|
||||
sectionIds = []
|
||||
for section in sections:
|
||||
sectionKey = section.key if isinstance(section, LibrarySection) else section
|
||||
sectionIds.append(allSectionIds[sectionKey.lower()])
|
||||
sectionKey = section.key if isinstance(section, LibrarySection) else section.lower()
|
||||
sectionIds.append(allSectionIds[sectionKey])
|
||||
return sectionIds
|
||||
|
||||
def _filterDictToStr(self, filterDict):
|
||||
|
@ -799,28 +802,28 @@ class MyPlexUser(PlexObject):
|
|||
|
||||
class Section(PlexObject):
|
||||
""" This refers to a shared section. The raw xml for the data presented here
|
||||
can be found at: https://plex.tv/api/servers/{machineId}/shared_servers/{serverId}
|
||||
can be found at: https://plex.tv/api/servers/{machineId}/shared_servers
|
||||
|
||||
Attributes:
|
||||
TAG (str): section
|
||||
id (int): shared section id
|
||||
sectionKey (str): what key we use for this section
|
||||
title (str): Title of the section
|
||||
sectionId (str): shared section id
|
||||
type (str): movie, tvshow, artist
|
||||
id (int): The shared section ID
|
||||
key (int): The shared library section key
|
||||
shared (bool): If this section is shared with the user
|
||||
title (str): Title of the section
|
||||
type (str): movie, tvshow, artist
|
||||
|
||||
"""
|
||||
TAG = 'Section'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
# self.id = utils.cast(int, data.attrib.get('id')) # Havnt decided if this should be changed.
|
||||
self.sectionKey = data.attrib.get('key')
|
||||
self.id = utils.cast(int, data.attrib.get('id'))
|
||||
self.key = utils.cast(int, data.attrib.get('key'))
|
||||
self.shared = utils.cast(bool, data.attrib.get('shared', '0'))
|
||||
self.title = data.attrib.get('title')
|
||||
self.sectionId = data.attrib.get('id')
|
||||
self.type = data.attrib.get('type')
|
||||
self.shared = utils.cast(bool, data.attrib.get('shared'))
|
||||
self.sectionId = self.id # For backwards compatibility
|
||||
self.sectionKey = self.key # For backwards compatibility
|
||||
|
||||
def history(self, maxresults=9999999, mindate=None):
|
||||
""" Get all Play History for a user for this section in this shared server.
|
||||
|
|
|
@ -46,7 +46,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'photo'
|
||||
|
@ -186,7 +186,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'photo'
|
||||
|
|
|
@ -237,7 +237,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
uri = uri + '&limit=%s' % str(limit)
|
||||
|
||||
for category, value in kwargs.items():
|
||||
sectionChoices = section.listChoices(category)
|
||||
sectionChoices = section.listFilterChoices(category)
|
||||
for choice in sectionChoices:
|
||||
if str(choice.title).lower() == str(value).lower():
|
||||
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
||||
|
|
|
@ -217,19 +217,41 @@ class PlexServer(PlexObject):
|
|||
return q.attrib.get('token')
|
||||
|
||||
def systemAccounts(self):
|
||||
""" Returns a list of :class:`~plexapi.server.SystemAccounts` objects this server contains. """
|
||||
""" Returns a list of :class:`~plexapi.server.SystemAccount` objects this server contains. """
|
||||
if self._systemAccounts is None:
|
||||
key = '/accounts'
|
||||
self._systemAccounts = self.fetchItems(key, SystemAccount)
|
||||
return self._systemAccounts
|
||||
|
||||
def systemAccount(self, accountID):
|
||||
""" Returns the :class:`~plexapi.server.SystemAccount` object for the specified account ID.
|
||||
|
||||
Parameters:
|
||||
accountID (int): The :class:`~plexapi.server.SystemAccount` ID.
|
||||
"""
|
||||
try:
|
||||
return next(account for account in self.systemAccounts() if account.id == accountID)
|
||||
except StopIteration:
|
||||
raise NotFound('Unknown account with accountID=%s' % accountID) from None
|
||||
|
||||
def systemDevices(self):
|
||||
""" Returns a list of :class:`~plexapi.server.SystemDevices` objects this server contains. """
|
||||
""" Returns a list of :class:`~plexapi.server.SystemDevice` objects this server contains. """
|
||||
if self._systemDevices is None:
|
||||
key = '/devices'
|
||||
self._systemDevices = self.fetchItems(key, SystemDevice)
|
||||
return self._systemDevices
|
||||
|
||||
def systemDevice(self, deviceID):
|
||||
""" Returns the :class:`~plexapi.server.SystemDevice` object for the specified device ID.
|
||||
|
||||
Parameters:
|
||||
deviceID (int): The :class:`~plexapi.server.SystemDevice` ID.
|
||||
"""
|
||||
try:
|
||||
return next(device for device in self.systemDevices() if device.id == deviceID)
|
||||
except StopIteration:
|
||||
raise NotFound('Unknown device with deviceID=%s' % deviceID) from None
|
||||
|
||||
def myPlexAccount(self):
|
||||
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
||||
token to access this server. If you are not the owner of this PlexServer
|
||||
|
@ -512,7 +534,7 @@ class PlexServer(PlexObject):
|
|||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
def search(self, query, mediatype=None, limit=None):
|
||||
def search(self, query, mediatype=None, limit=None, sectionId=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
`Hub Search <https://www.plex.tv/blog/seek-plex-shall-find-leveling-web-app/>`_
|
||||
against all items in your Plex library. This searches genres, actors, directors,
|
||||
|
@ -526,10 +548,11 @@ class PlexServer(PlexObject):
|
|||
|
||||
Parameters:
|
||||
query (str): Query to use when searching your library.
|
||||
mediatype (str): Optionally limit your search to the specified media type.
|
||||
mediatype (str, optional): Limit your search to the specified media type.
|
||||
actor, album, artist, autotag, collection, director, episode, game, genre,
|
||||
movie, photo, photoalbum, place, playlist, shared, show, tag, track
|
||||
limit (int): Optionally limit to the specified number of results per Hub.
|
||||
limit (int, optional): Limit to the specified number of results per Hub.
|
||||
sectionId (int, optional): The section ID (key) of the library to search within.
|
||||
"""
|
||||
results = []
|
||||
params = {
|
||||
|
@ -538,6 +561,8 @@ class PlexServer(PlexObject):
|
|||
'includeExternalMedia': 1}
|
||||
if limit:
|
||||
params['limit'] = limit
|
||||
if sectionId:
|
||||
params['sectionId'] = sectionId
|
||||
key = '/hubs/search?%s' % urlencode(params)
|
||||
for hub in self.fetchItems(key, Hub):
|
||||
if mediatype:
|
||||
|
@ -842,6 +867,7 @@ class SystemDevice(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'Device'
|
||||
clientIdentifier (str): The unique identifier for the device.
|
||||
createdAt (datatime): Datetime the device was created.
|
||||
id (int): The ID of the device (not the same as :class:`~plexapi.myplex.MyPlexDevice` ID).
|
||||
key (str): API URL (/devices/<id>)
|
||||
|
@ -852,6 +878,7 @@ class SystemDevice(PlexObject):
|
|||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||
self.id = cast(int, data.attrib.get('id'))
|
||||
self.key = '/devices/%s' % self.id
|
||||
|
@ -894,19 +921,11 @@ class StatisticsBandwidth(PlexObject):
|
|||
|
||||
def account(self):
|
||||
""" Returns the :class:`~plexapi.server.SystemAccount` associated with the bandwidth data. """
|
||||
accounts = self._server.systemAccounts()
|
||||
try:
|
||||
return next(account for account in accounts if account.id == self.accountID)
|
||||
except StopIteration:
|
||||
raise NotFound('Unknown account for this bandwidth data: accountID=%s' % self.accountID)
|
||||
return self._server.systemAccount(self.accountID)
|
||||
|
||||
def device(self):
|
||||
""" Returns the :class:`~plexapi.server.SystemDevice` associated with the bandwidth data. """
|
||||
devices = self._server.systemDevices()
|
||||
try:
|
||||
return next(device for device in devices if device.id == self.deviceID)
|
||||
except StopIteration:
|
||||
raise NotFound('Unknown device for this bandwidth data: deviceID=%s' % self.deviceID)
|
||||
return self._server.systemDevice(self.deviceID)
|
||||
|
||||
|
||||
class StatisticsResources(PlexObject):
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import os
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from plexapi import library, media, settings, utils
|
||||
from plexapi import library, media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.mixins import ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.exceptions import BadRequest
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
||||
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
|
||||
|
||||
|
@ -48,7 +48,7 @@ class Video(PlexPartialObject):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'video'
|
||||
|
@ -248,7 +248,7 @@ class Video(PlexPartialObject):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Movie(Video, Playable, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
|
||||
""" Represents a single Movie.
|
||||
|
||||
|
@ -381,7 +381,7 @@ class Movie(Video, Playable, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatc
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Show(Video, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin):
|
||||
""" Represents a single Show (including all seasons and episodes).
|
||||
|
||||
|
@ -489,41 +489,6 @@ class Show(Video, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMa
|
|||
""" Returns True if the show is fully watched. """
|
||||
return bool(self.viewedLeafCount == self.leafCount)
|
||||
|
||||
def preferences(self):
|
||||
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
|
||||
items = []
|
||||
data = self._server.query(self._details_key)
|
||||
for item in data.iter('Preferences'):
|
||||
for elem in item:
|
||||
setting = settings.Preferences(data=elem, server=self._server)
|
||||
setting._initpath = self.key
|
||||
items.append(setting)
|
||||
|
||||
return items
|
||||
|
||||
def editAdvanced(self, **kwargs):
|
||||
""" Edit a show's advanced settings. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
preferences = {pref.id: list(pref.enumValues.keys()) for pref in self.preferences()}
|
||||
for settingID, value in kwargs.items():
|
||||
enumValues = preferences.get(settingID)
|
||||
if value in enumValues:
|
||||
data[settingID] = value
|
||||
else:
|
||||
raise NotFound('%s not found in %s' % (value, enumValues))
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
def defaultAdvanced(self):
|
||||
""" Edit all of show's advanced settings to default. """
|
||||
data = {}
|
||||
key = '%s/prefs?' % self.key
|
||||
for preference in self.preferences():
|
||||
data[preference.id] = preference.default
|
||||
url = key + urlencode(data)
|
||||
self._server.query(url, method=self._server._session.put)
|
||||
|
||||
def hubs(self):
|
||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||
data = self._server.query(self._details_key)
|
||||
|
@ -832,7 +797,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin,
|
|||
# https://forums.plex.tv/t/parentratingkey-not-in-episode-xml-when-seasons-are-hidden/300553
|
||||
if self.skipParent and not self.parentRatingKey:
|
||||
# Parse the parentRatingKey from the parentThumb
|
||||
if self.parentThumb.startswith('/library/metadata/'):
|
||||
if self.parentThumb and self.parentThumb.startswith('/library/metadata/'):
|
||||
self.parentRatingKey = utils.cast(int, self.parentThumb.split('/')[3])
|
||||
# Get the parentRatingKey from the season's ratingKey
|
||||
if not self.parentRatingKey and self.grandparentRatingKey:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue