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 = ""
|
||||||
self._buffer_accent_count = 0
|
self._buffer_accent_count = 0
|
||||||
elif (
|
elif (
|
||||||
character not in {"<", ">", "-", "="}
|
character not in {"<", ">", "-", "=", "~", "|", "_"}
|
||||||
and character.isdigit() is False
|
and character.isdigit() is False
|
||||||
and is_symbol(character)
|
and is_symbol(character)
|
||||||
):
|
):
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
Expose version
|
Expose version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "2.0.11"
|
__version__ = "2.0.12"
|
||||||
VERSION = __version__.split(".")
|
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_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
|
||||||
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
||||||
|
|
||||||
# Plex Header Configuation
|
# Plex Header Configuration
|
||||||
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
|
X_PLEX_PROVIDES = CONFIG.get('header.provides', 'controller')
|
||||||
X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platorm', uname()[0]))
|
X_PLEX_PLATFORM = CONFIG.get('header.platform', CONFIG.get('header.platform', uname()[0]))
|
||||||
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
|
X_PLEX_PLATFORM_VERSION = CONFIG.get('header.platform_version', uname()[2])
|
||||||
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
|
X_PLEX_PRODUCT = CONFIG.get('header.product', PROJECT)
|
||||||
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)
|
X_PLEX_VERSION = CONFIG.get('header.version', VERSION)
|
||||||
|
|
|
@ -2,12 +2,16 @@
|
||||||
import os
|
import os
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
from plexapi import library, media, utils
|
from plexapi import media, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
|
from plexapi.mixins import (
|
||||||
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin
|
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||||
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
|
||||||
|
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
|
||||||
|
TrackArtistMixin, TrackDiscNumberMixin, TrackNumberMixin,
|
||||||
|
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||||
|
)
|
||||||
from plexapi.playlist import Playlist
|
from plexapi.playlist import Playlist
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +42,7 @@ class Audio(PlexPartialObject):
|
||||||
title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.).
|
title (str): Name of the artist, album, or track (Jason Mraz, We Sing, Lucky, etc.).
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'artist', 'album', or 'track'.
|
type (str): 'artist', 'album', or 'track'.
|
||||||
updatedAt (datatime): Datetime the item was updated.
|
updatedAt (datetime): Datetime the item was updated.
|
||||||
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
|
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
|
||||||
viewCount (int): Count of times the item was played.
|
viewCount (int): Count of times the item was played.
|
||||||
"""
|
"""
|
||||||
|
@ -125,8 +129,13 @@ class Audio(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
class Artist(
|
||||||
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
Audio,
|
||||||
|
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeMixin,
|
||||||
|
SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
|
CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||||
|
):
|
||||||
""" Represents a single Artist.
|
""" Represents a single Artist.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -138,9 +147,11 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
||||||
countries (List<:class:`~plexapi.media.Country`>): List country objects.
|
countries (List<:class:`~plexapi.media.Country`>): List country objects.
|
||||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
locations (List<str>): List of folder paths where the artist is found on disk.
|
locations (List<str>): List of folder paths where the artist is found on disk.
|
||||||
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
|
similar (List<:class:`~plexapi.media.Similar`>): List of similar objects.
|
||||||
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
|
styles (List<:class:`~plexapi.media.Style`>): List of style objects.
|
||||||
|
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
|
||||||
"""
|
"""
|
||||||
TAG = 'Directory'
|
TAG = 'Directory'
|
||||||
TYPE = 'artist'
|
TYPE = 'artist'
|
||||||
|
@ -153,26 +164,23 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
||||||
self.countries = self.findItems(data, media.Country)
|
self.countries = self.findItems(data, media.Country)
|
||||||
self.genres = self.findItems(data, media.Genre)
|
self.genres = self.findItems(data, media.Genre)
|
||||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
self.styles = self.findItems(data, media.Style)
|
self.styles = self.findItems(data, media.Style)
|
||||||
|
self.theme = data.attrib.get('theme')
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for album in self.albums():
|
for album in self.albums():
|
||||||
yield album
|
yield album
|
||||||
|
|
||||||
def hubs(self):
|
|
||||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
|
||||||
data = self._server.query(self._details_key)
|
|
||||||
return self.findItems(data, library.Hub, rtag='Related')
|
|
||||||
|
|
||||||
def album(self, title):
|
def album(self, title):
|
||||||
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the album to return.
|
title (str): Title of the album to return.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f"/library/sections/{self.librarySectionID}/all?artist.id={self.ratingKey}&type=9"
|
||||||
return self.fetchItem(key, Album, title__iexact=title)
|
return self.fetchItem(key, Album, title__iexact=title)
|
||||||
|
|
||||||
def albums(self, **kwargs):
|
def albums(self, **kwargs):
|
||||||
|
@ -230,8 +238,13 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
class Album(
|
||||||
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
|
Audio,
|
||||||
|
UnmatchMatchMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||||
|
OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin,
|
||||||
|
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin
|
||||||
|
):
|
||||||
""" Represents a single Album.
|
""" Represents a single Album.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -248,6 +261,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
||||||
parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
parentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
||||||
parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>).
|
parentKey (str): API URL of the album artist (/library/metadata/<parentRatingKey>).
|
||||||
parentRatingKey (int): Unique key identifying the album artist.
|
parentRatingKey (int): Unique key identifying the album artist.
|
||||||
|
parentTheme (str): URL to artist theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
|
||||||
parentThumb (str): URL to album artist thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
parentThumb (str): URL to album artist thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||||
parentTitle (str): Name of the album artist.
|
parentTitle (str): Name of the album artist.
|
||||||
rating (float): Album rating (7.9; 9.8; 8.1).
|
rating (float): Album rating (7.9; 9.8; 8.1).
|
||||||
|
@ -274,6 +288,7 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
||||||
self.parentGuid = data.attrib.get('parentGuid')
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||||
|
self.parentTheme = data.attrib.get('parentTheme')
|
||||||
self.parentThumb = data.attrib.get('parentThumb')
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
|
@ -298,10 +313,14 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = '/library/metadata/%s/children' % self.ratingKey
|
||||||
if title is not None:
|
if title is not None and not isinstance(title, int):
|
||||||
return self.fetchItem(key, Track, title__iexact=title)
|
return self.fetchItem(key, Track, title__iexact=title)
|
||||||
elif track is not None:
|
elif track is not None or isinstance(title, int):
|
||||||
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=track)
|
if isinstance(title, int):
|
||||||
|
index = title
|
||||||
|
else:
|
||||||
|
index = track
|
||||||
|
return self.fetchItem(key, Track, parentTitle__iexact=self.title, index=index)
|
||||||
raise BadRequest('Missing argument: title or track is required')
|
raise BadRequest('Missing argument: title or track is required')
|
||||||
|
|
||||||
def tracks(self, **kwargs):
|
def tracks(self, **kwargs):
|
||||||
|
@ -337,8 +356,13 @@ class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
|
class Track(
|
||||||
CollectionMixin, MoodMixin):
|
Audio, Playable,
|
||||||
|
ExtrasMixin, RatingMixin,
|
||||||
|
ArtUrlMixin, PosterUrlMixin, ThemeUrlMixin,
|
||||||
|
TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin,
|
||||||
|
CollectionMixin, LabelMixin, MoodMixin
|
||||||
|
):
|
||||||
""" Represents a single Track.
|
""" Represents a single Track.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -351,19 +375,23 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
|
||||||
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
|
||||||
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
|
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
|
||||||
grandparentRatingKey (int): Unique key identifying the album artist.
|
grandparentRatingKey (int): Unique key identifying the album artist.
|
||||||
|
grandparentTheme (str): URL to artist theme resource (/library/metadata/<grandparentRatingkey>/theme/<themeid>).
|
||||||
|
(/library/metadata/<grandparentRatingkey>/theme/<themeid>).
|
||||||
grandparentThumb (str): URL to album artist thumbnail image
|
grandparentThumb (str): URL to album artist thumbnail image
|
||||||
(/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
|
(/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
|
||||||
grandparentTitle (str): Name of the album artist for the track.
|
grandparentTitle (str): Name of the album artist for the track.
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||||
originalTitle (str): The artist for the track.
|
originalTitle (str): The artist for the track.
|
||||||
parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9).
|
parentGuid (str): Plex GUID for the album (plex://album/5d07cd8e403c640290f180f9).
|
||||||
parentIndex (int): Album index.
|
parentIndex (int): Disc number of the track.
|
||||||
parentKey (str): API URL of the album (/library/metadata/<parentRatingKey>).
|
parentKey (str): API URL of the album (/library/metadata/<parentRatingKey>).
|
||||||
parentRatingKey (int): Unique key identifying the album.
|
parentRatingKey (int): Unique key identifying the album.
|
||||||
parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
parentThumb (str): URL to album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||||
parentTitle (str): Name of the album for the track.
|
parentTitle (str): Name of the album for the track.
|
||||||
primaryExtraKey (str) API URL for the primary extra for the track.
|
primaryExtraKey (str) API URL for the primary extra for the track.
|
||||||
ratingCount (int): Number of ratings contributing to the rating score.
|
ratingCount (int): Number of ratings contributing to the rating score.
|
||||||
|
skipCount (int): Number of times the track has been skipped.
|
||||||
viewOffset (int): View offset in milliseconds.
|
viewOffset (int): View offset in milliseconds.
|
||||||
year (int): Year the track was released.
|
year (int): Year the track was released.
|
||||||
"""
|
"""
|
||||||
|
@ -381,18 +409,21 @@ class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin,
|
||||||
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
self.grandparentGuid = data.attrib.get('grandparentGuid')
|
||||||
self.grandparentKey = data.attrib.get('grandparentKey')
|
self.grandparentKey = data.attrib.get('grandparentKey')
|
||||||
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
|
||||||
|
self.grandparentTheme = data.attrib.get('grandparentTheme')
|
||||||
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
self.grandparentThumb = data.attrib.get('grandparentThumb')
|
||||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.originalTitle = data.attrib.get('originalTitle')
|
self.originalTitle = data.attrib.get('originalTitle')
|
||||||
self.parentGuid = data.attrib.get('parentGuid')
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentIndex = data.attrib.get('parentIndex')
|
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||||
self.parentKey = data.attrib.get('parentKey')
|
self.parentKey = data.attrib.get('parentKey')
|
||||||
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
|
||||||
self.parentThumb = data.attrib.get('parentThumb')
|
self.parentThumb = data.attrib.get('parentThumb')
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
|
||||||
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
|
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
|
||||||
|
self.skipCount = utils.cast(int, data.attrib.get('skipCount'))
|
||||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,9 @@ class PlexObject(object):
|
||||||
if data is not None:
|
if data is not None:
|
||||||
self._loadData(data)
|
self._loadData(data)
|
||||||
self._details_key = self._buildDetailsKey()
|
self._details_key = self._buildDetailsKey()
|
||||||
self._autoReload = False
|
self._overwriteNone = True
|
||||||
|
self._edits = None # Save batch edits for a single API call
|
||||||
|
self._autoReload = True # Automatically reload the object when accessing a missing attribute
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri'))
|
uid = self._clean(self.firstAttr('_baseurl', 'key', 'id', 'playQueueID', 'uri'))
|
||||||
|
@ -65,9 +67,9 @@ class PlexObject(object):
|
||||||
if attr in _DONT_OVERWRITE_SESSION_KEYS and value == []:
|
if attr in _DONT_OVERWRITE_SESSION_KEYS and value == []:
|
||||||
value = getattr(self, attr, [])
|
value = getattr(self, attr, [])
|
||||||
|
|
||||||
autoReload = self.__dict__.get('_autoReload')
|
overwriteNone = self.__dict__.get('_overwriteNone')
|
||||||
# Don't overwrite an attr with None unless it's a private variable or not auto reload
|
# Don't overwrite an attr with None unless it's a private variable or overwrite None is True
|
||||||
if value is not None or attr.startswith('_') or attr not in self.__dict__ or not autoReload:
|
if value is not None or attr.startswith('_') or attr not in self.__dict__ or overwriteNone:
|
||||||
self.__dict__[attr] = value
|
self.__dict__[attr] = value
|
||||||
|
|
||||||
def _clean(self, value):
|
def _clean(self, value):
|
||||||
|
@ -169,9 +171,14 @@ class PlexObject(object):
|
||||||
raise BadRequest('ekey was not provided')
|
raise BadRequest('ekey was not provided')
|
||||||
if isinstance(ekey, int):
|
if isinstance(ekey, int):
|
||||||
ekey = '/library/metadata/%s' % ekey
|
ekey = '/library/metadata/%s' % ekey
|
||||||
for elem in self._server.query(ekey):
|
data = self._server.query(ekey)
|
||||||
|
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||||
|
for elem in data:
|
||||||
if self._checkAttrs(elem, **kwargs):
|
if self._checkAttrs(elem, **kwargs):
|
||||||
return self._buildItem(elem, cls, ekey)
|
item = self._buildItem(elem, cls, ekey)
|
||||||
|
if librarySectionID:
|
||||||
|
item.librarySectionID = librarySectionID
|
||||||
|
return item
|
||||||
clsname = cls.__name__ if cls else 'None'
|
clsname = cls.__name__ if cls else 'None'
|
||||||
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
|
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
|
||||||
|
|
||||||
|
@ -196,7 +203,7 @@ class PlexObject(object):
|
||||||
Any XML attribute can be filtered when fetching results. Filtering is done before
|
Any XML attribute can be filtered when fetching results. Filtering is done before
|
||||||
the Python objects are built to help keep things speedy. For example, passing in
|
the Python objects are built to help keep things speedy. For example, passing in
|
||||||
``viewCount=0`` will only return matching items where the view count is ``0``.
|
``viewCount=0`` will only return matching items where the view count is ``0``.
|
||||||
Note that case matters when specifying attributes. Attributes futher down in the XML
|
Note that case matters when specifying attributes. Attributes further down in the XML
|
||||||
tree can be filtered by *prepending* the attribute with each element tag ``Tag__``.
|
tree can be filtered by *prepending* the attribute with each element tag ``Tag__``.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
@ -228,12 +235,12 @@ class PlexObject(object):
|
||||||
* ``__exists`` (*bool*): Value is or is not present in the attrs.
|
* ``__exists`` (*bool*): Value is or is not present in the attrs.
|
||||||
* ``__gt``: Value is greater than specified arg.
|
* ``__gt``: Value is greater than specified arg.
|
||||||
* ``__gte``: Value is greater than or equal to specified arg.
|
* ``__gte``: Value is greater than or equal to specified arg.
|
||||||
* ``__icontains``: Case insensative value contains specified arg.
|
* ``__icontains``: Case insensitive value contains specified arg.
|
||||||
* ``__iendswith``: Case insensative value ends with specified arg.
|
* ``__iendswith``: Case insensitive value ends with specified arg.
|
||||||
* ``__iexact``: Case insensative value matches specified arg.
|
* ``__iexact``: Case insensitive value matches specified arg.
|
||||||
* ``__in``: Value is in a specified list or tuple.
|
* ``__in``: Value is in a specified list or tuple.
|
||||||
* ``__iregex``: Case insensative value matches the specified regular expression.
|
* ``__iregex``: Case insensitive value matches the specified regular expression.
|
||||||
* ``__istartswith``: Case insensative value starts with specified arg.
|
* ``__istartswith``: Case insensitive value starts with specified arg.
|
||||||
* ``__lt``: Value is less than specified arg.
|
* ``__lt``: Value is less than specified arg.
|
||||||
* ``__lte``: Value is less than or equal to specified arg.
|
* ``__lte``: Value is less than or equal to specified arg.
|
||||||
* ``__regex``: Value matches the specified regular expression.
|
* ``__regex``: Value matches the specified regular expression.
|
||||||
|
@ -276,9 +283,9 @@ class PlexObject(object):
|
||||||
kwargs['etag'] = cls.TAG
|
kwargs['etag'] = cls.TAG
|
||||||
if cls and cls.TYPE and 'type' not in kwargs:
|
if cls and cls.TYPE and 'type' not in kwargs:
|
||||||
kwargs['type'] = cls.TYPE
|
kwargs['type'] = cls.TYPE
|
||||||
# rtag to iter on a specific root tag
|
# rtag to iter on a specific root tag using breadth-first search
|
||||||
if rtag:
|
if rtag:
|
||||||
data = next(data.iter(rtag), [])
|
data = next(utils.iterXMLBFS(data, rtag), [])
|
||||||
# loop through all data elements to find matches
|
# loop through all data elements to find matches
|
||||||
items = []
|
items = []
|
||||||
for elem in data:
|
for elem in data:
|
||||||
|
@ -298,9 +305,9 @@ class PlexObject(object):
|
||||||
def listAttrs(self, data, attr, rtag=None, **kwargs):
|
def listAttrs(self, data, attr, rtag=None, **kwargs):
|
||||||
""" Return a list of values from matching attribute. """
|
""" Return a list of values from matching attribute. """
|
||||||
results = []
|
results = []
|
||||||
# rtag to iter on a specific root tag
|
# rtag to iter on a specific root tag using breadth-first search
|
||||||
if rtag:
|
if rtag:
|
||||||
data = next(data.iter(rtag), [])
|
data = next(utils.iterXMLBFS(data, rtag), [])
|
||||||
for elem in data:
|
for elem in data:
|
||||||
kwargs['%s__exists' % attr] = True
|
kwargs['%s__exists' % attr] = True
|
||||||
if self._checkAttrs(elem, **kwargs):
|
if self._checkAttrs(elem, **kwargs):
|
||||||
|
@ -340,7 +347,7 @@ class PlexObject(object):
|
||||||
"""
|
"""
|
||||||
return self._reload(key=key, **kwargs)
|
return self._reload(key=key, **kwargs)
|
||||||
|
|
||||||
def _reload(self, key=None, _autoReload=False, **kwargs):
|
def _reload(self, key=None, _overwriteNone=True, **kwargs):
|
||||||
""" Perform the actual reload. """
|
""" Perform the actual reload. """
|
||||||
details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key
|
details_key = self._buildDetailsKey(**kwargs) if kwargs else self._details_key
|
||||||
key = key or details_key or self.key
|
key = key or details_key or self.key
|
||||||
|
@ -348,9 +355,9 @@ class PlexObject(object):
|
||||||
raise Unsupported('Cannot reload an object not built from a URL.')
|
raise Unsupported('Cannot reload an object not built from a URL.')
|
||||||
self._initpath = key
|
self._initpath = key
|
||||||
data = self._server.query(key)
|
data = self._server.query(key)
|
||||||
self._autoReload = _autoReload
|
self._overwriteNone = _overwriteNone
|
||||||
self._loadData(data[0])
|
self._loadData(data[0])
|
||||||
self._autoReload = False
|
self._overwriteNone = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _checkAttrs(self, elem, **kwargs):
|
def _checkAttrs(self, elem, **kwargs):
|
||||||
|
@ -392,7 +399,7 @@ class PlexObject(object):
|
||||||
# check were looking for the tag
|
# check were looking for the tag
|
||||||
if attr.lower() == 'etag':
|
if attr.lower() == 'etag':
|
||||||
return [elem.tag]
|
return [elem.tag]
|
||||||
# loop through attrs so we can perform case-insensative match
|
# loop through attrs so we can perform case-insensitive match
|
||||||
for _attr, value in elem.attrib.items():
|
for _attr, value in elem.attrib.items():
|
||||||
if attr.lower() == _attr.lower():
|
if attr.lower() == _attr.lower():
|
||||||
return [value]
|
return [value]
|
||||||
|
@ -414,6 +421,10 @@ class PlexObject(object):
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
raise NotImplementedError('Abstract method not implemented.')
|
raise NotImplementedError('Abstract method not implemented.')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _searchType(self):
|
||||||
|
return self.TYPE
|
||||||
|
|
||||||
|
|
||||||
class PlexPartialObject(PlexObject):
|
class PlexPartialObject(PlexObject):
|
||||||
""" Not all objects in the Plex listings return the complete list of elements
|
""" Not all objects in the Plex listings return the complete list of elements
|
||||||
|
@ -455,20 +466,21 @@ class PlexPartialObject(PlexObject):
|
||||||
def __getattribute__(self, attr):
|
def __getattribute__(self, attr):
|
||||||
# Dragons inside.. :-/
|
# Dragons inside.. :-/
|
||||||
value = super(PlexPartialObject, self).__getattribute__(attr)
|
value = super(PlexPartialObject, self).__getattribute__(attr)
|
||||||
# Check a few cases where we dont want to reload
|
# Check a few cases where we don't want to reload
|
||||||
if attr in _DONT_RELOAD_FOR_KEYS: return value
|
if attr in _DONT_RELOAD_FOR_KEYS: return value
|
||||||
if attr in _DONT_OVERWRITE_SESSION_KEYS: return value
|
if attr in _DONT_OVERWRITE_SESSION_KEYS: return value
|
||||||
if attr in USER_DONT_RELOAD_FOR_KEYS: return value
|
if attr in USER_DONT_RELOAD_FOR_KEYS: return value
|
||||||
if attr.startswith('_'): return value
|
if attr.startswith('_'): return value
|
||||||
if value not in (None, []): return value
|
if value not in (None, []): return value
|
||||||
if self.isFullObject(): return value
|
if self.isFullObject(): return value
|
||||||
|
if self._autoReload is False: return value
|
||||||
# Log the reload.
|
# Log the reload.
|
||||||
clsname = self.__class__.__name__
|
clsname = self.__class__.__name__
|
||||||
title = self.__dict__.get('title', self.__dict__.get('name'))
|
title = self.__dict__.get('title', self.__dict__.get('name'))
|
||||||
objname = "%s '%s'" % (clsname, title) if title else clsname
|
objname = "%s '%s'" % (clsname, title) if title else clsname
|
||||||
log.debug("Reloading %s for attr '%s'", objname, attr)
|
log.debug("Reloading %s for attr '%s'", objname, attr)
|
||||||
# Reload and return the value
|
# Reload and return the value
|
||||||
self._reload(_autoReload=True)
|
self._reload()
|
||||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||||
|
|
||||||
def analyze(self):
|
def analyze(self):
|
||||||
|
@ -507,44 +519,79 @@ class PlexPartialObject(PlexObject):
|
||||||
|
|
||||||
def _edit(self, **kwargs):
|
def _edit(self, **kwargs):
|
||||||
""" Actually edit an object. """
|
""" Actually edit an object. """
|
||||||
|
if isinstance(self._edits, dict):
|
||||||
|
self._edits.update(kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
if 'id' not in kwargs:
|
if 'id' not in kwargs:
|
||||||
kwargs['id'] = self.ratingKey
|
kwargs['id'] = self.ratingKey
|
||||||
if 'type' not in kwargs:
|
if 'type' not in kwargs:
|
||||||
kwargs['type'] = utils.searchType(self.type)
|
kwargs['type'] = utils.searchType(self._searchType)
|
||||||
|
|
||||||
part = '/library/sections/%s/all?%s' % (self.librarySectionID,
|
part = '/library/sections/%s/all%s' % (self.librarySectionID,
|
||||||
urlencode(kwargs))
|
utils.joinArgs(kwargs))
|
||||||
self._server.query(part, method=self._server._session.put)
|
self._server.query(part, method=self._server._session.put)
|
||||||
|
return self
|
||||||
|
|
||||||
def edit(self, **kwargs):
|
def edit(self, **kwargs):
|
||||||
""" Edit an object.
|
""" Edit an object.
|
||||||
|
Note: This is a low level method and you need to know all the field/tag keys.
|
||||||
|
See :class:`~plexapi.mixins.EditFieldMixin` and :class:`~plexapi.mixins.EditTagsMixin`
|
||||||
|
for individual field and tag editing methods.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
kwargs (dict): Dict of settings to edit.
|
kwargs (dict): Dict of settings to edit.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
{'type': 1,
|
|
||||||
'id': movie.ratingKey,
|
|
||||||
'collection[0].tag.tag': 'Super',
|
|
||||||
'collection.locked': 0}
|
|
||||||
"""
|
|
||||||
self._edit(**kwargs)
|
|
||||||
|
|
||||||
def _edit_tags(self, tag, items, locked=True, remove=False):
|
.. code-block:: python
|
||||||
""" Helper to edit tags.
|
|
||||||
|
edits = {
|
||||||
|
'type': 1,
|
||||||
|
'id': movie.ratingKey,
|
||||||
|
'title.value': 'A new title',
|
||||||
|
'title.locked': 1,
|
||||||
|
'summary.value': 'This is a summary.',
|
||||||
|
'summary.locked': 1,
|
||||||
|
'collection[0].tag.tag': 'A tag',
|
||||||
|
'collection.locked': 1}
|
||||||
|
}
|
||||||
|
movie.edit(**edits)
|
||||||
|
|
||||||
Parameters:
|
|
||||||
tag (str): Tag name.
|
|
||||||
items (list): List of tags to add.
|
|
||||||
locked (bool): True to lock the field.
|
|
||||||
remove (bool): True to remove the tags in items.
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(items, list):
|
return self._edit(**kwargs)
|
||||||
items = [items]
|
|
||||||
value = getattr(self, utils.tag_plural(tag))
|
def batchEdits(self):
|
||||||
existing_tags = [t.tag for t in value if t and remove is False]
|
""" Enable batch editing mode to save API calls.
|
||||||
tag_edits = utils.tag_helper(tag, existing_tags + items, locked, remove)
|
Must call :func:`~plexapi.base.PlexPartialObject.saveEdits` at the end to save all the edits.
|
||||||
self.edit(**tag_edits)
|
See :class:`~plexapi.mixins.EditFieldMixin` and :class:`~plexapi.mixins.EditTagsMixin`
|
||||||
|
for individual field and tag editing methods.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Batch editing multiple fields and tags in a single API call
|
||||||
|
Movie.batchEdits()
|
||||||
|
Movie.editTitle('A New Title').editSummary('A new summary').editTagline('A new tagline') \\
|
||||||
|
.addCollection('New Collection').removeGenre('Action').addLabel('Favorite')
|
||||||
|
Movie.saveEdits()
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._edits = {}
|
||||||
|
return self
|
||||||
|
|
||||||
|
def saveEdits(self):
|
||||||
|
""" Save all the batch edits and automatically reload the object.
|
||||||
|
See :func:`~plexapi.base.PlexPartialObject.batchEdits` for details.
|
||||||
|
"""
|
||||||
|
if not isinstance(self._edits, dict):
|
||||||
|
raise BadRequest('Batch editing mode not enabled. Must call `batchEdits()` first.')
|
||||||
|
|
||||||
|
edits = self._edits
|
||||||
|
self._edits = None
|
||||||
|
self._edit(**edits)
|
||||||
|
return self.reload()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
""" Refreshing a Library or individual item causes the metadata for the item to be
|
""" Refreshing a Library or individual item causes the metadata for the item to be
|
||||||
|
@ -709,7 +756,7 @@ class Playable(object):
|
||||||
filename = part.file
|
filename = part.file
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
# So this seems to be a alot slower but allows transcode.
|
# So this seems to be a a lot slower but allows transcode.
|
||||||
download_url = self.getStreamURL(**kwargs)
|
download_url = self.getStreamURL(**kwargs)
|
||||||
else:
|
else:
|
||||||
download_url = self._server.url('%s?download=1' % part.key)
|
download_url = self._server.url('%s?download=1' % part.key)
|
||||||
|
@ -746,7 +793,7 @@ class Playable(object):
|
||||||
key = '/:/progress?key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s' % (self.ratingKey,
|
key = '/:/progress?key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s' % (self.ratingKey,
|
||||||
time, state)
|
time, state)
|
||||||
self._server.query(key)
|
self._server.query(key)
|
||||||
self._reload(_autoReload=True)
|
self._reload(_overwriteNone=False)
|
||||||
|
|
||||||
def updateTimeline(self, time, state='stopped', duration=None):
|
def updateTimeline(self, time, state='stopped', duration=None):
|
||||||
""" Set the timeline progress for this video.
|
""" Set the timeline progress for this video.
|
||||||
|
@ -764,7 +811,7 @@ class Playable(object):
|
||||||
key = '/:/timeline?ratingKey=%s&key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s%s'
|
key = '/:/timeline?ratingKey=%s&key=%s&identifier=com.plexapp.plugins.library&time=%d&state=%s%s'
|
||||||
key %= (self.ratingKey, self.key, time, state, durationStr)
|
key %= (self.ratingKey, self.key, time, state, durationStr)
|
||||||
self._server.query(key)
|
self._server.query(key)
|
||||||
self._reload(_autoReload=True)
|
self._reload(_overwriteNone=False)
|
||||||
|
|
||||||
|
|
||||||
class MediaContainer(PlexObject):
|
class MediaContainer(PlexObject):
|
||||||
|
|
|
@ -23,10 +23,10 @@ class PlexClient(PlexObject):
|
||||||
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional).
|
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional).
|
||||||
data (ElementTree): Response from PlexServer used to build this object (optional).
|
data (ElementTree): Response from PlexServer used to build this object (optional).
|
||||||
initpath (str): Path used to generate data.
|
initpath (str): Path used to generate data.
|
||||||
baseurl (str): HTTP URL to connect dirrectly to this client.
|
baseurl (str): HTTP URL to connect directly to this client.
|
||||||
identifier (str): The resource/machine identifier for the desired client.
|
identifier (str): The resource/machine identifier for the desired client.
|
||||||
May be necessary when connecting to a specific proxied client (optional).
|
May be necessary when connecting to a specific proxied client (optional).
|
||||||
token (str): X-Plex-Token used for authenication (optional).
|
token (str): X-Plex-Token used for authentication (optional).
|
||||||
session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
|
session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
|
||||||
timeout (int): timeout in seconds on initial connect to client (default config.TIMEOUT).
|
timeout (int): timeout in seconds on initial connect to client (default config.TIMEOUT).
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ class PlexClient(PlexObject):
|
||||||
session (:class:`~requests.Session`): Session object used for connection.
|
session (:class:`~requests.Session`): Session object used for connection.
|
||||||
state (str): Unknown
|
state (str): Unknown
|
||||||
title (str): Name of this client (Johns iPhone, etc).
|
title (str): Name of this client (Johns iPhone, etc).
|
||||||
token (str): X-Plex-Token used for authenication
|
token (str): X-Plex-Token used for authentication
|
||||||
vendor (str): Unknown
|
vendor (str): Unknown
|
||||||
version (str): Device version (4.6.1, etc).
|
version (str): Device version (4.6.1, etc).
|
||||||
_baseurl (str): HTTP address of the client.
|
_baseurl (str): HTTP address of the client.
|
||||||
|
@ -131,7 +131,7 @@ class PlexClient(PlexObject):
|
||||||
self.platformVersion = data.attrib.get('platformVersion')
|
self.platformVersion = data.attrib.get('platformVersion')
|
||||||
self.title = data.attrib.get('title') or data.attrib.get('name')
|
self.title = data.attrib.get('title') or data.attrib.get('name')
|
||||||
# Active session details
|
# Active session details
|
||||||
# Since protocolCapabilities is missing from /sessions we cant really control this player without
|
# Since protocolCapabilities is missing from /sessions we can't really control this player without
|
||||||
# creating a client manually.
|
# creating a client manually.
|
||||||
# Add this in next breaking release.
|
# Add this in next breaking release.
|
||||||
# if self._initpath == 'status/sessions':
|
# if self._initpath == 'status/sessions':
|
||||||
|
@ -210,8 +210,8 @@ class PlexClient(PlexObject):
|
||||||
controller = command.split('/')[0]
|
controller = command.split('/')[0]
|
||||||
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
|
||||||
if controller not in self.protocolCapabilities:
|
if controller not in self.protocolCapabilities:
|
||||||
log.debug('Client %s doesnt support %s controller.'
|
log.debug("Client %s doesn't support %s controller."
|
||||||
'What your trying might not work' % (self.title, controller))
|
"What your trying might not work" % (self.title, controller))
|
||||||
|
|
||||||
proxy = self._proxyThroughServer if proxy is None else proxy
|
proxy = self._proxyThroughServer if proxy is None else proxy
|
||||||
query = self._server.query if proxy else self.query
|
query = self._server.query if proxy else self.query
|
||||||
|
@ -318,21 +318,21 @@ class PlexClient(PlexObject):
|
||||||
Parameters:
|
Parameters:
|
||||||
media (:class:`~plexapi.media.Media`): Media object to navigate to.
|
media (:class:`~plexapi.media.Media`): Media object to navigate to.
|
||||||
**params (dict): Additional GET parameters to include with the command.
|
**params (dict): Additional GET parameters to include with the command.
|
||||||
|
|
||||||
Raises:
|
|
||||||
:exc:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
|
||||||
"""
|
"""
|
||||||
if not self._server:
|
|
||||||
raise Unsupported('A server must be specified before using this command.')
|
|
||||||
server_url = media._server._baseurl.split(':')
|
server_url = media._server._baseurl.split(':')
|
||||||
self.sendCommand('mirror/details', **dict({
|
command = {
|
||||||
'machineIdentifier': self._server.machineIdentifier,
|
'machineIdentifier': media._server.machineIdentifier,
|
||||||
'address': server_url[1].strip('/'),
|
'address': server_url[1].strip('/'),
|
||||||
'port': server_url[-1],
|
'port': server_url[-1],
|
||||||
'key': media.key,
|
'key': media.key,
|
||||||
'protocol': server_url[0],
|
'protocol': server_url[0],
|
||||||
'token': media._server.createToken()
|
**params,
|
||||||
}, **params))
|
}
|
||||||
|
token = media._server.createToken()
|
||||||
|
if token:
|
||||||
|
command["token"] = token
|
||||||
|
|
||||||
|
self.sendCommand("mirror/details", **command)
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Playback Commands
|
# Playback Commands
|
||||||
|
@ -488,12 +488,7 @@ class PlexClient(PlexObject):
|
||||||
representing the beginning (default 0).
|
representing the beginning (default 0).
|
||||||
**params (dict): Optional additional parameters to include in the playback request. See
|
**params (dict): Optional additional parameters to include in the playback request. See
|
||||||
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
|
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
|
||||||
|
|
||||||
Raises:
|
|
||||||
:exc:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
|
|
||||||
"""
|
"""
|
||||||
if not self._server:
|
|
||||||
raise Unsupported('A server must be specified before using this command.')
|
|
||||||
server_url = media._server._baseurl.split(':')
|
server_url = media._server._baseurl.split(':')
|
||||||
server_port = server_url[-1].strip('/')
|
server_port = server_url[-1].strip('/')
|
||||||
|
|
||||||
|
@ -509,19 +504,24 @@ class PlexClient(PlexObject):
|
||||||
if mediatype == "audio":
|
if mediatype == "audio":
|
||||||
mediatype = "music"
|
mediatype = "music"
|
||||||
|
|
||||||
playqueue = media if isinstance(media, PlayQueue) else self._server.createPlayQueue(media)
|
playqueue = media if isinstance(media, PlayQueue) else media._server.createPlayQueue(media)
|
||||||
self.sendCommand('playback/playMedia', **dict({
|
command = {
|
||||||
'providerIdentifier': 'com.plexapp.plugins.library',
|
'providerIdentifier': 'com.plexapp.plugins.library',
|
||||||
'machineIdentifier': self._server.machineIdentifier,
|
'machineIdentifier': media._server.machineIdentifier,
|
||||||
'protocol': server_url[0],
|
'protocol': server_url[0],
|
||||||
'address': server_url[1].strip('/'),
|
'address': server_url[1].strip('/'),
|
||||||
'port': server_port,
|
'port': server_port,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'key': media.key or playqueue.selectedItem.key,
|
'key': media.key or playqueue.selectedItem.key,
|
||||||
'token': media._server.createToken(),
|
|
||||||
'type': mediatype,
|
'type': mediatype,
|
||||||
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
|
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
|
||||||
}, **params))
|
**params,
|
||||||
|
}
|
||||||
|
token = media._server.createToken()
|
||||||
|
if token:
|
||||||
|
command["token"] = token
|
||||||
|
|
||||||
|
self.sendCommand("playback/playMedia", **command)
|
||||||
|
|
||||||
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=DEFAULT_MTYPE):
|
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=DEFAULT_MTYPE):
|
||||||
""" Set multiple playback parameters at once.
|
""" Set multiple playback parameters at once.
|
||||||
|
|
|
@ -5,14 +5,24 @@ from plexapi import media, utils
|
||||||
from plexapi.base import PlexPartialObject
|
from plexapi.base import PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||||
from plexapi.library import LibrarySection
|
from plexapi.library import LibrarySection
|
||||||
from plexapi.mixins import AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin
|
from plexapi.mixins import (
|
||||||
from plexapi.mixins import LabelMixin, SmartFilterMixin
|
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeMixin,
|
||||||
|
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
|
LabelMixin
|
||||||
|
)
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, LabelMixin, SmartFilterMixin):
|
class Collection(
|
||||||
|
PlexPartialObject,
|
||||||
|
AdvancedSettingsMixin, SmartFilterMixin, HubsMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeMixin,
|
||||||
|
ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
|
LabelMixin
|
||||||
|
):
|
||||||
""" Represents a single Collection.
|
""" Represents a single Collection.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -22,9 +32,10 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
||||||
artBlurHash (str): BlurHash string for artwork image.
|
artBlurHash (str): BlurHash string for artwork image.
|
||||||
childCount (int): Number of items in the collection.
|
childCount (int): Number of items in the collection.
|
||||||
collectionMode (str): How the items in the collection are displayed.
|
collectionFilterBasedOnUser (int): Which user's activity is used for the collection filtering.
|
||||||
|
collectionMode (int): How the items in the collection are displayed.
|
||||||
collectionPublished (bool): True if the collection is published to the Plex homepage.
|
collectionPublished (bool): True if the collection is published to the Plex homepage.
|
||||||
collectionSort (str): How to sort the items in the collection.
|
collectionSort (int): How to sort the items in the collection.
|
||||||
content (str): The filter URI string for smart collections.
|
content (str): The filter URI string for smart collections.
|
||||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||||
|
@ -43,12 +54,13 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
smart (bool): True if the collection is a smart collection.
|
smart (bool): True if the collection is a smart collection.
|
||||||
subtype (str): Media type of the items in the collection (movie, show, artist, or album).
|
subtype (str): Media type of the items in the collection (movie, show, artist, or album).
|
||||||
summary (str): Summary of the collection.
|
summary (str): Summary of the collection.
|
||||||
|
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
|
||||||
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
||||||
thumbBlurHash (str): BlurHash string for thumbnail image.
|
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||||
title (str): Name of the collection.
|
title (str): Name of the collection.
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'collection'
|
type (str): 'collection'
|
||||||
updatedAt (datatime): Datetime the collection was updated.
|
updatedAt (datetime): Datetime the collection was updated.
|
||||||
userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars).
|
userRating (float): Rating of the collection (0.0 - 10.0) equaling (0 stars - 5 stars).
|
||||||
"""
|
"""
|
||||||
TAG = 'Directory'
|
TAG = 'Directory'
|
||||||
|
@ -60,6 +72,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
self.art = data.attrib.get('art')
|
self.art = data.attrib.get('art')
|
||||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||||
|
self.collectionFilterBasedOnUser = utils.cast(int, data.attrib.get('collectionFilterBasedOnUser', '0'))
|
||||||
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1'))
|
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1'))
|
||||||
self.collectionPublished = utils.cast(bool, data.attrib.get('collectionPublished', '0'))
|
self.collectionPublished = utils.cast(bool, data.attrib.get('collectionPublished', '0'))
|
||||||
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0'))
|
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0'))
|
||||||
|
@ -81,6 +94,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
self.smart = utils.cast(bool, data.attrib.get('smart', '0'))
|
self.smart = utils.cast(bool, data.attrib.get('smart', '0'))
|
||||||
self.subtype = data.attrib.get('subtype')
|
self.subtype = data.attrib.get('subtype')
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
|
self.theme = data.attrib.get('theme')
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
|
@ -184,6 +198,32 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
""" Alias to :func:`~plexapi.library.Collection.item`. """
|
""" Alias to :func:`~plexapi.library.Collection.item`. """
|
||||||
return self.item(title)
|
return self.item(title)
|
||||||
|
|
||||||
|
def filterUserUpdate(self, user=None):
|
||||||
|
""" Update the collection filtering user advanced setting.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user (str): One of the following values:
|
||||||
|
"admin" (Always the server admin user),
|
||||||
|
"user" (User currently viewing the content)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
collection.updateMode(user="user")
|
||||||
|
"""
|
||||||
|
if not self.smart:
|
||||||
|
raise BadRequest('Cannot change collection filtering user for a non-smart collection.')
|
||||||
|
|
||||||
|
user_dict = {
|
||||||
|
'admin': 0,
|
||||||
|
'user': 1
|
||||||
|
}
|
||||||
|
key = user_dict.get(user)
|
||||||
|
if key is None:
|
||||||
|
raise BadRequest('Unknown collection filtering user: %s. Options %s' % (user, list(user_dict)))
|
||||||
|
self.editAdvanced(collectionFilterBasedOnUser=key)
|
||||||
|
|
||||||
def modeUpdate(self, mode=None):
|
def modeUpdate(self, mode=None):
|
||||||
""" Update the collection mode advanced setting.
|
""" Update the collection mode advanced setting.
|
||||||
|
|
||||||
|
@ -208,7 +248,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
}
|
}
|
||||||
key = mode_dict.get(mode)
|
key = mode_dict.get(mode)
|
||||||
if key is None:
|
if key is None:
|
||||||
raise BadRequest('Unknown collection mode : %s. Options %s' % (mode, list(mode_dict)))
|
raise BadRequest('Unknown collection mode: %s. Options %s' % (mode, list(mode_dict)))
|
||||||
self.editAdvanced(collectionMode=key)
|
self.editAdvanced(collectionMode=key)
|
||||||
|
|
||||||
def sortUpdate(self, sort=None):
|
def sortUpdate(self, sort=None):
|
||||||
|
@ -216,7 +256,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
sort (str): One of the following values:
|
sort (str): One of the following values:
|
||||||
"realease" (Order Collection by realease dates),
|
"release" (Order Collection by release dates),
|
||||||
"alpha" (Order Collection alphabetically),
|
"alpha" (Order Collection alphabetically),
|
||||||
"custom" (Custom collection order)
|
"custom" (Custom collection order)
|
||||||
|
|
||||||
|
@ -226,6 +266,9 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
|
|
||||||
collection.updateSort(mode="alpha")
|
collection.updateSort(mode="alpha")
|
||||||
"""
|
"""
|
||||||
|
if self.smart:
|
||||||
|
raise BadRequest('Cannot change collection order for a smart collection.')
|
||||||
|
|
||||||
sort_dict = {
|
sort_dict = {
|
||||||
'release': 0,
|
'release': 0,
|
||||||
'alpha': 1,
|
'alpha': 1,
|
||||||
|
@ -340,6 +383,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
}))
|
}))
|
||||||
self._server.query(key, method=self._server._session.put)
|
self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
|
@deprecated('use editTitle, editSortTitle, editContentRating, and editSummary instead')
|
||||||
def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs):
|
def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs):
|
||||||
""" Edit the collection.
|
""" Edit the collection.
|
||||||
|
|
||||||
|
@ -364,7 +408,7 @@ class Collection(PlexPartialObject, AdvancedSettingsMixin, ArtMixin, PosterMixin
|
||||||
args['summary.locked'] = 1
|
args['summary.locked'] = 1
|
||||||
|
|
||||||
args.update(kwargs)
|
args.update(kwargs)
|
||||||
super(Collection, self).edit(**args)
|
self._edit(**args)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
""" Delete the collection. """
|
""" Delete the collection. """
|
||||||
|
|
|
@ -62,4 +62,5 @@ def reset_base_headers():
|
||||||
'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME,
|
'X-Plex-Device-Name': plexapi.X_PLEX_DEVICE_NAME,
|
||||||
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
|
'X-Plex-Client-Identifier': plexapi.X_PLEX_IDENTIFIER,
|
||||||
'X-Plex-Sync-Version': '2',
|
'X-Plex-Sync-Version': '2',
|
||||||
|
'X-Plex-Features': 'external-media',
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
# Library version
|
# Library version
|
||||||
MAJOR_VERSION = 4
|
MAJOR_VERSION = 4
|
||||||
MINOR_VERSION = 9
|
MINOR_VERSION = 11
|
||||||
PATCH_VERSION = 2
|
PATCH_VERSION = 0
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
|
|
|
@ -15,7 +15,7 @@ import struct
|
||||||
class GDM:
|
class GDM:
|
||||||
"""Base class to discover GDM services.
|
"""Base class to discover GDM services.
|
||||||
|
|
||||||
Atrributes:
|
Attributes:
|
||||||
entries (List<dict>): List of server and/or client data discovered.
|
entries (List<dict>): List of server and/or client data discovered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class Library(PlexObject):
|
||||||
if elem.attrib.get('type') == cls.TYPE:
|
if elem.attrib.get('type') == cls.TYPE:
|
||||||
section = cls(self._server, elem, key)
|
section = cls(self._server, elem, key)
|
||||||
self._sectionsByID[section.key] = section
|
self._sectionsByID[section.key] = section
|
||||||
self._sectionsByTitle[section.title.lower()] = section
|
self._sectionsByTitle[section.title.lower().strip()] = section
|
||||||
|
|
||||||
def sections(self):
|
def sections(self):
|
||||||
""" Returns a list of all media sections in this library. Library sections may be any of
|
""" Returns a list of all media sections in this library. Library sections may be any of
|
||||||
|
@ -59,10 +59,11 @@ class Library(PlexObject):
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the section to return.
|
title (str): Title of the section to return.
|
||||||
"""
|
"""
|
||||||
if not self._sectionsByTitle or title not in self._sectionsByTitle:
|
normalized_title = title.lower().strip()
|
||||||
|
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
|
||||||
self._loadSections()
|
self._loadSections()
|
||||||
try:
|
try:
|
||||||
return self._sectionsByTitle[title.lower()]
|
return self._sectionsByTitle[normalized_title]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise NotFound('Invalid library section: %s' % title) from None
|
raise NotFound('Invalid library section: %s' % title) from None
|
||||||
|
|
||||||
|
@ -125,7 +126,7 @@ class Library(PlexObject):
|
||||||
def search(self, title=None, libtype=None, **kwargs):
|
def search(self, title=None, libtype=None, **kwargs):
|
||||||
""" Searching within a library section is much more powerful. It seems certain
|
""" Searching within a library section is much more powerful. It seems certain
|
||||||
attributes on the media objects can be targeted to filter this search down
|
attributes on the media objects can be targeted to filter this search down
|
||||||
a bit, but I havent found the documentation for it.
|
a bit, but I haven't found the documentation for it.
|
||||||
|
|
||||||
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
|
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
|
||||||
such as actor=<id> seem to work, but require you already know the id of the actor.
|
such as actor=<id> seem to work, but require you already know the id of the actor.
|
||||||
|
@ -396,7 +397,7 @@ class LibrarySection(PlexObject):
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
self.uuid = data.attrib.get('uuid')
|
self.uuid = data.attrib.get('uuid')
|
||||||
# Private attrs as we dont want a reload.
|
# Private attrs as we don't want a reload.
|
||||||
self._filterTypes = None
|
self._filterTypes = None
|
||||||
self._fieldTypes = None
|
self._fieldTypes = None
|
||||||
self._totalViewSize = None
|
self._totalViewSize = None
|
||||||
|
@ -599,12 +600,13 @@ class LibrarySection(PlexObject):
|
||||||
return self.fetchItem(key, title__iexact=title)
|
return self.fetchItem(key, title__iexact=title)
|
||||||
|
|
||||||
def getGuid(self, guid):
|
def getGuid(self, guid):
|
||||||
""" Returns the media item with the specified external IMDB, TMDB, or TVDB ID.
|
""" Returns the media item with the specified external Plex, IMDB, TMDB, or TVDB ID.
|
||||||
Note: Only available for the Plex Movie and Plex TV Series agents.
|
Note: Only available for the Plex Movie and Plex TV Series agents.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
guid (str): The external guid of the item to return.
|
guid (str): The external guid of the item to return.
|
||||||
Examples: IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
|
Examples: Plex ``plex://show/5d9c086c46115600200aa2fe``
|
||||||
|
IMDB ``imdb://tt0944947``, TMDB ``tmdb://1399``, TVDB ``tvdb://121361``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
|
:exc:`~plexapi.exceptions.NotFound`: The guid is not found in the library.
|
||||||
|
@ -613,21 +615,32 @@ class LibrarySection(PlexObject):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
result1 = library.getGuid('imdb://tt0944947')
|
result1 = library.getGuid('plex://show/5d9c086c46115600200aa2fe')
|
||||||
result2 = library.getGuid('tmdb://1399')
|
result2 = library.getGuid('imdb://tt0944947')
|
||||||
result3 = library.getGuid('tvdb://121361')
|
result3 = library.getGuid('tmdb://1399')
|
||||||
|
result4 = library.getGuid('tvdb://121361')
|
||||||
|
|
||||||
# Alternatively, create your own guid lookup dictionary for faster performance
|
# Alternatively, create your own guid lookup dictionary for faster performance
|
||||||
guidLookup = {guid.id: item for item in library.all() for guid in item.guids}
|
guidLookup = {}
|
||||||
result1 = guidLookup['imdb://tt0944947']
|
for item in library.all():
|
||||||
result2 = guidLookup['tmdb://1399']
|
guidLookup[item.guid] = item
|
||||||
result3 = guidLookup['tvdb://121361']
|
guidLookup.update({guid.id for guid in item.guids}}
|
||||||
|
|
||||||
|
result1 = guidLookup['plex://show/5d9c086c46115600200aa2fe']
|
||||||
|
result2 = guidLookup['imdb://tt0944947']
|
||||||
|
result4 = guidLookup['tmdb://1399']
|
||||||
|
result5 = guidLookup['tvdb://121361']
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dummy = self.search(maxresults=1)[0]
|
if guid.startswith('plex://'):
|
||||||
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-'))
|
result = self.search(guid=guid)[0]
|
||||||
return self.search(guid=match[0].guid)[0]
|
return result
|
||||||
|
else:
|
||||||
|
dummy = self.search(maxresults=1)[0]
|
||||||
|
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-'))
|
||||||
|
return self.search(guid=match[0].guid)[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise NotFound("Guid '%s' is not found in the library" % guid) from None
|
raise NotFound("Guid '%s' is not found in the library" % guid) from None
|
||||||
|
|
||||||
|
@ -1271,7 +1284,7 @@ class LibrarySection(PlexObject):
|
||||||
* See :func:`~plexapi.library.LibrarySection.listOperators` to get a list of all available operators.
|
* See :func:`~plexapi.library.LibrarySection.listOperators` to get a list of all available operators.
|
||||||
* See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values.
|
* See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values.
|
||||||
|
|
||||||
The following filter fields are just some examples of the possible filters. The list is not exaustive,
|
The following filter fields are just some examples of the possible filters. The list is not exhaustive,
|
||||||
and not all filters apply to all library types.
|
and not all filters apply to all library types.
|
||||||
|
|
||||||
* **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor.
|
* **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor.
|
||||||
|
@ -1334,7 +1347,7 @@ class LibrarySection(PlexObject):
|
||||||
Some filters may be prefixed by the ``libtype`` separated by a ``.`` (e.g. ``show.collection``,
|
Some filters may be prefixed by the ``libtype`` separated by a ``.`` (e.g. ``show.collection``,
|
||||||
``episode.title``, ``artist.style``, ``album.genre``, ``track.userRating``, etc.). This should not be
|
``episode.title``, ``artist.style``, ``album.genre``, ``track.userRating``, etc.). This should not be
|
||||||
confused with the ``libtype`` parameter. If no ``libtype`` prefix is provided, then the default library
|
confused with the ``libtype`` parameter. If no ``libtype`` prefix is provided, then the default library
|
||||||
type is assumed. For example, in a TV show library ``viewCout`` is assumed to be ``show.viewCount``.
|
type is assumed. For example, in a TV show library ``viewCount`` is assumed to be ``show.viewCount``.
|
||||||
If you want to filter using episode view count then you must specify ``episode.viewCount`` explicitly.
|
If you want to filter using episode view count then you must specify ``episode.viewCount`` explicitly.
|
||||||
In addition, if the filter does not exist for the default library type it will fallback to the most
|
In addition, if the filter does not exist for the default library type it will fallback to the most
|
||||||
specific ``libtype`` available. For example, ``show.unwatched`` does not exists so it will fallback to
|
specific ``libtype`` available. For example, ``show.unwatched`` does not exists so it will fallback to
|
||||||
|
@ -2236,16 +2249,61 @@ class FilteringType(PlexObject):
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
self.type = data.attrib.get('type')
|
self.type = data.attrib.get('type')
|
||||||
|
|
||||||
# Add additional manual sorts and fields which are available
|
self._librarySectionID = self._parent().key
|
||||||
|
|
||||||
|
# Add additional manual filters, sorts, and fields which are available
|
||||||
# but not exposed on the Plex server
|
# but not exposed on the Plex server
|
||||||
|
self.filters += self._manualFilters()
|
||||||
self.sorts += self._manualSorts()
|
self.sorts += self._manualSorts()
|
||||||
self.fields += self._manualFields()
|
self.fields += self._manualFields()
|
||||||
|
|
||||||
|
def _manualFilters(self):
|
||||||
|
""" Manually add additional filters which are available
|
||||||
|
but not exposed on the Plex server.
|
||||||
|
"""
|
||||||
|
# Filters: (filter, type, title)
|
||||||
|
additionalFilters = [
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.type == 'season':
|
||||||
|
additionalFilters.extend([
|
||||||
|
('label', 'string', 'Labels')
|
||||||
|
])
|
||||||
|
elif self.type == 'episode':
|
||||||
|
additionalFilters.extend([
|
||||||
|
('label', 'string', 'Labels')
|
||||||
|
])
|
||||||
|
elif self.type == 'artist':
|
||||||
|
additionalFilters.extend([
|
||||||
|
('label', 'string', 'Labels')
|
||||||
|
])
|
||||||
|
elif self.type == 'track':
|
||||||
|
additionalFilters.extend([
|
||||||
|
('label', 'string', 'Labels')
|
||||||
|
])
|
||||||
|
elif self.type == 'collection':
|
||||||
|
additionalFilters.extend([
|
||||||
|
('label', 'string', 'Labels')
|
||||||
|
])
|
||||||
|
|
||||||
|
manualFilters = []
|
||||||
|
for filterTag, filterType, filterTitle in additionalFilters:
|
||||||
|
filterKey = '/library/sections/%s/%s?type=%s' % (
|
||||||
|
self._librarySectionID, filterTag, utils.searchType(self.type)
|
||||||
|
)
|
||||||
|
filterXML = (
|
||||||
|
'<Filter filter="%s" filterType="%s" key="%s" title="%s" type="filter" />'
|
||||||
|
% (filterTag, filterType, filterKey, filterTitle)
|
||||||
|
)
|
||||||
|
manualFilters.append(self._manuallyLoadXML(filterXML, FilteringFilter))
|
||||||
|
|
||||||
|
return manualFilters
|
||||||
|
|
||||||
def _manualSorts(self):
|
def _manualSorts(self):
|
||||||
""" Manually add additional sorts which are available
|
""" Manually add additional sorts which are available
|
||||||
but not exposed on the Plex server.
|
but not exposed on the Plex server.
|
||||||
"""
|
"""
|
||||||
# Sorts: key, dir, title
|
# Sorts: (key, dir, title)
|
||||||
additionalSorts = [
|
additionalSorts = [
|
||||||
('guid', 'asc', 'Guid'),
|
('guid', 'asc', 'Guid'),
|
||||||
('id', 'asc', 'Rating Key'),
|
('id', 'asc', 'Rating Key'),
|
||||||
|
@ -2275,8 +2333,10 @@ class FilteringType(PlexObject):
|
||||||
|
|
||||||
manualSorts = []
|
manualSorts = []
|
||||||
for sortField, sortDir, sortTitle in additionalSorts:
|
for sortField, sortDir, sortTitle in additionalSorts:
|
||||||
sortXML = ('<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />'
|
sortXML = (
|
||||||
% (sortDir, sortField, sortField, sortTitle))
|
'<Sort defaultDirection="%s" descKey="%s:desc" key="%s" title="%s" />'
|
||||||
|
% (sortDir, sortField, sortField, sortTitle)
|
||||||
|
)
|
||||||
manualSorts.append(self._manuallyLoadXML(sortXML, FilteringSort))
|
manualSorts.append(self._manuallyLoadXML(sortXML, FilteringSort))
|
||||||
|
|
||||||
return manualSorts
|
return manualSorts
|
||||||
|
@ -2285,7 +2345,7 @@ class FilteringType(PlexObject):
|
||||||
""" Manually add additional fields which are available
|
""" Manually add additional fields which are available
|
||||||
but not exposed on the Plex server.
|
but not exposed on the Plex server.
|
||||||
"""
|
"""
|
||||||
# Fields: key, type, title
|
# Fields: (key, type, title)
|
||||||
additionalFields = [
|
additionalFields = [
|
||||||
('guid', 'string', 'Guid'),
|
('guid', 'string', 'Guid'),
|
||||||
('id', 'integer', 'Rating Key'),
|
('id', 'integer', 'Rating Key'),
|
||||||
|
@ -2311,31 +2371,41 @@ class FilteringType(PlexObject):
|
||||||
additionalFields.extend([
|
additionalFields.extend([
|
||||||
('addedAt', 'date', 'Date Season Added'),
|
('addedAt', 'date', 'Date Season Added'),
|
||||||
('unviewedLeafCount', 'integer', 'Episode Unplayed Count'),
|
('unviewedLeafCount', 'integer', 'Episode Unplayed Count'),
|
||||||
('year', 'integer', 'Season Year')
|
('year', 'integer', 'Season Year'),
|
||||||
|
('label', 'tag', 'Label')
|
||||||
])
|
])
|
||||||
elif self.type == 'episode':
|
elif self.type == 'episode':
|
||||||
additionalFields.extend([
|
additionalFields.extend([
|
||||||
('audienceRating', 'integer', 'Audience Rating'),
|
('audienceRating', 'integer', 'Audience Rating'),
|
||||||
('duration', 'integer', 'Duration'),
|
('duration', 'integer', 'Duration'),
|
||||||
('rating', 'integer', 'Critic Rating'),
|
('rating', 'integer', 'Critic Rating'),
|
||||||
('viewOffset', 'integer', 'View Offset')
|
('viewOffset', 'integer', 'View Offset'),
|
||||||
|
('label', 'tag', 'Label')
|
||||||
|
])
|
||||||
|
elif self.type == 'artist':
|
||||||
|
additionalFields.extend([
|
||||||
|
('label', 'tag', 'Label')
|
||||||
])
|
])
|
||||||
elif self.type == 'track':
|
elif self.type == 'track':
|
||||||
additionalFields.extend([
|
additionalFields.extend([
|
||||||
('duration', 'integer', 'Duration'),
|
('duration', 'integer', 'Duration'),
|
||||||
('viewOffset', 'integer', 'View Offset')
|
('viewOffset', 'integer', 'View Offset'),
|
||||||
|
('label', 'tag', 'Label')
|
||||||
])
|
])
|
||||||
elif self.type == 'collection':
|
elif self.type == 'collection':
|
||||||
additionalFields.extend([
|
additionalFields.extend([
|
||||||
('addedAt', 'date', 'Date Added')
|
('addedAt', 'date', 'Date Added'),
|
||||||
|
('label', 'tag', 'Label')
|
||||||
])
|
])
|
||||||
|
|
||||||
prefix = '' if self.type == 'movie' else self.type + '.'
|
prefix = '' if self.type == 'movie' else self.type + '.'
|
||||||
|
|
||||||
manualFields = []
|
manualFields = []
|
||||||
for field, fieldType, fieldTitle in additionalFields:
|
for field, fieldType, fieldTitle in additionalFields:
|
||||||
fieldXML = ('<Field key="%s%s" title="%s" type="%s"/>'
|
fieldXML = (
|
||||||
% (prefix, field, fieldTitle, fieldType))
|
'<Field key="%s%s" title="%s" type="%s"/>'
|
||||||
|
% (prefix, field, fieldTitle, fieldType)
|
||||||
|
)
|
||||||
manualFields.append(self._manuallyLoadXML(fieldXML, FilteringField))
|
manualFields.append(self._manuallyLoadXML(fieldXML, FilteringField))
|
||||||
|
|
||||||
return manualFields
|
return manualFields
|
||||||
|
|
|
@ -39,7 +39,7 @@ class Media(PlexObject):
|
||||||
|
|
||||||
<Photo_only_attributes>: The following attributes are only available for photos.
|
<Photo_only_attributes>: The following attributes are only available for photos.
|
||||||
|
|
||||||
* aperture (str): The apeture used to take the photo.
|
* aperture (str): The aperture used to take the photo.
|
||||||
* exposure (str): The exposure used to take the photo.
|
* exposure (str): The exposure used to take the photo.
|
||||||
* iso (int): The iso used to take the photo.
|
* iso (int): The iso used to take the photo.
|
||||||
* lens (str): The lens used to take the photo.
|
* lens (str): The lens used to take the photo.
|
||||||
|
@ -93,7 +93,7 @@ class Media(PlexObject):
|
||||||
try:
|
try:
|
||||||
return self._server.query(part, method=self._server._session.delete)
|
return self._server.query(part, method=self._server._session.delete)
|
||||||
except BadRequest:
|
except BadRequest:
|
||||||
log.error("Failed to delete %s. This could be because you havn't allowed "
|
log.error("Failed to delete %s. This could be because you haven't allowed "
|
||||||
"items to be deleted" % part)
|
"items to be deleted" % part)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ class MediaPartStream(PlexObject):
|
||||||
id (int): The unique ID for this stream on the server.
|
id (int): The unique ID for this stream on the server.
|
||||||
index (int): The index of the stream.
|
index (int): The index of the stream.
|
||||||
language (str): The language of the stream (ex: English, ไทย).
|
language (str): The language of the stream (ex: English, ไทย).
|
||||||
languageCode (str): The Ascii language code of the stream (ex: eng, tha).
|
languageCode (str): The ASCII language code of the stream (ex: eng, tha).
|
||||||
requiredBandwidths (str): The required bandwidths to stream the file.
|
requiredBandwidths (str): The required bandwidths to stream the file.
|
||||||
selected (bool): True if this stream is selected.
|
selected (bool): True if this stream is selected.
|
||||||
streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`,
|
streamType (int): The stream type (1= :class:`~plexapi.media.VideoStream`,
|
||||||
|
@ -283,8 +283,8 @@ class VideoStream(MediaPartStream):
|
||||||
duration (int): The duration of video stream in milliseconds.
|
duration (int): The duration of video stream in milliseconds.
|
||||||
frameRate (float): The frame rate of the video stream (ex: 23.976).
|
frameRate (float): The frame rate of the video stream (ex: 23.976).
|
||||||
frameRateMode (str): The frame rate mode of the video stream.
|
frameRateMode (str): The frame rate mode of the video stream.
|
||||||
hasScallingMatrix (bool): True if video stream has a scaling matrix.
|
hasScalingMatrix (bool): True if video stream has a scaling matrix.
|
||||||
height (int): The hight of the video stream in pixels (ex: 1080).
|
height (int): The height of the video stream in pixels (ex: 1080).
|
||||||
level (int): The codec encoding level of the video stream (ex: 41).
|
level (int): The codec encoding level of the video stream (ex: 41).
|
||||||
profile (str): The profile of the video stream (ex: asp).
|
profile (str): The profile of the video stream (ex: asp).
|
||||||
pixelAspectRatio (str): The pixel aspect ratio of the video stream.
|
pixelAspectRatio (str): The pixel aspect ratio of the video stream.
|
||||||
|
@ -323,7 +323,7 @@ class VideoStream(MediaPartStream):
|
||||||
self.duration = utils.cast(int, data.attrib.get('duration'))
|
self.duration = utils.cast(int, data.attrib.get('duration'))
|
||||||
self.frameRate = utils.cast(float, data.attrib.get('frameRate'))
|
self.frameRate = utils.cast(float, data.attrib.get('frameRate'))
|
||||||
self.frameRateMode = data.attrib.get('frameRateMode')
|
self.frameRateMode = data.attrib.get('frameRateMode')
|
||||||
self.hasScallingMatrix = utils.cast(bool, data.attrib.get('hasScallingMatrix'))
|
self.hasScalingMatrix = utils.cast(bool, data.attrib.get('hasScalingMatrix'))
|
||||||
self.height = utils.cast(int, data.attrib.get('height'))
|
self.height = utils.cast(int, data.attrib.get('height'))
|
||||||
self.level = utils.cast(int, data.attrib.get('level'))
|
self.level = utils.cast(int, data.attrib.get('level'))
|
||||||
self.profile = data.attrib.get('profile')
|
self.profile = data.attrib.get('profile')
|
||||||
|
@ -400,7 +400,7 @@ class SubtitleStream(MediaPartStream):
|
||||||
container (str): The container of the subtitle stream.
|
container (str): The container of the subtitle stream.
|
||||||
forced (bool): True if this is a forced subtitle.
|
forced (bool): True if this is a forced subtitle.
|
||||||
format (str): The format of the subtitle stream (ex: srt).
|
format (str): The format of the subtitle stream (ex: srt).
|
||||||
headerCommpression (str): The header compression of the subtitle stream.
|
headerCompression (str): The header compression of the subtitle stream.
|
||||||
transient (str): Unknown.
|
transient (str): Unknown.
|
||||||
"""
|
"""
|
||||||
TAG = 'Stream'
|
TAG = 'Stream'
|
||||||
|
@ -468,7 +468,7 @@ class TranscodeSession(PlexObject):
|
||||||
audioDecision (str): The transcode decision for the audio stream.
|
audioDecision (str): The transcode decision for the audio stream.
|
||||||
complete (bool): True if the transcode is complete.
|
complete (bool): True if the transcode is complete.
|
||||||
container (str): The container of the transcoded media.
|
container (str): The container of the transcoded media.
|
||||||
context (str): The context for the transcode sesson.
|
context (str): The context for the transcode session.
|
||||||
duration (int): The duration of the transcoded media in milliseconds.
|
duration (int): The duration of the transcoded media in milliseconds.
|
||||||
height (int): The height of the transcoded media in pixels.
|
height (int): The height of the transcoded media in pixels.
|
||||||
key (str): API URL (ex: /transcode/sessions/<id>).
|
key (str): API URL (ex: /transcode/sessions/<id>).
|
||||||
|
@ -572,7 +572,7 @@ class Optimized(PlexObject):
|
||||||
"""
|
"""
|
||||||
key = '%s/%s/items' % (self._initpath, self.id)
|
key = '%s/%s/items' % (self._initpath, self.id)
|
||||||
return self.fetchItems(key)
|
return self.fetchItems(key)
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
""" Remove an Optimized item"""
|
""" Remove an Optimized item"""
|
||||||
key = '%s/%s' % (self._initpath, self.id)
|
key = '%s/%s' % (self._initpath, self.id)
|
||||||
|
@ -893,7 +893,7 @@ class Guid(GuidTag):
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Review(PlexObject):
|
class Review(PlexObject):
|
||||||
""" Represents a single Review for a Movie.
|
""" Represents a single Review for a Movie.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Review'
|
TAG (str): 'Review'
|
||||||
filter (str): filter for reviews?
|
filter (str): filter for reviews?
|
||||||
|
@ -917,19 +917,17 @@ class Review(PlexObject):
|
||||||
self.text = data.attrib.get('text')
|
self.text = data.attrib.get('text')
|
||||||
|
|
||||||
|
|
||||||
class BaseImage(PlexObject):
|
class BaseResource(PlexObject):
|
||||||
""" Base class for all Art, Banner, and Poster objects.
|
""" Base class for all Art, Banner, Poster, and Theme objects.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Photo'
|
TAG (str): 'Photo' or 'Track'
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
provider (str): The source of the poster or art.
|
provider (str): The source of the art or poster, None for Theme objects.
|
||||||
ratingKey (str): Unique key identifying the poster or art.
|
ratingKey (str): Unique key identifying the resource.
|
||||||
selected (bool): True if the poster or art is currently selected.
|
selected (bool): True if the resource is currently selected.
|
||||||
thumb (str): The URL to retrieve the poster or art thumbnail.
|
thumb (str): The URL to retrieve the resource thumbnail.
|
||||||
"""
|
"""
|
||||||
TAG = 'Photo'
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
self.key = data.attrib.get('key')
|
self.key = data.attrib.get('key')
|
||||||
|
@ -947,16 +945,24 @@ class BaseImage(PlexObject):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Art(BaseImage):
|
class Art(BaseResource):
|
||||||
""" Represents a single Art object. """
|
""" Represents a single Art object. """
|
||||||
|
TAG = 'Photo'
|
||||||
|
|
||||||
|
|
||||||
class Banner(BaseImage):
|
class Banner(BaseResource):
|
||||||
""" Represents a single Banner object. """
|
""" Represents a single Banner object. """
|
||||||
|
TAG = 'Photo'
|
||||||
|
|
||||||
|
|
||||||
class Poster(BaseImage):
|
class Poster(BaseResource):
|
||||||
""" Represents a single Poster object. """
|
""" Represents a single Poster object. """
|
||||||
|
TAG = 'Photo'
|
||||||
|
|
||||||
|
|
||||||
|
class Theme(BaseResource):
|
||||||
|
""" Represents a single Theme object. """
|
||||||
|
TAG = 'Track'
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
|
@ -1106,3 +1112,41 @@ class AgentMediaType(Agent):
|
||||||
@deprecated('use "languageCodes" instead')
|
@deprecated('use "languageCodes" instead')
|
||||||
def languageCode(self):
|
def languageCode(self):
|
||||||
return self.languageCodes
|
return self.languageCodes
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Availability(PlexObject):
|
||||||
|
""" Represents a single online streaming service Availability.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Availability'
|
||||||
|
country (str): The streaming service country.
|
||||||
|
offerType (str): Subscription, buy, or rent from the streaming service.
|
||||||
|
platform (str): The platform slug for the streaming service.
|
||||||
|
platformColorThumb (str): Thumbnail icon for the streaming service.
|
||||||
|
platformInfo (str): The streaming service platform info.
|
||||||
|
platformUrl (str): The URL to the media on the streaming service.
|
||||||
|
price (float): The price to buy or rent from the streaming service.
|
||||||
|
priceDescription (str): The display price to buy or rent from the streaming service.
|
||||||
|
quality (str): The video quality on the streaming service.
|
||||||
|
title (str): The title of the streaming service.
|
||||||
|
url (str): The Plex availability URL.
|
||||||
|
"""
|
||||||
|
TAG = 'Availability'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<{self.__class__.__name__}:{self.platform}:{self.offerType}>'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self._data = data
|
||||||
|
self.country = data.attrib.get('country')
|
||||||
|
self.offerType = data.attrib.get('offerType')
|
||||||
|
self.platform = data.attrib.get('platform')
|
||||||
|
self.platformColorThumb = data.attrib.get('platformColorThumb')
|
||||||
|
self.platformInfo = data.attrib.get('platformInfo')
|
||||||
|
self.platformUrl = data.attrib.get('platformUrl')
|
||||||
|
self.price = utils.cast(float, data.attrib.get('price'))
|
||||||
|
self.priceDescription = data.attrib.get('priceDescription')
|
||||||
|
self.quality = data.attrib.get('quality')
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
self.url = data.attrib.get('url')
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import copy
|
import copy
|
||||||
|
import html
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
@ -52,7 +53,7 @@ class MyPlexAccount(PlexObject):
|
||||||
roles: (List<str>) Lit of account roles. Plexpass membership listed here.
|
roles: (List<str>) Lit of account roles. Plexpass membership listed here.
|
||||||
scrobbleTypes (str): Description
|
scrobbleTypes (str): Description
|
||||||
secure (bool): Description
|
secure (bool): Description
|
||||||
subscriptionActive (bool): True if your subsctiption is active.
|
subscriptionActive (bool): True if your subscription is active.
|
||||||
subscriptionFeatures: (List<str>) List of features allowed on your subscription.
|
subscriptionFeatures: (List<str>) List of features allowed on your subscription.
|
||||||
subscriptionPlan (str): Name of subscription plan.
|
subscriptionPlan (str): Name of subscription plan.
|
||||||
subscriptionStatus (str): String representation of `subscriptionActive`.
|
subscriptionStatus (str): String representation of `subscriptionActive`.
|
||||||
|
@ -72,14 +73,12 @@ class MyPlexAccount(PlexObject):
|
||||||
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
|
REMOVEHOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete
|
||||||
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
|
SIGNIN = 'https://plex.tv/users/sign_in.xml' # get with auth
|
||||||
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
|
WEBHOOKS = 'https://plex.tv/api/v2/user/webhooks' # get, post with data
|
||||||
OPTOUTS = 'https://plex.tv/api/v2/user/%(userUUID)s/settings/opt_outs' # get
|
OPTOUTS = 'https://plex.tv/api/v2/user/{userUUID}/settings/opt_outs' # get
|
||||||
LINK = 'https://plex.tv/api/v2/pins/link' # put
|
LINK = 'https://plex.tv/api/v2/pins/link' # put
|
||||||
# Hub sections
|
# Hub sections
|
||||||
VOD = 'https://vod.provider.plex.tv/' # get
|
VOD = 'https://vod.provider.plex.tv' # get
|
||||||
WEBSHOWS = 'https://webshows.provider.plex.tv/' # get
|
MUSIC = 'https://music.provider.plex.tv' # get
|
||||||
NEWS = 'https://news.provider.plex.tv/' # get
|
METADATA = 'https://metadata.provider.plex.tv'
|
||||||
PODCASTS = 'https://podcasts.provider.plex.tv/' # get
|
|
||||||
MUSIC = 'https://music.provider.plex.tv/' # get
|
|
||||||
# Key may someday switch to the following url. For now the current value works.
|
# Key may someday switch to the following url. For now the current value works.
|
||||||
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
|
# https://plex.tv/api/v2/user?X-Plex-Token={token}&X-Plex-Client-Identifier={clientId}
|
||||||
key = 'https://plex.tv/users/account'
|
key = 'https://plex.tv/users/account'
|
||||||
|
@ -182,6 +181,8 @@ class MyPlexAccount(PlexObject):
|
||||||
raise NotFound(message)
|
raise NotFound(message)
|
||||||
else:
|
else:
|
||||||
raise BadRequest(message)
|
raise BadRequest(message)
|
||||||
|
if headers.get('Accept') == 'application/json':
|
||||||
|
return response.json()
|
||||||
data = response.text.encode('utf8')
|
data = response.text.encode('utf8')
|
||||||
return ElementTree.fromstring(data) if data.strip() else None
|
return ElementTree.fromstring(data) if data.strip() else None
|
||||||
|
|
||||||
|
@ -228,7 +229,7 @@ class MyPlexAccount(PlexObject):
|
||||||
of the user to be added.
|
of the user to be added.
|
||||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||||
containing the library sections to share.
|
containing the library sections to share.
|
||||||
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names
|
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
|
||||||
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
||||||
allowSync (Bool): Set True to allow user to sync content.
|
allowSync (Bool): Set True to allow user to sync content.
|
||||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||||
|
@ -268,7 +269,7 @@ class MyPlexAccount(PlexObject):
|
||||||
of the user to be added.
|
of the user to be added.
|
||||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||||
containing the library sections to share.
|
containing the library sections to share.
|
||||||
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names
|
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
|
||||||
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
||||||
allowSync (Bool): Set True to allow user to sync content.
|
allowSync (Bool): Set True to allow user to sync content.
|
||||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||||
|
@ -317,7 +318,7 @@ class MyPlexAccount(PlexObject):
|
||||||
of the user to be added.
|
of the user to be added.
|
||||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||||
containing the library sections to share.
|
containing the library sections to share.
|
||||||
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names
|
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
|
||||||
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
||||||
allowSync (Bool): Set True to allow user to sync content.
|
allowSync (Bool): Set True to allow user to sync content.
|
||||||
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
allowCameraUpload (Bool): Set True to allow user to upload photos.
|
||||||
|
@ -420,7 +421,7 @@ class MyPlexAccount(PlexObject):
|
||||||
of the user to be updated.
|
of the user to be updated.
|
||||||
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
server (:class:`~plexapi.server.PlexServer`): `PlexServer` object, or machineIdentifier
|
||||||
containing the library sections to share.
|
containing the library sections to share.
|
||||||
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objecs, or names
|
sections (List<:class:`~plexapi.library.LibrarySection`>): List of `LibrarySection` objects, or names
|
||||||
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
to be shared (default None). `sections` must be defined in order to update shared libraries.
|
||||||
removeSections (Bool): Set True to remove all shares. Supersedes sections.
|
removeSections (Bool): Set True to remove all shares. Supersedes sections.
|
||||||
allowSync (Bool): Set True to allow user to sync content.
|
allowSync (Bool): Set True to allow user to sync content.
|
||||||
|
@ -565,7 +566,7 @@ class MyPlexAccount(PlexObject):
|
||||||
""" Converts friend filters to a string representation for transport. """
|
""" Converts friend filters to a string representation for transport. """
|
||||||
values = []
|
values = []
|
||||||
for key, vals in filterDict.items():
|
for key, vals in filterDict.items():
|
||||||
if key not in ('contentRating', 'label'):
|
if key not in ('contentRating', 'label', 'contentRating!', 'label!'):
|
||||||
raise BadRequest('Unknown filter key: %s', key)
|
raise BadRequest('Unknown filter key: %s', key)
|
||||||
values.append('%s=%s' % (key, '%2C'.join(vals)))
|
values.append('%s=%s' % (key, '%2C'.join(vals)))
|
||||||
return '|'.join(values)
|
return '|'.join(values)
|
||||||
|
@ -614,7 +615,7 @@ class MyPlexAccount(PlexObject):
|
||||||
clientId (str): an identifier of a client to query SyncItems for.
|
clientId (str): an identifier of a client to query SyncItems for.
|
||||||
|
|
||||||
If both `client` and `clientId` provided the client would be preferred.
|
If both `client` and `clientId` provided the client would be preferred.
|
||||||
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier.
|
If neither `client` nor `clientId` provided the clientId would be set to current clients's identifier.
|
||||||
"""
|
"""
|
||||||
if client:
|
if client:
|
||||||
clientId = client.clientIdentifier
|
clientId = client.clientIdentifier
|
||||||
|
@ -635,14 +636,14 @@ class MyPlexAccount(PlexObject):
|
||||||
sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set.
|
sync_item (:class:`~plexapi.sync.SyncItem`): prepared SyncItem object with all fields set.
|
||||||
|
|
||||||
If both `client` and `clientId` provided the client would be preferred.
|
If both `client` and `clientId` provided the client would be preferred.
|
||||||
If neither `client` nor `clientId` provided the clientId would be set to current clients`s identifier.
|
If neither `client` nor `clientId` provided the clientId would be set to current clients's identifier.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
:class:`~plexapi.sync.SyncItem`: an instance of created syncItem.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: When client with provided clientId wasn`t found.
|
:exc:`~plexapi.exceptions.BadRequest`: When client with provided clientId wasn't found.
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: Provided client doesn`t provides `sync-target`.
|
:exc:`~plexapi.exceptions.BadRequest`: Provided client doesn't provides `sync-target`.
|
||||||
"""
|
"""
|
||||||
if not client and not clientId:
|
if not client and not clientId:
|
||||||
clientId = X_PLEX_IDENTIFIER
|
clientId = X_PLEX_IDENTIFIER
|
||||||
|
@ -657,7 +658,7 @@ class MyPlexAccount(PlexObject):
|
||||||
raise BadRequest('Unable to find client by clientId=%s', clientId)
|
raise BadRequest('Unable to find client by clientId=%s', clientId)
|
||||||
|
|
||||||
if 'sync-target' not in client.provides:
|
if 'sync-target' not in client.provides:
|
||||||
raise BadRequest('Received client doesn`t provides sync-target')
|
raise BadRequest("Received client doesn't provides sync-target")
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'SyncItem[title]': sync_item.title,
|
'SyncItem[title]': sync_item.title,
|
||||||
|
@ -698,6 +699,7 @@ class MyPlexAccount(PlexObject):
|
||||||
|
|
||||||
def history(self, maxresults=9999999, mindate=None):
|
def history(self, maxresults=9999999, mindate=None):
|
||||||
""" Get Play History for all library sections on all servers for the owner.
|
""" Get Play History for all library sections on all servers for the owner.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
maxresults (int): Only return the specified number of results (optional).
|
maxresults (int): Only return the specified number of results (optional).
|
||||||
mindate (datetime): Min datetime to return results from.
|
mindate (datetime): Min datetime to return results from.
|
||||||
|
@ -709,47 +711,155 @@ class MyPlexAccount(PlexObject):
|
||||||
hist.extend(conn.history(maxresults=maxresults, mindate=mindate, accountID=1))
|
hist.extend(conn.history(maxresults=maxresults, mindate=mindate, accountID=1))
|
||||||
return hist
|
return hist
|
||||||
|
|
||||||
|
def onlineMediaSources(self):
|
||||||
|
""" Returns a list of user account Online Media Sources settings :class:`~plexapi.myplex.AccountOptOut`
|
||||||
|
"""
|
||||||
|
url = self.OPTOUTS.format(userUUID=self.uuid)
|
||||||
|
elem = self.query(url)
|
||||||
|
return self.findItems(elem, cls=AccountOptOut, etag='optOut')
|
||||||
|
|
||||||
def videoOnDemand(self):
|
def videoOnDemand(self):
|
||||||
""" Returns a list of VOD Hub items :class:`~plexapi.library.Hub`
|
""" Returns a list of VOD Hub items :class:`~plexapi.library.Hub`
|
||||||
"""
|
"""
|
||||||
req = requests.get(self.VOD + 'hubs/', headers={'X-Plex-Token': self._token})
|
data = self.query(f'{self.VOD}/hubs')
|
||||||
elem = ElementTree.fromstring(req.text)
|
return self.findItems(data)
|
||||||
return self.findItems(elem)
|
|
||||||
|
|
||||||
def webShows(self):
|
|
||||||
""" Returns a list of Webshow Hub items :class:`~plexapi.library.Hub`
|
|
||||||
"""
|
|
||||||
req = requests.get(self.WEBSHOWS + 'hubs/', headers={'X-Plex-Token': self._token})
|
|
||||||
elem = ElementTree.fromstring(req.text)
|
|
||||||
return self.findItems(elem)
|
|
||||||
|
|
||||||
def news(self):
|
|
||||||
""" Returns a list of News Hub items :class:`~plexapi.library.Hub`
|
|
||||||
"""
|
|
||||||
req = requests.get(self.NEWS + 'hubs/sections/all', headers={'X-Plex-Token': self._token})
|
|
||||||
elem = ElementTree.fromstring(req.text)
|
|
||||||
return self.findItems(elem)
|
|
||||||
|
|
||||||
def podcasts(self):
|
|
||||||
""" Returns a list of Podcasts Hub items :class:`~plexapi.library.Hub`
|
|
||||||
"""
|
|
||||||
req = requests.get(self.PODCASTS + 'hubs/', headers={'X-Plex-Token': self._token})
|
|
||||||
elem = ElementTree.fromstring(req.text)
|
|
||||||
return self.findItems(elem)
|
|
||||||
|
|
||||||
def tidal(self):
|
def tidal(self):
|
||||||
""" Returns a list of tidal Hub items :class:`~plexapi.library.Hub`
|
""" Returns a list of tidal Hub items :class:`~plexapi.library.Hub`
|
||||||
"""
|
"""
|
||||||
req = requests.get(self.MUSIC + 'hubs/', headers={'X-Plex-Token': self._token})
|
data = self.query(f'{self.MUSIC}/hubs')
|
||||||
elem = ElementTree.fromstring(req.text)
|
return self.findItems(data)
|
||||||
return self.findItems(elem)
|
|
||||||
|
def watchlist(self, filter=None, sort=None, libtype=None, **kwargs):
|
||||||
|
""" Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` items in the user's watchlist.
|
||||||
|
Note: The objects returned are from Plex's online metadata. To get the matching item on a Plex server,
|
||||||
|
search for the media using the guid.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
filter (str, optional): 'available' or 'released' to only return items that are available or released,
|
||||||
|
otherwise return all items.
|
||||||
|
sort (str, optional): In the format ``field:dir``. Available fields are ``watchlistedAt`` (Added At),
|
||||||
|
``titleSort`` (Title), ``originallyAvailableAt`` (Release Date), or ``rating`` (Critic Rating).
|
||||||
|
``dir`` can be ``asc`` or ``desc``.
|
||||||
|
libtype (str, optional): 'movie' or 'show' to only return movies or shows, otherwise return all items.
|
||||||
|
**kwargs (dict): Additional custom filters to apply to the search results.
|
||||||
|
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Watchlist for released movies sorted by critic rating in descending order
|
||||||
|
watchlist = account.watchlist(filter='released', sort='rating:desc', libtype='movie')
|
||||||
|
item = watchlist[0] # First item in the watchlist
|
||||||
|
|
||||||
|
# Search for the item on a Plex server
|
||||||
|
result = plex.library.search(guid=item.guid, libtype=item.type)
|
||||||
|
|
||||||
def onlineMediaSources(self):
|
|
||||||
""" Returns a list of user account Online Media Sources settings :class:`~plexapi.myplex.AccountOptOut`
|
|
||||||
"""
|
"""
|
||||||
url = self.OPTOUTS % {'userUUID': self.uuid}
|
params = {
|
||||||
elem = self.query(url)
|
'includeCollections': 1,
|
||||||
return self.findItems(elem, cls=AccountOptOut, etag='optOut')
|
'includeExternalMedia': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if not filter:
|
||||||
|
filter = 'all'
|
||||||
|
if sort:
|
||||||
|
params['sort'] = sort
|
||||||
|
if libtype:
|
||||||
|
params['type'] = utils.searchType(libtype)
|
||||||
|
|
||||||
|
params.update(kwargs)
|
||||||
|
data = self.query(f'{self.METADATA}/library/sections/watchlist/{filter}', params=params)
|
||||||
|
return self.findItems(data)
|
||||||
|
|
||||||
|
def onWatchlist(self, item):
|
||||||
|
""" Returns True if the item is on the user's watchlist.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
item (:class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`): Item to check
|
||||||
|
if it is on the user's watchlist.
|
||||||
|
"""
|
||||||
|
ratingKey = item.guid.rsplit('/', 1)[-1]
|
||||||
|
data = self.query(f"{self.METADATA}/library/metadata/{ratingKey}/userState")
|
||||||
|
return bool(data.find('UserState').attrib.get('watchlistedAt'))
|
||||||
|
|
||||||
|
def addToWatchlist(self, items):
|
||||||
|
""" Add media items to the user's watchlist
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
items (List): List of :class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`
|
||||||
|
objects to be added to the watchlist.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:exc:`~plexapi.exceptions.BadRequest`: When trying to add invalid or existing
|
||||||
|
media to the watchlist.
|
||||||
|
"""
|
||||||
|
if not isinstance(items, list):
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if self.onWatchlist(item):
|
||||||
|
raise BadRequest('"%s" is already on the watchlist' % item.title)
|
||||||
|
ratingKey = item.guid.rsplit('/', 1)[-1]
|
||||||
|
self.query(f'{self.METADATA}/actions/addToWatchlist?ratingKey={ratingKey}', method=self._session.put)
|
||||||
|
|
||||||
|
def removeFromWatchlist(self, items):
|
||||||
|
""" Remove media items from the user's watchlist
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
items (List): List of :class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`
|
||||||
|
objects to be added to the watchlist.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:exc:`~plexapi.exceptions.BadRequest`: When trying to remove invalid or non-existing
|
||||||
|
media to the watchlist.
|
||||||
|
"""
|
||||||
|
if not isinstance(items, list):
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not self.onWatchlist(item):
|
||||||
|
raise BadRequest('"%s" is not on the watchlist' % item.title)
|
||||||
|
ratingKey = item.guid.rsplit('/', 1)[-1]
|
||||||
|
self.query(f'{self.METADATA}/actions/removeFromWatchlist?ratingKey={ratingKey}', method=self._session.put)
|
||||||
|
|
||||||
|
def searchDiscover(self, query, limit=30):
|
||||||
|
""" Search for movies and TV shows in Discover.
|
||||||
|
Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` objects.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
query (str): Search query.
|
||||||
|
limit (int, optional): Limit to the specified number of results. Default 30.
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
'query': query,
|
||||||
|
'limit ': limit,
|
||||||
|
'searchTypes': 'movies,tv',
|
||||||
|
'includeMetadata': 1
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self.query(f'{self.METADATA}/library/search', headers=headers, params=params)
|
||||||
|
searchResults = data['MediaContainer'].get('SearchResult', [])
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for result in searchResults:
|
||||||
|
metadata = result['Metadata']
|
||||||
|
type = metadata['type']
|
||||||
|
if type == 'movie':
|
||||||
|
tag = 'Video'
|
||||||
|
elif type == 'show':
|
||||||
|
tag = 'Directory'
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
attrs = ''.join(f'{k}="{html.escape(str(v))}" ' for k, v in metadata.items())
|
||||||
|
xml = f'<{tag} {attrs}/>'
|
||||||
|
results.append(self._manuallyLoadXML(xml))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def link(self, pin):
|
def link(self, pin):
|
||||||
""" Link a device to the account using a pin code.
|
""" Link a device to the account using a pin code.
|
||||||
|
@ -790,7 +900,7 @@ class MyPlexUser(PlexObject):
|
||||||
restricted (str): Unknown.
|
restricted (str): Unknown.
|
||||||
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
|
servers (List<:class:`~plexapi.myplex.<MyPlexServerShare`>)): Servers shared with the user.
|
||||||
thumb (str): Link to the users avatar.
|
thumb (str): Link to the users avatar.
|
||||||
title (str): Seems to be an aliad for username.
|
title (str): Seems to be an alias for username.
|
||||||
username (str): User's username.
|
username (str): User's username.
|
||||||
"""
|
"""
|
||||||
TAG = 'User'
|
TAG = 'User'
|
||||||
|
@ -1103,7 +1213,7 @@ class MyPlexResource(PlexObject):
|
||||||
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
:exc:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
|
||||||
"""
|
"""
|
||||||
connections = self.preferred_connections(ssl, timeout, locations, schemes)
|
connections = self.preferred_connections(ssl, timeout, locations, schemes)
|
||||||
# Try connecting to all known resource connections in parellel, but
|
# Try connecting to all known resource connections in parallel, but
|
||||||
# only return the first server (in order) that provides a response.
|
# only return the first server (in order) that provides a response.
|
||||||
cls = PlexServer if 'server' in self.provides else PlexClient
|
cls = PlexServer if 'server' in self.provides else PlexClient
|
||||||
listargs = [[cls, url, self.accessToken, timeout] for url in connections]
|
listargs = [[cls, url, self.accessToken, timeout] for url in connections]
|
||||||
|
@ -1215,7 +1325,7 @@ class MyPlexDevice(PlexObject):
|
||||||
""" Returns an instance of :class:`~plexapi.sync.SyncList` for current device.
|
""" Returns an instance of :class:`~plexapi.sync.SyncList` for current device.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: when the device doesn`t provides `sync-target`.
|
:exc:`~plexapi.exceptions.BadRequest`: when the device doesn't provides `sync-target`.
|
||||||
"""
|
"""
|
||||||
if 'sync-target' not in self.provides:
|
if 'sync-target' not in self.provides:
|
||||||
raise BadRequest('Requested syncList for device which do not provides sync-target')
|
raise BadRequest('Requested syncList for device which do not provides sync-target')
|
||||||
|
|
|
@ -5,11 +5,21 @@ from urllib.parse import quote_plus
|
||||||
from plexapi import media, utils, video
|
from plexapi import media, utils, video
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, RatingMixin, TagMixin
|
from plexapi.mixins import (
|
||||||
|
RatingMixin,
|
||||||
|
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin,
|
||||||
|
SortTitleMixin, SummaryMixin, TitleMixin, PhotoCapturedTimeMixin,
|
||||||
|
TagMixin
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
class Photoalbum(
|
||||||
|
PlexPartialObject,
|
||||||
|
RatingMixin,
|
||||||
|
ArtMixin, PosterMixin,
|
||||||
|
SortTitleMixin, SummaryMixin, TitleMixin
|
||||||
|
):
|
||||||
""" Represents a single Photoalbum (collection of photos).
|
""" Represents a single Photoalbum (collection of photos).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -33,11 +43,12 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
||||||
title (str): Name of the photo album. (Trip to Disney World)
|
title (str): Name of the photo album. (Trip to Disney World)
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'photo'
|
type (str): 'photo'
|
||||||
updatedAt (datatime): Datetime the photo album was updated.
|
updatedAt (datetime): Datetime the photo album was updated.
|
||||||
userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars).
|
userRating (float): Rating of the photo album (0.0 - 10.0) equaling (0 stars - 5 stars).
|
||||||
"""
|
"""
|
||||||
TAG = 'Directory'
|
TAG = 'Directory'
|
||||||
TYPE = 'photo'
|
TYPE = 'photo'
|
||||||
|
_searchType = 'photoalbum'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
""" Load attribute values from Plex XML response. """
|
""" Load attribute values from Plex XML response. """
|
||||||
|
@ -109,7 +120,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
||||||
return self.episode(title)
|
return self.episode(title)
|
||||||
|
|
||||||
def download(self, savepath=None, keep_original_name=False, subfolders=False):
|
def download(self, savepath=None, keep_original_name=False, subfolders=False):
|
||||||
""" Download all photos and clips from the photo ablum. See :func:`~plexapi.base.Playable.download` for details.
|
""" Download all photos and clips from the photo album. See :func:`~plexapi.base.Playable.download` for details.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
savepath (str): Defaults to current working dir.
|
savepath (str): Defaults to current working dir.
|
||||||
|
@ -131,7 +142,13 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin, RatingMixin):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixin, TagMixin):
|
class Photo(
|
||||||
|
PlexPartialObject, Playable,
|
||||||
|
RatingMixin,
|
||||||
|
ArtUrlMixin, PosterUrlMixin,
|
||||||
|
PhotoCapturedTimeMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
|
TagMixin
|
||||||
|
):
|
||||||
""" Represents a single Photo.
|
""" Represents a single Photo.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -164,7 +181,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
|
||||||
title (str): Name of the photo.
|
title (str): Name of the photo.
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'photo'
|
type (str): 'photo'
|
||||||
updatedAt (datatime): Datetime the photo was updated.
|
updatedAt (datetime): Datetime the photo was updated.
|
||||||
userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars).
|
userRating (float): Rating of the photo (0.0 - 10.0) equaling (0 stars - 5 stars).
|
||||||
year (int): Year the photo was taken.
|
year (int): Year the photo was taken.
|
||||||
"""
|
"""
|
||||||
|
@ -223,7 +240,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, RatingMixi
|
||||||
elif self.parentKey:
|
elif self.parentKey:
|
||||||
return self._server.library.sectionByID(self.photoalbum().librarySectionID)
|
return self._server.library.sectionByID(self.photoalbum().librarySectionID)
|
||||||
else:
|
else:
|
||||||
raise BadRequest('Unable to get section for photo, can`t find librarySectionID')
|
raise BadRequest("Unable to get section for photo, can't find librarySectionID")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locations(self):
|
def locations(self):
|
||||||
|
|
|
@ -6,13 +6,17 @@ from plexapi import media, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
from plexapi.exceptions import BadRequest, NotFound, Unsupported
|
||||||
from plexapi.library import LibrarySection
|
from plexapi.library import LibrarySection
|
||||||
from plexapi.mixins import ArtMixin, PosterMixin, SmartFilterMixin
|
from plexapi.mixins import SmartFilterMixin, ArtMixin, PosterMixin
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMixin):
|
class Playlist(
|
||||||
|
PlexPartialObject, Playable,
|
||||||
|
SmartFilterMixin,
|
||||||
|
ArtMixin, PosterMixin
|
||||||
|
):
|
||||||
""" Represents a single Playlist.
|
""" Represents a single Playlist.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -39,7 +43,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
|
||||||
summary (str): Summary of the playlist.
|
summary (str): Summary of the playlist.
|
||||||
title (str): Name of the playlist.
|
title (str): Name of the playlist.
|
||||||
type (str): 'playlist'
|
type (str): 'playlist'
|
||||||
updatedAt (datatime): Datetime the playlist was updated.
|
updatedAt (datetime): Datetime the playlist was updated.
|
||||||
"""
|
"""
|
||||||
TAG = 'Playlist'
|
TAG = 'Playlist'
|
||||||
TYPE = 'playlist'
|
TYPE = 'playlist'
|
||||||
|
|
|
@ -314,7 +314,7 @@ class PlexServer(PlexObject):
|
||||||
def myPlexAccount(self):
|
def myPlexAccount(self):
|
||||||
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
""" Returns a :class:`~plexapi.myplex.MyPlexAccount` object using the same
|
||||||
token to access this server. If you are not the owner of this PlexServer
|
token to access this server. If you are not the owner of this PlexServer
|
||||||
you're likley to recieve an authentication error calling this.
|
you're likely to receive an authentication error calling this.
|
||||||
"""
|
"""
|
||||||
if self._myPlexAccount is None:
|
if self._myPlexAccount is None:
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
|
@ -323,7 +323,7 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
def _myPlexClientPorts(self):
|
def _myPlexClientPorts(self):
|
||||||
""" Sometimes the PlexServer does not properly advertise port numbers required
|
""" Sometimes the PlexServer does not properly advertise port numbers required
|
||||||
to connect. This attemps to look up device port number from plex.tv.
|
to connect. This attempts to look up device port number from plex.tv.
|
||||||
See issue #126: Make PlexServer.clients() more user friendly.
|
See issue #126: Make PlexServer.clients() more user friendly.
|
||||||
https://github.com/pkkid/python-plexapi/issues/126
|
https://github.com/pkkid/python-plexapi/issues/126
|
||||||
"""
|
"""
|
||||||
|
@ -393,7 +393,6 @@ class PlexServer(PlexObject):
|
||||||
"""
|
"""
|
||||||
if isinstance(path, Path):
|
if isinstance(path, Path):
|
||||||
path = path.path
|
path = path.path
|
||||||
path = os.path.normpath(path)
|
|
||||||
paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)]
|
paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)]
|
||||||
return path in paths
|
return path in paths
|
||||||
|
|
||||||
|
@ -524,14 +523,39 @@ class PlexServer(PlexObject):
|
||||||
filepath = utils.download(url, self._token, None, savepath, self._session, unpack=unpack)
|
filepath = utils.download(url, self._token, None, savepath, self._session, unpack=unpack)
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
|
def butlerTasks(self):
|
||||||
|
""" Return a list of :class:`~plexapi.base.ButlerTask` objects. """
|
||||||
|
return self.fetchItems('/butler')
|
||||||
|
|
||||||
|
def runButlerTask(self, task):
|
||||||
|
""" Manually run a butler task immediately instead of waiting for the scheduled task to run.
|
||||||
|
Note: The butler task is run asynchronously. Check Plex Web to monitor activity.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
task (str): The name of the task to run. (e.g. 'BackupDatabase')
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
availableTasks = [task.name for task in plex.butlerTasks()]
|
||||||
|
print("Available butler tasks:", availableTasks)
|
||||||
|
"""
|
||||||
|
validTasks = [task.name for task in self.butlerTasks()]
|
||||||
|
if task not in validTasks:
|
||||||
|
raise BadRequest(
|
||||||
|
f'Invalid butler task: {task}. Available tasks are: {validTasks}'
|
||||||
|
)
|
||||||
|
self.query(f'/butler/{task}', method=self._session.post)
|
||||||
|
|
||||||
@deprecated('use "checkForUpdate" instead')
|
@deprecated('use "checkForUpdate" instead')
|
||||||
def check_for_update(self, force=True, download=False):
|
def check_for_update(self, force=True, download=False):
|
||||||
return self.checkForUpdate()
|
return self.checkForUpdate(force=force, download=download)
|
||||||
|
|
||||||
def checkForUpdate(self, force=True, download=False):
|
def checkForUpdate(self, force=True, download=False):
|
||||||
""" Returns a :class:`~plexapi.base.Release` object containing release info.
|
""" Returns a :class:`~plexapi.base.Release` object containing release info.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
force (bool): Force server to check for new releases
|
force (bool): Force server to check for new releases
|
||||||
download (bool): Download if a update is available.
|
download (bool): Download if a update is available.
|
||||||
"""
|
"""
|
||||||
|
@ -730,7 +754,7 @@ class PlexServer(PlexObject):
|
||||||
return self.fetchItems('/transcode/sessions')
|
return self.fetchItems('/transcode/sessions')
|
||||||
|
|
||||||
def startAlertListener(self, callback=None):
|
def startAlertListener(self, callback=None):
|
||||||
""" Creates a websocket connection to the Plex Server to optionally recieve
|
""" Creates a websocket connection to the Plex Server to optionally receive
|
||||||
notifications. These often include messages from Plex about media scans
|
notifications. These often include messages from Plex about media scans
|
||||||
as well as updates to currently running Transcode Sessions.
|
as well as updates to currently running Transcode Sessions.
|
||||||
|
|
||||||
|
@ -738,7 +762,7 @@ class PlexServer(PlexObject):
|
||||||
>> pip install websocket-client
|
>> pip install websocket-client
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
callback (func): Callback function to call on recieved messages.
|
callback (func): Callback function to call on received messages.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exception.Unsupported`: Websocket-client not installed.
|
:exc:`~plexapi.exception.Unsupported`: Websocket-client not installed.
|
||||||
|
@ -1078,7 +1102,7 @@ class SystemDevice(PlexObject):
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Device'
|
TAG (str): 'Device'
|
||||||
clientIdentifier (str): The unique identifier for the device.
|
clientIdentifier (str): The unique identifier for the device.
|
||||||
createdAt (datatime): Datetime the device was created.
|
createdAt (datetime): Datetime the device was created.
|
||||||
id (int): The ID of the device (not the same as :class:`~plexapi.myplex.MyPlexDevice` ID).
|
id (int): The ID of the device (not the same as :class:`~plexapi.myplex.MyPlexDevice` ID).
|
||||||
key (str): API URL (/devices/<id>)
|
key (str): API URL (/devices/<id>)
|
||||||
name (str): The name of the device.
|
name (str): The name of the device.
|
||||||
|
@ -1102,11 +1126,11 @@ class StatisticsBandwidth(PlexObject):
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'StatisticsBandwidth'
|
TAG (str): 'StatisticsBandwidth'
|
||||||
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
|
accountID (int): The associated :class:`~plexapi.server.SystemAccount` ID.
|
||||||
at (datatime): Datetime of the bandwidth data.
|
at (datetime): Datetime of the bandwidth data.
|
||||||
bytes (int): The total number of bytes for the specified timespan.
|
bytes (int): The total number of bytes for the specified time span.
|
||||||
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
|
deviceID (int): The associated :class:`~plexapi.server.SystemDevice` ID.
|
||||||
lan (bool): True or False wheter the bandwidth is local or remote.
|
lan (bool): True or False whether the bandwidth is local or remote.
|
||||||
timespan (int): The timespan for the bandwidth data.
|
timespan (int): The time span for the bandwidth data.
|
||||||
1: months, 2: weeks, 3: days, 4: hours, 6: seconds.
|
1: months, 2: weeks, 3: days, 4: hours, 6: seconds.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -1143,12 +1167,12 @@ class StatisticsResources(PlexObject):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'StatisticsResources'
|
TAG (str): 'StatisticsResources'
|
||||||
at (datatime): Datetime of the resource data.
|
at (datetime): Datetime of the resource data.
|
||||||
hostCpuUtilization (float): The system CPU usage %.
|
hostCpuUtilization (float): The system CPU usage %.
|
||||||
hostMemoryUtilization (float): The Plex Media Server CPU usage %.
|
hostMemoryUtilization (float): The Plex Media Server CPU usage %.
|
||||||
processCpuUtilization (float): The system RAM usage %.
|
processCpuUtilization (float): The system RAM usage %.
|
||||||
processMemoryUtilization (float): The Plex Media Server RAM usage %.
|
processMemoryUtilization (float): The Plex Media Server RAM usage %.
|
||||||
timespan (int): The timespan for the resource data (6: seconds).
|
timespan (int): The time span for the resource data (6: seconds).
|
||||||
"""
|
"""
|
||||||
TAG = 'StatisticsResources'
|
TAG = 'StatisticsResources'
|
||||||
|
|
||||||
|
@ -1166,3 +1190,28 @@ class StatisticsResources(PlexObject):
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self._clean(int(self.at.timestamp()))
|
self._clean(int(self.at.timestamp()))
|
||||||
] if p])
|
] if p])
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class ButlerTask(PlexObject):
|
||||||
|
""" Represents a single scheduled butler task.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'ButlerTask'
|
||||||
|
description (str): The description of the task.
|
||||||
|
enabled (bool): Whether the task is enabled.
|
||||||
|
interval (int): The interval the task is run in days.
|
||||||
|
name (str): The name of the task.
|
||||||
|
scheduleRandomized (bool): Whether the task schedule is randomized.
|
||||||
|
title (str): The title of the task.
|
||||||
|
"""
|
||||||
|
TAG = 'ButlerTask'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self._data = data
|
||||||
|
self.description = data.attrib.get('description')
|
||||||
|
self.enabled = utils.cast(bool, data.attrib.get('enabled'))
|
||||||
|
self.interval = utils.cast(int, data.attrib.get('interval'))
|
||||||
|
self.name = data.attrib.get('name')
|
||||||
|
self.scheduleRandomized = utils.cast(bool, data.attrib.get('scheduleRandomized'))
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
|
|
@ -71,7 +71,7 @@ class Settings(PlexObject):
|
||||||
return self.groups().get(group, [])
|
return self.groups().get(group, [])
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
""" Save any outstanding settnig changes to the :class:`~plexapi.server.PlexServer`. This
|
""" Save any outstanding setting changes to the :class:`~plexapi.server.PlexServer`. This
|
||||||
performs a full reload() of Settings after complete.
|
performs a full reload() of Settings after complete.
|
||||||
"""
|
"""
|
||||||
params = {}
|
params = {}
|
||||||
|
@ -100,7 +100,7 @@ class Setting(PlexObject):
|
||||||
hidden (bool): True if this is a hidden setting.
|
hidden (bool): True if this is a hidden setting.
|
||||||
advanced (bool): True if this is an advanced setting.
|
advanced (bool): True if this is an advanced setting.
|
||||||
group (str): Group name this setting is categorized as.
|
group (str): Group name this setting is categorized as.
|
||||||
enumValues (list,dict): List or dictionary of valis values for this setting.
|
enumValues (list,dict): List or dictionary of valid values for this setting.
|
||||||
"""
|
"""
|
||||||
_bool_cast = lambda x: bool(x == 'true' or x == '1')
|
_bool_cast = lambda x: bool(x == 'true' or x == '1')
|
||||||
_bool_str = lambda x: str(x).lower()
|
_bool_str = lambda x: str(x).lower()
|
||||||
|
@ -143,7 +143,7 @@ class Setting(PlexObject):
|
||||||
return enumstr.split('|')
|
return enumstr.split('|')
|
||||||
|
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
""" Set a new value for this setitng. NOTE: You must call plex.settings.save() for before
|
""" Set a new value for this setting. NOTE: You must call plex.settings.save() for before
|
||||||
any changes to setting values are persisted to the :class:`~plexapi.server.PlexServer`.
|
any changes to setting values are persisted to the :class:`~plexapi.server.PlexServer`.
|
||||||
"""
|
"""
|
||||||
# check a few things up front
|
# check a few things up front
|
||||||
|
|
|
@ -14,7 +14,7 @@ class PlexSonosClient(PlexClient):
|
||||||
speakers linked to your Plex account. It also requires remote access to
|
speakers linked to your Plex account. It also requires remote access to
|
||||||
be working properly.
|
be working properly.
|
||||||
|
|
||||||
More details on the Sonos integration are avaialble here:
|
More details on the Sonos integration are available here:
|
||||||
https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/
|
https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/
|
||||||
|
|
||||||
The Sonos API emulates the Plex player control API closely:
|
The Sonos API emulates the Plex player control API closely:
|
||||||
|
@ -38,7 +38,7 @@ class PlexSonosClient(PlexClient):
|
||||||
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
|
||||||
session (:class:`~requests.Session`): Session object used for connection.
|
session (:class:`~requests.Session`): Session object used for connection.
|
||||||
title (str): Name of this Sonos speaker.
|
title (str): Name of this Sonos speaker.
|
||||||
token (str): X-Plex-Token used for authenication
|
token (str): X-Plex-Token used for authentication
|
||||||
_baseurl (str): Address of public Plex Sonos API endpoint.
|
_baseurl (str): Address of public Plex Sonos API endpoint.
|
||||||
_commandId (int): Counter for commands sent to Plex API.
|
_commandId (int): Counter for commands sent to Plex API.
|
||||||
_token (str): Token associated with linked Plex account.
|
_token (str): Token associated with linked Plex account.
|
||||||
|
|
|
@ -9,6 +9,7 @@ import time
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import warnings
|
import warnings
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
|
@ -55,7 +56,7 @@ class SecretsFilter(logging.Filter):
|
||||||
|
|
||||||
def registerPlexObject(cls):
|
def registerPlexObject(cls):
|
||||||
""" Registry of library types we may come across when parsing XML. This allows us to
|
""" Registry of library types we may come across when parsing XML. This allows us to
|
||||||
define a few helper functions to dynamically convery the XML into objects. See
|
define a few helper functions to dynamically convert the XML into objects. See
|
||||||
buildItem() below for an example.
|
buildItem() below for an example.
|
||||||
"""
|
"""
|
||||||
etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE))
|
etype = getattr(cls, 'STREAMTYPE', getattr(cls, 'TAGTYPE', cls.TYPE))
|
||||||
|
@ -72,7 +73,7 @@ def cast(func, value):
|
||||||
only support str, int, float, bool. Should be extended if needed.
|
only support str, int, float, bool. Should be extended if needed.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
func (func): Calback function to used cast to type (int, bool, float).
|
func (func): Callback function to used cast to type (int, bool, float).
|
||||||
value (any): value to be cast and returned.
|
value (any): value to be cast and returned.
|
||||||
"""
|
"""
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
@ -114,7 +115,7 @@ def lowerFirst(s):
|
||||||
|
|
||||||
|
|
||||||
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
|
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
|
||||||
""" Returns the value at the specified attrstr location within a nexted tree of
|
""" Returns the value at the specified attrstr location within a nested tree of
|
||||||
dicts, lists, tuples, functions, classes, etc. The lookup is done recursively
|
dicts, lists, tuples, functions, classes, etc. The lookup is done recursively
|
||||||
for each key in attrstr (split by by the delimiter) This function is heavily
|
for each key in attrstr (split by by the delimiter) This function is heavily
|
||||||
influenced by the lookups used in Django templates.
|
influenced by the lookups used in Django templates.
|
||||||
|
@ -194,7 +195,7 @@ def threaded(callback, listargs):
|
||||||
args += [results, len(results)]
|
args += [results, len(results)]
|
||||||
results.append(None)
|
results.append(None)
|
||||||
threads.append(Thread(target=callback, args=args, kwargs=dict(job_is_done_event=job_is_done_event)))
|
threads.append(Thread(target=callback, args=args, kwargs=dict(job_is_done_event=job_is_done_event)))
|
||||||
threads[-1].setDaemon(True)
|
threads[-1].daemon = True
|
||||||
threads[-1].start()
|
threads[-1].start()
|
||||||
while not job_is_done_event.is_set():
|
while not job_is_done_event.is_set():
|
||||||
if all(not t.is_alive() for t in threads):
|
if all(not t.is_alive() for t in threads):
|
||||||
|
@ -304,7 +305,7 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
|
||||||
filename (str): Filename of the downloaded file, default None.
|
filename (str): Filename of the downloaded file, default None.
|
||||||
savepath (str): Defaults to current working dir.
|
savepath (str): Defaults to current working dir.
|
||||||
chunksize (int): What chunksize read/write at the time.
|
chunksize (int): What chunksize read/write at the time.
|
||||||
mocked (bool): Helper to do evertything except write the file.
|
mocked (bool): Helper to do everything except write the file.
|
||||||
unpack (bool): Unpack the zip file.
|
unpack (bool): Unpack the zip file.
|
||||||
showstatus(bool): Display a progressbar.
|
showstatus(bool): Display a progressbar.
|
||||||
|
|
||||||
|
@ -361,40 +362,6 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
|
||||||
return fullpath
|
return fullpath
|
||||||
|
|
||||||
|
|
||||||
def tag_singular(tag):
|
|
||||||
if tag == 'countries':
|
|
||||||
return 'country'
|
|
||||||
elif tag == 'similar':
|
|
||||||
return 'similar'
|
|
||||||
else:
|
|
||||||
return tag[:-1]
|
|
||||||
|
|
||||||
|
|
||||||
def tag_plural(tag):
|
|
||||||
if tag == 'country':
|
|
||||||
return 'countries'
|
|
||||||
elif tag == 'similar':
|
|
||||||
return 'similar'
|
|
||||||
else:
|
|
||||||
return tag + 's'
|
|
||||||
|
|
||||||
|
|
||||||
def tag_helper(tag, items, locked=True, remove=False):
|
|
||||||
""" Simple tag helper for editing a object. """
|
|
||||||
if not isinstance(items, list):
|
|
||||||
items = [items]
|
|
||||||
data = {}
|
|
||||||
if not remove:
|
|
||||||
for i, item in enumerate(items):
|
|
||||||
tagname = '%s[%s].tag.tag' % (tag, i)
|
|
||||||
data[tagname] = item
|
|
||||||
if remove:
|
|
||||||
tagname = '%s[].tag.tag-' % tag
|
|
||||||
data[tagname] = ','.join(items)
|
|
||||||
data['%s.locked' % tag] = 1 if locked else 0
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def getMyPlexAccount(opts=None): # pragma: no cover
|
def getMyPlexAccount(opts=None): # pragma: no cover
|
||||||
""" Helper function tries to get a MyPlex Account instance by checking
|
""" Helper function tries to get a MyPlex Account instance by checking
|
||||||
the the following locations for a username and password. This is
|
the the following locations for a username and password. This is
|
||||||
|
@ -485,7 +452,7 @@ def getAgentIdentifier(section, agent):
|
||||||
if agent in identifiers:
|
if agent in identifiers:
|
||||||
return ag.identifier
|
return ag.identifier
|
||||||
agents += identifiers
|
agents += identifiers
|
||||||
raise NotFound('Couldnt find "%s" in agents list (%s)' %
|
raise NotFound('Could not find "%s" in agents list (%s)' %
|
||||||
(agent, ', '.join(agents)))
|
(agent, ', '.join(agents)))
|
||||||
|
|
||||||
|
|
||||||
|
@ -506,3 +473,15 @@ def deprecated(message, stacklevel=2):
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def iterXMLBFS(root, tag=None):
|
||||||
|
""" Iterate through an XML tree using a breadth-first search.
|
||||||
|
If tag is specified, only return nodes with that tag.
|
||||||
|
"""
|
||||||
|
queue = deque([root])
|
||||||
|
while queue:
|
||||||
|
node = queue.popleft()
|
||||||
|
if tag is None or node.tag == tag:
|
||||||
|
yield node
|
||||||
|
queue.extend(list(node))
|
||||||
|
|
|
@ -2,12 +2,17 @@
|
||||||
import os
|
import os
|
||||||
from urllib.parse import quote_plus, urlencode
|
from urllib.parse import quote_plus, urlencode
|
||||||
|
|
||||||
from plexapi import library, media, utils
|
from plexapi import media, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
from plexapi.mixins import (
|
||||||
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin
|
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||||
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
|
ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin, ThemeUrlMixin, ThemeMixin,
|
||||||
|
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||||
|
SummaryMixin, TaglineMixin, TitleMixin,
|
||||||
|
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||||
|
WatchlistMixin
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Video(PlexPartialObject):
|
class Video(PlexPartialObject):
|
||||||
|
@ -35,7 +40,7 @@ class Video(PlexPartialObject):
|
||||||
title (str): Name of the movie, show, season, episode, or clip.
|
title (str): Name of the movie, show, season, episode, or clip.
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
type (str): 'movie', 'show', 'season', 'episode', or 'clip'.
|
type (str): 'movie', 'show', 'season', 'episode', or 'clip'.
|
||||||
updatedAt (datatime): Datetime the item was updated.
|
updatedAt (datetime): Datetime the item was updated.
|
||||||
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
|
userRating (float): Rating of the item (0.0 - 10.0) equaling (0 stars - 5 stars).
|
||||||
viewCount (int): Count of times the item was played.
|
viewCount (int): Count of times the item was played.
|
||||||
"""
|
"""
|
||||||
|
@ -76,7 +81,7 @@ class Video(PlexPartialObject):
|
||||||
return self._server.url(part, includeToken=True) if part else None
|
return self._server.url(part, includeToken=True) if part else None
|
||||||
|
|
||||||
def markWatched(self):
|
def markWatched(self):
|
||||||
""" Mark the video as palyed. """
|
""" Mark the video as played. """
|
||||||
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
key = '/:/scrobble?key=%s&identifier=com.plexapp.plugins.library' % self.ratingKey
|
||||||
self._server.query(key)
|
self._server.query(key)
|
||||||
|
|
||||||
|
@ -107,6 +112,15 @@ class Video(PlexPartialObject):
|
||||||
""" Returns str, default title for a new syncItem. """
|
""" Returns str, default title for a new syncItem. """
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def audioStreams(self):
|
||||||
|
""" Returns a list of :class:`~plexapi.media.AudioStream` objects for all MediaParts. """
|
||||||
|
streams = []
|
||||||
|
|
||||||
|
parts = self.iterParts()
|
||||||
|
for part in parts:
|
||||||
|
streams += part.audioStreams()
|
||||||
|
return streams
|
||||||
|
|
||||||
def subtitleStreams(self):
|
def subtitleStreams(self):
|
||||||
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
|
""" Returns a list of :class:`~plexapi.media.SubtitleStream` objects for all MediaParts. """
|
||||||
streams = []
|
streams = []
|
||||||
|
@ -261,8 +275,15 @@ class Video(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
class Movie(
|
||||||
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
|
Video, Playable,
|
||||||
|
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeMixin,
|
||||||
|
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||||
|
SummaryMixin, TaglineMixin, TitleMixin,
|
||||||
|
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin,
|
||||||
|
WatchlistMixin
|
||||||
|
):
|
||||||
""" Represents a single Movie.
|
""" Represents a single Movie.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -280,7 +301,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
|
||||||
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
|
||||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
languageOverride (str): Setting that indicates if a languge is used to override metadata
|
languageOverride (str): Setting that indicates if a language is used to override metadata
|
||||||
(eg. en-CA, None = Library default).
|
(eg. en-CA, None = Library default).
|
||||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||||
originallyAvailableAt (datetime): Datetime the movie was released.
|
originallyAvailableAt (datetime): Datetime the movie was released.
|
||||||
|
@ -293,6 +314,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
|
||||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||||
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
|
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
|
||||||
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
|
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
|
||||||
|
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
|
||||||
useOriginalTitle (int): Setting that indicates if the original title is used for the movie
|
useOriginalTitle (int): Setting that indicates if the original title is used for the movie
|
||||||
(-1 = Library default, 0 = No, 1 = Yes).
|
(-1 = Library default, 0 = No, 1 = Yes).
|
||||||
viewOffset (int): View offset in milliseconds.
|
viewOffset (int): View offset in milliseconds.
|
||||||
|
@ -331,6 +353,7 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
self.studio = data.attrib.get('studio')
|
self.studio = data.attrib.get('studio')
|
||||||
self.tagline = data.attrib.get('tagline')
|
self.tagline = data.attrib.get('tagline')
|
||||||
|
self.theme = data.attrib.get('theme')
|
||||||
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
|
self.useOriginalTitle = utils.cast(int, data.attrib.get('useOriginalTitle', '-1'))
|
||||||
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||||
self.writers = self.findItems(data, media.Writer)
|
self.writers = self.findItems(data, media.Writer)
|
||||||
|
@ -365,20 +388,17 @@ class Movie(Video, Playable, AdvancedSettingsMixin, ArtMixin, PosterMixin, Ratin
|
||||||
data = self._server.query(self._details_key)
|
data = self._server.query(self._details_key)
|
||||||
return self.findItems(data, media.Review, rtag='Video')
|
return self.findItems(data, media.Review, rtag='Video')
|
||||||
|
|
||||||
def extras(self):
|
|
||||||
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
|
|
||||||
data = self._server.query(self._details_key)
|
|
||||||
return self.findItems(data, Extra, rtag='Extras')
|
|
||||||
|
|
||||||
def hubs(self):
|
|
||||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
|
||||||
data = self._server.query(self._details_key)
|
|
||||||
return self.findItems(data, library.Hub, rtag='Related')
|
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, RatingMixin, SplitMergeMixin, UnmatchMatchMixin,
|
class Show(
|
||||||
CollectionMixin, GenreMixin, LabelMixin):
|
Video,
|
||||||
|
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, RatingMixin,
|
||||||
|
ArtMixin, BannerMixin, PosterMixin, ThemeMixin,
|
||||||
|
ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin,
|
||||||
|
SummaryMixin, TaglineMixin, TitleMixin,
|
||||||
|
CollectionMixin, GenreMixin, LabelMixin,
|
||||||
|
WatchlistMixin
|
||||||
|
):
|
||||||
""" Represents a single Show (including all seasons and episodes).
|
""" Represents a single Show (including all seasons and episodes).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -407,7 +427,7 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
|
||||||
index (int): Plex index number for the show.
|
index (int): Plex index number for the show.
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
languageOverride (str): Setting that indicates if a languge is used to override metadata
|
languageOverride (str): Setting that indicates if a language is used to override metadata
|
||||||
(eg. en-CA, None = Library default).
|
(eg. en-CA, None = Library default).
|
||||||
leafCount (int): Number of items in the show view.
|
leafCount (int): Number of items in the show view.
|
||||||
locations (List<str>): List of folder paths where the show is found on disk.
|
locations (List<str>): List of folder paths where the show is found on disk.
|
||||||
|
@ -483,11 +503,6 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
|
||||||
""" Returns True if the show is fully watched. """
|
""" Returns True if the show is fully watched. """
|
||||||
return bool(self.viewedLeafCount == self.leafCount)
|
return bool(self.viewedLeafCount == self.leafCount)
|
||||||
|
|
||||||
def hubs(self):
|
|
||||||
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
|
|
||||||
data = self._server.query(self._details_key)
|
|
||||||
return self.findItems(data, library.Hub, rtag='Related')
|
|
||||||
|
|
||||||
def onDeck(self):
|
def onDeck(self):
|
||||||
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
|
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
|
||||||
If show is unwatched, return will likely be the first episode.
|
If show is unwatched, return will likely be the first episode.
|
||||||
|
@ -574,7 +589,13 @@ class Show(Video, AdvancedSettingsMixin, ArtMixin, BannerMixin, PosterMixin, Rat
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
class Season(
|
||||||
|
Video,
|
||||||
|
ExtrasMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||||
|
SummaryMixin, TitleMixin,
|
||||||
|
CollectionMixin, LabelMixin
|
||||||
|
):
|
||||||
""" Represents a single Show Season (including all episodes).
|
""" Represents a single Show Season (including all episodes).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -584,6 +605,7 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
||||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||||
index (int): Season number.
|
index (int): Season number.
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
leafCount (int): Number of items in the season view.
|
leafCount (int): Number of items in the season view.
|
||||||
parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
|
parentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
|
||||||
parentIndex (int): Plex index number for the show.
|
parentIndex (int): Plex index number for the show.
|
||||||
|
@ -607,6 +629,7 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
||||||
self.guids = self.findItems(data, media.Guid)
|
self.guids = self.findItems(data, media.Guid)
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
self.key = self.key.replace('/children', '') # FIX_BUG_50
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||||
self.parentGuid = data.attrib.get('parentGuid')
|
self.parentGuid = data.attrib.get('parentGuid')
|
||||||
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
|
||||||
|
@ -709,8 +732,13 @@ class Season(Video, ArtMixin, PosterMixin, RatingMixin, CollectionMixin):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
class Episode(
|
||||||
CollectionMixin, DirectorMixin, WriterMixin):
|
Video, Playable,
|
||||||
|
ExtrasMixin, RatingMixin,
|
||||||
|
ArtMixin, PosterMixin, ThemeUrlMixin,
|
||||||
|
ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin,
|
||||||
|
CollectionMixin, DirectorMixin, LabelMixin, WriterMixin
|
||||||
|
):
|
||||||
""" Represents a single Shows Episode.
|
""" Represents a single Shows Episode.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -733,6 +761,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
||||||
grandparentTitle (str): Name of the show for the episode.
|
grandparentTitle (str): Name of the show for the episode.
|
||||||
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
|
||||||
index (int): Episode number.
|
index (int): Episode number.
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
|
markers (List<:class:`~plexapi.media.Marker`>): List of marker objects.
|
||||||
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
media (List<:class:`~plexapi.media.Media`>): List of media objects.
|
||||||
originallyAvailableAt (datetime): Datetime the episode was released.
|
originallyAvailableAt (datetime): Datetime the episode was released.
|
||||||
|
@ -777,6 +806,7 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
||||||
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
self.grandparentTitle = data.attrib.get('grandparentTitle')
|
||||||
self.guids = self.findItems(data, media.Guid)
|
self.guids = self.findItems(data, media.Guid)
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
self.markers = self.findItems(data, media.Marker)
|
self.markers = self.findItems(data, media.Marker)
|
||||||
self.media = self.findItems(data, media.Media)
|
self.media = self.findItems(data, media.Media)
|
||||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
|
@ -879,7 +909,10 @@ class Episode(Video, Playable, ArtMixin, PosterMixin, RatingMixin,
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
|
class Clip(
|
||||||
|
Video, Playable,
|
||||||
|
ArtUrlMixin, PosterUrlMixin
|
||||||
|
):
|
||||||
""" Represents a single Clip.
|
""" Represents a single Clip.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|
|
@ -27,7 +27,7 @@ MarkupSafe==2.1.1
|
||||||
musicbrainzngs==0.7.1
|
musicbrainzngs==0.7.1
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
paho-mqtt==1.6.1
|
paho-mqtt==1.6.1
|
||||||
plexapi==4.9.2
|
plexapi==4.11.0
|
||||||
portend==3.1.0
|
portend==3.1.0
|
||||||
profilehooks==1.12.0
|
profilehooks==1.12.0
|
||||||
PyJWT==2.4.0
|
PyJWT==2.4.0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue