Update PlexAPI to 4.4.0

This commit is contained in:
JonnyWong16 2021-03-07 12:06:56 -08:00
parent 0e4de17853
commit 6fb4b35076
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
15 changed files with 799 additions and 525 deletions

View file

@ -15,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
# PlexAPI Settings
PROJECT = 'PlexAPI'
VERSION = '4.3.1'
VERSION = '4.4.0'
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)

View file

@ -84,4 +84,4 @@ class AlertListener(threading.Thread):
This is to support compatibility with current and previous releases of websocket-client.
"""
err = args[-1]
log.error('AlertListener Error: %s' % err)
log.error('AlertListener Error: %s', err)

View file

@ -4,6 +4,9 @@ from urllib.parse import quote_plus
from plexapi import library, media, utils
from plexapi.base import Playable, PlexPartialObject
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):
@ -65,18 +68,6 @@ class Audio(PlexPartialObject):
self.userRating = utils.cast(float, data.attrib.get('userRating', 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):
""" 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
@ -123,7 +114,8 @@ class Audio(PlexPartialObject):
@utils.registerPlexObject
class Artist(Audio):
class Artist(Audio, ArtMixin, PosterMixin, SplitMergeMixin, UnmatchMatchMixin,
CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin):
""" Represents a single Artist.
Attributes:
@ -226,7 +218,8 @@ class Artist(Audio):
@utils.registerPlexObject
class Album(Audio):
class Album(Audio, ArtMixin, PosterMixin, UnmatchMatchMixin,
CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin):
""" Represents a single Album.
Attributes:
@ -332,7 +325,7 @@ class Album(Audio):
@utils.registerPlexObject
class Track(Audio, Playable):
class Track(Audio, Playable, ArtUrlMixin, PosterUrlMixin, MoodMixin):
""" Represents a single Track.
Attributes:

View file

@ -5,9 +5,10 @@ from urllib.parse import quote_plus, urlencode
from plexapi import log, utils
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 = {
'exact': lambda v, q: v == q,
'iexact': lambda v, q: v.lower() == q.lower(),
@ -47,6 +48,7 @@ class PlexObject(object):
self._data = data
self._initpath = initpath or self.key
self._parent = weakref.ref(parent) if parent else None
self._details_key = None
if data is not None:
self._loadData(data)
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])
def __setattr__(self, attr, value):
# Don't overwrite an attr with None or [] unless it's a private variable
if value not in [None, []] or attr.startswith('_') or attr not in self.__dict__:
# Don't overwrite session specific attr with []
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
def _clean(self, value):
@ -119,9 +124,9 @@ class PlexObject(object):
See all possible `**kwargs*` in :func:`~plexapi.base.PlexObject.fetchItem`.
"""
obj = self
while obj._parent is not None:
while obj and obj._parent is not None:
obj = obj._parent()
if obj._checkAttrs(obj._data, **kwargs):
if obj and obj._checkAttrs(obj._data, **kwargs):
return True
return False
@ -227,7 +232,7 @@ class PlexObject(object):
def firstAttr(self, *attrs):
""" Return the first attribute in attrs that is not None. """
for attr in attrs:
value = self.__dict__.get(attr)
value = getattr(self, attr, None)
if value is not None:
return value
@ -384,6 +389,7 @@ class PlexPartialObject(PlexObject):
value = super(PlexPartialObject, self).__getattribute__(attr)
# Check a few cases where we dont want to reload
if attr in DONT_RELOAD_FOR_KEYS: return value
if attr in DONT_OVERWRITE_SESSION_KEYS: return value
if attr.startswith('_'): return value
if value not in (None, []): return value
if self.isFullObject(): return value
@ -391,7 +397,7 @@ class PlexPartialObject(PlexObject):
clsname = self.__class__.__name__
title = self.__dict__.get('title', self.__dict__.get('name'))
objname = "%s '%s'" % (clsname, title) if title else clsname
log.debug("Reloading %s for attr '%s'" % (objname, attr))
log.debug("Reloading %s for attr '%s'", objname, attr)
# Reload and return the value
self.reload()
return super(PlexPartialObject, self).__getattribute__(attr)
@ -452,49 +458,20 @@ class PlexPartialObject(PlexObject):
self._server.query(part, method=self._server._session.put)
def _edit_tags(self, tag, items, locked=True, remove=False):
""" Helper to edit and refresh a tags.
""" Helper to edit tags.
Parameters:
tag (str): tag name
items (list): list of tags to add
locked (bool): lock this field.
remove (bool): If this is active remove the tags in items.
tag (str): Tag name.
items (list): List of tags to add.
locked (bool): True to lock the field.
remove (bool): True to remove the tags in items.
"""
if not isinstance(items, list):
items = [items]
value = getattr(self, tag + 's')
existing_cols = [t.tag for t in value if t and remove is False]
d = tag_helper(tag, existing_cols + items, locked, remove)
self.edit(**d)
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)
value = getattr(self, tag_plural(tag))
existing_tags = [t.tag for t in value if t and remove is False]
tag_edits = tag_helper(tag, existing_tags + items, locked, remove)
self.edit(**tag_edits)
def refresh(self):
""" 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)
except BadRequest: # pragma: no cover
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
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)
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):
""" 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:
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):
""" Start playback on the specified client.
@ -834,17 +657,3 @@ class Playable(object):
key %= (self.ratingKey, self.key, time, state, durationStr)
self._server.query(key)
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
View 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)

View file

@ -13,11 +13,14 @@ import struct
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):
self.entries = []
self.last_scan = None
def scan(self, scan_for_clients=False):
"""Scan the network."""
@ -35,7 +38,7 @@ class GDM:
"""Return a list of entries that match the content_type."""
self.scan()
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):
"""Return a list of entries that match the search parameters."""

View file

@ -2,7 +2,7 @@
from urllib.parse import quote, quote_plus, unquote, urlencode
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.settings import Setting
from plexapi.utils import deprecated
@ -723,7 +723,7 @@ class LibrarySection(PlexObject):
result = set()
choices = self.listChoices(category, libtype)
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:
item = str((item.id or item.tag) if isinstance(item, media.MediaTag) else item).lower()
# find most logical choice(s) to use in url
@ -1525,206 +1525,6 @@ class FirstCharacter(PlexObject):
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
class Path(PlexObject):
""" Represents a single directory Path.

View file

@ -708,10 +708,10 @@ class Collection(MediaTag):
@utils.registerPlexObject
class Label(MediaTag):
""" Represents a single label media tag.
""" Represents a single Label media tag.
Attributes:
TAG (str): 'label'
TAG (str): 'Label'
FILTER (str): 'label'
"""
TAG = 'Label'
@ -720,10 +720,10 @@ class Label(MediaTag):
@utils.registerPlexObject
class Tag(MediaTag):
""" Represents a single tag media tag.
""" Represents a single Tag media tag.
Attributes:
TAG (str): 'tag'
TAG (str): 'Tag'
FILTER (str): 'tag'
"""
TAG = 'Tag'
@ -807,20 +807,25 @@ class Style(MediaTag):
FILTER = 'style'
@utils.registerPlexObject
class Poster(PlexObject):
""" Represents a Poster.
class BaseImage(PlexObject):
""" Base class for all Art, Banner, and Poster objects.
Attributes:
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'
def _loadData(self, data):
self._data = data
self.key = data.attrib.get('key')
self.provider = data.attrib.get('provider')
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')
def select(self):
@ -832,6 +837,18 @@ class Poster(PlexObject):
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
class Producer(MediaTag):
""" Represents a single Producer media tag.

489
lib/plexapi/mixins.py Normal file
View 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)

View file

@ -4,10 +4,11 @@ from urllib.parse import quote_plus
from plexapi import media, utils, video
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest
from plexapi.mixins import ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, TagMixin
@utils.registerPlexObject
class Photoalbum(PlexPartialObject):
class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin):
""" Represents a single Photoalbum (collection of photos).
Attributes:
@ -136,7 +137,7 @@ class Photoalbum(PlexPartialObject):
@utils.registerPlexObject
class Photo(PlexPartialObject, Playable):
class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
""" Represents a single Photo.
Attributes:
@ -163,7 +164,7 @@ class Photo(PlexPartialObject, Playable):
parentTitle (str): Name of the photo album for the photo.
ratingKey (int): Unique key identifying 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>).
title (str): Name of the photo.
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.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
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.title = data.attrib.get('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.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):
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
return self.fetchItem(self.parentKey)

View file

@ -5,12 +5,13 @@ from plexapi import utils
from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, NotFound, Unsupported
from plexapi.library import LibrarySection
from plexapi.mixins import ArtMixin, PosterMixin
from plexapi.playqueue import PlayQueue
from plexapi.utils import cast, toDatetime
@utils.registerPlexObject
class Playlist(PlexPartialObject, Playable):
class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
""" Represents a single Playlist.
Attributes:
@ -62,6 +63,11 @@ class Playlist(PlexPartialObject, Playable):
for item in self.items():
yield item
@property
def thumb(self):
""" Alias to self.composite. """
return self.composite
@property
def metadataType(self):
if self.isVideo:
@ -311,41 +317,3 @@ class Playlist(PlexPartialObject, Playable):
raise Unsupported('Unsupported playlist content')
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()

View file

@ -15,11 +15,12 @@ from plexapi.media import Conversion, Optimized
from plexapi.playlist import Playlist
from plexapi.playqueue import PlayQueue
from plexapi.settings import Settings
from plexapi.utils import cast
from plexapi.utils import cast, deprecated
from requests.status_codes import _codes as codes
# 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 photo as _photo # 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)
return filepath
@deprecated('use "checkForUpdate" instead')
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.
Parameters:
@ -390,7 +395,7 @@ class PlexServer(PlexObject):
def isLatest(self):
""" 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
def installUpdate(self):
@ -398,7 +403,7 @@ class PlexServer(PlexObject):
# We can add this but dunno how useful this is since it sometimes
# requires user action using a gui.
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:
# figure out what method this is..
return self.query(part, method=self._session.put)
@ -787,6 +792,20 @@ class Activity(PlexObject):
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):
""" Represents a single system account.

View file

@ -44,7 +44,7 @@ class Settings(PlexObject):
def all(self):
""" 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):
""" 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.
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()
TYPES = {
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},

View file

@ -176,7 +176,7 @@ def threaded(callback, listargs):
threads[-1].setDaemon(True)
threads[-1].start()
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
time.sleep(0.05)
@ -334,6 +334,24 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
return fullpath
def tag_singular(tag):
if tag == 'countries':
return 'country'
elif tag == 'similar':
return 'similar'
else:
return tag[:-1]
def tag_plural(tag):
if tag == 'country':
return 'countries'
elif tag == 'similar':
return 'similar'
else:
return tag + 's'
def tag_helper(tag, items, locked=True, remove=False):
""" Simple tag helper for editing a object. """
if not isinstance(items, list):
@ -448,7 +466,7 @@ def base64str(text):
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
def deprecated(message):
def deprecated(message, stacklevel=2):
def decorator(func):
"""This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
@ -456,7 +474,7 @@ def deprecated(message):
@functools.wraps(func)
def wrapper(*args, **kwargs):
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)
return func(*args, **kwargs)
return wrapper

View file

@ -5,6 +5,9 @@ from urllib.parse import quote_plus, urlencode
from plexapi import library, media, settings, utils
from plexapi.base import Playable, PlexPartialObject
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):
@ -64,20 +67,6 @@ class Video(PlexPartialObject):
""" Returns True if this video is watched. """
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):
""" Returns the full url for something. Typically used for getting a specific image. """
return self._server.url(part, includeToken=True) if part else None
@ -259,7 +248,8 @@ class Video(PlexPartialObject):
@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.
Attributes:
@ -385,7 +375,8 @@ class Movie(Playable, Video):
@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).
Attributes:
@ -403,6 +394,7 @@ class Show(Video):
leafCount (int): Number of items in the show view.
locations (List<str>): List of folder paths where the show is found on disk.
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).
roles (List<:class:`~plexapi.media.Role`>): List of role 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.locations = self.listAttrs(data, 'path', etag='Location')
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.roles = self.findItems(data, media.Role)
self.similar = self.findItems(data, media.Similar)
@ -583,7 +576,7 @@ class Show(Video):
@utils.registerPlexObject
class Season(Video):
class Season(Video, ArtMixin, PosterMixin):
""" Represents a single Show Season (including all episodes).
Attributes:
@ -709,7 +702,8 @@ class Season(Video):
@utils.registerPlexObject
class Episode(Playable, Video):
class Episode(Video, Playable, ArtMixin, PosterMixin,
DirectorMixin, WriterMixin):
""" Represents a single Shows Episode.
Attributes:
@ -738,6 +732,7 @@ class Episode(Playable, Video):
parentThumb (str): URL to season thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the season for the episode.
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.
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
year (int): Year episode was released.
@ -774,10 +769,23 @@ class Episode(Playable, Video):
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
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.writers = self.findItems(data, media.Writer)
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):
return '<%s>' % ':'.join([p for p in [
self.__class__.__name__,
@ -832,8 +840,8 @@ class Episode(Playable, Video):
@utils.registerPlexObject
class Clip(Playable, Video):
"""Represents a single Clip.
class Clip(Video, Playable, ArtUrlMixin, PosterUrlMixin):
""" Represents a single Clip.
Attributes:
TAG (str): 'Video'
@ -855,7 +863,7 @@ class Clip(Playable, Video):
METADATA_TYPE = 'clip'
def _loadData(self, data):
"""Load attribute values from Plex XML response."""
""" Load attribute values from Plex XML response. """
Video._loadData(self, data)
Playable._loadData(self, data)
self._data = data