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
|
# PlexAPI Settings
|
||||||
PROJECT = 'PlexAPI'
|
PROJECT = 'PlexAPI'
|
||||||
VERSION = '4.4.1'
|
VERSION = '4.5.2'
|
||||||
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
||||||
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, 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)
|
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 import library, media, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest
|
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 SplitMergeMixin, UnmatchMatchMixin
|
||||||
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
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.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '')
|
self.key = data.attrib.get('key', '')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'audio'
|
self.listType = 'audio'
|
||||||
|
@ -114,7 +114,7 @@ class Audio(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Artist(Audio, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||||
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
||||||
""" Represents a single Artist.
|
""" Represents a single Artist.
|
||||||
|
|
||||||
|
|
|
@ -144,34 +144,9 @@ class PlexObject(object):
|
||||||
it only returns those items. By default we convert the xml elements
|
it only returns those items. By default we convert the xml elements
|
||||||
with the best guess PlexObjects based on tag and type attrs.
|
with the best guess PlexObjects based on tag and type attrs.
|
||||||
etag (str): Only fetch items with the specified tag.
|
etag (str): Only fetch items with the specified tag.
|
||||||
**kwargs (dict): Optionally add attribute filters on the items to fetch. For
|
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||||
example, passing in viewCount=0 will only return matching items. Filtering
|
See :func:`~plexapi.base.PlexObject.fetchItems` for more details
|
||||||
is done before the Python objects are built to help keep things speedy.
|
on how this is used.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
if ekey is None:
|
if ekey is None:
|
||||||
raise BadRequest('ekey was not provided')
|
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):
|
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
|
""" 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
|
and attrs.
|
||||||
on how this is used.
|
|
||||||
|
|
||||||
Parameters:
|
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_start (None, int): offset to get a subset of the data
|
||||||
container_size (None, int): How many items in 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 = {}
|
url_kw = {}
|
||||||
|
@ -204,7 +243,7 @@ class PlexObject(object):
|
||||||
data = self._server.query(ekey, params=url_kw)
|
data = self._server.query(ekey, params=url_kw)
|
||||||
items = self.findItems(data, cls, ekey, **kwargs)
|
items = self.findItems(data, cls, ekey, **kwargs)
|
||||||
|
|
||||||
librarySectionID = data.attrib.get('librarySectionID')
|
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||||
if librarySectionID:
|
if librarySectionID:
|
||||||
for item in items:
|
for item in items:
|
||||||
item.librarySectionID = librarySectionID
|
item.librarySectionID = librarySectionID
|
||||||
|
@ -526,6 +565,8 @@ class Playable(object):
|
||||||
transcodeSessions (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
|
transcodeSessions (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
|
||||||
if item is being transcoded (None otherwise).
|
if item is being transcoded (None otherwise).
|
||||||
viewedAt (datetime): Datetime item was last viewed (history).
|
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).
|
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).
|
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.session = self.findItems(data, etag='Session') # session
|
||||||
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
|
self.viewedAt = utils.toDatetime(data.attrib.get('viewedAt')) # history
|
||||||
self.accountID = utils.cast(int, data.attrib.get('accountID')) # 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.playlistItemID = utils.cast(int, data.attrib.get('playlistItemID')) # playlist
|
||||||
self.playQueueItemID = utils.cast(int, data.attrib.get('playQueueItemID')) # playqueue
|
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).
|
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||||
initpath (str): Path used to generate data.
|
initpath (str): Path used to generate data.
|
||||||
baseurl (str): HTTP URL to connect dirrectly to this client.
|
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).
|
token (str): X-Plex-Token used for authenication (optional).
|
||||||
session (:class:`~requests.Session`): requests.Session object if you want more control (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).
|
timeout (int): timeout in seconds on initial connect to client (default config.TIMEOUT).
|
||||||
|
@ -59,9 +61,10 @@ class PlexClient(PlexObject):
|
||||||
key = '/resources'
|
key = '/resources'
|
||||||
|
|
||||||
def __init__(self, server=None, data=None, initpath=None, baseurl=None,
|
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)
|
super(PlexClient, self).__init__(server, data, initpath)
|
||||||
self._baseurl = baseurl.strip('/') if baseurl else None
|
self._baseurl = baseurl.strip('/') if baseurl else None
|
||||||
|
self._clientIdentifier = identifier
|
||||||
self._token = logfilter.add_secret(token)
|
self._token = logfilter.add_secret(token)
|
||||||
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
self._showSecrets = CONFIG.get('log.show_secrets', '').lower() == 'true'
|
||||||
server_session = server._session if server else None
|
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.')
|
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||||
self._initpath = self.key
|
self._initpath = self.key
|
||||||
data = self.query(self.key, timeout=timeout)
|
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
|
return self
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||||
self.labels = self.findItems(data, media.Label)
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from urllib.parse import quote_plus, urlencode
|
from urllib.parse import quote_plus, urlencode
|
||||||
|
|
||||||
from plexapi import media, utils
|
from plexapi import media, settings, utils
|
||||||
from plexapi.exceptions import NotFound
|
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):
|
class ArtUrlMixin(object):
|
||||||
""" Mixin for Plex objects that can have a background artwork url. """
|
""" Mixin for Plex objects that can have a background artwork url. """
|
||||||
|
|
||||||
|
|
|
@ -499,15 +499,18 @@ class MyPlexAccount(PlexObject):
|
||||||
url = self.PLEXSERVERS.replace('{machineId}', machineIdentifier)
|
url = self.PLEXSERVERS.replace('{machineId}', machineIdentifier)
|
||||||
data = self.query(url, self._session.get)
|
data = self.query(url, self._session.get)
|
||||||
for elem in data[0]:
|
for elem in data[0]:
|
||||||
allSectionIds[elem.attrib.get('id', '').lower()] = elem.attrib.get('id')
|
_id = utils.cast(int, elem.attrib.get('id'))
|
||||||
allSectionIds[elem.attrib.get('title', '').lower()] = elem.attrib.get('id')
|
_key = utils.cast(int, elem.attrib.get('key'))
|
||||||
allSectionIds[elem.attrib.get('key', '').lower()] = elem.attrib.get('id')
|
_title = elem.attrib.get('title', '').lower()
|
||||||
|
allSectionIds[_id] = _id
|
||||||
|
allSectionIds[_key] = _id
|
||||||
|
allSectionIds[_title] = _id
|
||||||
log.debug(allSectionIds)
|
log.debug(allSectionIds)
|
||||||
# Convert passed in section items to section ids from above lookup
|
# Convert passed in section items to section ids from above lookup
|
||||||
sectionIds = []
|
sectionIds = []
|
||||||
for section in sections:
|
for section in sections:
|
||||||
sectionKey = section.key if isinstance(section, LibrarySection) else section
|
sectionKey = section.key if isinstance(section, LibrarySection) else section.lower()
|
||||||
sectionIds.append(allSectionIds[sectionKey.lower()])
|
sectionIds.append(allSectionIds[sectionKey])
|
||||||
return sectionIds
|
return sectionIds
|
||||||
|
|
||||||
def _filterDictToStr(self, filterDict):
|
def _filterDictToStr(self, filterDict):
|
||||||
|
@ -799,28 +802,28 @@ class MyPlexUser(PlexObject):
|
||||||
|
|
||||||
class Section(PlexObject):
|
class Section(PlexObject):
|
||||||
""" This refers to a shared section. The raw xml for the data presented here
|
""" 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:
|
Attributes:
|
||||||
TAG (str): section
|
TAG (str): section
|
||||||
id (int): shared section id
|
id (int): The shared section ID
|
||||||
sectionKey (str): what key we use for this section
|
key (int): The shared library section key
|
||||||
title (str): Title of the section
|
|
||||||
sectionId (str): shared section id
|
|
||||||
type (str): movie, tvshow, artist
|
|
||||||
shared (bool): If this section is shared with the user
|
shared (bool): If this section is shared with the user
|
||||||
|
title (str): Title of the section
|
||||||
|
type (str): movie, tvshow, artist
|
||||||
|
|
||||||
"""
|
"""
|
||||||
TAG = 'Section'
|
TAG = 'Section'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
# self.id = utils.cast(int, data.attrib.get('id')) # Havnt decided if this should be changed.
|
self.id = utils.cast(int, data.attrib.get('id'))
|
||||||
self.sectionKey = data.attrib.get('key')
|
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.title = data.attrib.get('title')
|
||||||
self.sectionId = data.attrib.get('id')
|
|
||||||
self.type = data.attrib.get('type')
|
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):
|
def history(self, maxresults=9999999, mindate=None):
|
||||||
""" Get all Play History for a user for this section in this shared server.
|
""" 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.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'photo'
|
self.listType = 'photo'
|
||||||
|
@ -186,7 +186,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '')
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'photo'
|
self.listType = 'photo'
|
||||||
|
|
|
@ -237,7 +237,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
||||||
uri = uri + '&limit=%s' % str(limit)
|
uri = uri + '&limit=%s' % str(limit)
|
||||||
|
|
||||||
for category, value in kwargs.items():
|
for category, value in kwargs.items():
|
||||||
sectionChoices = section.listChoices(category)
|
sectionChoices = section.listFilterChoices(category)
|
||||||
for choice in sectionChoices:
|
for choice in sectionChoices:
|
||||||
if str(choice.title).lower() == str(value).lower():
|
if str(choice.title).lower() == str(value).lower():
|
||||||
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
||||||
|
|
|
@ -217,19 +217,41 @@ class PlexServer(PlexObject):
|
||||||
return q.attrib.get('token')
|
return q.attrib.get('token')
|
||||||
|
|
||||||
def systemAccounts(self):
|
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:
|
if self._systemAccounts is None:
|
||||||
key = '/accounts'
|
key = '/accounts'
|
||||||
self._systemAccounts = self.fetchItems(key, SystemAccount)
|
self._systemAccounts = self.fetchItems(key, SystemAccount)
|
||||||
return self._systemAccounts
|
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):
|
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:
|
if self._systemDevices is None:
|
||||||
key = '/devices'
|
key = '/devices'
|
||||||
self._systemDevices = self.fetchItems(key, SystemDevice)
|
self._systemDevices = self.fetchItems(key, SystemDevice)
|
||||||
return self._systemDevices
|
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):
|
def myPlexAccount(self):
|
||||||
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
||||||
token to access this server. If you are not the owner of this PlexServer
|
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')
|
data = response.text.encode('utf8')
|
||||||
return ElementTree.fromstring(data) if data.strip() else None
|
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
|
""" 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/>`_
|
`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,
|
against all items in your Plex library. This searches genres, actors, directors,
|
||||||
|
@ -526,10 +548,11 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
query (str): Query to use when searching your library.
|
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,
|
actor, album, artist, autotag, collection, director, episode, game, genre,
|
||||||
movie, photo, photoalbum, place, playlist, shared, show, tag, track
|
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 = []
|
results = []
|
||||||
params = {
|
params = {
|
||||||
|
@ -538,6 +561,8 @@ class PlexServer(PlexObject):
|
||||||
'includeExternalMedia': 1}
|
'includeExternalMedia': 1}
|
||||||
if limit:
|
if limit:
|
||||||
params['limit'] = limit
|
params['limit'] = limit
|
||||||
|
if sectionId:
|
||||||
|
params['sectionId'] = sectionId
|
||||||
key = '/hubs/search?%s' % urlencode(params)
|
key = '/hubs/search?%s' % urlencode(params)
|
||||||
for hub in self.fetchItems(key, Hub):
|
for hub in self.fetchItems(key, Hub):
|
||||||
if mediatype:
|
if mediatype:
|
||||||
|
@ -842,6 +867,7 @@ class SystemDevice(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Device'
|
TAG (str): 'Device'
|
||||||
|
clientIdentifier (str): The unique identifier for the device.
|
||||||
createdAt (datatime): Datetime the device was created.
|
createdAt (datatime): Datetime the device was created.
|
||||||
id (int): The ID of the device (not the same as :class:`~plexapi.myplex.MyPlexDevice` ID).
|
id (int): The ID of the device (not the same as :class:`~plexapi.myplex.MyPlexDevice` ID).
|
||||||
key (str): API URL (/devices/<id>)
|
key (str): API URL (/devices/<id>)
|
||||||
|
@ -852,6 +878,7 @@ class SystemDevice(PlexObject):
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
|
self.clientIdentifier = data.attrib.get('clientIdentifier')
|
||||||
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
self.createdAt = utils.toDatetime(data.attrib.get('createdAt'))
|
||||||
self.id = cast(int, data.attrib.get('id'))
|
self.id = cast(int, data.attrib.get('id'))
|
||||||
self.key = '/devices/%s' % self.id
|
self.key = '/devices/%s' % self.id
|
||||||
|
@ -894,19 +921,11 @@ class StatisticsBandwidth(PlexObject):
|
||||||
|
|
||||||
def account(self):
|
def account(self):
|
||||||
""" Returns the :class:`~plexapi.server.SystemAccount` associated with the bandwidth data. """
|
""" Returns the :class:`~plexapi.server.SystemAccount` associated with the bandwidth data. """
|
||||||
accounts = self._server.systemAccounts()
|
return self._server.systemAccount(self.accountID)
|
||||||
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)
|
|
||||||
|
|
||||||
def device(self):
|
def device(self):
|
||||||
""" Returns the :class:`~plexapi.server.SystemDevice` associated with the bandwidth data. """
|
""" Returns the :class:`~plexapi.server.SystemDevice` associated with the bandwidth data. """
|
||||||
devices = self._server.systemDevices()
|
return self._server.systemDevice(self.deviceID)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticsResources(PlexObject):
|
class StatisticsResources(PlexObject):
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
import os
|
import os
|
||||||
from urllib.parse import quote_plus, urlencode
|
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.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.mixins import ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
||||||
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
||||||
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
|
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.guid = data.attrib.get('guid')
|
||||||
self.key = data.attrib.get('key', '')
|
self.key = data.attrib.get('key', '')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'video'
|
self.listType = 'video'
|
||||||
|
@ -248,7 +248,7 @@ class Video(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@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):
|
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
|
||||||
""" Represents a single Movie.
|
""" Represents a single Movie.
|
||||||
|
|
||||||
|
@ -381,7 +381,7 @@ class Movie(Video, Playable, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatc
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Show(Video, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||||
CollectionMixin, GenreMixin, LabelMixin):
|
CollectionMixin, GenreMixin, LabelMixin):
|
||||||
""" Represents a single Show (including all seasons and episodes).
|
""" 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. """
|
""" Returns True if the show is fully watched. """
|
||||||
return bool(self.viewedLeafCount == self.leafCount)
|
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):
|
def hubs(self):
|
||||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
||||||
data = self._server.query(self._details_key)
|
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
|
# https://forums.plex.tv/t/parentratingkey-not-in-episode-xml-when-seasons-are-hidden/300553
|
||||||
if self.skipParent and not self.parentRatingKey:
|
if self.skipParent and not self.parentRatingKey:
|
||||||
# Parse the parentRatingKey from the parentThumb
|
# 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])
|
self.parentRatingKey = utils.cast(int, self.parentThumb.split('/')[3])
|
||||||
# Get the parentRatingKey from the season's ratingKey
|
# Get the parentRatingKey from the season's ratingKey
|
||||||
if not self.parentRatingKey and self.grandparentRatingKey:
|
if not self.parentRatingKey and self.grandparentRatingKey:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue