Update PlexAPI to 4.5.2

This commit is contained in:
JonnyWong16 2021-04-05 13:57:16 -07:00
parent f6ca1dfa28
commit 74b4e45915
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
12 changed files with 947 additions and 362 deletions

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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. """

View file

@ -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.

View file

@ -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'

View file

@ -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))

View file

@ -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):

View file

@ -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: