mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11:15 -07:00
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:
parent
f1b95f5837
commit
399fd6ff91
22 changed files with 1421 additions and 589 deletions
|
@ -314,7 +314,7 @@ class SuperWeirdWordPlugin(MessDetectorPlugin):
|
|||
self._buffer = ""
|
||||
self._buffer_accent_count = 0
|
||||
elif (
|
||||
character not in {"<", ">", "-", "="}
|
||||
character not in {"<", ">", "-", "=", "~", "|", "_"}
|
||||
and character.isdigit() is False
|
||||
and is_symbol(character)
|
||||
):
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
Expose version
|
||||
"""
|
||||
|
||||
__version__ = "2.0.11"
|
||||
__version__ = "2.0.12"
|
||||
VERSION = __version__.split(".")
|
||||
|
|
|
@ -21,9 +21,9 @@ TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
|||
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
|
||||
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
||||
|
||||
# Plex Header Configuation
|
||||
# Plex Header Configuration
|
||||
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_PRODUCT = CONFIG.get('header.product', PROJECT)
|
||||
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
import os
|
||||
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.exceptions import BadRequest
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin
|
||||
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
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
|
||||
|
||||
|
||||
|
@ -38,7 +42,7 @@ class Audio(PlexPartialObject):
|
|||
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).
|
||||
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).
|
||||
viewCount (int): Count of times the item was played.
|
||||
"""
|
||||
|
@ -125,8 +129,13 @@ class Audio(PlexPartialObject):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
||||
class Artist(
|
||||
Audio,
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||
):
|
||||
""" Represents a single Artist.
|
||||
|
||||
Attributes:
|
||||
|
@ -138,9 +147,11 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
|||
countries (List<:class:`~plexapi.media.Country`>): List country objects.
|
||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
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.
|
||||
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
|
||||
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
|
||||
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'artist'
|
||||
|
@ -153,26 +164,23 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
|||
self.countries = self.findItems(data, media.Country)
|
||||
self.genres = self.findItems(data, media.Genre)
|
||||
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.similar = self.findItems(data, media.Similar)
|
||||
self.styles = self.findItems(data, media.Style)
|
||||
self.theme = data.attrib.get('theme')
|
||||
|
||||
def __iter__(self):
|
||||
for album in self.albums():
|
||||
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):
|
||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||
|
||||
Parameters:
|
||||
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)
|
||||
|
||||
def albums(self, **kwargs):
|
||||
|
@ -230,8 +238,13 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
|
||||
class Album(
|
||||
Audio,
|
||||
UnmatchMatchMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
|
||||
):
|
||||
""" Represents a single Album.
|
||||
|
||||
Attributes:
|
||||
|
@ -248,6 +261,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
|||
parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
||||
parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>).
|
||||
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>).
|
||||
parentTitle (str): Name of the album artist.
|
||||
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.parentKey = data.attrib.get('parentKey')
|
||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentTheme = data.attrib.get('parentTheme')
|
||||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
elif track is not None:
|
||||
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track)
|
||||
elif track is not None or isinstance(title, int):
|
||||
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')
|
||||
|
||||
def tracks(self, **kwargs):
|
||||
|
@ -337,8 +356,13 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
|
||||
CollectionMixin, MoodMixin):
|
||||
class Track(
|
||||
Audio, Playable,
|
||||
ExtrasMixin, RatingMixin,
|
||||
ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin,
|
||||
TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin,
|
||||
CollectionMixin, LabelMixin, MoodMixin
|
||||
):
|
||||
""" Represents a single Track.
|
||||
|
||||
Attributes:
|
||||
|
@ -351,19 +375,23 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
|
|||
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
||||
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
|
||||
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
|
||||
(/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
|
||||
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.
|
||||
originalTitle (str): The artist for the track.
|
||||
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>).
|
||||
parentRatingKey (int): Unique key identifying the album.
|
||||
parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||
parentTitle (str): Name of the album for the track.
|
||||
primaryExtraKey (str) API URL for the primary extra for the track.
|
||||
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.
|
||||
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.grandparentKey = data.attrib.get('grandparentKey')
|
||||
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
||||
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originalTitle = data.attrib.get('originalTitle')
|
||||
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.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||
self.parentThumb = data.attrib.get('parentThumb')
|
||||
self.parentTitle = data.attrib.get('parentTitle')
|
||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
||||
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.year = utils.cast(int, data.attrib.get('year'))
|
||||
|
||||
|
|
|
@ -53,7 +53,9 @@ class PlexObject(object):
|
|||
if data is not None:
|
||||
self._loadData(data)
|
||||
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):
|
||||
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 == []:
|
||||
value = getattr(self, attr, [])
|
||||
|
||||
autoReload = self.__dict__.get('_autoReload')
|
||||
# Don't overwrite an attr with None unless it's a private variable or not auto reload
|
||||
if value is not None or attr.startswith('_') or attr not in self.__dict__ or not autoReload:
|
||||
overwriteNone = self.__dict__.get('_overwriteNone')
|
||||
# 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 overwriteNone:
|
||||
self.__dict__[attr] = value
|
||||
|
||||
def _clean(self, value):
|
||||
|
@ -169,9 +171,14 @@ class PlexObject(object):
|
|||
raise BadRequest('ekey was not provided')
|
||||
if isinstance(ekey, int):
|
||||
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):
|
||||
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'
|
||||
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
|
||||
the Python objects are built to help keep things speedy. For example, passing in
|
||||
``viewCount=0`` will only return matching items where the view count is ``0``.
|
||||
Note that case matters when specifying attributes. Attributes futher down in the XML
|
||||
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__``.
|
||||
|
||||
Examples:
|
||||
|
@ -228,12 +235,12 @@ class PlexObject(object):
|
|||
* ``__exists`` (*bool*): Value is or is not present in the attrs.
|
||||
* ``__gt``: Value is greater than specified arg.
|
||||
* ``__gte``: Value is greater than or equal to specified arg.
|
||||
* ``__icontains``: Case insensative value contains specified arg.
|
||||
* ``__iendswith``: Case insensative value ends with specified arg.
|
||||
* ``__iexact``: Case insensative value matches specified arg.
|
||||
* ``__icontains``: Case insensitive value contains specified arg.
|
||||
* ``__iendswith``: Case insensitive value ends with specified arg.
|
||||
* ``__iexact``: Case insensitive value matches specified arg.
|
||||
* ``__in``: Value is in a specified list or tuple.
|
||||
* ``__iregex``: Case insensative value matches the specified regular expression.
|
||||
* ``__istartswith``: Case insensative value starts with specified arg.
|
||||
* ``__iregex``: Case insensitive value matches the specified regular expression.
|
||||
* ``__istartswith``: Case insensitive value starts with specified arg.
|
||||
* ``__lt``: Value is less than specified arg.
|
||||
* ``__lte``: Value is less than or equal to specified arg.
|
||||
* ``__regex``: Value matches the specified regular expression.
|
||||
|
@ -276,9 +283,9 @@ class PlexObject(object):
|
|||
kwargs['etag'] = cls.TAG
|
||||
if cls and cls.TYPE and 'type' not in kwargs:
|
||||
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:
|
||||
data = next(data.iter(rtag), [])
|
||||
data = next(utils.iterXMLBFS(data, rtag), [])
|
||||
# loop through all data elements to find matches
|
||||
items = []
|
||||
for elem in data:
|
||||
|
@ -298,9 +305,9 @@ class PlexObject(object):
|
|||
def listAttrs(self, data, attr, rtag=None, **kwargs):
|
||||
""" Return a list of values from matching attribute. """
|
||||
results = []
|
||||
# rtag to iter on a specific root tag
|
||||
# rtag to iter on a specific root tag using breadth-first search
|
||||
if rtag:
|
||||
data = next(data.iter(rtag), [])
|
||||
data = next(utils.iterXMLBFS(data, rtag), [])
|
||||
for elem in data:
|
||||
kwargs['%s__exists' % attr] = True
|
||||
if self._checkAttrs(elem, **kwargs):
|
||||
|
@ -340,7 +347,7 @@ class PlexObject(object):
|
|||
"""
|
||||
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. """
|
||||
details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_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.')
|
||||
self._initpath = key
|
||||
data = self._server.query(key)
|
||||
self._autoReload = _autoReload
|
||||
self._overwriteNone = _overwriteNone
|
||||
self._loadData(data[0])
|
||||
self._autoReload = False
|
||||
self._overwriteNone = True
|
||||
return self
|
||||
|
||||
def _checkAttrs(self, elem, **kwargs):
|
||||
|
@ -392,7 +399,7 @@ class PlexObject(object):
|
|||
# check were looking for the tag
|
||||
if attr.lower() == 'etag':
|
||||
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():
|
||||
if attr.lower() == _attr.lower():
|
||||
return [value]
|
||||
|
@ -414,6 +421,10 @@ class PlexObject(object):
|
|||
def _loadData(self, data):
|
||||
raise NotImplementedError('Abstract method not implemented.')
|
||||
|
||||
@property
|
||||
def _searchType(self):
|
||||
return self.TYPE
|
||||
|
||||
|
||||
class PlexPartialObject(PlexObject):
|
||||
""" Not all objects in the Plex listings return the complete list of elements
|
||||
|
@ -455,20 +466,21 @@ class PlexPartialObject(PlexObject):
|
|||
def __getattribute__(self, attr):
|
||||
# Dragons inside.. :-/
|
||||
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_OVERWRITE_SESSION_KEYS: return value
|
||||
if attr in USER_DONT_RELOAD_FOR_KEYS: return value
|
||||
if attr.startswith('_'): return value
|
||||
if value not in (None, []): return value
|
||||
if self.isFullObject(): return value
|
||||
if self._autoReload is False: return value
|
||||
# Log the reload.
|
||||
clsname = self.__class__.__name__
|
||||
title = self.__dict__.get('title', self.__dict__.get('name'))
|
||||
objname = "%s '%s'" % (clsname, title) if title else clsname
|
||||
log.debug("Reloading %s for attr '%s'", objname, attr)
|
||||
# Reload and return the value
|
||||
self._reload(_autoReload=True)
|
||||
self._reload()
|
||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||
|
||||
def analyze(self):
|
||||
|
@ -507,44 +519,79 @@ class PlexPartialObject(PlexObject):
|
|||
|
||||
def _edit(self, **kwargs):
|
||||
""" Actually edit an object. """
|
||||
if isinstance(self._edits, dict):
|
||||
self._edits.update(kwargs)
|
||||
return self
|
||||
|
||||
if 'id' not in kwargs:
|
||||
kwargs['id'] = self.ratingKey
|
||||
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,
|
||||
urlencode(kwargs))
|
||||
part = '/library/sections/%s/all%s' % (self.librarySectionID,
|
||||
utils.joinArgs(kwargs))
|
||||
self._server.query(part, method=self._server._session.put)
|
||||
return self
|
||||
|
||||
def edit(self, **kwargs):
|
||||
""" 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:
|
||||
kwargs (dict): Dict of settings to edit.
|
||||
|
||||
Example:
|
||||
{'type': 1,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
edits = {
|
||||
'type': 1,
|
||||
'id': movie.ratingKey,
|
||||
'collection[0].tag.tag': 'Super',
|
||||
'collection.locked': 0}
|
||||
"""
|
||||
self._edit(**kwargs)
|
||||
'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)
|
||||
|
||||
def _edit_tags(self, tag, items, locked=True, remove=False):
|
||||
""" Helper to edit tags.
|
||||
|
||||
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):
|
||||
items = [items]
|
||||
value = getattr(self, utils.tag_plural(tag))
|
||||
existing_tags = [t.tag for t in value if t and remove is False]
|
||||
tag_edits = utils.tag_helper(tag, existing_tags + items, locked, remove)
|
||||
self.edit(**tag_edits)
|
||||
return self._edit(**kwargs)
|
||||
|
||||
def batchEdits(self):
|
||||
""" Enable batch editing mode to save API calls.
|
||||
Must call :func:`~plexapi.base.PlexPartialObject.saveEdits` at the end to save all the 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):
|
||||
""" Refreshing a Library or individual item causes the metadata for the item to be
|
||||
|
@ -746,7 +793,7 @@ class Playable(object):
|
|||
key = '/:/progress?key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s' % (self.ratingKey,
|
||||
time, state)
|
||||
self._server.query(key)
|
||||
self._reload(_autoReload=True)
|
||||
self._reload(_overwriteNone=False)
|
||||
|
||||
def updateTimeline(self, time, state='stopped', duration=None):
|
||||
""" 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 %= (self.ratingKey, self.key, time, state, durationStr)
|
||||
self._server.query(key)
|
||||
self._reload(_autoReload=True)
|
||||
self._reload(_overwriteNone=False)
|
||||
|
||||
|
||||
class MediaContainer(PlexObject):
|
||||
|
|
|
@ -23,10 +23,10 @@ class PlexClient(PlexObject):
|
|||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional).
|
||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||
initpath (str): Path used to generate data.
|
||||
baseurl (str): HTTP URL to connect dirrectly to this client.
|
||||
baseurl (str): HTTP URL to connect directly to this client.
|
||||
identifier (str): The resource/machine identifier for the desired client.
|
||||
May be necessary when connecting to a specific proxied client (optional).
|
||||
token (str): X-Plex-Token used for authenication (optional).
|
||||
token (str): X-Plex-Token used for authentication (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).
|
||||
|
||||
|
@ -48,7 +48,7 @@ class PlexClient(PlexObject):
|
|||
session (:class:`~requests.Session`): Session object used for connection.
|
||||
state (str): Unknown
|
||||
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
|
||||
version (str): Device version (4.6.1, etc).
|
||||
_baseurl (str): HTTP address of the client.
|
||||
|
@ -131,7 +131,7 @@ class PlexClient(PlexObject):
|
|||
self.platformVersion = data.attrib.get('platformVersion')
|
||||
self.title = data.attrib.get('title') or data.attrib.get('name')
|
||||
# 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.
|
||||
# Add this in next breaking release.
|
||||
# if self._initpath == 'status/sessions':
|
||||
|
@ -210,8 +210,8 @@ class PlexClient(PlexObject):
|
|||
controller = command.split('/')[0]
|
||||
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||
if controller not in self.protocolCapabilities:
|
||||
log.debug('Client %s doesnt support %s controller.'
|
||||
'What your trying might not work' % (self.title, controller))
|
||||
log.debug("Client %s doesn't support %s controller."
|
||||
"What your trying might not work" % (self.title, controller))
|
||||
|
||||
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||
query = self._server.query if proxy else self.query
|
||||
|
@ -318,21 +318,21 @@ class PlexClient(PlexObject):
|
|||
Parameters:
|
||||
media (:class:`~plexapi.media.Media`): Media object to navigate to.
|
||||
**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(':')
|
||||
self.sendCommand('mirror/details', **dict({
|
||||
'machineIdentifier': self._server.machineIdentifier,
|
||||
command = {
|
||||
'machineIdentifier': media._server.machineIdentifier,
|
||||
'address': server_url[1].strip('/'),
|
||||
'port': server_url[-1],
|
||||
'key': media.key,
|
||||
'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
|
||||
|
@ -488,12 +488,7 @@ class PlexClient(PlexObject):
|
|||
representing the beginning (default 0).
|
||||
**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
|
||||
|
||||
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_port = server_url[-1].strip('/')
|
||||
|
||||
|
@ -509,19 +504,24 @@ class PlexClient(PlexObject):
|
|||
if mediatype == "audio":
|
||||
mediatype = "music"
|
||||
|
||||
playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media)
|
||||
self.sendCommand('playback/playMedia', **dict({
|
||||
playqueue = media if isinstance(media, PlayQueue) else media._server.createPlayQueue(media)
|
||||
command = {
|
||||
'providerIdentifier': 'com.plexapp.plugins.library',
|
||||
'machineIdentifier': self._server.machineIdentifier,
|
||||
'machineIdentifier': media._server.machineIdentifier,
|
||||
'protocol': server_url[0],
|
||||
'address': server_url[1].strip('/'),
|
||||
'port': server_port,
|
||||
'offset': offset,
|
||||
'key': media.key or playqueue.selectedItem.key,
|
||||
'token': media._server.createToken(),
|
||||
'type': mediatype,
|
||||
'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):
|
||||
""" Set multiple playback parameters at once.
|
||||
|
|
|
@ -5,14 +5,24 @@ from plexapi import media, utils
|
|||
from plexapi.base import PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
from plexapi.library import LibrarySection
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin
|
||||
from plexapi.mixins import LabelMixin, SmartFilterMixin
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeMixin,
|
||||
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
LabelMixin
|
||||
)
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import deprecated
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Attributes:
|
||||
|
@ -22,9 +32,10 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
||||
artBlurHash (str): BlurHash string for artwork image.
|
||||
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.
|
||||
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.
|
||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||
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.
|
||||
subtype (str): Media type of the items in the collection (movie, show, artist, or album).
|
||||
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>).
|
||||
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||
title (str): Name of the collection.
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
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).
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
|
@ -60,6 +72,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
self.art = data.attrib.get('art')
|
||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||
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.collectionPublished = utils.cast(bool, data.attrib.get('collectionPublished', '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.subtype = data.attrib.get('subtype')
|
||||
self.summary = data.attrib.get('summary')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.thumb = data.attrib.get('thumb')
|
||||
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||
self.title = data.attrib.get('title')
|
||||
|
@ -184,6 +198,32 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
""" Alias to :func:`~plexapi.library.Collection.item`. """
|
||||
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):
|
||||
""" Update the collection mode advanced setting.
|
||||
|
||||
|
@ -216,7 +256,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
|
||||
Parameters:
|
||||
sort (str): One of the following values:
|
||||
"realease" (Order Collection by realease dates),
|
||||
"release" (Order Collection by release dates),
|
||||
"alpha" (Order Collection alphabetically),
|
||||
"custom" (Custom collection order)
|
||||
|
||||
|
@ -226,6 +266,9 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
|
||||
collection.updateSort(mode="alpha")
|
||||
"""
|
||||
if self.smart:
|
||||
raise BadRequest('Cannot change collection order for a smart collection.')
|
||||
|
||||
sort_dict = {
|
||||
'release': 0,
|
||||
'alpha': 1,
|
||||
|
@ -340,6 +383,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
}))
|
||||
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):
|
||||
""" Edit the collection.
|
||||
|
||||
|
@ -364,7 +408,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
|||
args['summary.locked'] = 1
|
||||
|
||||
args.update(kwargs)
|
||||
super(Collection, self).edit(**args)
|
||||
self._edit(**args)
|
||||
|
||||
def delete(self):
|
||||
""" Delete the collection. """
|
||||
|
|
|
@ -62,4 +62,5 @@ def reset_base_headers():
|
|||
'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME,
|
||||
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
|
||||
'X-Plex-Sync-Version': '2',
|
||||
'X-Plex-Features': 'external-media',
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
# Library version
|
||||
MAJOR_VERSION = 4
|
||||
MINOR_VERSION = 9
|
||||
PATCH_VERSION = 2
|
||||
MINOR_VERSION = 11
|
||||
PATCH_VERSION = 0
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
|
|
|
@ -15,7 +15,7 @@ import struct
|
|||
class GDM:
|
||||
"""Base class to discover GDM services.
|
||||
|
||||
Atrributes:
|
||||
Attributes:
|
||||
entries (List<dict>): List of server and/or client data discovered.
|
||||
"""
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class Library(PlexObject):
|
|||
if elem.attrib.get('type') == cls.TYPE:
|
||||
section = cls(self._server, elem, key)
|
||||
self._sectionsByID[section.key] = section
|
||||
self._sectionsByTitle[section.title.lower()] = section
|
||||
self._sectionsByTitle[section.title.lower().strip()] = section
|
||||
|
||||
def sections(self):
|
||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||
|
@ -59,10 +59,11 @@ class Library(PlexObject):
|
|||
Parameters:
|
||||
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()
|
||||
try:
|
||||
return self._sectionsByTitle[title.lower()]
|
||||
return self._sectionsByTitle[normalized_title]
|
||||
except KeyError:
|
||||
raise NotFound('Invalid library section: %s' % title) from None
|
||||
|
||||
|
@ -125,7 +126,7 @@ class Library(PlexObject):
|
|||
def search(self, title=None, libtype=None, **kwargs):
|
||||
""" 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
|
||||
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
|
||||
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.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||
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._fieldTypes = None
|
||||
self._totalViewSize = None
|
||||
|
@ -599,12 +600,13 @@ class LibrarySection(PlexObject):
|
|||
return self.fetchItem(key, title__iexact=title)
|
||||
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
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:
|
||||
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
|
||||
|
@ -613,18 +615,29 @@ class LibrarySection(PlexObject):
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
result1 = library.getGuid('imdb://tt0944947')
|
||||
result2 = library.getGuid('tmdb://1399')
|
||||
result3 = library.getGuid('tvdb://121361')
|
||||
result1 = library.getGuid('plex://show/5d9c086c46115600200aa2fe')
|
||||
result2 = library.getGuid('imdb://tt0944947')
|
||||
result3 = library.getGuid('tmdb://1399')
|
||||
result4 = library.getGuid('tvdb://121361')
|
||||
|
||||
# Alternatively, create your own guid lookup dictionary for faster performance
|
||||
guidLookup = {guid.id: item for item in library.all() for guid in item.guids}
|
||||
result1 = guidLookup['imdb://tt0944947']
|
||||
result2 = guidLookup['tmdb://1399']
|
||||
result3 = guidLookup['tvdb://121361']
|
||||
guidLookup = {}
|
||||
for item in library.all():
|
||||
guidLookup[item.guid] = item
|
||||
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:
|
||||
if guid.startswith('plex://'):
|
||||
result = self.search(guid=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]
|
||||
|
@ -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.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.
|
||||
|
||||
* **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``,
|
||||
``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
|
||||
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.
|
||||
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
|
||||
|
@ -2236,16 +2249,61 @@ class FilteringType(PlexObject):
|
|||
self.title = data.attrib.get('title')
|
||||
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
|
||||
self.filters += self._manualFilters()
|
||||
self.sorts += self._manualSorts()
|
||||
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):
|
||||
""" Manually add additional sorts which are available
|
||||
but not exposed on the Plex server.
|
||||
"""
|
||||
# Sorts: key, dir, title
|
||||
# Sorts: (key, dir, title)
|
||||
additionalSorts = [
|
||||
('guid', 'asc', 'Guid'),
|
||||
('id', 'asc', 'Rating Key'),
|
||||
|
@ -2275,8 +2333,10 @@ class FilteringType(PlexObject):
|
|||
|
||||
manualSorts = []
|
||||
for sortField, sortDir, sortTitle in additionalSorts:
|
||||
sortXML = ('<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />'
|
||||
% (sortDir, sortField, sortField, sortTitle))
|
||||
sortXML = (
|
||||
'<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />'
|
||||
% (sortDir, sortField, sortField, sortTitle)
|
||||
)
|
||||
manualSorts.append(self._manuallyLoadXML(sortXML, FilteringSort))
|
||||
|
||||
return manualSorts
|
||||
|
@ -2285,7 +2345,7 @@ class FilteringType(PlexObject):
|
|||
""" Manually add additional fields which are available
|
||||
but not exposed on the Plex server.
|
||||
"""
|
||||
# Fields: key, type, title
|
||||
# Fields: (key, type, title)
|
||||
additionalFields = [
|
||||
('guid', 'string', 'Guid'),
|
||||
('id', 'integer', 'Rating Key'),
|
||||
|
@ -2311,31 +2371,41 @@ class FilteringType(PlexObject):
|
|||
additionalFields.extend([
|
||||
('addedAt', 'date', 'Date Season Added'),
|
||||
('unviewedLeafCount', 'integer', 'Episode Unplayed Count'),
|
||||
('year', 'integer', 'Season Year')
|
||||
('year', 'integer', 'Season Year'),
|
||||
('label', 'tag', 'Label')
|
||||
])
|
||||
elif self.type == 'episode':
|
||||
additionalFields.extend([
|
||||
('audienceRating', 'integer', 'Audience Rating'),
|
||||
('duration', 'integer', 'Duration'),
|
||||
('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':
|
||||
additionalFields.extend([
|
||||
('duration', 'integer', 'Duration'),
|
||||
('viewOffset', 'integer', 'View Offset')
|
||||
('viewOffset', 'integer', 'View Offset'),
|
||||
('label', 'tag', 'Label')
|
||||
])
|
||||
elif self.type == 'collection':
|
||||
additionalFields.extend([
|
||||
('addedAt', 'date', 'Date Added')
|
||||
('addedAt', 'date', 'Date Added'),
|
||||
('label', 'tag', 'Label')
|
||||
])
|
||||
|
||||
prefix = '' if self.type == 'movie' else self.type + '.'
|
||||
|
||||
manualFields = []
|
||||
for field, fieldType, fieldTitle in additionalFields:
|
||||
fieldXML = ('<Field key="%s%s" title="%s" type="%s"/>'
|
||||
% (prefix, field, fieldTitle, fieldType))
|
||||
fieldXML = (
|
||||
'<Field key="%s%s" title="%s" type="%s"/>'
|
||||
% (prefix, field, fieldTitle, fieldType)
|
||||
)
|
||||
manualFields.append(self._manuallyLoadXML(fieldXML, FilteringField))
|
||||
|
||||
return manualFields
|
||||
|
|
|
@ -39,7 +39,7 @@ class Media(PlexObject):
|
|||
|
||||
<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.
|
||||
* iso (int): The iso used to take the photo.
|
||||
* lens (str): The lens used to take the photo.
|
||||
|
@ -93,7 +93,7 @@ class Media(PlexObject):
|
|||
try:
|
||||
return self._server.query(part, method=self._server._session.delete)
|
||||
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)
|
||||
raise
|
||||
|
||||
|
@ -224,7 +224,7 @@ class MediaPartStream(PlexObject):
|
|||
id (int): The unique ID for this stream on the server.
|
||||
index (int): The index of the stream.
|
||||
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.
|
||||
selected (bool): True if this stream is selected.
|
||||
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.
|
||||
frameRate (float): The frame rate of the video stream (ex: 23.976).
|
||||
frameRateMode (str): The frame rate mode of the video stream.
|
||||
hasScallingMatrix (bool): True if video stream has a scaling matrix.
|
||||
height (int): The hight of the video stream in pixels (ex: 1080).
|
||||
hasScalingMatrix (bool): True if video stream has a scaling matrix.
|
||||
height (int): The height of the video stream in pixels (ex: 1080).
|
||||
level (int): The codec encoding level of the video stream (ex: 41).
|
||||
profile (str): The profile of the video stream (ex: asp).
|
||||
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.frameRate = utils.cast(float, data.attrib.get('frameRate'))
|
||||
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.level = utils.cast(int, data.attrib.get('level'))
|
||||
self.profile = data.attrib.get('profile')
|
||||
|
@ -400,7 +400,7 @@ class SubtitleStream(MediaPartStream):
|
|||
container (str): The container of the subtitle stream.
|
||||
forced (bool): True if this is a forced subtitle.
|
||||
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.
|
||||
"""
|
||||
TAG = 'Stream'
|
||||
|
@ -468,7 +468,7 @@ class TranscodeSession(PlexObject):
|
|||
audioDecision (str): The transcode decision for the audio stream.
|
||||
complete (bool): True if the transcode is complete.
|
||||
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.
|
||||
height (int): The height of the transcoded media in pixels.
|
||||
key (str): API URL (ex: /transcode/sessions/<id>).
|
||||
|
@ -917,19 +917,17 @@ class Review(PlexObject):
|
|||
self.text = data.attrib.get('text')
|
||||
|
||||
|
||||
class BaseImage(PlexObject):
|
||||
""" Base class for all Art, Banner, and Poster objects.
|
||||
class BaseResource(PlexObject):
|
||||
""" Base class for all Art, Banner, Poster, and Theme objects.
|
||||
|
||||
Attributes:
|
||||
TAG (str): 'Photo'
|
||||
TAG (str): 'Photo' or 'Track'
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
provider (str): The source of the poster or art.
|
||||
ratingKey (str): Unique key identifying the poster or art.
|
||||
selected (bool): True if the poster or art is currently selected.
|
||||
thumb (str): The URL to retrieve the poster or art thumbnail.
|
||||
provider (str): The source of the art or poster, None for Theme objects.
|
||||
ratingKey (str): Unique key identifying the resource.
|
||||
selected (bool): True if the resource is currently selected.
|
||||
thumb (str): The URL to retrieve the resource thumbnail.
|
||||
"""
|
||||
TAG = 'Photo'
|
||||
|
||||
def _loadData(self, data):
|
||||
self._data = data
|
||||
self.key = data.attrib.get('key')
|
||||
|
@ -947,16 +945,24 @@ class BaseImage(PlexObject):
|
|||
pass
|
||||
|
||||
|
||||
class Art(BaseImage):
|
||||
class Art(BaseResource):
|
||||
""" Represents a single Art object. """
|
||||
TAG = 'Photo'
|
||||
|
||||
|
||||
class Banner(BaseImage):
|
||||
class Banner(BaseResource):
|
||||
""" Represents a single Banner object. """
|
||||
TAG = 'Photo'
|
||||
|
||||
|
||||
class Poster(BaseImage):
|
||||
class Poster(BaseResource):
|
||||
""" Represents a single Poster object. """
|
||||
TAG = 'Photo'
|
||||
|
||||
|
||||
class Theme(BaseResource):
|
||||
""" Represents a single Theme object. """
|
||||
TAG = 'Track'
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
@ -1106,3 +1112,41 @@ class AgentMediaType(Agent):
|
|||
@deprecated('use "languageCodes" instead')
|
||||
def languageCode(self):
|
||||
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
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import copy
|
||||
import html
|
||||
import threading
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
@ -52,7 +53,7 @@ class MyPlexAccount(PlexObject):
|
|||
roles: (List<str>) Lit of account roles. Plexpass membership listed here.
|
||||
scrobbleTypes (str): 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.
|
||||
subscriptionPlan (str): Name of subscription plan.
|
||||
subscriptionStatus (str): String representation of `subscriptionActive`.
|
||||
|
@ -72,14 +73,12 @@ class MyPlexAccount(PlexObject):
|
|||
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
|
||||
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
|
||||
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
|
||||
# Hub sections
|
||||
VOD = 'https://vod.provider.plex.tv/' # get
|
||||
WEBSHOWS = 'https://webshows.provider.plex.tv/' # get
|
||||
NEWS = 'https://news.provider.plex.tv/' # get
|
||||
PODCASTS = 'https://podcasts.provider.plex.tv/' # get
|
||||
MUSIC = 'https://music.provider.plex.tv/' # get
|
||||
VOD = 'https://vod.provider.plex.tv' # get
|
||||
MUSIC = 'https://music.provider.plex.tv' # get
|
||||
METADATA = 'https://metadata.provider.plex.tv'
|
||||
# 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}
|
||||
key = 'https://plex.tv/users/account'
|
||||
|
@ -182,6 +181,8 @@ class MyPlexAccount(PlexObject):
|
|||
raise NotFound(message)
|
||||
else:
|
||||
raise BadRequest(message)
|
||||
if headers.get('Accept') == 'application/json':
|
||||
return response.json()
|
||||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
|
@ -228,7 +229,7 @@ class MyPlexAccount(PlexObject):
|
|||
of the user to be added.
|
||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||
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.
|
||||
allowSync (Bool): Set True to allow user to sync content.
|
||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||
|
@ -268,7 +269,7 @@ class MyPlexAccount(PlexObject):
|
|||
of the user to be added.
|
||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||
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.
|
||||
allowSync (Bool): Set True to allow user to sync content.
|
||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||
|
@ -317,7 +318,7 @@ class MyPlexAccount(PlexObject):
|
|||
of the user to be added.
|
||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||
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.
|
||||
allowSync (Bool): Set True to allow user to sync content.
|
||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||
|
@ -420,7 +421,7 @@ class MyPlexAccount(PlexObject):
|
|||
of the user to be updated.
|
||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||
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.
|
||||
removeSections (Bool): Set True to remove all shares. Supersedes sections.
|
||||
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. """
|
||||
values = []
|
||||
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)
|
||||
values.append('%s=%s' % (key, '%2C'.join(vals)))
|
||||
return '|'.join(values)
|
||||
|
@ -614,7 +615,7 @@ class MyPlexAccount(PlexObject):
|
|||
clientId (str): an identifier of a client to query SyncItems for.
|
||||
|
||||
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:
|
||||
clientId = client.clientIdentifier
|
||||
|
@ -635,14 +636,14 @@ class MyPlexAccount(PlexObject):
|
|||
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 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:
|
||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||
|
||||
Raises:
|
||||
: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`: When client with provided clientId wasn't found.
|
||||
:exc:`~plexapi.exceptions.BadRequest`: Provided client doesn't provides `sync-target`.
|
||||
"""
|
||||
if not client and not clientId:
|
||||
clientId = X_PLEX_IDENTIFIER
|
||||
|
@ -657,7 +658,7 @@ class MyPlexAccount(PlexObject):
|
|||
raise BadRequest('Unable to find client by clientId=%s', clientId)
|
||||
|
||||
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 = {
|
||||
'SyncItem[title]': sync_item.title,
|
||||
|
@ -698,6 +699,7 @@ class MyPlexAccount(PlexObject):
|
|||
|
||||
def history(self, maxresults=9999999, mindate=None):
|
||||
""" Get Play History for all library sections on all servers for the owner.
|
||||
|
||||
Parameters:
|
||||
maxresults (int): Only return the specified number of results (optional).
|
||||
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))
|
||||
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):
|
||||
""" Returns a list of VOD Hub items :class:`~plexapi.library.Hub`
|
||||
"""
|
||||
req = requests.get(self.VOD + 'hubs/', headers={'X-Plex-Token': self._token})
|
||||
elem = ElementTree.fromstring(req.text)
|
||||
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)
|
||||
data = self.query(f'{self.VOD}/hubs')
|
||||
return self.findItems(data)
|
||||
|
||||
def tidal(self):
|
||||
""" Returns a list of tidal Hub items :class:`~plexapi.library.Hub`
|
||||
"""
|
||||
req = requests.get(self.MUSIC + 'hubs/', headers={'X-Plex-Token': self._token})
|
||||
elem = ElementTree.fromstring(req.text)
|
||||
return self.findItems(elem)
|
||||
data = self.query(f'{self.MUSIC}/hubs')
|
||||
return self.findItems(data)
|
||||
|
||||
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}
|
||||
elem = self.query(url)
|
||||
return self.findItems(elem, cls=AccountOptOut, etag='optOut')
|
||||
params = {
|
||||
'includeCollections': 1,
|
||||
'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):
|
||||
""" Link a device to the account using a pin code.
|
||||
|
@ -790,7 +900,7 @@ class MyPlexUser(PlexObject):
|
|||
restricted (str): Unknown.
|
||||
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
|
||||
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.
|
||||
"""
|
||||
TAG = 'User'
|
||||
|
@ -1103,7 +1213,7 @@ class MyPlexResource(PlexObject):
|
|||
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
||||
"""
|
||||
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.
|
||||
cls = PlexServer if 'server' in self.provides else PlexClient
|
||||
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.
|
||||
|
||||
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:
|
||||
raise BadRequest('Requested syncList for device which do not provides sync-target')
|
||||
|
|
|
@ -5,11 +5,21 @@ from urllib.parse import quote_plus
|
|||
from plexapi import media, utils, video
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
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
|
||||
class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
||||
class Photoalbum(
|
||||
PlexPartialObject,
|
||||
RatingMixin,
|
||||
ArtMixin, PosterMixin,
|
||||
SortTitleMixin, SummaryMixin, TitleMixin
|
||||
):
|
||||
""" Represents a single Photoalbum (collection of photos).
|
||||
|
||||
Attributes:
|
||||
|
@ -33,11 +43,12 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
|||
title (str): Name of the photo album. (Trip to Disney World)
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
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).
|
||||
"""
|
||||
TAG = 'Directory'
|
||||
TYPE = 'photo'
|
||||
_searchType = 'photoalbum'
|
||||
|
||||
def _loadData(self, data):
|
||||
""" Load attribute values from Plex XML response. """
|
||||
|
@ -109,7 +120,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
|||
return self.episode(title)
|
||||
|
||||
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:
|
||||
savepath (str): Defaults to current working dir.
|
||||
|
@ -131,7 +142,13 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
|||
|
||||
|
||||
@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.
|
||||
|
||||
Attributes:
|
||||
|
@ -164,7 +181,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
|
|||
title (str): Name of the photo.
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
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).
|
||||
year (int): Year the photo was taken.
|
||||
"""
|
||||
|
@ -223,7 +240,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
|
|||
elif self.parentKey:
|
||||
return self._server.library.sectionByID(self.photoalbum().librarySectionID)
|
||||
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
|
||||
def locations(self):
|
||||
|
|
|
@ -6,13 +6,17 @@ from plexapi import media, utils
|
|||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||
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.utils import deprecated
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMixin):
|
||||
class Playlist(
|
||||
PlexPartialObject, Playable,
|
||||
SmartFilterMixin,
|
||||
ArtMixin, PosterMixin
|
||||
):
|
||||
""" Represents a single Playlist.
|
||||
|
||||
Attributes:
|
||||
|
@ -39,7 +43,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
|
|||
summary (str): Summary of the playlist.
|
||||
title (str): Name of the playlist.
|
||||
type (str): 'playlist'
|
||||
updatedAt (datatime): Datetime the playlist was updated.
|
||||
updatedAt (datetime): Datetime the playlist was updated.
|
||||
"""
|
||||
TAG = 'Playlist'
|
||||
TYPE = 'playlist'
|
||||
|
|
|
@ -314,7 +314,7 @@ class PlexServer(PlexObject):
|
|||
def myPlexAccount(self):
|
||||
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
||||
token to access this server. If you are not the owner of this PlexServer
|
||||
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:
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
|
@ -323,7 +323,7 @@ class PlexServer(PlexObject):
|
|||
|
||||
def _myPlexClientPorts(self):
|
||||
""" 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.
|
||||
https://github.com/pkkid/python-plexapi/issues/126
|
||||
"""
|
||||
|
@ -393,7 +393,6 @@ class PlexServer(PlexObject):
|
|||
"""
|
||||
if isinstance(path, Path):
|
||||
path = path.path
|
||||
path = os.path.normpath(path)
|
||||
paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)]
|
||||
return path in paths
|
||||
|
||||
|
@ -524,9 +523,34 @@ class PlexServer(PlexObject):
|
|||
filepath = utils.download(url, self._token, None, savepath, self._session, unpack=unpack)
|
||||
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')
|
||||
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):
|
||||
""" Returns a :class:`~plexapi.base.Release` object containing release info.
|
||||
|
@ -730,7 +754,7 @@ class PlexServer(PlexObject):
|
|||
return self.fetchItems('/transcode/sessions')
|
||||
|
||||
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
|
||||
as well as updates to currently running Transcode Sessions.
|
||||
|
||||
|
@ -738,7 +762,7 @@ class PlexServer(PlexObject):
|
|||
>> pip install websocket-client
|
||||
|
||||
Parameters:
|
||||
callback (func): Callback function to call on recieved messages.
|
||||
callback (func): Callback function to call on received messages.
|
||||
|
||||
Raises:
|
||||
:exc:`~plexapi.exception.Unsupported`: Websocket-client not installed.
|
||||
|
@ -1078,7 +1102,7 @@ class SystemDevice(PlexObject):
|
|||
Attributes:
|
||||
TAG (str): '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).
|
||||
key (str): API URL (/devices/<id>)
|
||||
name (str): The name of the device.
|
||||
|
@ -1102,10 +1126,10 @@ class StatisticsBandwidth(PlexObject):
|
|||
Attributes:
|
||||
TAG (str): 'StatisticsBandwidth'
|
||||
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 time span.
|
||||
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 time span for the bandwidth data.
|
||||
1: months, 2: weeks, 3: days, 4: hours, 6: seconds.
|
||||
|
||||
|
@ -1143,7 +1167,7 @@ class StatisticsResources(PlexObject):
|
|||
|
||||
Attributes:
|
||||
TAG (str): 'StatisticsResources'
|
||||
at (datatime): Datetime of the resource data.
|
||||
at (datetime): Datetime of the resource data.
|
||||
hostCpuUtilization (float): The system CPU usage %.
|
||||
hostMemoryUtilization (float): The Plex Media Server CPU usage %.
|
||||
processCpuUtilization (float): The system RAM usage %.
|
||||
|
@ -1166,3 +1190,28 @@ class StatisticsResources(PlexObject):
|
|||
self.__class__.__name__,
|
||||
self._clean(int(self.at.timestamp()))
|
||||
] 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')
|
||||
|
|
|
@ -71,7 +71,7 @@ class Settings(PlexObject):
|
|||
return self.groups().get(group, [])
|
||||
|
||||
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.
|
||||
"""
|
||||
params = {}
|
||||
|
@ -100,7 +100,7 @@ class Setting(PlexObject):
|
|||
hidden (bool): True if this is a hidden setting.
|
||||
advanced (bool): True if this is an advanced setting.
|
||||
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_str = lambda x: str(x).lower()
|
||||
|
@ -143,7 +143,7 @@ class Setting(PlexObject):
|
|||
return enumstr.split('|')
|
||||
|
||||
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`.
|
||||
"""
|
||||
# check a few things up front
|
||||
|
|
|
@ -14,7 +14,7 @@ class PlexSonosClient(PlexClient):
|
|||
speakers linked to your Plex account. It also requires remote access to
|
||||
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/
|
||||
|
||||
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.
|
||||
session (:class:`~requests.Session`): Session object used for connection.
|
||||
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.
|
||||
_commandId (int): Counter for commands sent to Plex API.
|
||||
_token (str): Token associated with linked Plex account.
|
||||
|
|
|
@ -9,6 +9,7 @@ import time
|
|||
import unicodedata
|
||||
import warnings
|
||||
import zipfile
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from getpass import getpass
|
||||
from threading import Event, Thread
|
||||
|
@ -55,7 +56,7 @@ class SecretsFilter(logging.Filter):
|
|||
|
||||
def registerPlexObject(cls):
|
||||
""" 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.
|
||||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
if value is not None:
|
||||
|
@ -114,7 +115,7 @@ def lowerFirst(s):
|
|||
|
||||
|
||||
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
|
||||
for each key in attrstr (split by by the delimiter) This function is heavily
|
||||
influenced by the lookups used in Django templates.
|
||||
|
@ -194,7 +195,7 @@ def threaded(callback, listargs):
|
|||
args += [results, len(results)]
|
||||
results.append(None)
|
||||
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()
|
||||
while not job_is_done_event.is_set():
|
||||
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.
|
||||
savepath (str): Defaults to current working dir.
|
||||
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.
|
||||
showstatus(bool): Display a progressbar.
|
||||
|
||||
|
@ -361,40 +362,6 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
|
|||
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
|
||||
""" Helper function tries to get a MyPlex Account instance by checking
|
||||
the the following locations for a username and password. This is
|
||||
|
@ -485,7 +452,7 @@ def getAgentIdentifier(section, agent):
|
|||
if agent in identifiers:
|
||||
return ag.identifier
|
||||
agents += identifiers
|
||||
raise NotFound('Couldnt find "%s" in agents list (%s)' %
|
||||
raise NotFound('Could not find "%s" in agents list (%s)' %
|
||||
(agent, ', '.join(agents)))
|
||||
|
||||
|
||||
|
@ -506,3 +473,15 @@ def deprecated(message, stacklevel=2):
|
|||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
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))
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
import os
|
||||
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.exceptions import BadRequest
|
||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
||||
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin
|
||||
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
|
||||
from plexapi.mixins import (
|
||||
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||
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):
|
||||
|
@ -35,7 +40,7 @@ class Video(PlexPartialObject):
|
|||
title (str): Name of the movie, show, season, episode, or clip.
|
||||
titleSort (str): Title to use when sorting (defaults to title).
|
||||
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).
|
||||
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
|
||||
|
||||
def markWatched(self):
|
||||
""" Mark the video as palyed. """
|
||||
""" Mark the video as played. """
|
||||
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||
self._server.query(key)
|
||||
|
||||
|
@ -107,6 +112,15 @@ class Video(PlexPartialObject):
|
|||
""" Returns str, default title for a new syncItem. """
|
||||
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):
|
||||
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
|
||||
streams = []
|
||||
|
@ -261,8 +275,15 @@ class Video(PlexPartialObject):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
|
||||
class Movie(
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
|
@ -280,7 +301,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
|
|||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid 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).
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
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.
|
||||
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?).
|
||||
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
|
||||
(-1 = Library default, 0 = No, 1 = Yes).
|
||||
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.studio = data.attrib.get('studio')
|
||||
self.tagline = data.attrib.get('tagline')
|
||||
self.theme = data.attrib.get('theme')
|
||||
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
|
||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||
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)
|
||||
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
|
||||
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||
CollectionMixin, GenreMixin, LabelMixin):
|
||||
class Show(
|
||||
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).
|
||||
|
||||
Attributes:
|
||||
|
@ -407,7 +427,7 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
|
|||
index (int): Plex index number for the show.
|
||||
key (str): API URL (/library/metadata/<ratingkey>).
|
||||
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).
|
||||
leafCount (int): Number of items in the show view.
|
||||
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. """
|
||||
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):
|
||||
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||
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
|
||||
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).
|
||||
|
||||
Attributes:
|
||||
|
@ -584,6 +605,7 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
|||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
index (int): Season number.
|
||||
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.
|
||||
parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
|
||||
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.index = utils.cast(int, data.attrib.get('index'))
|
||||
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.parentGuid = data.attrib.get('parentGuid')
|
||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||
|
@ -709,8 +732,13 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
||||
CollectionMixin, DirectorMixin, WriterMixin):
|
||||
class Episode(
|
||||
Video, Playable,
|
||||
ExtrasMixin, RatingMixin,
|
||||
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||
ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
|
||||
):
|
||||
""" Represents a single Shows Episode.
|
||||
|
||||
Attributes:
|
||||
|
@ -733,6 +761,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
|||
grandparentTitle (str): Name of the show for the episode.
|
||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||
index (int): Episode number.
|
||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
|
||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||
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.guids = self.findItems(data, media.Guid)
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.markers = self.findItems(data, media.Marker)
|
||||
self.media = self.findItems(data, media.Media)
|
||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||
|
@ -879,7 +909,10 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
|||
|
||||
|
||||
@utils.registerPlexObject
|
||||
class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
|
||||
class Clip(
|
||||
Video, Playable,
|
||||
ArtUrlMixin, PosterUrlMixin
|
||||
):
|
||||
""" Represents a single Clip.
|
||||
|
||||
Attributes:
|
||||
|
|
|
@ -27,7 +27,7 @@ MarkupSafe==2.1.1
|
|||
musicbrainzngs==0.7.1
|
||||
packaging==21.3
|
||||
paho-mqtt==1.6.1
|
||||
plexapi==4.9.2
|
||||
plexapi==4.11.0
|
||||
portend==3.1.0
|
||||
profilehooks==1.12.0
|
||||
PyJWT==2.4.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue