Bump plexapi from 4.9.2 to 4.11.0 (#1690)

* Bump plexapi from 4.9.2 to 4.10.1

Bumps [plexapi](https://github.com/pkkid/python-plexapi) from 4.9.2 to 4.10.1.
- [Release notes](https://github.com/pkkid/python-plexapi/releases)
- [Commits](https://github.com/pkkid/python-plexapi/compare/4.9.2...4.10.1)

---
updated-dependencies:
- dependency-name: plexapi
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update plexapi==4.11.0

* Update requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2022-05-18 11:24:15 -07:00 committed by GitHub
parent f1b95f5837
commit 399fd6ff91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1421 additions and 589 deletions

View file

@ -314,7 +314,7 @@ class SuperWeirdWordPlugin(MessDetectorPlugin):
self._buffer = "" self._buffer = ""
self._buffer_accent_count = 0 self._buffer_accent_count = 0
elif ( elif (
character not in {"<", ">", "-", "="} character not in {"<", ">", "-", "=", "~", "|", "_"}
and character.isdigit() is False and character.isdigit() is False
and is_symbol(character) and is_symbol(character)
): ):

View file

@ -2,5 +2,5 @@
Expose version Expose version
""" """
__version__ = "2.0.11" __version__ = "2.0.12"
VERSION = __version__.split(".") VERSION = __version__.split(".")

View file

@ -21,9 +21,9 @@ 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)
# Plex Header Configuation # Plex Header Configuration
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller') X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platorm', uname()[0])) X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platform', uname()[0]))
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2]) X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT) X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
X_PLEX_VERSION = CONFIG.get('header.version', VERSION) X_PLEX_VERSION = CONFIG.get('header.version', VERSION)

View file

@ -2,12 +2,16 @@
import os import os
from urllib.parse import quote_plus from urllib.parse import quote_plus
from plexapi import library, media, utils from plexapi import 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 AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin from plexapi.mixins import (
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
)
from plexapi.playlist import Playlist from plexapi.playlist import Playlist
@ -38,7 +42,7 @@ class Audio(PlexPartialObject):
title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.). title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.).
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
type (str): 'artist', 'album', or 'track'. type (str): 'artist', 'album', or 'track'.
updatedAt (datatime): Datetime the item was updated. updatedAt (datetime): Datetime the item was updated.
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars). userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
viewCount (int): Count of times the item was played. viewCount (int): Count of times the item was played.
""" """
@ -125,8 +129,13 @@ class Audio(PlexPartialObject):
@utils.registerPlexObject @utils.registerPlexObject
class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, class Artist(
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin): Audio,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
SortTitleMixin, SummaryMixin, TitleMixin,
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
):
""" Represents a single Artist. """ Represents a single Artist.
Attributes: Attributes:
@ -138,9 +147,11 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
countries (List<:class:`~plexapi.media.Country`>): List country objects. countries (List<:class:`~plexapi.media.Country`>): List country objects.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
locations (List<str>): List of folder paths where the artist is found on disk. locations (List<str>): List of folder paths where the artist is found on disk.
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
styles (List<:class:`~plexapi.media.Style`>): List of style objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects.
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
""" """
TAG = 'Directory' TAG = 'Directory'
TYPE = 'artist' TYPE = 'artist'
@ -153,26 +164,23 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
self.countries = self.findItems(data, media.Country) self.countries = self.findItems(data, media.Country)
self.genres = self.findItems(data, media.Genre) self.genres = self.findItems(data, media.Genre)
self.key = self.key.replace('/children', '') # FIX_BUG_50 self.key = self.key.replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label)
self.locations = self.listAttrs(data, 'path', etag='Location') self.locations = self.listAttrs(data, 'path', etag='Location')
self.similar = self.findItems(data, media.Similar) self.similar = self.findItems(data, media.Similar)
self.styles = self.findItems(data, media.Style) self.styles = self.findItems(data, media.Style)
self.theme = data.attrib.get('theme')
def __iter__(self): def __iter__(self):
for album in self.albums(): for album in self.albums():
yield album yield album
def hubs(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, library.Hub, rtag='Related')
def album(self, title): def album(self, title):
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title. """ Returns the :class:`~plexapi.audio.Album` that matches the specified title.
Parameters: Parameters:
title (str): Title of the album to return. title (str): Title of the album to return.
""" """
key = '/library/metadata/%s/children' % self.ratingKey key = f"/library/sections/{self.librarySectionID}/all?artist.id={self.ratingKey}&type=9"
return self.fetchItem(key, Album, title__iexact=title) return self.fetchItem(key, Album, title__iexact=title)
def albums(self, **kwargs): def albums(self, **kwargs):
@ -230,8 +238,13 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
@utils.registerPlexObject @utils.registerPlexObject
class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin, class Album(
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): Audio,
UnmatchMatchMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin,
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
):
""" Represents a single Album. """ Represents a single Album.
Attributes: Attributes:
@ -248,6 +261,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>). parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the album artist. parentRatingKey (int): Unique key identifying the album artist.
parentTheme (str): URL to artist theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
parentThumb (str): URL to album artist thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>). parentThumb (str): URL to album artist thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the album artist. parentTitle (str): Name of the album artist.
rating (float): Album rating (7.9; 9.8; 8.1). rating (float): Album rating (7.9; 9.8; 8.1).
@ -274,6 +288,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
self.parentGuid = data.attrib.get('parentGuid') self.parentGuid = data.attrib.get('parentGuid')
self.parentKey = data.attrib.get('parentKey') self.parentKey = data.attrib.get('parentKey')
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
self.parentTheme = data.attrib.get('parentTheme')
self.parentThumb = data.attrib.get('parentThumb') self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle') self.parentTitle = data.attrib.get('parentTitle')
self.rating = utils.cast(float, data.attrib.get('rating')) self.rating = utils.cast(float, data.attrib.get('rating'))
@ -298,10 +313,14 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing. :exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
""" """
key = '/library/metadata/%s/children' % self.ratingKey key = '/library/metadata/%s/children' % self.ratingKey
if title is not None: if title is not None and not isinstance(title, int):
return self.fetchItem(key, Track, title__iexact=title) return self.fetchItem(key, Track, title__iexact=title)
elif track is not None: elif track is not None or isinstance(title, int):
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track) if isinstance(title, int):
index = title
else:
index = track
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=index)
raise BadRequest('Missing argument: title or track is required') raise BadRequest('Missing argument: title or track is required')
def tracks(self, **kwargs): def tracks(self, **kwargs):
@ -337,8 +356,13 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
@utils.registerPlexObject @utils.registerPlexObject
class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, class Track(
CollectionMixin, MoodMixin): Audio, Playable,
ExtrasMixin, RatingMixin,
ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin,
TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin,
CollectionMixin, LabelMixin, MoodMixin
):
""" Represents a single Track. """ Represents a single Track.
Attributes: Attributes:
@ -351,19 +375,23 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c). grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>). grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
grandparentRatingKey (int): Unique key identifying the album artist. grandparentRatingKey (int): Unique key identifying the album artist.
grandparentTheme (str): URL to artist theme resource (/library/metadata/<grandparentRatingkey>/theme/<themeid>).
(/library/metadata/<grandparentRatingkey>/theme/<themeid>).
grandparentThumb (str): URL to album artist thumbnail image grandparentThumb (str): URL to album artist thumbnail image
(/library/metadata/<grandparentRatingKey>/thumb/<thumbid>). (/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
grandparentTitle (str): Name of the album artist for the track. grandparentTitle (str): Name of the album artist for the track.
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
media (List<:class:`~plexapi.media.Media`>): List of media objects. media (List<:class:`~plexapi.media.Media`>): List of media objects.
originalTitle (str): The artist for the track. originalTitle (str): The artist for the track.
parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9). parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9).
parentIndex (int): Album index. parentIndex (int): Disc number of the track.
parentKey (str): API URL of the album (/library/metadata/<parentRatingKey>). parentKey (str): API URL of the album (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the album. parentRatingKey (int): Unique key identifying the album.
parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>). parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the album for the track. parentTitle (str): Name of the album for the track.
primaryExtraKey (str) API URL for the primary extra for the track. primaryExtraKey (str) API URL for the primary extra for the track.
ratingCount (int): Number of ratings contributing to the rating score. ratingCount (int): Number of ratings contributing to the rating score.
skipCount (int): Number of times the track has been skipped.
viewOffset (int): View offset in milliseconds. viewOffset (int): View offset in milliseconds.
year (int): Year the track was released. year (int): Year the track was released.
""" """
@ -381,18 +409,21 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
self.grandparentGuid = data.attrib.get('grandparentGuid') self.grandparentGuid = data.attrib.get('grandparentGuid')
self.grandparentKey = data.attrib.get('grandparentKey') self.grandparentKey = data.attrib.get('grandparentKey')
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey')) self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
self.grandparentTheme = data.attrib.get('grandparentTheme')
self.grandparentThumb = data.attrib.get('grandparentThumb') self.grandparentThumb = data.attrib.get('grandparentThumb')
self.grandparentTitle = data.attrib.get('grandparentTitle') self.grandparentTitle = data.attrib.get('grandparentTitle')
self.labels = self.findItems(data, media.Label)
self.media = self.findItems(data, media.Media) self.media = self.findItems(data, media.Media)
self.originalTitle = data.attrib.get('originalTitle') self.originalTitle = data.attrib.get('originalTitle')
self.parentGuid = data.attrib.get('parentGuid') self.parentGuid = data.attrib.get('parentGuid')
self.parentIndex = data.attrib.get('parentIndex') self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
self.parentKey = data.attrib.get('parentKey') self.parentKey = data.attrib.get('parentKey')
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey')) self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
self.parentThumb = data.attrib.get('parentThumb') self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle') self.parentTitle = data.attrib.get('parentTitle')
self.primaryExtraKey = data.attrib.get('primaryExtraKey') self.primaryExtraKey = data.attrib.get('primaryExtraKey')
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
self.skipCount = utils.cast(int, data.attrib.get('skipCount'))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year')) self.year = utils.cast(int, data.attrib.get('year'))

View file

@ -53,7 +53,9 @@ class PlexObject(object):
if data is not None: if data is not None:
self._loadData(data) self._loadData(data)
self._details_key = self._buildDetailsKey() self._details_key = self._buildDetailsKey()
self._autoReload = False self._overwriteNone = True
self._edits = None # Save batch edits for a single API call
self._autoReload = True # Automatically reload the object when accessing a missing attribute
def __repr__(self): def __repr__(self):
uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri')) uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri'))
@ -65,9 +67,9 @@ class PlexObject(object):
if attr in _DONT_OVERWRITE_SESSION_KEYS and value == []: if attr in _DONT_OVERWRITE_SESSION_KEYS and value == []:
value = getattr(self, attr, []) value = getattr(self, attr, [])
autoReload = self.__dict__.get('_autoReload') overwriteNone = self.__dict__.get('_overwriteNone')
# Don't overwrite an attr with None unless it's a private variable or not auto reload # Don't overwrite an attr with None unless it's a private variable or overwrite None is True
if value is not None or attr.startswith('_') or attr not in self.__dict__ or not autoReload: if value is not None or attr.startswith('_') or attr not in self.__dict__ or overwriteNone:
self.__dict__[attr] = value self.__dict__[attr] = value
def _clean(self, value): def _clean(self, value):
@ -169,9 +171,14 @@ class PlexObject(object):
raise BadRequest('ekey was not provided') raise BadRequest('ekey was not provided')
if isinstance(ekey, int): if isinstance(ekey, int):
ekey = '/library/metadata/%s' % ekey ekey = '/library/metadata/%s' % ekey
for elem in self._server.query(ekey): data = self._server.query(ekey)
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
for elem in data:
if self._checkAttrs(elem, **kwargs): if self._checkAttrs(elem, **kwargs):
return self._buildItem(elem, cls, ekey) item = self._buildItem(elem, cls, ekey)
if librarySectionID:
item.librarySectionID = librarySectionID
return item
clsname = cls.__name__ if cls else 'None' clsname = cls.__name__ if cls else 'None'
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs)) raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
@ -196,7 +203,7 @@ class PlexObject(object):
Any XML attribute can be filtered when fetching results. Filtering is done before 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 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``. ``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 Note that case matters when specifying attributes. Attributes further down in the XML
tree can be filtered by *prepending* the attribute with each element tag ``Tag__``. tree can be filtered by *prepending* the attribute with each element tag ``Tag__``.
Examples: Examples:
@ -228,12 +235,12 @@ class PlexObject(object):
* ``__exists`` (*bool*): Value is or is not present in the attrs. * ``__exists`` (*bool*): Value is or is not present in the attrs.
* ``__gt``: Value is greater than specified arg. * ``__gt``: Value is greater than specified arg.
* ``__gte``: Value is greater than or equal to specified arg. * ``__gte``: Value is greater than or equal to specified arg.
* ``__icontains``: Case insensative value contains specified arg. * ``__icontains``: Case insensitive value contains specified arg.
* ``__iendswith``: Case insensative value ends with specified arg. * ``__iendswith``: Case insensitive value ends with specified arg.
* ``__iexact``: Case insensative value matches specified arg. * ``__iexact``: Case insensitive value matches specified arg.
* ``__in``: Value is in a specified list or tuple. * ``__in``: Value is in a specified list or tuple.
* ``__iregex``: Case insensative value matches the specified regular expression. * ``__iregex``: Case insensitive value matches the specified regular expression.
* ``__istartswith``: Case insensative value starts with specified arg. * ``__istartswith``: Case insensitive value starts with specified arg.
* ``__lt``: Value is less than specified arg. * ``__lt``: Value is less than specified arg.
* ``__lte``: Value is less than or equal to specified arg. * ``__lte``: Value is less than or equal to specified arg.
* ``__regex``: Value matches the specified regular expression. * ``__regex``: Value matches the specified regular expression.
@ -276,9 +283,9 @@ class PlexObject(object):
kwargs['etag'] = cls.TAG kwargs['etag'] = cls.TAG
if cls and cls.TYPE and 'type' not in kwargs: if cls and cls.TYPE and 'type' not in kwargs:
kwargs['type'] = cls.TYPE kwargs['type'] = cls.TYPE
# rtag to iter on a specific root tag # rtag to iter on a specific root tag using breadth-first search
if rtag: if rtag:
data = next(data.iter(rtag), []) data = next(utils.iterXMLBFS(data, rtag), [])
# loop through all data elements to find matches # loop through all data elements to find matches
items = [] items = []
for elem in data: for elem in data:
@ -298,9 +305,9 @@ class PlexObject(object):
def listAttrs(self, data, attr, rtag=None, **kwargs): def listAttrs(self, data, attr, rtag=None, **kwargs):
""" Return a list of values from matching attribute. """ """ Return a list of values from matching attribute. """
results = [] results = []
# rtag to iter on a specific root tag # rtag to iter on a specific root tag using breadth-first search
if rtag: if rtag:
data = next(data.iter(rtag), []) data = next(utils.iterXMLBFS(data, rtag), [])
for elem in data: for elem in data:
kwargs['%s__exists' % attr] = True kwargs['%s__exists' % attr] = True
if self._checkAttrs(elem, **kwargs): if self._checkAttrs(elem, **kwargs):
@ -340,7 +347,7 @@ class PlexObject(object):
""" """
return self._reload(key=key, **kwargs) return self._reload(key=key, **kwargs)
def _reload(self, key=None, _autoReload=False, **kwargs): def _reload(self, key=None, _overwriteNone=True, **kwargs):
""" Perform the actual reload. """ """ Perform the actual reload. """
details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key
key = key or details_key or self.key key = key or details_key or self.key
@ -348,9 +355,9 @@ class PlexObject(object):
raise Unsupported('Cannot reload an object not built from a URL.') raise Unsupported('Cannot reload an object not built from a URL.')
self._initpath = key self._initpath = key
data = self._server.query(key) data = self._server.query(key)
self._autoReload = _autoReload self._overwriteNone = _overwriteNone
self._loadData(data[0]) self._loadData(data[0])
self._autoReload = False self._overwriteNone = True
return self return self
def _checkAttrs(self, elem, **kwargs): def _checkAttrs(self, elem, **kwargs):
@ -392,7 +399,7 @@ class PlexObject(object):
# check were looking for the tag # check were looking for the tag
if attr.lower() == 'etag': if attr.lower() == 'etag':
return [elem.tag] return [elem.tag]
# loop through attrs so we can perform case-insensative match # loop through attrs so we can perform case-insensitive match
for _attr, value in elem.attrib.items(): for _attr, value in elem.attrib.items():
if attr.lower() == _attr.lower(): if attr.lower() == _attr.lower():
return [value] return [value]
@ -414,6 +421,10 @@ class PlexObject(object):
def _loadData(self, data): def _loadData(self, data):
raise NotImplementedError('Abstract method not implemented.') raise NotImplementedError('Abstract method not implemented.')
@property
def _searchType(self):
return self.TYPE
class PlexPartialObject(PlexObject): class PlexPartialObject(PlexObject):
""" Not all objects in the Plex listings return the complete list of elements """ Not all objects in the Plex listings return the complete list of elements
@ -455,20 +466,21 @@ class PlexPartialObject(PlexObject):
def __getattribute__(self, attr): def __getattribute__(self, attr):
# Dragons inside.. :-/ # Dragons inside.. :-/
value = super(PlexPartialObject, self).__getattribute__(attr) value = super(PlexPartialObject, self).__getattribute__(attr)
# Check a few cases where we dont want to reload # Check a few cases where we don't want to reload
if attr in _DONT_RELOAD_FOR_KEYS: return value if attr in _DONT_RELOAD_FOR_KEYS: return value
if attr in _DONT_OVERWRITE_SESSION_KEYS: return value if attr in _DONT_OVERWRITE_SESSION_KEYS: return value
if attr in USER_DONT_RELOAD_FOR_KEYS: return value if attr in USER_DONT_RELOAD_FOR_KEYS: return value
if attr.startswith('_'): return value if attr.startswith('_'): return value
if value not in (None, []): return value if value not in (None, []): return value
if self.isFullObject(): return value if self.isFullObject(): return value
if self._autoReload is False: return value
# Log the reload. # Log the reload.
clsname = self.__class__.__name__ clsname = self.__class__.__name__
title = self.__dict__.get('title', self.__dict__.get('name')) title = self.__dict__.get('title', self.__dict__.get('name'))
objname = "%s '%s'" % (clsname, title) if title else clsname objname = "%s '%s'" % (clsname, title) if title else clsname
log.debug("Reloading %s for attr '%s'", objname, attr) log.debug("Reloading %s for attr '%s'", objname, attr)
# Reload and return the value # Reload and return the value
self._reload(_autoReload=True) self._reload()
return super(PlexPartialObject, self).__getattribute__(attr) return super(PlexPartialObject, self).__getattribute__(attr)
def analyze(self): def analyze(self):
@ -507,44 +519,79 @@ class PlexPartialObject(PlexObject):
def _edit(self, **kwargs): def _edit(self, **kwargs):
""" Actually edit an object. """ """ Actually edit an object. """
if isinstance(self._edits, dict):
self._edits.update(kwargs)
return self
if 'id' not in kwargs: if 'id' not in kwargs:
kwargs['id'] = self.ratingKey kwargs['id'] = self.ratingKey
if 'type' not in kwargs: if 'type' not in kwargs:
kwargs['type'] = utils.searchType(self.type) kwargs['type'] = utils.searchType(self._searchType)
part = '/library/sections/%s/all?%s' % (self.librarySectionID, part = '/library/sections/%s/all%s' % (self.librarySectionID,
urlencode(kwargs)) utils.joinArgs(kwargs))
self._server.query(part, method=self._server._session.put) self._server.query(part, method=self._server._session.put)
return self
def edit(self, **kwargs): def edit(self, **kwargs):
""" Edit an object. """ Edit an object.
Note: This is a low level method and you need to know all the field/tag keys.
See :class:`~plexapi.mixins.EditFieldMixin` and :class:`~plexapi.mixins.EditTagsMixin`
for individual field and tag editing methods.
Parameters: Parameters:
kwargs (dict): Dict of settings to edit. kwargs (dict): Dict of settings to edit.
Example: Example:
{'type': 1,
'id': movie.ratingKey,
'collection[0].tag.tag': 'Super',
'collection.locked': 0}
"""
self._edit(**kwargs)
def _edit_tags(self, tag, items, locked=True, remove=False): .. code-block:: python
""" Helper to edit tags.
edits = {
'type': 1,
'id': movie.ratingKey,
'title.value': 'A new title',
'title.locked': 1,
'summary.value': 'This is a summary.',
'summary.locked': 1,
'collection[0].tag.tag': 'A tag',
'collection.locked': 1}
}
movie.edit(**edits)
Parameters:
tag (str): Tag name.
items (list): List of tags to add.
locked (bool): True to lock the field.
remove (bool): True to remove the tags in items.
""" """
if not isinstance(items, list): return self._edit(**kwargs)
items = [items]
value = getattr(self, utils.tag_plural(tag)) def batchEdits(self):
existing_tags = [t.tag for t in value if t and remove is False] """ Enable batch editing mode to save API calls.
tag_edits = utils.tag_helper(tag, existing_tags + items, locked, remove) Must call :func:`~plexapi.base.PlexPartialObject.saveEdits` at the end to save all the edits.
self.edit(**tag_edits) See :class:`~plexapi.mixins.EditFieldMixin` and :class:`~plexapi.mixins.EditTagsMixin`
for individual field and tag editing methods.
Example:
.. code-block:: python
# Batch editing multiple fields and tags in a single API call
Movie.batchEdits()
Movie.editTitle('A New Title').editSummary('A new summary').editTagline('A new tagline') \\
.addCollection('New Collection').removeGenre('Action').addLabel('Favorite')
Movie.saveEdits()
"""
self._edits = {}
return self
def saveEdits(self):
""" Save all the batch edits and automatically reload the object.
See :func:`~plexapi.base.PlexPartialObject.batchEdits` for details.
"""
if not isinstance(self._edits, dict):
raise BadRequest('Batch editing mode not enabled. Must call `batchEdits()` first.')
edits = self._edits
self._edits = None
self._edit(**edits)
return self.reload()
def refresh(self): def refresh(self):
""" Refreshing a Library or individual item causes the metadata for the item to be """ Refreshing a Library or individual item causes the metadata for the item to be
@ -709,7 +756,7 @@ class Playable(object):
filename = part.file filename = part.file
if kwargs: if kwargs:
# So this seems to be a alot slower but allows transcode. # So this seems to be a a lot slower but allows transcode.
download_url = self.getStreamURL(**kwargs) download_url = self.getStreamURL(**kwargs)
else: else:
download_url = self._server.url('%s?download=1' % part.key) download_url = self._server.url('%s?download=1' % part.key)
@ -746,7 +793,7 @@ class Playable(object):
key = '/:/progress?key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s' % (self.ratingKey, key = '/:/progress?key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s' % (self.ratingKey,
time, state) time, state)
self._server.query(key) self._server.query(key)
self._reload(_autoReload=True) self._reload(_overwriteNone=False)
def updateTimeline(self, time, state='stopped', duration=None): def updateTimeline(self, time, state='stopped', duration=None):
""" Set the timeline progress for this video. """ Set the timeline progress for this video.
@ -764,7 +811,7 @@ class Playable(object):
key = '/:/timeline?ratingKey=%s&key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s%s' key = '/:/timeline?ratingKey=%s&key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s%s'
key %= (self.ratingKey, self.key, time, state, durationStr) key %= (self.ratingKey, self.key, time, state, durationStr)
self._server.query(key) self._server.query(key)
self._reload(_autoReload=True) self._reload(_overwriteNone=False)
class MediaContainer(PlexObject): class MediaContainer(PlexObject):

View file

@ -23,10 +23,10 @@ class PlexClient(PlexObject):
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional). server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional).
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 directly to this client.
identifier (str): The resource/machine identifier for the desired client. identifier (str): The resource/machine identifier for the desired client.
May be necessary when connecting to a specific proxied client (optional). 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 authentication (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).
@ -48,7 +48,7 @@ class PlexClient(PlexObject):
session (:class:`~requests.Session`): Session object used for connection. session (:class:`~requests.Session`): Session object used for connection.
state (str): Unknown state (str): Unknown
title (str): Name of this client (Johns iPhone, etc). title (str): Name of this client (Johns iPhone, etc).
token (str): X-Plex-Token used for authenication token (str): X-Plex-Token used for authentication
vendor (str): Unknown vendor (str): Unknown
version (str): Device version (4.6.1, etc). version (str): Device version (4.6.1, etc).
_baseurl (str): HTTP address of the client. _baseurl (str): HTTP address of the client.
@ -131,7 +131,7 @@ class PlexClient(PlexObject):
self.platformVersion = data.attrib.get('platformVersion') self.platformVersion = data.attrib.get('platformVersion')
self.title = data.attrib.get('title') or data.attrib.get('name') self.title = data.attrib.get('title') or data.attrib.get('name')
# Active session details # Active session details
# Since protocolCapabilities is missing from /sessions we cant really control this player without # Since protocolCapabilities is missing from /sessions we can't really control this player without
# creating a client manually. # creating a client manually.
# Add this in next breaking release. # Add this in next breaking release.
# if self._initpath == 'status/sessions': # if self._initpath == 'status/sessions':
@ -210,8 +210,8 @@ class PlexClient(PlexObject):
controller = command.split('/')[0] controller = command.split('/')[0]
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
if controller not in self.protocolCapabilities: if controller not in self.protocolCapabilities:
log.debug('Client %s doesnt support %s controller.' log.debug("Client %s doesn't support %s controller."
'What your trying might not work' % (self.title, controller)) "What your trying might not work" % (self.title, controller))
proxy = self._proxyThroughServer if proxy is None else proxy proxy = self._proxyThroughServer if proxy is None else proxy
query = self._server.query if proxy else self.query query = self._server.query if proxy else self.query
@ -318,21 +318,21 @@ class PlexClient(PlexObject):
Parameters: Parameters:
media (:class:`~plexapi.media.Media`): Media object to navigate to. media (:class:`~plexapi.media.Media`): Media object to navigate to.
**params (dict): Additional GET parameters to include with the command. **params (dict): Additional GET parameters to include with the command.
Raises:
:exc:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
""" """
if not self._server:
raise Unsupported('A server must be specified before using this command.')
server_url = media._server._baseurl.split(':') server_url = media._server._baseurl.split(':')
self.sendCommand('mirror/details', **dict({ command = {
'machineIdentifier': self._server.machineIdentifier, 'machineIdentifier': media._server.machineIdentifier,
'address': server_url[1].strip('/'), 'address': server_url[1].strip('/'),
'port': server_url[-1], 'port': server_url[-1],
'key': media.key, 'key': media.key,
'protocol': server_url[0], 'protocol': server_url[0],
'token': media._server.createToken() **params,
}, **params)) }
token = media._server.createToken()
if token:
command["token"] = token
self.sendCommand("mirror/details", **command)
# ------------------- # -------------------
# Playback Commands # Playback Commands
@ -488,12 +488,7 @@ class PlexClient(PlexObject):
representing the beginning (default 0). representing the beginning (default 0).
**params (dict): Optional additional parameters to include in the playback request. See **params (dict): Optional additional parameters to include in the playback request. See
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
Raises:
:exc:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
""" """
if not self._server:
raise Unsupported('A server must be specified before using this command.')
server_url = media._server._baseurl.split(':') server_url = media._server._baseurl.split(':')
server_port = server_url[-1].strip('/') server_port = server_url[-1].strip('/')
@ -509,19 +504,24 @@ class PlexClient(PlexObject):
if mediatype == "audio": if mediatype == "audio":
mediatype = "music" mediatype = "music"
playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media) playqueue = media if isinstance(media, PlayQueue) else media._server.createPlayQueue(media)
self.sendCommand('playback/playMedia', **dict({ command = {
'providerIdentifier': 'com.plexapp.plugins.library', 'providerIdentifier': 'com.plexapp.plugins.library',
'machineIdentifier': self._server.machineIdentifier, 'machineIdentifier': media._server.machineIdentifier,
'protocol': server_url[0], 'protocol': server_url[0],
'address': server_url[1].strip('/'), 'address': server_url[1].strip('/'),
'port': server_port, 'port': server_port,
'offset': offset, 'offset': offset,
'key': media.key or playqueue.selectedItem.key, 'key': media.key or playqueue.selectedItem.key,
'token': media._server.createToken(),
'type': mediatype, 'type': mediatype,
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params)) **params,
}
token = media._server.createToken()
if token:
command["token"] = token
self.sendCommand("playback/playMedia", **command)
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=DEFAULT_MTYPE): def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=DEFAULT_MTYPE):
""" Set multiple playback parameters at once. """ Set multiple playback parameters at once.

View file

@ -5,14 +5,24 @@ from plexapi import media, utils
from plexapi.base import PlexPartialObject from plexapi.base import PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin from plexapi.mixins import (
from plexapi.mixins import LabelMixin, SmartFilterMixin AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
LabelMixin
)
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import deprecated from plexapi.utils import deprecated
@utils.registerPlexObject @utils.registerPlexObject
class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin, SmartFilterMixin): class Collection(
PlexPartialObject,
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
LabelMixin
):
""" Represents a single Collection. """ Represents a single Collection.
Attributes: Attributes:
@ -22,9 +32,10 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>). art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
artBlurHash (str): BlurHash string for artwork image. artBlurHash (str): BlurHash string for artwork image.
childCount (int): Number of items in the collection. childCount (int): Number of items in the collection.
collectionMode (str): How the items in the collection are displayed. collectionFilterBasedOnUser (int): Which user's activity is used for the collection filtering.
collectionMode (int): How the items in the collection are displayed.
collectionPublished (bool): True if the collection is published to the Plex homepage. collectionPublished (bool): True if the collection is published to the Plex homepage.
collectionSort (str): How to sort the items in the collection. collectionSort (int): How to sort the items in the collection.
content (str): The filter URI string for smart collections. content (str): The filter URI string for smart collections.
contentRating (str) Content rating (PG-13; NR; TV-G). contentRating (str) Content rating (PG-13; NR; TV-G).
fields (List<:class:`~plexapi.media.Field`>): List of field objects. fields (List<:class:`~plexapi.media.Field`>): List of field objects.
@ -43,12 +54,13 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
smart (bool): True if the collection is a smart collection. smart (bool): True if the collection is a smart collection.
subtype (str): Media type of the items in the collection (movie, show, artist, or album). subtype (str): Media type of the items in the collection (movie, show, artist, or album).
summary (str): Summary of the collection. summary (str): Summary of the collection.
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>). thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
thumbBlurHash (str): BlurHash string for thumbnail image. thumbBlurHash (str): BlurHash string for thumbnail image.
title (str): Name of the collection. title (str): Name of the collection.
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
type (str): 'collection' type (str): 'collection'
updatedAt (datatime): Datetime the collection was updated. updatedAt (datetime): Datetime the collection was updated.
userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars). userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars).
""" """
TAG = 'Directory' TAG = 'Directory'
@ -60,6 +72,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
self.art = data.attrib.get('art') self.art = data.attrib.get('art')
self.artBlurHash = data.attrib.get('artBlurHash') self.artBlurHash = data.attrib.get('artBlurHash')
self.childCount = utils.cast(int, data.attrib.get('childCount')) self.childCount = utils.cast(int, data.attrib.get('childCount'))
self.collectionFilterBasedOnUser = utils.cast(int, data.attrib.get('collectionFilterBasedOnUser', '0'))
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1')) self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1'))
self.collectionPublished = utils.cast(bool, data.attrib.get('collectionPublished', '0')) self.collectionPublished = utils.cast(bool, data.attrib.get('collectionPublished', '0'))
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0')) self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0'))
@ -81,6 +94,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
self.smart = utils.cast(bool, data.attrib.get('smart', '0')) self.smart = utils.cast(bool, data.attrib.get('smart', '0'))
self.subtype = data.attrib.get('subtype') self.subtype = data.attrib.get('subtype')
self.summary = data.attrib.get('summary') self.summary = data.attrib.get('summary')
self.theme = data.attrib.get('theme')
self.thumb = data.attrib.get('thumb') self.thumb = data.attrib.get('thumb')
self.thumbBlurHash = data.attrib.get('thumbBlurHash') self.thumbBlurHash = data.attrib.get('thumbBlurHash')
self.title = data.attrib.get('title') self.title = data.attrib.get('title')
@ -184,6 +198,32 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
""" Alias to :func:`~plexapi.library.Collection.item`. """ """ Alias to :func:`~plexapi.library.Collection.item`. """
return self.item(title) return self.item(title)
def filterUserUpdate(self, user=None):
""" Update the collection filtering user advanced setting.
Parameters:
user (str): One of the following values:
"admin" (Always the server admin user),
"user" (User currently viewing the content)
Example:
.. code-block:: python
collection.updateMode(user="user")
"""
if not self.smart:
raise BadRequest('Cannot change collection filtering user for a non-smart collection.')
user_dict = {
'admin': 0,
'user': 1
}
key = user_dict.get(user)
if key is None:
raise BadRequest('Unknown collection filtering user: %s. Options %s' % (user, list(user_dict)))
self.editAdvanced(collectionFilterBasedOnUser=key)
def modeUpdate(self, mode=None): def modeUpdate(self, mode=None):
""" Update the collection mode advanced setting. """ Update the collection mode advanced setting.
@ -208,7 +248,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
} }
key = mode_dict.get(mode) key = mode_dict.get(mode)
if key is None: if key is None:
raise BadRequest('Unknown collection mode : %s. Options %s' % (mode, list(mode_dict))) raise BadRequest('Unknown collection mode: %s. Options %s' % (mode, list(mode_dict)))
self.editAdvanced(collectionMode=key) self.editAdvanced(collectionMode=key)
def sortUpdate(self, sort=None): def sortUpdate(self, sort=None):
@ -216,7 +256,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
Parameters: Parameters:
sort (str): One of the following values: sort (str): One of the following values:
"realease" (Order Collection by realease dates), "release" (Order Collection by release dates),
"alpha" (Order Collection alphabetically), "alpha" (Order Collection alphabetically),
"custom" (Custom collection order) "custom" (Custom collection order)
@ -226,6 +266,9 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
collection.updateSort(mode="alpha") collection.updateSort(mode="alpha")
""" """
if self.smart:
raise BadRequest('Cannot change collection order for a smart collection.')
sort_dict = { sort_dict = {
'release': 0, 'release': 0,
'alpha': 1, 'alpha': 1,
@ -340,6 +383,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
})) }))
self._server.query(key, method=self._server._session.put) self._server.query(key, method=self._server._session.put)
@deprecated('use editTitle, editSortTitle, editContentRating, and editSummary instead')
def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs): def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs):
""" Edit the collection. """ Edit the collection.
@ -364,7 +408,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
args['summary.locked'] = 1 args['summary.locked'] = 1
args.update(kwargs) args.update(kwargs)
super(Collection, self).edit(**args) self._edit(**args)
def delete(self): def delete(self):
""" Delete the collection. """ """ Delete the collection. """

View file

@ -62,4 +62,5 @@ def reset_base_headers():
'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME, 'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME,
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER, 'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
'X-Plex-Sync-Version': '2', 'X-Plex-Sync-Version': '2',
'X-Plex-Features': 'external-media',
} }

View file

@ -3,7 +3,7 @@
# Library version # Library version
MAJOR_VERSION = 4 MAJOR_VERSION = 4
MINOR_VERSION = 9 MINOR_VERSION = 11
PATCH_VERSION = 2 PATCH_VERSION = 0
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"

View file

@ -15,7 +15,7 @@ import struct
class GDM: class GDM:
"""Base class to discover GDM services. """Base class to discover GDM services.
Atrributes: Attributes:
entries (List<dict>): List of server and/or client data discovered. entries (List<dict>): List of server and/or client data discovered.
""" """

View file

@ -43,7 +43,7 @@ class Library(PlexObject):
if elem.attrib.get('type') == cls.TYPE: if elem.attrib.get('type') == cls.TYPE:
section = cls(self._server, elem, key) section = cls(self._server, elem, key)
self._sectionsByID[section.key] = section self._sectionsByID[section.key] = section
self._sectionsByTitle[section.title.lower()] = section self._sectionsByTitle[section.title.lower().strip()] = section
def sections(self): def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of """ Returns a list of all media sections in this library. Library sections may be any of
@ -59,10 +59,11 @@ class Library(PlexObject):
Parameters: Parameters:
title (str): Title of the section to return. title (str): Title of the section to return.
""" """
if not self._sectionsByTitle or title not in self._sectionsByTitle: normalized_title = title.lower().strip()
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
self._loadSections() self._loadSections()
try: try:
return self._sectionsByTitle[title.lower()] return self._sectionsByTitle[normalized_title]
except KeyError: except KeyError:
raise NotFound('Invalid library section: %s' % title) from None raise NotFound('Invalid library section: %s' % title) from None
@ -125,7 +126,7 @@ class Library(PlexObject):
def search(self, title=None, libtype=None, **kwargs): def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain """ Searching within a library section is much more powerful. It seems certain
attributes on the media objects can be targeted to filter this search down attributes on the media objects can be targeted to filter this search down
a bit, but I havent found the documentation for it. a bit, but I haven't found the documentation for it.
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
such as actor=<id> seem to work, but require you already know the id of the actor. such as actor=<id> seem to work, but require you already know the id of the actor.
@ -396,7 +397,7 @@ class LibrarySection(PlexObject):
self.type = data.attrib.get('type') self.type = data.attrib.get('type')
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt')) self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
self.uuid = data.attrib.get('uuid') self.uuid = data.attrib.get('uuid')
# Private attrs as we dont want a reload. # Private attrs as we don't want a reload.
self._filterTypes = None self._filterTypes = None
self._fieldTypes = None self._fieldTypes = None
self._totalViewSize = None self._totalViewSize = None
@ -599,12 +600,13 @@ class LibrarySection(PlexObject):
return self.fetchItem(key, title__iexact=title) return self.fetchItem(key, title__iexact=title)
def getGuid(self, guid): def getGuid(self, guid):
""" Returns the media item with the specified external IMDB, TMDB, or TVDB ID. """ Returns the media item with the specified external Plex, IMDB, TMDB, or TVDB ID.
Note: Only available for the Plex Movie and Plex TV Series agents. Note: Only available for the Plex Movie and Plex TV Series agents.
Parameters: Parameters:
guid (str): The external guid of the item to return. guid (str): The external guid of the item to return.
Examples: IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``. Examples: Plex ``plex://show/5d9c086c46115600200aa2fe``
IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
Raises: Raises:
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library. :exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
@ -613,21 +615,32 @@ class LibrarySection(PlexObject):
.. code-block:: python .. code-block:: python
result1 = library.getGuid('imdb://tt0944947') result1 = library.getGuid('plex://show/5d9c086c46115600200aa2fe')
result2 = library.getGuid('tmdb://1399') result2 = library.getGuid('imdb://tt0944947')
result3 = library.getGuid('tvdb://121361') result3 = library.getGuid('tmdb://1399')
result4 = library.getGuid('tvdb://121361')
# Alternatively, create your own guid lookup dictionary for faster performance # Alternatively, create your own guid lookup dictionary for faster performance
guidLookup = {guid.id: item for item in library.all() for guid in item.guids} guidLookup = {}
result1 = guidLookup['imdb://tt0944947'] for item in library.all():
result2 = guidLookup['tmdb://1399'] guidLookup[item.guid] = item
result3 = guidLookup['tvdb://121361'] guidLookup.update({guid.id for guid in item.guids}}
result1 = guidLookup['plex://show/5d9c086c46115600200aa2fe']
result2 = guidLookup['imdb://tt0944947']
result4 = guidLookup['tmdb://1399']
result5 = guidLookup['tvdb://121361']
""" """
try: try:
dummy = self.search(maxresults=1)[0] if guid.startswith('plex://'):
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-')) result = self.search(guid=guid)[0]
return self.search(guid=match[0].guid)[0] return result
else:
dummy = self.search(maxresults=1)[0]
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-'))
return self.search(guid=match[0].guid)[0]
except IndexError: except IndexError:
raise NotFound("Guid '%s' is not found in the library" % guid) from None raise NotFound("Guid '%s' is not found in the library" % guid) from None
@ -1271,7 +1284,7 @@ class LibrarySection(PlexObject):
* See :func:`~plexapi.library.LibrarySection.listOperators` to get a list of all available operators. * See :func:`~plexapi.library.LibrarySection.listOperators` to get a list of all available operators.
* See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values. * See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values.
The following filter fields are just some examples of the possible filters. The list is not exaustive, The following filter fields are just some examples of the possible filters. The list is not exhaustive,
and not all filters apply to all library types. and not all filters apply to all library types.
* **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor. * **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor.
@ -1334,7 +1347,7 @@ class LibrarySection(PlexObject):
Some filters may be prefixed by the ``libtype`` separated by a ``.`` (e.g. ``show.collection``, Some filters may be prefixed by the ``libtype`` separated by a ``.`` (e.g. ``show.collection``,
``episode.title``, ``artist.style``, ``album.genre``, ``track.userRating``, etc.). This should not be ``episode.title``, ``artist.style``, ``album.genre``, ``track.userRating``, etc.). This should not be
confused with the ``libtype`` parameter. If no ``libtype`` prefix is provided, then the default library confused with the ``libtype`` parameter. If no ``libtype`` prefix is provided, then the default library
type is assumed. For example, in a TV show library ``viewCout`` is assumed to be ``show.viewCount``. type is assumed. For example, in a TV show library ``viewCount`` is assumed to be ``show.viewCount``.
If you want to filter using episode view count then you must specify ``episode.viewCount`` explicitly. If you want to filter using episode view count then you must specify ``episode.viewCount`` explicitly.
In addition, if the filter does not exist for the default library type it will fallback to the most In addition, if the filter does not exist for the default library type it will fallback to the most
specific ``libtype`` available. For example, ``show.unwatched`` does not exists so it will fallback to specific ``libtype`` available. For example, ``show.unwatched`` does not exists so it will fallback to
@ -2236,16 +2249,61 @@ class FilteringType(PlexObject):
self.title = data.attrib.get('title') self.title = data.attrib.get('title')
self.type = data.attrib.get('type') self.type = data.attrib.get('type')
# Add additional manual sorts and fields which are available self._librarySectionID = self._parent().key
# Add additional manual filters, sorts, and fields which are available
# but not exposed on the Plex server # but not exposed on the Plex server
self.filters += self._manualFilters()
self.sorts += self._manualSorts() self.sorts += self._manualSorts()
self.fields += self._manualFields() self.fields += self._manualFields()
def _manualFilters(self):
""" Manually add additional filters which are available
but not exposed on the Plex server.
"""
# Filters: (filter, type, title)
additionalFilters = [
]
if self.type == 'season':
additionalFilters.extend([
('label', 'string', 'Labels')
])
elif self.type == 'episode':
additionalFilters.extend([
('label', 'string', 'Labels')
])
elif self.type == 'artist':
additionalFilters.extend([
('label', 'string', 'Labels')
])
elif self.type == 'track':
additionalFilters.extend([
('label', 'string', 'Labels')
])
elif self.type == 'collection':
additionalFilters.extend([
('label', 'string', 'Labels')
])
manualFilters = []
for filterTag, filterType, filterTitle in additionalFilters:
filterKey = '/library/sections/%s/%s?type=%s' % (
self._librarySectionID, filterTag, utils.searchType(self.type)
)
filterXML = (
'<Filter filter="%s" filterType="%s" key="%s" title="%s" type="filter" />'
% (filterTag, filterType, filterKey, filterTitle)
)
manualFilters.append(self._manuallyLoadXML(filterXML, FilteringFilter))
return manualFilters
def _manualSorts(self): def _manualSorts(self):
""" Manually add additional sorts which are available """ Manually add additional sorts which are available
but not exposed on the Plex server. but not exposed on the Plex server.
""" """
# Sorts: key, dir, title # Sorts: (key, dir, title)
additionalSorts = [ additionalSorts = [
('guid', 'asc', 'Guid'), ('guid', 'asc', 'Guid'),
('id', 'asc', 'Rating Key'), ('id', 'asc', 'Rating Key'),
@ -2275,8 +2333,10 @@ class FilteringType(PlexObject):
manualSorts = [] manualSorts = []
for sortField, sortDir, sortTitle in additionalSorts: for sortField, sortDir, sortTitle in additionalSorts:
sortXML = ('<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />' sortXML = (
% (sortDir, sortField, sortField, sortTitle)) '<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />'
% (sortDir, sortField, sortField, sortTitle)
)
manualSorts.append(self._manuallyLoadXML(sortXML, FilteringSort)) manualSorts.append(self._manuallyLoadXML(sortXML, FilteringSort))
return manualSorts return manualSorts
@ -2285,7 +2345,7 @@ class FilteringType(PlexObject):
""" Manually add additional fields which are available """ Manually add additional fields which are available
but not exposed on the Plex server. but not exposed on the Plex server.
""" """
# Fields: key, type, title # Fields: (key, type, title)
additionalFields = [ additionalFields = [
('guid', 'string', 'Guid'), ('guid', 'string', 'Guid'),
('id', 'integer', 'Rating Key'), ('id', 'integer', 'Rating Key'),
@ -2311,31 +2371,41 @@ class FilteringType(PlexObject):
additionalFields.extend([ additionalFields.extend([
('addedAt', 'date', 'Date Season Added'), ('addedAt', 'date', 'Date Season Added'),
('unviewedLeafCount', 'integer', 'Episode Unplayed Count'), ('unviewedLeafCount', 'integer', 'Episode Unplayed Count'),
('year', 'integer', 'Season Year') ('year', 'integer', 'Season Year'),
('label', 'tag', 'Label')
]) ])
elif self.type == 'episode': elif self.type == 'episode':
additionalFields.extend([ additionalFields.extend([
('audienceRating', 'integer', 'Audience Rating'), ('audienceRating', 'integer', 'Audience Rating'),
('duration', 'integer', 'Duration'), ('duration', 'integer', 'Duration'),
('rating', 'integer', 'Critic Rating'), ('rating', 'integer', 'Critic Rating'),
('viewOffset', 'integer', 'View Offset') ('viewOffset', 'integer', 'View Offset'),
('label', 'tag', 'Label')
])
elif self.type == 'artist':
additionalFields.extend([
('label', 'tag', 'Label')
]) ])
elif self.type == 'track': elif self.type == 'track':
additionalFields.extend([ additionalFields.extend([
('duration', 'integer', 'Duration'), ('duration', 'integer', 'Duration'),
('viewOffset', 'integer', 'View Offset') ('viewOffset', 'integer', 'View Offset'),
('label', 'tag', 'Label')
]) ])
elif self.type == 'collection': elif self.type == 'collection':
additionalFields.extend([ additionalFields.extend([
('addedAt', 'date', 'Date Added') ('addedAt', 'date', 'Date Added'),
('label', 'tag', 'Label')
]) ])
prefix = '' if self.type == 'movie' else self.type + '.' prefix = '' if self.type == 'movie' else self.type + '.'
manualFields = [] manualFields = []
for field, fieldType, fieldTitle in additionalFields: for field, fieldType, fieldTitle in additionalFields:
fieldXML = ('<Field key="%s%s" title="%s" type="%s"/>' fieldXML = (
% (prefix, field, fieldTitle, fieldType)) '<Field key="%s%s" title="%s" type="%s"/>'
% (prefix, field, fieldTitle, fieldType)
)
manualFields.append(self._manuallyLoadXML(fieldXML, FilteringField)) manualFields.append(self._manuallyLoadXML(fieldXML, FilteringField))
return manualFields return manualFields

View file

@ -39,7 +39,7 @@ class Media(PlexObject):
<Photo_only_attributes>: The following attributes are only available for photos. <Photo_only_attributes>: The following attributes are only available for photos.
* aperture (str): The apeture used to take the photo. * aperture (str): The aperture used to take the photo.
* exposure (str): The exposure used to take the photo. * exposure (str): The exposure used to take the photo.
* iso (int): The iso used to take the photo. * iso (int): The iso used to take the photo.
* lens (str): The lens used to take the photo. * lens (str): The lens used to take the photo.
@ -93,7 +93,7 @@ class Media(PlexObject):
try: try:
return self._server.query(part, method=self._server._session.delete) return self._server.query(part, method=self._server._session.delete)
except BadRequest: except BadRequest:
log.error("Failed to delete %s. This could be because you havn't allowed " log.error("Failed to delete %s. This could be because you haven't allowed "
"items to be deleted" % part) "items to be deleted" % part)
raise raise
@ -224,7 +224,7 @@ class MediaPartStream(PlexObject):
id (int): The unique ID for this stream on the server. id (int): The unique ID for this stream on the server.
index (int): The index of the stream. index (int): The index of the stream.
language (str): The language of the stream (ex: English, ไทย). language (str): The language of the stream (ex: English, ไทย).
languageCode (str): The Ascii language code of the stream (ex: eng, tha). languageCode (str): The ASCII language code of the stream (ex: eng, tha).
requiredBandwidths (str): The required bandwidths to stream the file. requiredBandwidths (str): The required bandwidths to stream the file.
selected (bool): True if this stream is selected. selected (bool): True if this stream is selected.
streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`, streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`,
@ -283,8 +283,8 @@ class VideoStream(MediaPartStream):
duration (int): The duration of video stream in milliseconds. duration (int): The duration of video stream in milliseconds.
frameRate (float): The frame rate of the video stream (ex: 23.976). frameRate (float): The frame rate of the video stream (ex: 23.976).
frameRateMode (str): The frame rate mode of the video stream. frameRateMode (str): The frame rate mode of the video stream.
hasScallingMatrix (bool): True if video stream has a scaling matrix. hasScalingMatrix (bool): True if video stream has a scaling matrix.
height (int): The hight of the video stream in pixels (ex: 1080). height (int): The height of the video stream in pixels (ex: 1080).
level (int): The codec encoding level of the video stream (ex: 41). level (int): The codec encoding level of the video stream (ex: 41).
profile (str): The profile of the video stream (ex: asp). profile (str): The profile of the video stream (ex: asp).
pixelAspectRatio (str): The pixel aspect ratio of the video stream. pixelAspectRatio (str): The pixel aspect ratio of the video stream.
@ -323,7 +323,7 @@ class VideoStream(MediaPartStream):
self.duration = utils.cast(int, data.attrib.get('duration')) self.duration = utils.cast(int, data.attrib.get('duration'))
self.frameRate = utils.cast(float, data.attrib.get('frameRate')) self.frameRate = utils.cast(float, data.attrib.get('frameRate'))
self.frameRateMode = data.attrib.get('frameRateMode') self.frameRateMode = data.attrib.get('frameRateMode')
self.hasScallingMatrix = utils.cast(bool, data.attrib.get('hasScallingMatrix')) self.hasScalingMatrix = utils.cast(bool, data.attrib.get('hasScalingMatrix'))
self.height = utils.cast(int, data.attrib.get('height')) self.height = utils.cast(int, data.attrib.get('height'))
self.level = utils.cast(int, data.attrib.get('level')) self.level = utils.cast(int, data.attrib.get('level'))
self.profile = data.attrib.get('profile') self.profile = data.attrib.get('profile')
@ -400,7 +400,7 @@ class SubtitleStream(MediaPartStream):
container (str): The container of the subtitle stream. container (str): The container of the subtitle stream.
forced (bool): True if this is a forced subtitle. forced (bool): True if this is a forced subtitle.
format (str): The format of the subtitle stream (ex: srt). format (str): The format of the subtitle stream (ex: srt).
headerCommpression (str): The header compression of the subtitle stream. headerCompression (str): The header compression of the subtitle stream.
transient (str): Unknown. transient (str): Unknown.
""" """
TAG = 'Stream' TAG = 'Stream'
@ -468,7 +468,7 @@ class TranscodeSession(PlexObject):
audioDecision (str): The transcode decision for the audio stream. audioDecision (str): The transcode decision for the audio stream.
complete (bool): True if the transcode is complete. complete (bool): True if the transcode is complete.
container (str): The container of the transcoded media. container (str): The container of the transcoded media.
context (str): The context for the transcode sesson. context (str): The context for the transcode session.
duration (int): The duration of the transcoded media in milliseconds. duration (int): The duration of the transcoded media in milliseconds.
height (int): The height of the transcoded media in pixels. height (int): The height of the transcoded media in pixels.
key (str): API URL (ex: /transcode/sessions/<id>). key (str): API URL (ex: /transcode/sessions/<id>).
@ -572,7 +572,7 @@ class Optimized(PlexObject):
""" """
key = '%s/%s/items' % (self._initpath, self.id) key = '%s/%s/items' % (self._initpath, self.id)
return self.fetchItems(key) return self.fetchItems(key)
def remove(self): def remove(self):
""" Remove an Optimized item""" """ Remove an Optimized item"""
key = '%s/%s' % (self._initpath, self.id) key = '%s/%s' % (self._initpath, self.id)
@ -893,7 +893,7 @@ class Guid(GuidTag):
@utils.registerPlexObject @utils.registerPlexObject
class Review(PlexObject): class Review(PlexObject):
""" Represents a single Review for a Movie. """ Represents a single Review for a Movie.
Attributes: Attributes:
TAG (str): 'Review' TAG (str): 'Review'
filter (str): filter for reviews? filter (str): filter for reviews?
@ -917,19 +917,17 @@ class Review(PlexObject):
self.text = data.attrib.get('text') self.text = data.attrib.get('text')
class BaseImage(PlexObject): class BaseResource(PlexObject):
""" Base class for all Art, Banner, and Poster objects. """ Base class for all Art, Banner, Poster, and Theme objects.
Attributes: Attributes:
TAG (str): 'Photo' TAG (str): 'Photo' or 'Track'
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
provider (str): The source of the poster or art. provider (str): The source of the art or poster, None for Theme objects.
ratingKey (str): Unique key identifying the poster or art. ratingKey (str): Unique key identifying the resource.
selected (bool): True if the poster or art is currently selected. selected (bool): True if the resource is currently selected.
thumb (str): The URL to retrieve the poster or art thumbnail. thumb (str): The URL to retrieve the resource thumbnail.
""" """
TAG = 'Photo'
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.key = data.attrib.get('key') self.key = data.attrib.get('key')
@ -947,16 +945,24 @@ class BaseImage(PlexObject):
pass pass
class Art(BaseImage): class Art(BaseResource):
""" Represents a single Art object. """ """ Represents a single Art object. """
TAG = 'Photo'
class Banner(BaseImage): class Banner(BaseResource):
""" Represents a single Banner object. """ """ Represents a single Banner object. """
TAG = 'Photo'
class Poster(BaseImage): class Poster(BaseResource):
""" Represents a single Poster object. """ """ Represents a single Poster object. """
TAG = 'Photo'
class Theme(BaseResource):
""" Represents a single Theme object. """
TAG = 'Track'
@utils.registerPlexObject @utils.registerPlexObject
@ -1106,3 +1112,41 @@ class AgentMediaType(Agent):
@deprecated('use "languageCodes" instead') @deprecated('use "languageCodes" instead')
def languageCode(self): def languageCode(self):
return self.languageCodes return self.languageCodes
@utils.registerPlexObject
class Availability(PlexObject):
""" Represents a single online streaming service Availability.
Attributes:
TAG (str): 'Availability'
country (str): The streaming service country.
offerType (str): Subscription, buy, or rent from the streaming service.
platform (str): The platform slug for the streaming service.
platformColorThumb (str): Thumbnail icon for the streaming service.
platformInfo (str): The streaming service platform info.
platformUrl (str): The URL to the media on the streaming service.
price (float): The price to buy or rent from the streaming service.
priceDescription (str): The display price to buy or rent from the streaming service.
quality (str): The video quality on the streaming service.
title (str): The title of the streaming service.
url (str): The Plex availability URL.
"""
TAG = 'Availability'
def __repr__(self):
return f'<{self.__class__.__name__}:{self.platform}:{self.offerType}>'
def _loadData(self, data):
self._data = data
self.country = data.attrib.get('country')
self.offerType = data.attrib.get('offerType')
self.platform = data.attrib.get('platform')
self.platformColorThumb = data.attrib.get('platformColorThumb')
self.platformInfo = data.attrib.get('platformInfo')
self.platformUrl = data.attrib.get('platformUrl')
self.price = utils.cast(float, data.attrib.get('price'))
self.priceDescription = data.attrib.get('priceDescription')
self.quality = data.attrib.get('quality')
self.title = data.attrib.get('title')
self.url = data.attrib.get('url')

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy import copy
import html
import threading import threading
import time import time
from xml.etree import ElementTree from xml.etree import ElementTree
@ -52,7 +53,7 @@ class MyPlexAccount(PlexObject):
roles: (List<str>) Lit of account roles. Plexpass membership listed here. roles: (List<str>) Lit of account roles. Plexpass membership listed here.
scrobbleTypes (str): Description scrobbleTypes (str): Description
secure (bool): Description secure (bool): Description
subscriptionActive (bool): True if your subsctiption is active. subscriptionActive (bool): True if your subscription is active.
subscriptionFeatures: (List<str>) List of features allowed on your subscription. subscriptionFeatures: (List<str>) List of features allowed on your subscription.
subscriptionPlan (str): Name of subscription plan. subscriptionPlan (str): Name of subscription plan.
subscriptionStatus (str): String representation of `subscriptionActive`. subscriptionStatus (str): String representation of `subscriptionActive`.
@ -72,14 +73,12 @@ class MyPlexAccount(PlexObject):
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
OPTOUTS = 'https://plex.tv/api/v2/user/%(userUUID)s/settings/opt_outs' # get OPTOUTS = 'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs' # get
LINK = 'https://plex.tv/api/v2/pins/link' # put LINK = 'https://plex.tv/api/v2/pins/link' # put
# Hub sections # Hub sections
VOD = 'https://vod.provider.plex.tv/' # get VOD = 'https://vod.provider.plex.tv' # get
WEBSHOWS = 'https://webshows.provider.plex.tv/' # get MUSIC = 'https://music.provider.plex.tv' # get
NEWS = 'https://news.provider.plex.tv/' # get METADATA = 'https://metadata.provider.plex.tv'
PODCASTS = 'https://podcasts.provider.plex.tv/' # get
MUSIC = 'https://music.provider.plex.tv/' # get
# Key may someday switch to the following url. For now the current value works. # Key may someday switch to the following url. For now the current value works.
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId} # https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
key = 'https://plex.tv/users/account' key = 'https://plex.tv/users/account'
@ -182,6 +181,8 @@ class MyPlexAccount(PlexObject):
raise NotFound(message) raise NotFound(message)
else: else:
raise BadRequest(message) raise BadRequest(message)
if headers.get('Accept') == 'application/json':
return response.json()
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
@ -228,7 +229,7 @@ class MyPlexAccount(PlexObject):
of the user to be added. of the user to be added.
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
containing the library sections to share. containing the library sections to share.
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
to be shared (default None). `sections` must be defined in order to update shared libraries. to be shared (default None). `sections` must be defined in order to update shared libraries.
allowSync (Bool): Set True to allow user to sync content. allowSync (Bool): Set True to allow user to sync content.
allowCameraUpload (Bool): Set True to allow user to upload photos. allowCameraUpload (Bool): Set True to allow user to upload photos.
@ -268,7 +269,7 @@ class MyPlexAccount(PlexObject):
of the user to be added. of the user to be added.
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
containing the library sections to share. containing the library sections to share.
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
to be shared (default None). `sections` must be defined in order to update shared libraries. to be shared (default None). `sections` must be defined in order to update shared libraries.
allowSync (Bool): Set True to allow user to sync content. allowSync (Bool): Set True to allow user to sync content.
allowCameraUpload (Bool): Set True to allow user to upload photos. allowCameraUpload (Bool): Set True to allow user to upload photos.
@ -317,7 +318,7 @@ class MyPlexAccount(PlexObject):
of the user to be added. of the user to be added.
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
containing the library sections to share. containing the library sections to share.
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
to be shared (default None). `sections` must be defined in order to update shared libraries. to be shared (default None). `sections` must be defined in order to update shared libraries.
allowSync (Bool): Set True to allow user to sync content. allowSync (Bool): Set True to allow user to sync content.
allowCameraUpload (Bool): Set True to allow user to upload photos. allowCameraUpload (Bool): Set True to allow user to upload photos.
@ -420,7 +421,7 @@ class MyPlexAccount(PlexObject):
of the user to be updated. of the user to be updated.
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
containing the library sections to share. containing the library sections to share.
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
to be shared (default None). `sections` must be defined in order to update shared libraries. to be shared (default None). `sections` must be defined in order to update shared libraries.
removeSections (Bool): Set True to remove all shares. Supersedes sections. removeSections (Bool): Set True to remove all shares. Supersedes sections.
allowSync (Bool): Set True to allow user to sync content. allowSync (Bool): Set True to allow user to sync content.
@ -565,7 +566,7 @@ class MyPlexAccount(PlexObject):
""" Converts friend filters to a string representation for transport. """ """ Converts friend filters to a string representation for transport. """
values = [] values = []
for key, vals in filterDict.items(): for key, vals in filterDict.items():
if key not in ('contentRating', 'label'): if key not in ('contentRating', 'label', 'contentRating!', 'label!'):
raise BadRequest('Unknown filter key: %s', key) raise BadRequest('Unknown filter key: %s', key)
values.append('%s=%s' % (key, '%2C'.join(vals))) values.append('%s=%s' % (key, '%2C'.join(vals)))
return '|'.join(values) return '|'.join(values)
@ -614,7 +615,7 @@ class MyPlexAccount(PlexObject):
clientId (str): an identifier of a client to query SyncItems for. clientId (str): an identifier of a client to query SyncItems for.
If both `client` and `clientId` provided the client would be preferred. If both `client` and `clientId` provided the client would be preferred.
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier. If neither `client` nor `clientId` provided the clientId would be set to current clients's identifier.
""" """
if client: if client:
clientId = client.clientIdentifier clientId = client.clientIdentifier
@ -635,14 +636,14 @@ class MyPlexAccount(PlexObject):
sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set. sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set.
If both `client` and `clientId` provided the client would be preferred. If both `client` and `clientId` provided the client would be preferred.
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier. If neither `client` nor `clientId` provided the clientId would be set to current clients's identifier.
Returns: Returns:
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem. :class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
Raises: Raises:
:exc:`~plexapi.exceptions.BadRequest`: When client with provided clientId wasn`t found. :exc:`~plexapi.exceptions.BadRequest`: When client with provided clientId wasn't found.
:exc:`~plexapi.exceptions.BadRequest`: Provided client doesn`t provides `sync-target`. :exc:`~plexapi.exceptions.BadRequest`: Provided client doesn't provides `sync-target`.
""" """
if not client and not clientId: if not client and not clientId:
clientId = X_PLEX_IDENTIFIER clientId = X_PLEX_IDENTIFIER
@ -657,7 +658,7 @@ class MyPlexAccount(PlexObject):
raise BadRequest('Unable to find client by clientId=%s', clientId) raise BadRequest('Unable to find client by clientId=%s', clientId)
if 'sync-target' not in client.provides: if 'sync-target' not in client.provides:
raise BadRequest('Received client doesn`t provides sync-target') raise BadRequest("Received client doesn't provides sync-target")
params = { params = {
'SyncItem[title]': sync_item.title, 'SyncItem[title]': sync_item.title,
@ -698,6 +699,7 @@ class MyPlexAccount(PlexObject):
def history(self, maxresults=9999999, mindate=None): def history(self, maxresults=9999999, mindate=None):
""" Get Play History for all library sections on all servers for the owner. """ Get Play History for all library sections on all servers for the owner.
Parameters: Parameters:
maxresults (int): Only return the specified number of results (optional). maxresults (int): Only return the specified number of results (optional).
mindate (datetime): Min datetime to return results from. mindate (datetime): Min datetime to return results from.
@ -709,47 +711,155 @@ class MyPlexAccount(PlexObject):
hist.extend(conn.history(maxresults=maxresults, mindate=mindate, accountID=1)) hist.extend(conn.history(maxresults=maxresults, mindate=mindate, accountID=1))
return hist return hist
def onlineMediaSources(self):
""" Returns a list of user account Online Media Sources settings :class:`~plexapi.myplex.AccountOptOut`
"""
url = self.OPTOUTS.format(userUUID=self.uuid)
elem = self.query(url)
return self.findItems(elem, cls=AccountOptOut, etag='optOut')
def videoOnDemand(self): def videoOnDemand(self):
""" Returns a list of VOD Hub items :class:`~plexapi.library.Hub` """ Returns a list of VOD Hub items :class:`~plexapi.library.Hub`
""" """
req = requests.get(self.VOD + 'hubs/', headers={'X-Plex-Token': self._token}) data = self.query(f'{self.VOD}/hubs')
elem = ElementTree.fromstring(req.text) return self.findItems(data)
return self.findItems(elem)
def webShows(self):
""" Returns a list of Webshow Hub items :class:`~plexapi.library.Hub`
"""
req = requests.get(self.WEBSHOWS + 'hubs/', headers={'X-Plex-Token': self._token})
elem = ElementTree.fromstring(req.text)
return self.findItems(elem)
def news(self):
""" Returns a list of News Hub items :class:`~plexapi.library.Hub`
"""
req = requests.get(self.NEWS + 'hubs/sections/all', headers={'X-Plex-Token': self._token})
elem = ElementTree.fromstring(req.text)
return self.findItems(elem)
def podcasts(self):
""" Returns a list of Podcasts Hub items :class:`~plexapi.library.Hub`
"""
req = requests.get(self.PODCASTS + 'hubs/', headers={'X-Plex-Token': self._token})
elem = ElementTree.fromstring(req.text)
return self.findItems(elem)
def tidal(self): def tidal(self):
""" Returns a list of tidal Hub items :class:`~plexapi.library.Hub` """ Returns a list of tidal Hub items :class:`~plexapi.library.Hub`
""" """
req = requests.get(self.MUSIC + 'hubs/', headers={'X-Plex-Token': self._token}) data = self.query(f'{self.MUSIC}/hubs')
elem = ElementTree.fromstring(req.text) return self.findItems(data)
return self.findItems(elem)
def watchlist(self, filter=None, sort=None, libtype=None, **kwargs):
""" Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` items in the user's watchlist.
Note: The objects returned are from Plex's online metadata. To get the matching item on a Plex server,
search for the media using the guid.
Parameters:
filter (str, optional): 'available' or 'released' to only return items that are available or released,
otherwise return all items.
sort (str, optional): In the format ``field:dir``. Available fields are ``watchlistedAt`` (Added At),
``titleSort`` (Title), ``originallyAvailableAt`` (Release Date), or ``rating`` (Critic Rating).
``dir`` can be ``asc`` or ``desc``.
libtype (str, optional): 'movie' or 'show' to only return movies or shows, otherwise return all items.
**kwargs (dict): Additional custom filters to apply to the search results.
Example:
.. code-block:: python
# Watchlist for released movies sorted by critic rating in descending order
watchlist = account.watchlist(filter='released', sort='rating:desc', libtype='movie')
item = watchlist[0] # First item in the watchlist
# Search for the item on a Plex server
result = plex.library.search(guid=item.guid, libtype=item.type)
def onlineMediaSources(self):
""" Returns a list of user account Online Media Sources settings :class:`~plexapi.myplex.AccountOptOut`
""" """
url = self.OPTOUTS % {'userUUID': self.uuid} params = {
elem = self.query(url) 'includeCollections': 1,
return self.findItems(elem, cls=AccountOptOut, etag='optOut') 'includeExternalMedia': 1
}
if not filter:
filter = 'all'
if sort:
params['sort'] = sort
if libtype:
params['type'] = utils.searchType(libtype)
params.update(kwargs)
data = self.query(f'{self.METADATA}/library/sections/watchlist/{filter}', params=params)
return self.findItems(data)
def onWatchlist(self, item):
""" Returns True if the item is on the user's watchlist.
Parameters:
item (:class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`): Item to check
if it is on the user's watchlist.
"""
ratingKey = item.guid.rsplit('/', 1)[-1]
data = self.query(f"{self.METADATA}/library/metadata/{ratingKey}/userState")
return bool(data.find('UserState').attrib.get('watchlistedAt'))
def addToWatchlist(self, items):
""" Add media items to the user's watchlist
Parameters:
items (List): List of :class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`
objects to be added to the watchlist.
Raises:
:exc:`~plexapi.exceptions.BadRequest`: When trying to add invalid or existing
media to the watchlist.
"""
if not isinstance(items, list):
items = [items]
for item in items:
if self.onWatchlist(item):
raise BadRequest('"%s" is already on the watchlist' % item.title)
ratingKey = item.guid.rsplit('/', 1)[-1]
self.query(f'{self.METADATA}/actions/addToWatchlist?ratingKey={ratingKey}', method=self._session.put)
def removeFromWatchlist(self, items):
""" Remove media items from the user's watchlist
Parameters:
items (List): List of :class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`
objects to be added to the watchlist.
Raises:
:exc:`~plexapi.exceptions.BadRequest`: When trying to remove invalid or non-existing
media to the watchlist.
"""
if not isinstance(items, list):
items = [items]
for item in items:
if not self.onWatchlist(item):
raise BadRequest('"%s" is not on the watchlist' % item.title)
ratingKey = item.guid.rsplit('/', 1)[-1]
self.query(f'{self.METADATA}/actions/removeFromWatchlist?ratingKey={ratingKey}', method=self._session.put)
def searchDiscover(self, query, limit=30):
""" Search for movies and TV shows in Discover.
Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` objects.
Parameters:
query (str): Search query.
limit (int, optional): Limit to the specified number of results. Default 30.
"""
headers = {
'Accept': 'application/json'
}
params = {
'query': query,
'limit ': limit,
'searchTypes': 'movies,tv',
'includeMetadata': 1
}
data = self.query(f'{self.METADATA}/library/search', headers=headers, params=params)
searchResults = data['MediaContainer'].get('SearchResult', [])
results = []
for result in searchResults:
metadata = result['Metadata']
type = metadata['type']
if type == 'movie':
tag = 'Video'
elif type == 'show':
tag = 'Directory'
else:
continue
attrs = ''.join(f'{k}="{html.escape(str(v))}" ' for k, v in metadata.items())
xml = f'<{tag} {attrs}/>'
results.append(self._manuallyLoadXML(xml))
return results
def link(self, pin): def link(self, pin):
""" Link a device to the account using a pin code. """ Link a device to the account using a pin code.
@ -790,7 +900,7 @@ class MyPlexUser(PlexObject):
restricted (str): Unknown. restricted (str): Unknown.
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user. servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
thumb (str): Link to the users avatar. thumb (str): Link to the users avatar.
title (str): Seems to be an aliad for username. title (str): Seems to be an alias for username.
username (str): User's username. username (str): User's username.
""" """
TAG = 'User' TAG = 'User'
@ -1103,7 +1213,7 @@ class MyPlexResource(PlexObject):
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource. :exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
""" """
connections = self.preferred_connections(ssl, timeout, locations, schemes) connections = self.preferred_connections(ssl, timeout, locations, schemes)
# Try connecting to all known resource connections in parellel, but # Try connecting to all known resource connections in parallel, but
# only return the first server (in order) that provides a response. # only return the first server (in order) that provides a response.
cls = PlexServer if 'server' in self.provides else PlexClient cls = PlexServer if 'server' in self.provides else PlexClient
listargs = [[cls, url, self.accessToken, timeout] for url in connections] listargs = [[cls, url, self.accessToken, timeout] for url in connections]
@ -1215,7 +1325,7 @@ class MyPlexDevice(PlexObject):
""" Returns an instance of :class:`~plexapi.sync.SyncList` for current device. """ Returns an instance of :class:`~plexapi.sync.SyncList` for current device.
Raises: Raises:
:exc:`~plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`. :exc:`~plexapi.exceptions.BadRequest`: when the device doesn't provides `sync-target`.
""" """
if 'sync-target' not in self.provides: if 'sync-target' not in self.provides:
raise BadRequest('Requested syncList for device which do not provides sync-target') raise BadRequest('Requested syncList for device which do not provides sync-target')

View file

@ -5,11 +5,21 @@ from urllib.parse import quote_plus
from plexapi import media, utils, video from plexapi import media, utils, video
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, RatingMixin, TagMixin from plexapi.mixins import (
RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin,
TagMixin
)
@utils.registerPlexObject @utils.registerPlexObject
class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin): class Photoalbum(
PlexPartialObject,
RatingMixin,
ArtMixin, PosterMixin,
SortTitleMixin, SummaryMixin, TitleMixin
):
""" Represents a single Photoalbum (collection of photos). """ Represents a single Photoalbum (collection of photos).
Attributes: Attributes:
@ -33,11 +43,12 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
title (str): Name of the photo album. (Trip to Disney World) title (str): Name of the photo album. (Trip to Disney World)
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
type (str): 'photo' type (str): 'photo'
updatedAt (datatime): Datetime the photo album was updated. updatedAt (datetime): Datetime the photo album was updated.
userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars). userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars).
""" """
TAG = 'Directory' TAG = 'Directory'
TYPE = 'photo' TYPE = 'photo'
_searchType = 'photoalbum'
def _loadData(self, data): def _loadData(self, data):
""" Load attribute values from Plex XML response. """ """ Load attribute values from Plex XML response. """
@ -109,7 +120,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
return self.episode(title) return self.episode(title)
def download(self, savepath=None, keep_original_name=False, subfolders=False): def download(self, savepath=None, keep_original_name=False, subfolders=False):
""" Download all photos and clips from the photo ablum. See :func:`~plexapi.base.Playable.download` for details. """ Download all photos and clips from the photo album. See :func:`~plexapi.base.Playable.download` for details.
Parameters: Parameters:
savepath (str): Defaults to current working dir. savepath (str): Defaults to current working dir.
@ -131,7 +142,13 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
@utils.registerPlexObject @utils.registerPlexObject
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin): class Photo(
PlexPartialObject, Playable,
RatingMixin,
ArtUrlMixin, PosterUrlMixin,
PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin,
TagMixin
):
""" Represents a single Photo. """ Represents a single Photo.
Attributes: Attributes:
@ -164,7 +181,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
title (str): Name of the photo. title (str): Name of the photo.
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
type (str): 'photo' type (str): 'photo'
updatedAt (datatime): Datetime the photo was updated. updatedAt (datetime): Datetime the photo was updated.
userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars). userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars).
year (int): Year the photo was taken. year (int): Year the photo was taken.
""" """
@ -223,7 +240,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
elif self.parentKey: elif self.parentKey:
return self._server.library.sectionByID(self.photoalbum().librarySectionID) return self._server.library.sectionByID(self.photoalbum().librarySectionID)
else: else:
raise BadRequest('Unable to get section for photo, can`t find librarySectionID') raise BadRequest("Unable to get section for photo, can't find librarySectionID")
@property @property
def locations(self): def locations(self):

View file

@ -6,13 +6,17 @@ from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.mixins import ArtMixin, PosterMixin, SmartFilterMixin from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import deprecated from plexapi.utils import deprecated
@utils.registerPlexObject @utils.registerPlexObject
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMixin): class Playlist(
PlexPartialObject, Playable,
SmartFilterMixin,
ArtMixin, PosterMixin
):
""" Represents a single Playlist. """ Represents a single Playlist.
Attributes: Attributes:
@ -39,7 +43,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
summary (str): Summary of the playlist. summary (str): Summary of the playlist.
title (str): Name of the playlist. title (str): Name of the playlist.
type (str): 'playlist' type (str): 'playlist'
updatedAt (datatime): Datetime the playlist was updated. updatedAt (datetime): Datetime the playlist was updated.
""" """
TAG = 'Playlist' TAG = 'Playlist'
TYPE = 'playlist' TYPE = 'playlist'

View file

@ -314,7 +314,7 @@ class PlexServer(PlexObject):
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
you're likley to recieve an authentication error calling this. you're likely to receive an authentication error calling this.
""" """
if self._myPlexAccount is None: if self._myPlexAccount is None:
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
@ -323,7 +323,7 @@ class PlexServer(PlexObject):
def _myPlexClientPorts(self): def _myPlexClientPorts(self):
""" Sometimes the PlexServer does not properly advertise port numbers required """ Sometimes the PlexServer does not properly advertise port numbers required
to connect. This attemps to look up device port number from plex.tv. to connect. This attempts to look up device port number from plex.tv.
See issue #126: Make PlexServer.clients() more user friendly. See issue #126: Make PlexServer.clients() more user friendly.
https://github.com/pkkid/python-plexapi/issues/126 https://github.com/pkkid/python-plexapi/issues/126
""" """
@ -393,7 +393,6 @@ class PlexServer(PlexObject):
""" """
if isinstance(path, Path): if isinstance(path, Path):
path = path.path path = path.path
path = os.path.normpath(path)
paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)] paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)]
return path in paths return path in paths
@ -524,14 +523,39 @@ class PlexServer(PlexObject):
filepath = utils.download(url, self._token, None, savepath, self._session, unpack=unpack) filepath = utils.download(url, self._token, None, savepath, self._session, unpack=unpack)
return filepath return filepath
def butlerTasks(self):
""" Return a list of :class:`~plexapi.base.ButlerTask` objects. """
return self.fetchItems('/butler')
def runButlerTask(self, task):
""" Manually run a butler task immediately instead of waiting for the scheduled task to run.
Note: The butler task is run asynchronously. Check Plex Web to monitor activity.
Parameters:
task (str): The name of the task to run. (e.g. 'BackupDatabase')
Example:
.. code-block:: python
availableTasks = [task.name for task in plex.butlerTasks()]
print("Available butler tasks:", availableTasks)
"""
validTasks = [task.name for task in self.butlerTasks()]
if task not in validTasks:
raise BadRequest(
f'Invalid butler task: {task}. Available tasks are: {validTasks}'
)
self.query(f'/butler/{task}', method=self._session.post)
@deprecated('use "checkForUpdate" instead') @deprecated('use "checkForUpdate" instead')
def check_for_update(self, force=True, download=False): def check_for_update(self, force=True, download=False):
return self.checkForUpdate() return self.checkForUpdate(force=force, download=download)
def checkForUpdate(self, force=True, download=False): def checkForUpdate(self, force=True, download=False):
""" Returns a :class:`~plexapi.base.Release` object containing release info. """ Returns a :class:`~plexapi.base.Release` object containing release info.
Parameters: Parameters:
force (bool): Force server to check for new releases force (bool): Force server to check for new releases
download (bool): Download if a update is available. download (bool): Download if a update is available.
""" """
@ -730,7 +754,7 @@ class PlexServer(PlexObject):
return self.fetchItems('/transcode/sessions') return self.fetchItems('/transcode/sessions')
def startAlertListener(self, callback=None): def startAlertListener(self, callback=None):
""" Creates a websocket connection to the Plex Server to optionally recieve """ Creates a websocket connection to the Plex Server to optionally receive
notifications. These often include messages from Plex about media scans notifications. These often include messages from Plex about media scans
as well as updates to currently running Transcode Sessions. as well as updates to currently running Transcode Sessions.
@ -738,7 +762,7 @@ class PlexServer(PlexObject):
>> pip install websocket-client >> pip install websocket-client
Parameters: Parameters:
callback (func): Callback function to call on recieved messages. callback (func): Callback function to call on received messages.
Raises: Raises:
:exc:`~plexapi.exception.Unsupported`: Websocket-client not installed. :exc:`~plexapi.exception.Unsupported`: Websocket-client not installed.
@ -1078,7 +1102,7 @@ class SystemDevice(PlexObject):
Attributes: Attributes:
TAG (str): 'Device' TAG (str): 'Device'
clientIdentifier (str): The unique identifier for the device. clientIdentifier (str): The unique identifier for the device.
createdAt (datatime): Datetime the device was created. createdAt (datetime): 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>)
name (str): The name of the device. name (str): The name of the device.
@ -1102,11 +1126,11 @@ class StatisticsBandwidth(PlexObject):
Attributes: Attributes:
TAG (str): 'StatisticsBandwidth' TAG (str): 'StatisticsBandwidth'
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID. accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
at (datatime): Datetime of the bandwidth data. at (datetime): Datetime of the bandwidth data.
bytes (int): The total number of bytes for the specified timespan. bytes (int): The total number of bytes for the specified time span.
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID. deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
lan (bool): True or False wheter the bandwidth is local or remote. lan (bool): True or False whether the bandwidth is local or remote.
timespan (int): The timespan for the bandwidth data. timespan (int): The time span for the bandwidth data.
1: months, 2: weeks, 3: days, 4: hours, 6: seconds. 1: months, 2: weeks, 3: days, 4: hours, 6: seconds.
""" """
@ -1143,12 +1167,12 @@ class StatisticsResources(PlexObject):
Attributes: Attributes:
TAG (str): 'StatisticsResources' TAG (str): 'StatisticsResources'
at (datatime): Datetime of the resource data. at (datetime): Datetime of the resource data.
hostCpuUtilization (float): The system CPU usage %. hostCpuUtilization (float): The system CPU usage %.
hostMemoryUtilization (float): The Plex Media Server CPU usage %. hostMemoryUtilization (float): The Plex Media Server CPU usage %.
processCpuUtilization (float): The system RAM usage %. processCpuUtilization (float): The system RAM usage %.
processMemoryUtilization (float): The Plex Media Server RAM usage %. processMemoryUtilization (float): The Plex Media Server RAM usage %.
timespan (int): The timespan for the resource data (6: seconds). timespan (int): The time span for the resource data (6: seconds).
""" """
TAG = 'StatisticsResources' TAG = 'StatisticsResources'
@ -1166,3 +1190,28 @@ class StatisticsResources(PlexObject):
self.__class__.__name__, self.__class__.__name__,
self._clean(int(self.at.timestamp())) self._clean(int(self.at.timestamp()))
] if p]) ] if p])
@utils.registerPlexObject
class ButlerTask(PlexObject):
""" Represents a single scheduled butler task.
Attributes:
TAG (str): 'ButlerTask'
description (str): The description of the task.
enabled (bool): Whether the task is enabled.
interval (int): The interval the task is run in days.
name (str): The name of the task.
scheduleRandomized (bool): Whether the task schedule is randomized.
title (str): The title of the task.
"""
TAG = 'ButlerTask'
def _loadData(self, data):
self._data = data
self.description = data.attrib.get('description')
self.enabled = utils.cast(bool, data.attrib.get('enabled'))
self.interval = utils.cast(int, data.attrib.get('interval'))
self.name = data.attrib.get('name')
self.scheduleRandomized = utils.cast(bool, data.attrib.get('scheduleRandomized'))
self.title = data.attrib.get('title')

View file

@ -71,7 +71,7 @@ class Settings(PlexObject):
return self.groups().get(group, []) return self.groups().get(group, [])
def save(self): def save(self):
""" Save any outstanding settnig changes to the :class:`~plexapi.server.PlexServer`. This """ Save any outstanding setting changes to the :class:`~plexapi.server.PlexServer`. This
performs a full reload() of Settings after complete. performs a full reload() of Settings after complete.
""" """
params = {} params = {}
@ -100,7 +100,7 @@ class Setting(PlexObject):
hidden (bool): True if this is a hidden setting. hidden (bool): True if this is a hidden setting.
advanced (bool): True if this is an advanced setting. advanced (bool): True if this is an advanced setting.
group (str): Group name this setting is categorized as. group (str): Group name this setting is categorized as.
enumValues (list,dict): List or dictionary of valis values for this setting. enumValues (list,dict): List or dictionary of valid values for this setting.
""" """
_bool_cast = lambda x: bool(x == 'true' or x == '1') _bool_cast = lambda x: bool(x == 'true' or x == '1')
_bool_str = lambda x: str(x).lower() _bool_str = lambda x: str(x).lower()
@ -143,7 +143,7 @@ class Setting(PlexObject):
return enumstr.split('|') return enumstr.split('|')
def set(self, value): def set(self, value):
""" Set a new value for this setitng. NOTE: You must call plex.settings.save() for before """ Set a new value for this setting. NOTE: You must call plex.settings.save() for before
any changes to setting values are persisted to the :class:`~plexapi.server.PlexServer`. any changes to setting values are persisted to the :class:`~plexapi.server.PlexServer`.
""" """
# check a few things up front # check a few things up front

View file

@ -14,7 +14,7 @@ class PlexSonosClient(PlexClient):
speakers linked to your Plex account. It also requires remote access to speakers linked to your Plex account. It also requires remote access to
be working properly. be working properly.
More details on the Sonos integration are avaialble here: More details on the Sonos integration are available here:
https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/ https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/
The Sonos API emulates the Plex player control API closely: The Sonos API emulates the Plex player control API closely:
@ -38,7 +38,7 @@ class PlexSonosClient(PlexClient):
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to. server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
session (:class:`~requests.Session`): Session object used for connection. session (:class:`~requests.Session`): Session object used for connection.
title (str): Name of this Sonos speaker. title (str): Name of this Sonos speaker.
token (str): X-Plex-Token used for authenication token (str): X-Plex-Token used for authentication
_baseurl (str): Address of public Plex Sonos API endpoint. _baseurl (str): Address of public Plex Sonos API endpoint.
_commandId (int): Counter for commands sent to Plex API. _commandId (int): Counter for commands sent to Plex API.
_token (str): Token associated with linked Plex account. _token (str): Token associated with linked Plex account.

View file

@ -9,6 +9,7 @@ import time
import unicodedata import unicodedata
import warnings import warnings
import zipfile import zipfile
from collections import deque
from datetime import datetime from datetime import datetime
from getpass import getpass from getpass import getpass
from threading import Event, Thread from threading import Event, Thread
@ -55,7 +56,7 @@ class SecretsFilter(logging.Filter):
def registerPlexObject(cls): def registerPlexObject(cls):
""" Registry of library types we may come across when parsing XML. This allows us to """ Registry of library types we may come across when parsing XML. This allows us to
define a few helper functions to dynamically convery the XML into objects. See define a few helper functions to dynamically convert the XML into objects. See
buildItem() below for an example. buildItem() below for an example.
""" """
etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE)) etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE))
@ -72,7 +73,7 @@ def cast(func, value):
only support str, int, float, bool. Should be extended if needed. only support str, int, float, bool. Should be extended if needed.
Parameters: Parameters:
func (func): Calback function to used cast to type (int, bool, float). func (func): Callback function to used cast to type (int, bool, float).
value (any): value to be cast and returned. value (any): value to be cast and returned.
""" """
if value is not None: if value is not None:
@ -114,7 +115,7 @@ def lowerFirst(s):
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
""" Returns the value at the specified attrstr location within a nexted tree of """ Returns the value at the specified attrstr location within a nested tree of
dicts, lists, tuples, functions, classes, etc. The lookup is done recursively dicts, lists, tuples, functions, classes, etc. The lookup is done recursively
for each key in attrstr (split by by the delimiter) This function is heavily for each key in attrstr (split by by the delimiter) This function is heavily
influenced by the lookups used in Django templates. influenced by the lookups used in Django templates.
@ -194,7 +195,7 @@ def threaded(callback, listargs):
args += [results, len(results)] args += [results, len(results)]
results.append(None) results.append(None)
threads.append(Thread(target=callback, args=args, kwargs=dict(job_is_done_event=job_is_done_event))) threads.append(Thread(target=callback, args=args, kwargs=dict(job_is_done_event=job_is_done_event)))
threads[-1].setDaemon(True) threads[-1].daemon = True
threads[-1].start() threads[-1].start()
while not job_is_done_event.is_set(): while not job_is_done_event.is_set():
if all(not t.is_alive() for t in threads): if all(not t.is_alive() for t in threads):
@ -304,7 +305,7 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
filename (str): Filename of the downloaded file, default None. filename (str): Filename of the downloaded file, default None.
savepath (str): Defaults to current working dir. savepath (str): Defaults to current working dir.
chunksize (int): What chunksize read/write at the time. chunksize (int): What chunksize read/write at the time.
mocked (bool): Helper to do evertything except write the file. mocked (bool): Helper to do everything except write the file.
unpack (bool): Unpack the zip file. unpack (bool): Unpack the zip file.
showstatus(bool): Display a progressbar. showstatus(bool): Display a progressbar.
@ -361,40 +362,6 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
return fullpath return fullpath
def tag_singular(tag):
if tag == 'countries':
return 'country'
elif tag == 'similar':
return 'similar'
else:
return tag[:-1]
def tag_plural(tag):
if tag == 'country':
return 'countries'
elif tag == 'similar':
return 'similar'
else:
return tag + 's'
def tag_helper(tag, items, locked=True, remove=False):
""" Simple tag helper for editing a object. """
if not isinstance(items, list):
items = [items]
data = {}
if not remove:
for i, item in enumerate(items):
tagname = '%s[%s].tag.tag' % (tag, i)
data[tagname] = item
if remove:
tagname = '%s[].tag.tag-' % tag
data[tagname] = ','.join(items)
data['%s.locked' % tag] = 1 if locked else 0
return data
def getMyPlexAccount(opts=None): # pragma: no cover def getMyPlexAccount(opts=None): # pragma: no cover
""" Helper function tries to get a MyPlex Account instance by checking """ Helper function tries to get a MyPlex Account instance by checking
the the following locations for a username and password. This is the the following locations for a username and password. This is
@ -485,7 +452,7 @@ def getAgentIdentifier(section, agent):
if agent in identifiers: if agent in identifiers:
return ag.identifier return ag.identifier
agents += identifiers agents += identifiers
raise NotFound('Couldnt find "%s" in agents list (%s)' % raise NotFound('Could not find "%s" in agents list (%s)' %
(agent, ', '.join(agents))) (agent, ', '.join(agents)))
@ -506,3 +473,15 @@ def deprecated(message, stacklevel=2):
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
return decorator return decorator
def iterXMLBFS(root, tag=None):
""" Iterate through an XML tree using a breadth-first search.
If tag is specified, only return nodes with that tag.
"""
queue = deque([root])
while queue:
node = queue.popleft()
if tag is None or node.tag == tag:
yield node
queue.extend(list(node))

View file

@ -2,12 +2,17 @@
import os import os
from urllib.parse import quote_plus, urlencode from urllib.parse import quote_plus, urlencode
from plexapi import library, media, utils from plexapi import 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 AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin from plexapi.mixins import (
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin
)
class Video(PlexPartialObject): class Video(PlexPartialObject):
@ -35,7 +40,7 @@ class Video(PlexPartialObject):
title (str): Name of the movie, show, season, episode, or clip. title (str): Name of the movie, show, season, episode, or clip.
titleSort (str): Title to use when sorting (defaults to title). titleSort (str): Title to use when sorting (defaults to title).
type (str): 'movie', 'show', 'season', 'episode', or 'clip'. type (str): 'movie', 'show', 'season', 'episode', or 'clip'.
updatedAt (datatime): Datetime the item was updated. updatedAt (datetime): Datetime the item was updated.
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars). userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
viewCount (int): Count of times the item was played. viewCount (int): Count of times the item was played.
""" """
@ -76,7 +81,7 @@ class Video(PlexPartialObject):
return self._server.url(part, includeToken=True) if part else None return self._server.url(part, includeToken=True) if part else None
def markWatched(self): def markWatched(self):
""" Mark the video as palyed. """ """ Mark the video as played. """
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
self._server.query(key) self._server.query(key)
@ -107,6 +112,15 @@ class Video(PlexPartialObject):
""" Returns str, default title for a new syncItem. """ """ Returns str, default title for a new syncItem. """
return self.title return self.title
def audioStreams(self):
""" Returns a list of :class:`~plexapi.media.AudioStream` objects for all MediaParts. """
streams = []
parts = self.iterParts()
for part in parts:
streams += part.audioStreams()
return streams
def subtitleStreams(self): def subtitleStreams(self):
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """ """ Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
streams = [] streams = []
@ -261,8 +275,15 @@ class Video(PlexPartialObject):
@utils.registerPlexObject @utils.registerPlexObject
class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, class Movie(
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin): Video, Playable,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
WatchlistMixin
):
""" Represents a single Movie. """ Represents a single Movie.
Attributes: Attributes:
@ -280,7 +301,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
labels (List<:class:`~plexapi.media.Label`>): List of label objects. labels (List<:class:`~plexapi.media.Label`>): List of label objects.
languageOverride (str): Setting that indicates if a languge is used to override metadata languageOverride (str): Setting that indicates if a language is used to override metadata
(eg. en-CA, None = Library default). (eg. en-CA, None = Library default).
media (List<:class:`~plexapi.media.Media`>): List of media objects. media (List<:class:`~plexapi.media.Media`>): List of media objects.
originallyAvailableAt (datetime): Datetime the movie was released. originallyAvailableAt (datetime): Datetime the movie was released.
@ -293,6 +314,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects. similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment). studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?). tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
useOriginalTitle (int): Setting that indicates if the original title is used for the movie useOriginalTitle (int): Setting that indicates if the original title is used for the movie
(-1 = Library default, 0 = No, 1 = Yes). (-1 = Library default, 0 = No, 1 = Yes).
viewOffset (int): View offset in milliseconds. viewOffset (int): View offset in milliseconds.
@ -331,6 +353,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
self.similar = self.findItems(data, media.Similar) self.similar = self.findItems(data, media.Similar)
self.studio = data.attrib.get('studio') self.studio = data.attrib.get('studio')
self.tagline = data.attrib.get('tagline') self.tagline = data.attrib.get('tagline')
self.theme = data.attrib.get('theme')
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1')) self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.writers = self.findItems(data, media.Writer) self.writers = self.findItems(data, media.Writer)
@ -365,20 +388,17 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
data = self._server.query(self._details_key) data = self._server.query(self._details_key)
return self.findItems(data, media.Review, rtag='Video') return self.findItems(data, media.Review, rtag='Video')
def extras(self):
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, Extra, rtag='Extras')
def hubs(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, library.Hub, rtag='Related')
@utils.registerPlexObject @utils.registerPlexObject
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin, class Show(
CollectionMixin, GenreMixin, LabelMixin): Video,
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
ArtMixin, BannerMixin, PosterMixin, ThemeMixin,
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
SummaryMixin, TaglineMixin, TitleMixin,
CollectionMixin, GenreMixin, LabelMixin,
WatchlistMixin
):
""" Represents a single Show (including all seasons and episodes). """ Represents a single Show (including all seasons and episodes).
Attributes: Attributes:
@ -407,7 +427,7 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
index (int): Plex index number for the show. index (int): Plex index number for the show.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects. labels (List<:class:`~plexapi.media.Label`>): List of label objects.
languageOverride (str): Setting that indicates if a languge is used to override metadata languageOverride (str): Setting that indicates if a language is used to override metadata
(eg. en-CA, None = Library default). (eg. en-CA, None = Library default).
leafCount (int): Number of items in the show view. leafCount (int): Number of items in the show view.
locations (List<str>): List of folder paths where the show is found on disk. locations (List<str>): List of folder paths where the show is found on disk.
@ -483,11 +503,6 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
""" 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 hubs(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
data = self._server.query(self._details_key)
return self.findItems(data, library.Hub, rtag='Related')
def onDeck(self): def onDeck(self):
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`. """ Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
If show is unwatched, return will likely be the first episode. If show is unwatched, return will likely be the first episode.
@ -574,7 +589,13 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
@utils.registerPlexObject @utils.registerPlexObject
class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin): class Season(
Video,
ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin,
SummaryMixin, TitleMixin,
CollectionMixin, LabelMixin
):
""" Represents a single Show Season (including all episodes). """ Represents a single Show Season (including all episodes).
Attributes: Attributes:
@ -584,6 +605,7 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
index (int): Season number. index (int): Season number.
key (str): API URL (/library/metadata/<ratingkey>). key (str): API URL (/library/metadata/<ratingkey>).
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
leafCount (int): Number of items in the season view. leafCount (int): Number of items in the season view.
parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6). parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
parentIndex (int): Plex index number for the show. parentIndex (int): Plex index number for the show.
@ -607,6 +629,7 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
self.guids = self.findItems(data, media.Guid) self.guids = self.findItems(data, media.Guid)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.key = self.key.replace('/children', '') # FIX_BUG_50 self.key = self.key.replace('/children', '') # FIX_BUG_50
self.labels = self.findItems(data, media.Label)
self.leafCount = utils.cast(int, data.attrib.get('leafCount')) self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
self.parentGuid = data.attrib.get('parentGuid') self.parentGuid = data.attrib.get('parentGuid')
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex')) self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
@ -709,8 +732,13 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
@utils.registerPlexObject @utils.registerPlexObject
class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin, class Episode(
CollectionMixin, DirectorMixin, WriterMixin): Video, Playable,
ExtrasMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin,
ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin,
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
):
""" Represents a single Shows Episode. """ Represents a single Shows Episode.
Attributes: Attributes:
@ -733,6 +761,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
grandparentTitle (str): Name of the show for the episode. grandparentTitle (str): Name of the show for the episode.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
index (int): Episode number. index (int): Episode number.
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects. markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
media (List<:class:`~plexapi.media.Media`>): List of media objects. media (List<:class:`~plexapi.media.Media`>): List of media objects.
originallyAvailableAt (datetime): Datetime the episode was released. originallyAvailableAt (datetime): Datetime the episode was released.
@ -777,6 +806,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
self.grandparentTitle = data.attrib.get('grandparentTitle') self.grandparentTitle = data.attrib.get('grandparentTitle')
self.guids = self.findItems(data, media.Guid) self.guids = self.findItems(data, media.Guid)
self.index = utils.cast(int, data.attrib.get('index')) self.index = utils.cast(int, data.attrib.get('index'))
self.labels = self.findItems(data, media.Label)
self.markers = self.findItems(data, media.Marker) self.markers = self.findItems(data, media.Marker)
self.media = self.findItems(data, media.Media) self.media = self.findItems(data, media.Media)
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d') self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
@ -879,7 +909,10 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
@utils.registerPlexObject @utils.registerPlexObject
class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin): class Clip(
Video, Playable,
ArtUrlMixin, PosterUrlMixin
):
""" Represents a single Clip. """ Represents a single Clip.
Attributes: Attributes:

View file

@ -27,7 +27,7 @@ MarkupSafe==2.1.1
musicbrainzngs==0.7.1 musicbrainzngs==0.7.1
packaging==21.3 packaging==21.3
paho-mqtt==1.6.1 paho-mqtt==1.6.1
plexapi==4.9.2 plexapi==4.11.0
portend==3.1.0 portend==3.1.0
profilehooks==1.12.0 profilehooks==1.12.0
PyJWT==2.4.0 PyJWT==2.4.0