mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 13:11:15 -07:00
Update PlexAPI to 4.4.0
This commit is contained in:
parent
0e4de17853
commit
6fb4b35076
15 changed files with 799 additions and 525 deletions
|
@ -15,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
|
||||||
|
|
||||||
# PlexAPI Settings
|
# PlexAPI Settings
|
||||||
PROJECT = 'PlexAPI'
|
PROJECT = 'PlexAPI'
|
||||||
VERSION = '4.3.1'
|
VERSION = '4.4.0'
|
||||||
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
|
||||||
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
|
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
|
||||||
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)
|
||||||
|
|
|
@ -84,4 +84,4 @@ class AlertListener(threading.Thread):
|
||||||
This is to support compatibility with current and previous releases of websocket-client.
|
This is to support compatibility with current and previous releases of websocket-client.
|
||||||
"""
|
"""
|
||||||
err = args[-1]
|
err = args[-1]
|
||||||
log.error('AlertListener Error: %s' % err)
|
log.error('AlertListener Error: %s', err)
|
||||||
|
|
|
@ -4,6 +4,9 @@ from urllib.parse import quote_plus
|
||||||
from plexapi import library, media, utils
|
from plexapi import library, media, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest
|
from plexapi.exceptions import BadRequest
|
||||||
|
from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
|
||||||
|
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
||||||
|
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
|
||||||
|
|
||||||
|
|
||||||
class Audio(PlexPartialObject):
|
class Audio(PlexPartialObject):
|
||||||
|
@ -65,18 +68,6 @@ class Audio(PlexPartialObject):
|
||||||
self.userRating = utils.cast(float, data.attrib.get('userRating', 0))
|
self.userRating = utils.cast(float, data.attrib.get('userRating', 0))
|
||||||
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbUrl(self):
|
|
||||||
""" Return url to for the thumbnail image. """
|
|
||||||
key = self.firstAttr('thumb', 'parentThumb', 'granparentThumb')
|
|
||||||
return self._server.url(key, includeToken=True) if key else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artUrl(self):
|
|
||||||
""" Return the first art url starting on the most specific for that item."""
|
|
||||||
art = self.firstAttr('art', 'grandparentArt')
|
|
||||||
return self._server.url(art, includeToken=True) if art else None
|
|
||||||
|
|
||||||
def url(self, part):
|
def url(self, part):
|
||||||
""" Returns the full URL for the audio item. Typically used for getting a specific track. """
|
""" Returns the full URL for the audio item. Typically used for getting a specific track. """
|
||||||
return self._server.url(part, includeToken=True) if part else None
|
return self._server.url(part, includeToken=True) if part else None
|
||||||
|
@ -123,7 +114,8 @@ class Audio(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Artist(Audio):
|
class Artist(Audio, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||||
|
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
|
||||||
""" Represents a single Artist.
|
""" Represents a single Artist.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -226,7 +218,8 @@ class Artist(Audio):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Album(Audio):
|
class Album(Audio, ArtMixin, PosterMixin, UnmatchMatchMixin,
|
||||||
|
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
|
||||||
""" Represents a single Album.
|
""" Represents a single Album.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -332,7 +325,7 @@ class Album(Audio):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Track(Audio, Playable):
|
class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, MoodMixin):
|
||||||
""" Represents a single Track.
|
""" Represents a single Track.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|
|
@ -5,9 +5,10 @@ from urllib.parse import quote_plus, urlencode
|
||||||
|
|
||||||
from plexapi import log, utils
|
from plexapi import log, utils
|
||||||
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
||||||
from plexapi.utils import tag_helper
|
from plexapi.utils import tag_plural, tag_helper
|
||||||
|
|
||||||
DONT_RELOAD_FOR_KEYS = ['key', 'session']
|
DONT_RELOAD_FOR_KEYS = {'key', 'session'}
|
||||||
|
DONT_OVERWRITE_SESSION_KEYS = {'usernames', 'players', 'transcodeSessions', 'session'}
|
||||||
OPERATORS = {
|
OPERATORS = {
|
||||||
'exact': lambda v, q: v == q,
|
'exact': lambda v, q: v == q,
|
||||||
'iexact': lambda v, q: v.lower() == q.lower(),
|
'iexact': lambda v, q: v.lower() == q.lower(),
|
||||||
|
@ -47,6 +48,7 @@ class PlexObject(object):
|
||||||
self._data = data
|
self._data = data
|
||||||
self._initpath = initpath or self.key
|
self._initpath = initpath or self.key
|
||||||
self._parent = weakref.ref(parent) if parent else None
|
self._parent = weakref.ref(parent) if parent else None
|
||||||
|
self._details_key = None
|
||||||
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()
|
||||||
|
@ -57,8 +59,11 @@ class PlexObject(object):
|
||||||
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid, name] if p])
|
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid, name] if p])
|
||||||
|
|
||||||
def __setattr__(self, attr, value):
|
def __setattr__(self, attr, value):
|
||||||
# Don't overwrite an attr with None or [] unless it's a private variable
|
# Don't overwrite session specific attr with []
|
||||||
if value not in [None, []] or attr.startswith('_') or attr not in self.__dict__:
|
if attr in DONT_OVERWRITE_SESSION_KEYS and value == []:
|
||||||
|
value = getattr(self, attr, [])
|
||||||
|
# Don't overwrite an attr with None unless it's a private variable
|
||||||
|
if value is not None or attr.startswith('_') or attr not in self.__dict__:
|
||||||
self.__dict__[attr] = value
|
self.__dict__[attr] = value
|
||||||
|
|
||||||
def _clean(self, value):
|
def _clean(self, value):
|
||||||
|
@ -113,15 +118,15 @@ class PlexObject(object):
|
||||||
def _isChildOf(self, **kwargs):
|
def _isChildOf(self, **kwargs):
|
||||||
""" Returns True if this object is a child of the given attributes.
|
""" Returns True if this object is a child of the given attributes.
|
||||||
This will search the parent objects all the way to the top.
|
This will search the parent objects all the way to the top.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
**kwargs (dict): The attributes and values to search for in the parent objects.
|
**kwargs (dict): The attributes and values to search for in the parent objects.
|
||||||
See all possible `**kwargs*` in :func:`~plexapi.base.PlexObject.fetchItem`.
|
See all possible `**kwargs*` in :func:`~plexapi.base.PlexObject.fetchItem`.
|
||||||
"""
|
"""
|
||||||
obj = self
|
obj = self
|
||||||
while obj._parent is not None:
|
while obj and obj._parent is not None:
|
||||||
obj = obj._parent()
|
obj = obj._parent()
|
||||||
if obj._checkAttrs(obj._data, **kwargs):
|
if obj and obj._checkAttrs(obj._data, **kwargs):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -227,7 +232,7 @@ class PlexObject(object):
|
||||||
def firstAttr(self, *attrs):
|
def firstAttr(self, *attrs):
|
||||||
""" Return the first attribute in attrs that is not None. """
|
""" Return the first attribute in attrs that is not None. """
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
value = self.__dict__.get(attr)
|
value = getattr(self, attr, None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@ -384,6 +389,7 @@ class PlexPartialObject(PlexObject):
|
||||||
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 dont 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.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
|
||||||
|
@ -391,7 +397,7 @@ class PlexPartialObject(PlexObject):
|
||||||
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()
|
self.reload()
|
||||||
return super(PlexPartialObject, self).__getattribute__(attr)
|
return super(PlexPartialObject, self).__getattribute__(attr)
|
||||||
|
@ -452,49 +458,20 @@ class PlexPartialObject(PlexObject):
|
||||||
self._server.query(part, method=self._server._session.put)
|
self._server.query(part, method=self._server._session.put)
|
||||||
|
|
||||||
def _edit_tags(self, tag, items, locked=True, remove=False):
|
def _edit_tags(self, tag, items, locked=True, remove=False):
|
||||||
""" Helper to edit and refresh a tags.
|
""" Helper to edit tags.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
tag (str): tag name
|
tag (str): Tag name.
|
||||||
items (list): list of tags to add
|
items (list): List of tags to add.
|
||||||
locked (bool): lock this field.
|
locked (bool): True to lock the field.
|
||||||
remove (bool): If this is active remove the tags in items.
|
remove (bool): True to remove the tags in items.
|
||||||
"""
|
"""
|
||||||
if not isinstance(items, list):
|
if not isinstance(items, list):
|
||||||
items = [items]
|
items = [items]
|
||||||
value = getattr(self, tag + 's')
|
value = getattr(self, tag_plural(tag))
|
||||||
existing_cols = [t.tag for t in value if t and remove is False]
|
existing_tags = [t.tag for t in value if t and remove is False]
|
||||||
d = tag_helper(tag, existing_cols + items, locked, remove)
|
tag_edits = tag_helper(tag, existing_tags + items, locked, remove)
|
||||||
self.edit(**d)
|
self.edit(**tag_edits)
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
def addCollection(self, collections):
|
|
||||||
""" Add a collection(s).
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
collections (list): list of strings
|
|
||||||
"""
|
|
||||||
self._edit_tags('collection', collections)
|
|
||||||
|
|
||||||
def removeCollection(self, collections):
|
|
||||||
""" Remove a collection(s). """
|
|
||||||
self._edit_tags('collection', collections, remove=True)
|
|
||||||
|
|
||||||
def addLabel(self, labels):
|
|
||||||
""" Add a label(s). """
|
|
||||||
self._edit_tags('label', labels)
|
|
||||||
|
|
||||||
def removeLabel(self, labels):
|
|
||||||
""" Remove a label(s). """
|
|
||||||
self._edit_tags('label', labels, remove=True)
|
|
||||||
|
|
||||||
def addGenre(self, genres):
|
|
||||||
""" Add a genre(s). """
|
|
||||||
self._edit_tags('genre', genres)
|
|
||||||
|
|
||||||
def removeGenre(self, genres):
|
|
||||||
""" Remove a genre(s). """
|
|
||||||
self._edit_tags('genre', genres, remove=True)
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -524,7 +501,7 @@ class PlexPartialObject(PlexObject):
|
||||||
return self._server.query(self.key, method=self._server._session.delete)
|
return self._server.query(self.key, method=self._server._session.delete)
|
||||||
except BadRequest: # pragma: no cover
|
except BadRequest: # pragma: no cover
|
||||||
log.error('Failed to delete %s. This could be because you '
|
log.error('Failed to delete %s. This could be because you '
|
||||||
'havnt allowed items to be deleted' % self.key)
|
'have not allowed items to be deleted', self.key)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def history(self, maxresults=9999999, mindate=None):
|
def history(self, maxresults=9999999, mindate=None):
|
||||||
|
@ -535,142 +512,6 @@ class PlexPartialObject(PlexObject):
|
||||||
"""
|
"""
|
||||||
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
|
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
|
||||||
|
|
||||||
def posters(self):
|
|
||||||
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('%s/posters' % self.key)
|
|
||||||
|
|
||||||
def uploadPoster(self, url=None, filepath=None):
|
|
||||||
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '%s/posters?url=%s' % (self.key, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '%s/posters?' % self.key
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setPoster(self, poster):
|
|
||||||
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
poster.select()
|
|
||||||
|
|
||||||
def arts(self):
|
|
||||||
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('%s/arts' % self.key)
|
|
||||||
|
|
||||||
def uploadArt(self, url=None, filepath=None):
|
|
||||||
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '/library/metadata/%s/arts?' % self.ratingKey
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setArt(self, art):
|
|
||||||
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
art.select()
|
|
||||||
|
|
||||||
def unmatch(self):
|
|
||||||
""" Unmatches metadata match from object. """
|
|
||||||
key = '/library/metadata/%s/unmatch' % self.ratingKey
|
|
||||||
self._server.query(key, method=self._server._session.put)
|
|
||||||
|
|
||||||
def matches(self, agent=None, title=None, year=None, language=None):
|
|
||||||
""" Return list of (:class:`~plexapi.media.SearchResult`) metadata matches.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
|
||||||
title (str): Title of item to search for
|
|
||||||
year (str): Year of item to search in
|
|
||||||
language (str) : Language of item to search in
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
1. video.matches()
|
|
||||||
2. video.matches(title="something", year=2020)
|
|
||||||
3. video.matches(title="something")
|
|
||||||
4. video.matches(year=2020)
|
|
||||||
5. video.matches(title="something", year="")
|
|
||||||
6. video.matches(title="", year=2020)
|
|
||||||
7. video.matches(title="", year="")
|
|
||||||
|
|
||||||
1. The default behaviour in Plex Web = no params in plexapi
|
|
||||||
2. Both title and year specified by user
|
|
||||||
3. Year automatically filled in
|
|
||||||
4. Title automatically filled in
|
|
||||||
5. Explicitly searches for title with blank year
|
|
||||||
6. Explicitly searches for blank title with year
|
|
||||||
7. I don't know what the user is thinking... return the same result as 1
|
|
||||||
|
|
||||||
For 2 to 7, the agent and language is automatically filled in
|
|
||||||
"""
|
|
||||||
key = '/library/metadata/%s/matches' % self.ratingKey
|
|
||||||
params = {'manual': 1}
|
|
||||||
|
|
||||||
if agent and not any([title, year, language]):
|
|
||||||
params['language'] = self.section().language
|
|
||||||
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
|
||||||
else:
|
|
||||||
if any(x is not None for x in [agent, title, year, language]):
|
|
||||||
if title is None:
|
|
||||||
params['title'] = self.title
|
|
||||||
else:
|
|
||||||
params['title'] = title
|
|
||||||
|
|
||||||
if year is None:
|
|
||||||
params['year'] = self.year
|
|
||||||
else:
|
|
||||||
params['year'] = year
|
|
||||||
|
|
||||||
params['language'] = language or self.section().language
|
|
||||||
|
|
||||||
if agent is None:
|
|
||||||
params['agent'] = self.section().agent
|
|
||||||
else:
|
|
||||||
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
|
||||||
|
|
||||||
key = key + '?' + urlencode(params)
|
|
||||||
data = self._server.query(key, method=self._server._session.get)
|
|
||||||
return self.findItems(data, initpath=key)
|
|
||||||
|
|
||||||
def fixMatch(self, searchResult=None, auto=False, agent=None):
|
|
||||||
""" Use match result to update show metadata.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
auto (bool): True uses first match from matches
|
|
||||||
False allows user to provide the match
|
|
||||||
searchResult (:class:`~plexapi.media.SearchResult`): Search result from
|
|
||||||
~plexapi.base.matches()
|
|
||||||
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
|
||||||
"""
|
|
||||||
key = '/library/metadata/%s/match' % self.ratingKey
|
|
||||||
if auto:
|
|
||||||
autoMatch = self.matches(agent=agent)
|
|
||||||
if autoMatch:
|
|
||||||
searchResult = autoMatch[0]
|
|
||||||
else:
|
|
||||||
raise NotFound('No matches found using this agent: (%s:%s)' % (agent, autoMatch))
|
|
||||||
elif not searchResult:
|
|
||||||
raise NotFound('fixMatch() requires either auto=True or '
|
|
||||||
'searchResult=:class:`~plexapi.media.SearchResult`.')
|
|
||||||
|
|
||||||
params = {'guid': searchResult.guid,
|
|
||||||
'name': searchResult.name}
|
|
||||||
|
|
||||||
data = key + '?' + urlencode(params)
|
|
||||||
self._server.query(data, method=self._server._session.put)
|
|
||||||
|
|
||||||
# The photo tag cant be built atm. TODO
|
|
||||||
# def arts(self):
|
|
||||||
# part = '%s/arts' % self.key
|
|
||||||
# return self.fetchItem(part)
|
|
||||||
|
|
||||||
# def poster(self):
|
|
||||||
# part = '%s/posters' % self.key
|
|
||||||
# return self.fetchItem(part, etag='Photo')
|
|
||||||
|
|
||||||
|
|
||||||
class Playable(object):
|
class Playable(object):
|
||||||
""" This is a general place to store functions specific to media that is Playable.
|
""" This is a general place to store functions specific to media that is Playable.
|
||||||
|
@ -739,24 +580,6 @@ class Playable(object):
|
||||||
for part in item.parts:
|
for part in item.parts:
|
||||||
yield part
|
yield part
|
||||||
|
|
||||||
def split(self):
|
|
||||||
"""Split a duplicate."""
|
|
||||||
key = '%s/split' % self.key
|
|
||||||
return self._server.query(key, method=self._server._session.put)
|
|
||||||
|
|
||||||
def merge(self, ratingKeys):
|
|
||||||
"""Merge duplicate items."""
|
|
||||||
if not isinstance(ratingKeys, list):
|
|
||||||
ratingKeys = str(ratingKeys).split(",")
|
|
||||||
|
|
||||||
key = '%s/merge?ids=%s' % (self.key, ','.join(ratingKeys))
|
|
||||||
return self._server.query(key, method=self._server._session.put)
|
|
||||||
|
|
||||||
def unmatch(self):
|
|
||||||
"""Unmatch a media file."""
|
|
||||||
key = '%s/unmatch' % self.key
|
|
||||||
return self._server.query(key, method=self._server._session.put)
|
|
||||||
|
|
||||||
def play(self, client):
|
def play(self, client):
|
||||||
""" Start playback on the specified client.
|
""" Start playback on the specified client.
|
||||||
|
|
||||||
|
@ -834,17 +657,3 @@ class Playable(object):
|
||||||
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()
|
self.reload()
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
|
||||||
class Release(PlexObject):
|
|
||||||
TAG = 'Release'
|
|
||||||
key = '/updater/status'
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
|
||||||
self.download_key = data.attrib.get('key')
|
|
||||||
self.version = data.attrib.get('version')
|
|
||||||
self.added = data.attrib.get('added')
|
|
||||||
self.fixed = data.attrib.get('fixed')
|
|
||||||
self.downloadURL = data.attrib.get('downloadURL')
|
|
||||||
self.state = data.attrib.get('state')
|
|
||||||
|
|
155
lib/plexapi/collection.py
Normal file
155
lib/plexapi/collection.py
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from plexapi import media, utils
|
||||||
|
from plexapi.base import PlexPartialObject
|
||||||
|
from plexapi.exceptions import BadRequest
|
||||||
|
from plexapi.mixins import ArtMixin, PosterMixin
|
||||||
|
from plexapi.mixins import LabelMixin
|
||||||
|
from plexapi.settings import Setting
|
||||||
|
from plexapi.utils import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
|
||||||
|
""" Represents a single Collection.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): 'Directory'
|
||||||
|
TYPE (str): 'collection'
|
||||||
|
addedAt (datetime): Datetime the collection was added to the library.
|
||||||
|
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
||||||
|
artBlurHash (str): BlurHash string for artwork image.
|
||||||
|
childCount (int): Number of items in the collection.
|
||||||
|
collectionMode (str): How the items in the collection are displayed.
|
||||||
|
collectionSort (str): How to sort the items in the collection.
|
||||||
|
contentRating (str) Content rating (PG-13; NR; TV-G).
|
||||||
|
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
||||||
|
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
|
||||||
|
index (int): Plex index number for the collection.
|
||||||
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
|
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
||||||
|
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
||||||
|
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
|
||||||
|
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
|
||||||
|
maxYear (int): Maximum year for the items in the collection.
|
||||||
|
minYear (int): Minimum year for the items in the collection.
|
||||||
|
ratingKey (int): Unique key identifying the collection.
|
||||||
|
subtype (str): Media type of the items in the collection (movie, show, artist, or album).
|
||||||
|
summary (str): Summary of the collection.
|
||||||
|
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
||||||
|
thumbBlurHash (str): BlurHash string for thumbnail image.
|
||||||
|
title (str): Name of the collection.
|
||||||
|
titleSort (str): Title to use when sorting (defaults to title).
|
||||||
|
type (str): 'collection'
|
||||||
|
updatedAt (datatime): Datetime the collection was updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TAG = 'Directory'
|
||||||
|
TYPE = 'collection'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
||||||
|
self.art = data.attrib.get('art')
|
||||||
|
self.artBlurHash = data.attrib.get('artBlurHash')
|
||||||
|
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
||||||
|
self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1'))
|
||||||
|
self.collectionSort = utils.cast(int, data.attrib.get('collectionSort', '0'))
|
||||||
|
self.contentRating = data.attrib.get('contentRating')
|
||||||
|
self.fields = self.findItems(data, media.Field)
|
||||||
|
self.guid = data.attrib.get('guid')
|
||||||
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
|
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||||
|
self.labels = self.findItems(data, media.Label)
|
||||||
|
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||||
|
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
|
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||||
|
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
||||||
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||||
|
self.subtype = data.attrib.get('subtype')
|
||||||
|
self.summary = data.attrib.get('summary')
|
||||||
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
||||||
|
self.title = data.attrib.get('title')
|
||||||
|
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||||
|
self.type = data.attrib.get('type')
|
||||||
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@deprecated('use "items" instead', stacklevel=3)
|
||||||
|
def children(self):
|
||||||
|
return self.items()
|
||||||
|
|
||||||
|
def item(self, title):
|
||||||
|
""" Returns the item in the collection that matches the specified title.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
title (str): Title of the item to return.
|
||||||
|
"""
|
||||||
|
key = '/library/metadata/%s/children' % self.ratingKey
|
||||||
|
return self.fetchItem(key, title__iexact=title)
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
""" Returns a list of all items in the collection. """
|
||||||
|
key = '/library/metadata/%s/children' % self.ratingKey
|
||||||
|
return self.fetchItems(key)
|
||||||
|
|
||||||
|
def get(self, title):
|
||||||
|
""" Alias to :func:`~plexapi.library.Collection.item`. """
|
||||||
|
return self.item(title)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return self.childCount
|
||||||
|
|
||||||
|
def _preferences(self):
|
||||||
|
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
|
||||||
|
items = []
|
||||||
|
data = self._server.query(self._details_key)
|
||||||
|
for item in data.iter('Setting'):
|
||||||
|
items.append(Setting(data=item, server=self._server))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def modeUpdate(self, mode=None):
|
||||||
|
""" Update Collection Mode
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
mode: default (Library default)
|
||||||
|
hide (Hide Collection)
|
||||||
|
hideItems (Hide Items in this Collection)
|
||||||
|
showItems (Show this Collection and its Items)
|
||||||
|
Example:
|
||||||
|
|
||||||
|
collection = 'plexapi.library.Collections'
|
||||||
|
collection.updateMode(mode="hide")
|
||||||
|
"""
|
||||||
|
mode_dict = {'default': -1,
|
||||||
|
'hide': 0,
|
||||||
|
'hideItems': 1,
|
||||||
|
'showItems': 2}
|
||||||
|
key = mode_dict.get(mode)
|
||||||
|
if key is None:
|
||||||
|
raise BadRequest('Unknown collection mode : %s. Options %s' % (mode, list(mode_dict)))
|
||||||
|
part = '/library/metadata/%s/prefs?collectionMode=%s' % (self.ratingKey, key)
|
||||||
|
return self._server.query(part, method=self._server._session.put)
|
||||||
|
|
||||||
|
def sortUpdate(self, sort=None):
|
||||||
|
""" Update Collection Sorting
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
sort: realease (Order Collection by realease dates)
|
||||||
|
alpha (Order Collection alphabetically)
|
||||||
|
custom (Custom collection order)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
colleciton = 'plexapi.library.Collections'
|
||||||
|
collection.updateSort(mode="alpha")
|
||||||
|
"""
|
||||||
|
sort_dict = {'release': 0,
|
||||||
|
'alpha': 1,
|
||||||
|
'custom': 2}
|
||||||
|
key = sort_dict.get(sort)
|
||||||
|
if key is None:
|
||||||
|
raise BadRequest('Unknown sort dir: %s. Options: %s' % (sort, list(sort_dict)))
|
||||||
|
part = '/library/metadata/%s/prefs?collectionSort=%s' % (self.ratingKey, key)
|
||||||
|
return self._server.query(part, method=self._server._session.put)
|
|
@ -13,11 +13,14 @@ import struct
|
||||||
|
|
||||||
|
|
||||||
class GDM:
|
class GDM:
|
||||||
"""Base class to discover GDM services."""
|
"""Base class to discover GDM services.
|
||||||
|
|
||||||
|
Atrributes:
|
||||||
|
entries (List<dict>): List of server and/or client data discovered.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.entries = []
|
self.entries = []
|
||||||
self.last_scan = None
|
|
||||||
|
|
||||||
def scan(self, scan_for_clients=False):
|
def scan(self, scan_for_clients=False):
|
||||||
"""Scan the network."""
|
"""Scan the network."""
|
||||||
|
@ -35,7 +38,7 @@ class GDM:
|
||||||
"""Return a list of entries that match the content_type."""
|
"""Return a list of entries that match the content_type."""
|
||||||
self.scan()
|
self.scan()
|
||||||
return [entry for entry in self.entries
|
return [entry for entry in self.entries
|
||||||
if value in entry['data']['Content_Type']]
|
if value in entry['data']['Content-Type']]
|
||||||
|
|
||||||
def find_by_data(self, values):
|
def find_by_data(self, values):
|
||||||
"""Return a list of entries that match the search parameters."""
|
"""Return a list of entries that match the search parameters."""
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from urllib.parse import quote, quote_plus, unquote, urlencode
|
from urllib.parse import quote, quote_plus, unquote, urlencode
|
||||||
|
|
||||||
from plexapi import X_PLEX_CONTAINER_SIZE, log, media, utils
|
from plexapi import X_PLEX_CONTAINER_SIZE, log, media, utils
|
||||||
from plexapi.base import OPERATORS, PlexObject, PlexPartialObject
|
from plexapi.base import OPERATORS, PlexObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
from plexapi.settings import Setting
|
from plexapi.settings import Setting
|
||||||
from plexapi.utils import deprecated
|
from plexapi.utils import deprecated
|
||||||
|
@ -723,7 +723,7 @@ class LibrarySection(PlexObject):
|
||||||
result = set()
|
result = set()
|
||||||
choices = self.listChoices(category, libtype)
|
choices = self.listChoices(category, libtype)
|
||||||
lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
|
lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
|
||||||
allowed = set(c.key for c in choices)
|
allowed = {c.key for c in choices}
|
||||||
for item in value:
|
for item in value:
|
||||||
item = str((item.id or item.tag) if isinstance(item, media.MediaTag) else item).lower()
|
item = str((item.id or item.tag) if isinstance(item, media.MediaTag) else item).lower()
|
||||||
# find most logical choice(s) to use in url
|
# find most logical choice(s) to use in url
|
||||||
|
@ -1525,206 +1525,6 @@ class FirstCharacter(PlexObject):
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
|
||||||
class Collections(PlexPartialObject):
|
|
||||||
""" Represents a single Collection.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
TAG (str): 'Directory'
|
|
||||||
TYPE (str): 'collection'
|
|
||||||
addedAt (datetime): Datetime the collection was added to the library.
|
|
||||||
art (str): URL to artwork image (/library/metadata/<ratingKey>/art/<artid>).
|
|
||||||
artBlurHash (str): BlurHash string for artwork image.
|
|
||||||
childCount (int): Number of items in the collection.
|
|
||||||
collectionMode (str): How the items in the collection are displayed.
|
|
||||||
collectionSort (str): How to sort the items in the collection.
|
|
||||||
contentRating (str) Content rating (PG-13; NR; TV-G).
|
|
||||||
fields (List<:class:`~plexapi.media.Field`>): List of field objects.
|
|
||||||
guid (str): Plex GUID for the collection (collection://XXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX).
|
|
||||||
index (int): Plex index number for the collection.
|
|
||||||
key (str): API URL (/library/metadata/<ratingkey>).
|
|
||||||
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
|
|
||||||
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
|
|
||||||
librarySectionKey (str): :class:`~plexapi.library.LibrarySection` key.
|
|
||||||
librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title.
|
|
||||||
maxYear (int): Maximum year for the items in the collection.
|
|
||||||
minYear (int): Minimum year for the items in the collection.
|
|
||||||
ratingKey (int): Unique key identifying the collection.
|
|
||||||
subtype (str): Media type of the items in the collection (movie, show, artist, or album).
|
|
||||||
summary (str): Summary of the collection.
|
|
||||||
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
|
||||||
thumbBlurHash (str): BlurHash string for thumbnail image.
|
|
||||||
title (str): Name of the collection.
|
|
||||||
titleSort (str): Title to use when sorting (defaults to title).
|
|
||||||
type (str): 'collection'
|
|
||||||
updatedAt (datatime): Datetime the collection was updated.
|
|
||||||
"""
|
|
||||||
|
|
||||||
TAG = 'Directory'
|
|
||||||
TYPE = 'collection'
|
|
||||||
|
|
||||||
def _loadData(self, data):
|
|
||||||
self.addedAt = utils.toDatetime(data.attrib.get('addedAt'))
|
|
||||||
self.art = data.attrib.get('art')
|
|
||||||
self.artBlurHash = data.attrib.get('artBlurHash')
|
|
||||||
self.childCount = utils.cast(int, data.attrib.get('childCount'))
|
|
||||||
self.collectionMode = data.attrib.get('collectionMode')
|
|
||||||
self.collectionSort = data.attrib.get('collectionSort')
|
|
||||||
self.contentRating = data.attrib.get('contentRating')
|
|
||||||
self.fields = self.findItems(data, media.Field)
|
|
||||||
self.guid = data.attrib.get('guid')
|
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
|
||||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
|
||||||
self.labels = self.findItems(data, media.Label)
|
|
||||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
|
||||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
|
||||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
|
||||||
self.minYear = utils.cast(int, data.attrib.get('minYear'))
|
|
||||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
|
||||||
self.subtype = data.attrib.get('subtype')
|
|
||||||
self.summary = data.attrib.get('summary')
|
|
||||||
self.thumb = data.attrib.get('thumb')
|
|
||||||
self.thumbBlurHash = data.attrib.get('thumbBlurHash')
|
|
||||||
self.title = data.attrib.get('title')
|
|
||||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
|
||||||
self.type = data.attrib.get('type')
|
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
|
||||||
|
|
||||||
@property
|
|
||||||
@deprecated('use "items" instead')
|
|
||||||
def children(self):
|
|
||||||
return self.fetchItems(self.key)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbUrl(self):
|
|
||||||
""" Return the thumbnail url for the collection."""
|
|
||||||
return self._server.url(self.thumb, includeToken=True) if self.thumb else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artUrl(self):
|
|
||||||
""" Return the art url for the collection."""
|
|
||||||
return self._server.url(self.art, includeToken=True) if self.art else None
|
|
||||||
|
|
||||||
def item(self, title):
|
|
||||||
""" Returns the item in the collection that matches the specified title.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
title (str): Title of the item to return.
|
|
||||||
"""
|
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
|
||||||
return self.fetchItem(key, title__iexact=title)
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
""" Returns a list of all items in the collection. """
|
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
|
||||||
return self.fetchItems(key)
|
|
||||||
|
|
||||||
def get(self, title):
|
|
||||||
""" Alias to :func:`~plexapi.library.Collection.item`. """
|
|
||||||
return self.item(title)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return self.childCount
|
|
||||||
|
|
||||||
def _preferences(self):
|
|
||||||
""" Returns a list of :class:`~plexapi.settings.Preferences` objects. """
|
|
||||||
items = []
|
|
||||||
data = self._server.query(self._details_key)
|
|
||||||
for item in data.iter('Setting'):
|
|
||||||
items.append(Setting(data=item, server=self._server))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
part = '/library/metadata/%s' % self.ratingKey
|
|
||||||
return self._server.query(part, method=self._server._session.delete)
|
|
||||||
|
|
||||||
def modeUpdate(self, mode=None):
|
|
||||||
""" Update Collection Mode
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
mode: default (Library default)
|
|
||||||
hide (Hide Collection)
|
|
||||||
hideItems (Hide Items in this Collection)
|
|
||||||
showItems (Show this Collection and its Items)
|
|
||||||
Example:
|
|
||||||
|
|
||||||
collection = 'plexapi.library.Collections'
|
|
||||||
collection.updateMode(mode="hide")
|
|
||||||
"""
|
|
||||||
mode_dict = {'default': '-1',
|
|
||||||
'hide': '0',
|
|
||||||
'hideItems': '1',
|
|
||||||
'showItems': '2'}
|
|
||||||
key = mode_dict.get(mode)
|
|
||||||
if key is None:
|
|
||||||
raise BadRequest('Unknown collection mode : %s. Options %s' % (mode, list(mode_dict)))
|
|
||||||
part = '/library/metadata/%s/prefs?collectionMode=%s' % (self.ratingKey, key)
|
|
||||||
return self._server.query(part, method=self._server._session.put)
|
|
||||||
|
|
||||||
def sortUpdate(self, sort=None):
|
|
||||||
""" Update Collection Sorting
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
sort: realease (Order Collection by realease dates)
|
|
||||||
alpha (Order Collection Alphabetically)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
colleciton = 'plexapi.library.Collections'
|
|
||||||
collection.updateSort(mode="alpha")
|
|
||||||
"""
|
|
||||||
sort_dict = {'release': '0',
|
|
||||||
'alpha': '1'}
|
|
||||||
key = sort_dict.get(sort)
|
|
||||||
if key is None:
|
|
||||||
raise BadRequest('Unknown sort dir: %s. Options: %s' % (sort, list(sort_dict)))
|
|
||||||
part = '/library/metadata/%s/prefs?collectionSort=%s' % (self.ratingKey, key)
|
|
||||||
return self._server.query(part, method=self._server._session.put)
|
|
||||||
|
|
||||||
def posters(self):
|
|
||||||
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey)
|
|
||||||
|
|
||||||
def uploadPoster(self, url=None, filepath=None):
|
|
||||||
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '/library/metadata/%s/posters?' % self.ratingKey
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setPoster(self, poster):
|
|
||||||
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
poster.select()
|
|
||||||
|
|
||||||
def arts(self):
|
|
||||||
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey)
|
|
||||||
|
|
||||||
def uploadArt(self, url=None, filepath=None):
|
|
||||||
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '/library/metadata/%s/arts?' % self.ratingKey
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setArt(self, art):
|
|
||||||
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
art.select()
|
|
||||||
|
|
||||||
# def edit(self, **kwargs):
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Path(PlexObject):
|
class Path(PlexObject):
|
||||||
""" Represents a single directory Path.
|
""" Represents a single directory Path.
|
||||||
|
|
|
@ -708,10 +708,10 @@ class Collection(MediaTag):
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Label(MediaTag):
|
class Label(MediaTag):
|
||||||
""" Represents a single label media tag.
|
""" Represents a single Label media tag.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'label'
|
TAG (str): 'Label'
|
||||||
FILTER (str): 'label'
|
FILTER (str): 'label'
|
||||||
"""
|
"""
|
||||||
TAG = 'Label'
|
TAG = 'Label'
|
||||||
|
@ -720,10 +720,10 @@ class Label(MediaTag):
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Tag(MediaTag):
|
class Tag(MediaTag):
|
||||||
""" Represents a single tag media tag.
|
""" Represents a single Tag media tag.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'tag'
|
TAG (str): 'Tag'
|
||||||
FILTER (str): 'tag'
|
FILTER (str): 'tag'
|
||||||
"""
|
"""
|
||||||
TAG = 'Tag'
|
TAG = 'Tag'
|
||||||
|
@ -807,20 +807,25 @@ class Style(MediaTag):
|
||||||
FILTER = 'style'
|
FILTER = 'style'
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
class BaseImage(PlexObject):
|
||||||
class Poster(PlexObject):
|
""" Base class for all Art, Banner, and Poster objects.
|
||||||
""" Represents a Poster.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Photo'
|
TAG (str): 'Photo'
|
||||||
|
key (str): API URL (/library/metadata/<ratingkey>).
|
||||||
|
provider (str): The source of the poster or art.
|
||||||
|
ratingKey (str): Unique key identifying the poster or art.
|
||||||
|
selected (bool): True if the poster or art is currently selected.
|
||||||
|
thumb (str): The URL to retrieve the poster or art thumbnail.
|
||||||
"""
|
"""
|
||||||
TAG = 'Photo'
|
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')
|
||||||
|
self.provider = data.attrib.get('provider')
|
||||||
self.ratingKey = data.attrib.get('ratingKey')
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
self.selected = data.attrib.get('selected')
|
self.selected = cast(bool, data.attrib.get('selected'))
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
|
|
||||||
def select(self):
|
def select(self):
|
||||||
|
@ -832,6 +837,18 @@ class Poster(PlexObject):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Art(BaseImage):
|
||||||
|
""" Represents a single Art object. """
|
||||||
|
|
||||||
|
|
||||||
|
class Banner(BaseImage):
|
||||||
|
""" Represents a single Banner object. """
|
||||||
|
|
||||||
|
|
||||||
|
class Poster(BaseImage):
|
||||||
|
""" Represents a single Poster object. """
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Producer(MediaTag):
|
class Producer(MediaTag):
|
||||||
""" Represents a single Producer media tag.
|
""" Represents a single Producer media tag.
|
||||||
|
|
489
lib/plexapi/mixins.py
Normal file
489
lib/plexapi/mixins.py
Normal file
|
@ -0,0 +1,489 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from urllib.parse import quote_plus, urlencode
|
||||||
|
|
||||||
|
from plexapi import media, utils
|
||||||
|
from plexapi.exceptions import NotFound
|
||||||
|
|
||||||
|
|
||||||
|
class ArtUrlMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have a background artwork url. """
|
||||||
|
|
||||||
|
@property
|
||||||
|
def artUrl(self):
|
||||||
|
""" Return the art url for the Plex object. """
|
||||||
|
art = self.firstAttr('art', 'grandparentArt')
|
||||||
|
return self._server.url(art, includeToken=True) if art else None
|
||||||
|
|
||||||
|
|
||||||
|
class ArtMixin(ArtUrlMixin):
|
||||||
|
""" Mixin for Plex objects that can have background artwork. """
|
||||||
|
|
||||||
|
def arts(self):
|
||||||
|
""" Returns list of available :class:`~plexapi.media.Art` objects. """
|
||||||
|
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey, cls=media.Art)
|
||||||
|
|
||||||
|
def uploadArt(self, url=None, filepath=None):
|
||||||
|
""" Upload a background artwork from a url or filepath.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url (str): The full URL to the image to upload.
|
||||||
|
filepath (str): The full file path the the image to upload.
|
||||||
|
"""
|
||||||
|
if url:
|
||||||
|
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
|
||||||
|
self._server.query(key, method=self._server._session.post)
|
||||||
|
elif filepath:
|
||||||
|
key = '/library/metadata/%s/arts?' % self.ratingKey
|
||||||
|
data = open(filepath, 'rb').read()
|
||||||
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
def setArt(self, art):
|
||||||
|
""" Set the background artwork for a Plex object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
art (:class:`~plexapi.media.Art`): The art object to select.
|
||||||
|
"""
|
||||||
|
art.select()
|
||||||
|
|
||||||
|
|
||||||
|
class BannerUrlMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have a banner url. """
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bannerUrl(self):
|
||||||
|
""" Return the banner url for the Plex object. """
|
||||||
|
banner = self.firstAttr('banner')
|
||||||
|
return self._server.url(banner, includeToken=True) if banner else None
|
||||||
|
|
||||||
|
|
||||||
|
class BannerMixin(BannerUrlMixin):
|
||||||
|
""" Mixin for Plex objects that can have banners. """
|
||||||
|
|
||||||
|
def banners(self):
|
||||||
|
""" Returns list of available :class:`~plexapi.media.Banner` objects. """
|
||||||
|
return self.fetchItems('/library/metadata/%s/banners' % self.ratingKey, cls=media.Banner)
|
||||||
|
|
||||||
|
def uploadBanner(self, url=None, filepath=None):
|
||||||
|
""" Upload a banner from a url or filepath.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url (str): The full URL to the image to upload.
|
||||||
|
filepath (str): The full file path the the image to upload.
|
||||||
|
"""
|
||||||
|
if url:
|
||||||
|
key = '/library/metadata/%s/banners?url=%s' % (self.ratingKey, quote_plus(url))
|
||||||
|
self._server.query(key, method=self._server._session.post)
|
||||||
|
elif filepath:
|
||||||
|
key = '/library/metadata/%s/banners?' % self.ratingKey
|
||||||
|
data = open(filepath, 'rb').read()
|
||||||
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
def setBanner(self, banner):
|
||||||
|
""" Set the banner for a Plex object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
banner (:class:`~plexapi.media.Banner`): The banner object to select.
|
||||||
|
"""
|
||||||
|
banner.select()
|
||||||
|
|
||||||
|
|
||||||
|
class PosterUrlMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have a poster url. """
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbUrl(self):
|
||||||
|
""" Return the thumb url for the Plex object. """
|
||||||
|
thumb = self.firstAttr('thumb', 'parentThumb', 'granparentThumb')
|
||||||
|
return self._server.url(thumb, includeToken=True) if thumb else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def posterUrl(self):
|
||||||
|
""" Alias to self.thumbUrl. """
|
||||||
|
return self.thumbUrl
|
||||||
|
|
||||||
|
|
||||||
|
class PosterMixin(PosterUrlMixin):
|
||||||
|
""" Mixin for Plex objects that can have posters. """
|
||||||
|
|
||||||
|
def posters(self):
|
||||||
|
""" Returns list of available :class:`~plexapi.media.Poster` objects. """
|
||||||
|
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey, cls=media.Poster)
|
||||||
|
|
||||||
|
def uploadPoster(self, url=None, filepath=None):
|
||||||
|
""" Upload a poster from a url or filepath.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url (str): The full URL to the image to upload.
|
||||||
|
filepath (str): The full file path the the image to upload.
|
||||||
|
"""
|
||||||
|
if url:
|
||||||
|
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
|
||||||
|
self._server.query(key, method=self._server._session.post)
|
||||||
|
elif filepath:
|
||||||
|
key = '/library/metadata/%s/posters?' % self.ratingKey
|
||||||
|
data = open(filepath, 'rb').read()
|
||||||
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
def setPoster(self, poster):
|
||||||
|
""" Set the poster for a Plex object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
poster (:class:`~plexapi.media.Poster`): The poster object to select.
|
||||||
|
"""
|
||||||
|
poster.select()
|
||||||
|
|
||||||
|
|
||||||
|
class SplitMergeMixin(object):
|
||||||
|
""" Mixin for Plex objects that can be split and merged. """
|
||||||
|
|
||||||
|
def split(self):
|
||||||
|
""" Split duplicated Plex object into separate objects. """
|
||||||
|
key = '/library/metadata/%s/split' % self.ratingKey
|
||||||
|
return self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
|
def merge(self, ratingKeys):
|
||||||
|
""" Merge other Plex objects into the current object.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ratingKeys (list): A list of rating keys to merge.
|
||||||
|
"""
|
||||||
|
if not isinstance(ratingKeys, list):
|
||||||
|
ratingKeys = str(ratingKeys).split(',')
|
||||||
|
|
||||||
|
key = '%s/merge?ids=%s' % (self.key, ','.join([str(r) for r in ratingKeys]))
|
||||||
|
return self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
|
|
||||||
|
class UnmatchMatchMixin(object):
|
||||||
|
""" Mixin for Plex objects that can be unmatched and matched. """
|
||||||
|
|
||||||
|
def unmatch(self):
|
||||||
|
""" Unmatches metadata match from object. """
|
||||||
|
key = '/library/metadata/%s/unmatch' % self.ratingKey
|
||||||
|
self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
|
def matches(self, agent=None, title=None, year=None, language=None):
|
||||||
|
""" Return list of (:class:`~plexapi.media.SearchResult`) metadata matches.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
||||||
|
title (str): Title of item to search for
|
||||||
|
year (str): Year of item to search in
|
||||||
|
language (str) : Language of item to search in
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
1. video.matches()
|
||||||
|
2. video.matches(title="something", year=2020)
|
||||||
|
3. video.matches(title="something")
|
||||||
|
4. video.matches(year=2020)
|
||||||
|
5. video.matches(title="something", year="")
|
||||||
|
6. video.matches(title="", year=2020)
|
||||||
|
7. video.matches(title="", year="")
|
||||||
|
|
||||||
|
1. The default behaviour in Plex Web = no params in plexapi
|
||||||
|
2. Both title and year specified by user
|
||||||
|
3. Year automatically filled in
|
||||||
|
4. Title automatically filled in
|
||||||
|
5. Explicitly searches for title with blank year
|
||||||
|
6. Explicitly searches for blank title with year
|
||||||
|
7. I don't know what the user is thinking... return the same result as 1
|
||||||
|
|
||||||
|
For 2 to 7, the agent and language is automatically filled in
|
||||||
|
"""
|
||||||
|
key = '/library/metadata/%s/matches' % self.ratingKey
|
||||||
|
params = {'manual': 1}
|
||||||
|
|
||||||
|
if agent and not any([title, year, language]):
|
||||||
|
params['language'] = self.section().language
|
||||||
|
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
||||||
|
else:
|
||||||
|
if any(x is not None for x in [agent, title, year, language]):
|
||||||
|
if title is None:
|
||||||
|
params['title'] = self.title
|
||||||
|
else:
|
||||||
|
params['title'] = title
|
||||||
|
|
||||||
|
if year is None:
|
||||||
|
params['year'] = self.year
|
||||||
|
else:
|
||||||
|
params['year'] = year
|
||||||
|
|
||||||
|
params['language'] = language or self.section().language
|
||||||
|
|
||||||
|
if agent is None:
|
||||||
|
params['agent'] = self.section().agent
|
||||||
|
else:
|
||||||
|
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
||||||
|
|
||||||
|
key = key + '?' + urlencode(params)
|
||||||
|
data = self._server.query(key, method=self._server._session.get)
|
||||||
|
return self.findItems(data, initpath=key)
|
||||||
|
|
||||||
|
def fixMatch(self, searchResult=None, auto=False, agent=None):
|
||||||
|
""" Use match result to update show metadata.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
auto (bool): True uses first match from matches
|
||||||
|
False allows user to provide the match
|
||||||
|
searchResult (:class:`~plexapi.media.SearchResult`): Search result from
|
||||||
|
~plexapi.base.matches()
|
||||||
|
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
||||||
|
"""
|
||||||
|
key = '/library/metadata/%s/match' % self.ratingKey
|
||||||
|
if auto:
|
||||||
|
autoMatch = self.matches(agent=agent)
|
||||||
|
if autoMatch:
|
||||||
|
searchResult = autoMatch[0]
|
||||||
|
else:
|
||||||
|
raise NotFound('No matches found using this agent: (%s:%s)' % (agent, autoMatch))
|
||||||
|
elif not searchResult:
|
||||||
|
raise NotFound('fixMatch() requires either auto=True or '
|
||||||
|
'searchResult=:class:`~plexapi.media.SearchResult`.')
|
||||||
|
|
||||||
|
params = {'guid': searchResult.guid,
|
||||||
|
'name': searchResult.name}
|
||||||
|
|
||||||
|
data = key + '?' + urlencode(params)
|
||||||
|
self._server.query(data, method=self._server._session.put)
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have collections. """
|
||||||
|
|
||||||
|
def addCollection(self, collections, locked=True):
|
||||||
|
""" Add a collection tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
collections (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('collection', collections, locked=locked)
|
||||||
|
|
||||||
|
def removeCollection(self, collections, locked=True):
|
||||||
|
""" Remove a collection tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
collections (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('collection', collections, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CountryMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have countries. """
|
||||||
|
|
||||||
|
def addCountry(self, countries, locked=True):
|
||||||
|
""" Add a country tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
countries (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('country', countries, locked=locked)
|
||||||
|
|
||||||
|
def removeCountry(self, countries, locked=True):
|
||||||
|
""" Remove a country tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
countries (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('country', countries, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class DirectorMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have directors. """
|
||||||
|
|
||||||
|
def addDirector(self, directors, locked=True):
|
||||||
|
""" Add a director tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
directors (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('director', directors, locked=locked)
|
||||||
|
|
||||||
|
def removeDirector(self, directors, locked=True):
|
||||||
|
""" Remove a director tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
directors (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('director', directors, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GenreMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have genres. """
|
||||||
|
|
||||||
|
def addGenre(self, genres, locked=True):
|
||||||
|
""" Add a genre tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
genres (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('genre', genres, locked=locked)
|
||||||
|
|
||||||
|
def removeGenre(self, genres, locked=True):
|
||||||
|
""" Remove a genre tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
genres (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('genre', genres, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LabelMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have labels. """
|
||||||
|
|
||||||
|
def addLabel(self, labels, locked=True):
|
||||||
|
""" Add a label tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
labels (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('label', labels, locked=locked)
|
||||||
|
|
||||||
|
def removeLabel(self, labels, locked=True):
|
||||||
|
""" Remove a label tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
labels (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('label', labels, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class MoodMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have moods. """
|
||||||
|
|
||||||
|
def addMood(self, moods, locked=True):
|
||||||
|
""" Add a mood tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
moods (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('mood', moods, locked=locked)
|
||||||
|
|
||||||
|
def removeMood(self, moods, locked=True):
|
||||||
|
""" Remove a mood tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
moods (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('mood', moods, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProducerMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have producers. """
|
||||||
|
|
||||||
|
def addProducer(self, producers, locked=True):
|
||||||
|
""" Add a producer tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
producers (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('producer', producers, locked=locked)
|
||||||
|
|
||||||
|
def removeProducer(self, producers, locked=True):
|
||||||
|
""" Remove a producer tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
producers (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('producer', producers, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SimilarArtistMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have similar artists. """
|
||||||
|
|
||||||
|
def addSimilarArtist(self, artists, locked=True):
|
||||||
|
""" Add a similar artist tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
artists (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('similar', artists, locked=locked)
|
||||||
|
|
||||||
|
def removeSimilarArtist(self, artists, locked=True):
|
||||||
|
""" Remove a similar artist tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
artists (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('similar', artists, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class StyleMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have styles. """
|
||||||
|
|
||||||
|
def addStyle(self, styles, locked=True):
|
||||||
|
""" Add a style tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
styles (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('style', styles, locked=locked)
|
||||||
|
|
||||||
|
def removeStyle(self, styles, locked=True):
|
||||||
|
""" Remove a style tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
styles (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('style', styles, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TagMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have tags. """
|
||||||
|
|
||||||
|
def addTag(self, tags, locked=True):
|
||||||
|
""" Add a tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
tags (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('tag', tags, locked=locked)
|
||||||
|
|
||||||
|
def removeTag(self, tags, locked=True):
|
||||||
|
""" Remove a tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
tags (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('tag', tags, locked=locked, remove=True)
|
||||||
|
|
||||||
|
|
||||||
|
class WriterMixin(object):
|
||||||
|
""" Mixin for Plex objects that can have writers. """
|
||||||
|
|
||||||
|
def addWriter(self, writers, locked=True):
|
||||||
|
""" Add a writer tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
writers (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('writer', writers, locked=locked)
|
||||||
|
|
||||||
|
def removeWriter(self, writers, locked=True):
|
||||||
|
""" Remove a writer tag(s).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
writers (list): List of strings.
|
||||||
|
locked (bool): True (default) to lock the field, False to unlock the field.
|
||||||
|
"""
|
||||||
|
self._edit_tags('writer', writers, locked=locked, remove=True)
|
|
@ -4,10 +4,11 @@ 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, TagMixin
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Photoalbum(PlexPartialObject):
|
class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin):
|
||||||
""" Represents a single Photoalbum (collection of photos).
|
""" Represents a single Photoalbum (collection of photos).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -136,7 +137,7 @@ class Photoalbum(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Photo(PlexPartialObject, Playable):
|
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
|
||||||
""" Represents a single Photo.
|
""" Represents a single Photo.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -163,7 +164,7 @@ class Photo(PlexPartialObject, Playable):
|
||||||
parentTitle (str): Name of the photo album for the photo.
|
parentTitle (str): Name of the photo album for the photo.
|
||||||
ratingKey (int): Unique key identifying the photo.
|
ratingKey (int): Unique key identifying the photo.
|
||||||
summary (str): Summary of the photo.
|
summary (str): Summary of the photo.
|
||||||
tag (List<:class:`~plexapi.media.Tag`>): List of tag objects.
|
tags (List<:class:`~plexapi.media.Tag`>): List of tag objects.
|
||||||
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
|
||||||
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).
|
||||||
|
@ -199,7 +200,7 @@ class Photo(PlexPartialObject, Playable):
|
||||||
self.parentTitle = data.attrib.get('parentTitle')
|
self.parentTitle = data.attrib.get('parentTitle')
|
||||||
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
|
||||||
self.summary = data.attrib.get('summary')
|
self.summary = data.attrib.get('summary')
|
||||||
self.tag = self.findItems(data, media.Tag)
|
self.tags = self.findItems(data, media.Tag)
|
||||||
self.thumb = data.attrib.get('thumb')
|
self.thumb = data.attrib.get('thumb')
|
||||||
self.title = data.attrib.get('title')
|
self.title = data.attrib.get('title')
|
||||||
self.titleSort = data.attrib.get('titleSort', self.title)
|
self.titleSort = data.attrib.get('titleSort', self.title)
|
||||||
|
@ -207,12 +208,6 @@ class Photo(PlexPartialObject, Playable):
|
||||||
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt'))
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbUrl(self):
|
|
||||||
"""Return URL for the thumbnail image."""
|
|
||||||
key = self.firstAttr('thumb', 'parentThumb', 'granparentThumb')
|
|
||||||
return self._server.url(key, includeToken=True) if key else None
|
|
||||||
|
|
||||||
def photoalbum(self):
|
def photoalbum(self):
|
||||||
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
|
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
|
||||||
return self.fetchItem(self.parentKey)
|
return self.fetchItem(self.parentKey)
|
||||||
|
|
|
@ -5,12 +5,13 @@ from plexapi import 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
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.utils import cast, toDatetime
|
from plexapi.utils import cast, toDatetime
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Playlist(PlexPartialObject, Playable):
|
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
||||||
""" Represents a single Playlist.
|
""" Represents a single Playlist.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -62,6 +63,11 @@ class Playlist(PlexPartialObject, Playable):
|
||||||
for item in self.items():
|
for item in self.items():
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumb(self):
|
||||||
|
""" Alias to self.composite. """
|
||||||
|
return self.composite
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadataType(self):
|
def metadataType(self):
|
||||||
if self.isVideo:
|
if self.isVideo:
|
||||||
|
@ -311,41 +317,3 @@ class Playlist(PlexPartialObject, Playable):
|
||||||
raise Unsupported('Unsupported playlist content')
|
raise Unsupported('Unsupported playlist content')
|
||||||
|
|
||||||
return myplex.sync(sync_item, client=client, clientId=clientId)
|
return myplex.sync(sync_item, client=client, clientId=clientId)
|
||||||
|
|
||||||
def posters(self):
|
|
||||||
""" Returns list of available poster objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey)
|
|
||||||
|
|
||||||
def uploadPoster(self, url=None, filepath=None):
|
|
||||||
""" Upload poster from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '/library/metadata/%s/posters?' % self.ratingKey
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setPoster(self, poster):
|
|
||||||
""" Set . :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
poster.select()
|
|
||||||
|
|
||||||
def arts(self):
|
|
||||||
""" Returns list of available art objects. :class:`~plexapi.media.Poster`. """
|
|
||||||
|
|
||||||
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey)
|
|
||||||
|
|
||||||
def uploadArt(self, url=None, filepath=None):
|
|
||||||
""" Upload art from url or filepath. :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video`. """
|
|
||||||
if url:
|
|
||||||
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
|
|
||||||
self._server.query(key, method=self._server._session.post)
|
|
||||||
elif filepath:
|
|
||||||
key = '/library/metadata/%s/arts?' % self.ratingKey
|
|
||||||
data = open(filepath, 'rb').read()
|
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
|
||||||
|
|
||||||
def setArt(self, art):
|
|
||||||
""" Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """
|
|
||||||
art.select()
|
|
||||||
|
|
|
@ -15,11 +15,12 @@ from plexapi.media import Conversion, Optimized
|
||||||
from plexapi.playlist import Playlist
|
from plexapi.playlist import Playlist
|
||||||
from plexapi.playqueue import PlayQueue
|
from plexapi.playqueue import PlayQueue
|
||||||
from plexapi.settings import Settings
|
from plexapi.settings import Settings
|
||||||
from plexapi.utils import cast
|
from plexapi.utils import cast, deprecated
|
||||||
from requests.status_codes import _codes as codes
|
from requests.status_codes import _codes as codes
|
||||||
|
|
||||||
# Need these imports to populate utils.PLEXOBJECTS
|
# Need these imports to populate utils.PLEXOBJECTS
|
||||||
from plexapi import audio as _audio # noqa: F401; noqa: F401
|
from plexapi import audio as _audio # noqa: F401
|
||||||
|
from plexapi import collection as _collection # noqa: F401
|
||||||
from plexapi import media as _media # noqa: F401
|
from plexapi import media as _media # noqa: F401
|
||||||
from plexapi import photo as _photo # noqa: F401
|
from plexapi import photo as _photo # noqa: F401
|
||||||
from plexapi import playlist as _playlist # noqa: F401
|
from plexapi import playlist as _playlist # noqa: F401
|
||||||
|
@ -374,7 +375,11 @@ 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
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|
||||||
|
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:
|
||||||
|
@ -390,7 +395,7 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
def isLatest(self):
|
def isLatest(self):
|
||||||
""" Check if the installed version of PMS is the latest. """
|
""" Check if the installed version of PMS is the latest. """
|
||||||
release = self.check_for_update(force=True)
|
release = self.checkForUpdate(force=True)
|
||||||
return release is None
|
return release is None
|
||||||
|
|
||||||
def installUpdate(self):
|
def installUpdate(self):
|
||||||
|
@ -398,7 +403,7 @@ class PlexServer(PlexObject):
|
||||||
# We can add this but dunno how useful this is since it sometimes
|
# We can add this but dunno how useful this is since it sometimes
|
||||||
# requires user action using a gui.
|
# requires user action using a gui.
|
||||||
part = '/updater/apply'
|
part = '/updater/apply'
|
||||||
release = self.check_for_update(force=True, download=True)
|
release = self.checkForUpdate(force=True, download=True)
|
||||||
if release and release.version != self.version:
|
if release and release.version != self.version:
|
||||||
# figure out what method this is..
|
# figure out what method this is..
|
||||||
return self.query(part, method=self._session.put)
|
return self.query(part, method=self._session.put)
|
||||||
|
@ -787,6 +792,20 @@ class Activity(PlexObject):
|
||||||
self.uuid = data.attrib.get('uuid')
|
self.uuid = data.attrib.get('uuid')
|
||||||
|
|
||||||
|
|
||||||
|
@utils.registerPlexObject
|
||||||
|
class Release(PlexObject):
|
||||||
|
TAG = 'Release'
|
||||||
|
key = '/updater/status'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self.download_key = data.attrib.get('key')
|
||||||
|
self.version = data.attrib.get('version')
|
||||||
|
self.added = data.attrib.get('added')
|
||||||
|
self.fixed = data.attrib.get('fixed')
|
||||||
|
self.downloadURL = data.attrib.get('downloadURL')
|
||||||
|
self.state = data.attrib.get('state')
|
||||||
|
|
||||||
|
|
||||||
class SystemAccount(PlexObject):
|
class SystemAccount(PlexObject):
|
||||||
""" Represents a single system account.
|
""" Represents a single system account.
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class Settings(PlexObject):
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
""" Returns a list of all :class:`~plexapi.settings.Setting` objects available. """
|
""" Returns a list of all :class:`~plexapi.settings.Setting` objects available. """
|
||||||
return list(v for id, v in sorted(self._settings.items()))
|
return [v for id, v in sorted(self._settings.items())]
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
""" Return the :class:`~plexapi.settings.Setting` object with the specified id. """
|
""" Return the :class:`~plexapi.settings.Setting` object with the specified id. """
|
||||||
|
@ -102,7 +102,7 @@ class Setting(PlexObject):
|
||||||
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 valis values for this setting.
|
||||||
"""
|
"""
|
||||||
_bool_cast = lambda x: True if x == 'true' or x == '1' else False
|
_bool_cast = lambda x: bool(x == 'true' or x == '1')
|
||||||
_bool_str = lambda x: str(x).lower()
|
_bool_str = lambda x: str(x).lower()
|
||||||
TYPES = {
|
TYPES = {
|
||||||
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
||||||
|
|
|
@ -176,7 +176,7 @@ def threaded(callback, listargs):
|
||||||
threads[-1].setDaemon(True)
|
threads[-1].setDaemon(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):
|
||||||
break
|
break
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
@ -334,6 +334,24 @@ 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):
|
def tag_helper(tag, items, locked=True, remove=False):
|
||||||
""" Simple tag helper for editing a object. """
|
""" Simple tag helper for editing a object. """
|
||||||
if not isinstance(items, list):
|
if not isinstance(items, list):
|
||||||
|
@ -448,7 +466,7 @@ def base64str(text):
|
||||||
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def deprecated(message):
|
def deprecated(message, stacklevel=2):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
"""This is a decorator which can be used to mark functions
|
"""This is a decorator which can be used to mark functions
|
||||||
as deprecated. It will result in a warning being emitted
|
as deprecated. It will result in a warning being emitted
|
||||||
|
@ -456,7 +474,7 @@ def deprecated(message):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
msg = 'Call to deprecated function or method "%s", %s.' % (func.__name__, message)
|
msg = 'Call to deprecated function or method "%s", %s.' % (func.__name__, message)
|
||||||
warnings.warn(msg, category=DeprecationWarning, stacklevel=3)
|
warnings.warn(msg, category=DeprecationWarning, stacklevel=stacklevel)
|
||||||
log.warning(msg)
|
log.warning(msg)
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
@ -5,6 +5,9 @@ from urllib.parse import quote_plus, urlencode
|
||||||
from plexapi import library, media, settings, utils
|
from plexapi import library, media, settings, utils
|
||||||
from plexapi.base import Playable, PlexPartialObject
|
from plexapi.base import Playable, PlexPartialObject
|
||||||
from plexapi.exceptions import BadRequest, NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
|
from plexapi.mixins import ArtUrlMixin, ArtMixin, BannerMixin, PosterUrlMixin, PosterMixin
|
||||||
|
from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin
|
||||||
|
from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin
|
||||||
|
|
||||||
|
|
||||||
class Video(PlexPartialObject):
|
class Video(PlexPartialObject):
|
||||||
|
@ -64,20 +67,6 @@ class Video(PlexPartialObject):
|
||||||
""" Returns True if this video is watched. """
|
""" Returns True if this video is watched. """
|
||||||
return bool(self.viewCount > 0) if self.viewCount else False
|
return bool(self.viewCount > 0) if self.viewCount else False
|
||||||
|
|
||||||
@property
|
|
||||||
def thumbUrl(self):
|
|
||||||
""" Return the first first thumbnail url starting on
|
|
||||||
the most specific thumbnail for that item.
|
|
||||||
"""
|
|
||||||
thumb = self.firstAttr('thumb', 'parentThumb', 'granparentThumb')
|
|
||||||
return self._server.url(thumb, includeToken=True) if thumb else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def artUrl(self):
|
|
||||||
""" Return the first first art url starting on the most specific for that item."""
|
|
||||||
art = self.firstAttr('art', 'grandparentArt')
|
|
||||||
return self._server.url(art, includeToken=True) if art else None
|
|
||||||
|
|
||||||
def url(self, part):
|
def url(self, part):
|
||||||
""" Returns the full url for something. Typically used for getting a specific image. """
|
""" Returns the full url for something. Typically used for getting a specific image. """
|
||||||
return self._server.url(part, includeToken=True) if part else None
|
return self._server.url(part, includeToken=True) if part else None
|
||||||
|
@ -259,7 +248,8 @@ class Video(PlexPartialObject):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Movie(Playable, Video):
|
class Movie(Video, Playable, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||||
|
CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin):
|
||||||
""" Represents a single Movie.
|
""" Represents a single Movie.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -385,7 +375,8 @@ class Movie(Playable, Video):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Show(Video):
|
class Show(Video, ArtMixin, BannerMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
|
||||||
|
CollectionMixin, GenreMixin, LabelMixin):
|
||||||
""" Represents a single Show (including all seasons and episodes).
|
""" Represents a single Show (including all seasons and episodes).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -403,6 +394,7 @@ class Show(Video):
|
||||||
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.
|
||||||
originallyAvailableAt (datetime): Datetime the show was released.
|
originallyAvailableAt (datetime): Datetime the show was released.
|
||||||
|
originalTitle (str): The original title of the show.
|
||||||
rating (float): Show rating (7.9; 9.8; 8.1).
|
rating (float): Show rating (7.9; 9.8; 8.1).
|
||||||
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
|
||||||
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
|
||||||
|
@ -430,6 +422,7 @@ class Show(Video):
|
||||||
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
|
||||||
self.locations = self.listAttrs(data, 'path', etag='Location')
|
self.locations = self.listAttrs(data, 'path', etag='Location')
|
||||||
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt'), '%Y-%m-%d')
|
||||||
|
self.originalTitle = data.attrib.get('originalTitle')
|
||||||
self.rating = utils.cast(float, data.attrib.get('rating'))
|
self.rating = utils.cast(float, data.attrib.get('rating'))
|
||||||
self.roles = self.findItems(data, media.Role)
|
self.roles = self.findItems(data, media.Role)
|
||||||
self.similar = self.findItems(data, media.Similar)
|
self.similar = self.findItems(data, media.Similar)
|
||||||
|
@ -583,7 +576,7 @@ class Show(Video):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Season(Video):
|
class Season(Video, ArtMixin, PosterMixin):
|
||||||
""" Represents a single Show Season (including all episodes).
|
""" Represents a single Show Season (including all episodes).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -709,7 +702,8 @@ class Season(Video):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Episode(Playable, Video):
|
class Episode(Video, Playable, ArtMixin, PosterMixin,
|
||||||
|
DirectorMixin, WriterMixin):
|
||||||
""" Represents a single Shows Episode.
|
""" Represents a single Shows Episode.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
@ -738,6 +732,7 @@ class Episode(Playable, Video):
|
||||||
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
|
||||||
parentTitle (str): Name of the season for the episode.
|
parentTitle (str): Name of the season for the episode.
|
||||||
rating (float): Episode rating (7.9; 9.8; 8.1).
|
rating (float): Episode rating (7.9; 9.8; 8.1).
|
||||||
|
skipParent (bool): True if the show's seasons are set to hidden.
|
||||||
viewOffset (int): View offset in milliseconds.
|
viewOffset (int): View offset in milliseconds.
|
||||||
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
|
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
|
||||||
year (int): Year episode was released.
|
year (int): Year episode was released.
|
||||||
|
@ -774,10 +769,23 @@ class Episode(Playable, Video):
|
||||||
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'))
|
||||||
|
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
|
||||||
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)
|
||||||
self.year = utils.cast(int, data.attrib.get('year'))
|
self.year = utils.cast(int, data.attrib.get('year'))
|
||||||
|
|
||||||
|
# If seasons are hidden, parentKey and parentRatingKey are missing from the XML response.
|
||||||
|
# https://forums.plex.tv/t/parentratingkey-not-in-episode-xml-when-seasons-are-hidden/300553
|
||||||
|
if self.skipParent and not self.parentRatingKey:
|
||||||
|
# Parse the parentRatingKey from the parentThumb
|
||||||
|
if self.parentThumb.startswith('/library/metadata/'):
|
||||||
|
self.parentRatingKey = utils.cast(int, self.parentThumb.split('/')[3])
|
||||||
|
# Get the parentRatingKey from the season's ratingKey
|
||||||
|
if not self.parentRatingKey and self.grandparentRatingKey:
|
||||||
|
self.parentRatingKey = self.show().season(season=self.parentIndex).ratingKey
|
||||||
|
if self.parentRatingKey:
|
||||||
|
self.parentKey = '/library/metadata/%s' % self.parentRatingKey
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s>' % ':'.join([p for p in [
|
return '<%s>' % ':'.join([p for p in [
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
|
@ -832,8 +840,8 @@ class Episode(Playable, Video):
|
||||||
|
|
||||||
|
|
||||||
@utils.registerPlexObject
|
@utils.registerPlexObject
|
||||||
class Clip(Playable, Video):
|
class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
|
||||||
"""Represents a single Clip.
|
""" Represents a single Clip.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
TAG (str): 'Video'
|
TAG (str): 'Video'
|
||||||
|
@ -855,7 +863,7 @@ class Clip(Playable, Video):
|
||||||
METADATA_TYPE = 'clip'
|
METADATA_TYPE = 'clip'
|
||||||
|
|
||||||
def _loadData(self, data):
|
def _loadData(self, data):
|
||||||
"""Load attribute values from Plex XML response."""
|
""" Load attribute values from Plex XML response. """
|
||||||
Video._loadData(self, data)
|
Video._loadData(self, data)
|
||||||
Playable._loadData(self, data)
|
Playable._loadData(self, data)
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue