Bump plexapi from 4.8.0 to 4.9.1 (#1627)

* Bump plexapi from 4.8.0 to 4.9.1

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

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

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

* Update plexapi==4.9.1

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
This commit is contained in:
dependabot[bot] 2022-01-25 11:08:49 -08:00 committed by GitHub
parent a4ab5ab9be
commit 7f0abe0fe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 254 additions and 76 deletions

View file

@ -8,6 +8,7 @@ from plexapi.exceptions import BadRequest
from plexapi.mixins import AdvancedSettingsMixin, ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin
from plexapi.mixins import RatingMixin, SplitMergeMixin, UnmatchMatchMixin
from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin
from plexapi.playlist import Playlist
class Audio(PlexPartialObject):
@ -222,6 +223,11 @@ class Artist(Audio, AdvancedSettingsMixin, ArtMixin, PosterMixin, RatingMixin, S
filepaths += track.download(_savepath, keep_original_name, **kwargs)
return filepaths
def station(self):
""" Returns a :class:`~plexapi.playlist.Playlist` artist radio station or `None`. """
key = '%s?includeStations=1' % self.key
return next(iter(self.fetchItems(key, cls=Playlist, rtag="Stations")), None)
@utils.registerPlexObject
class Album(Audio, ArtMixin, PosterMixin, RatingMixin, UnmatchMatchMixin,

View file

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

View file

@ -82,6 +82,28 @@ class Library(PlexObject):
except KeyError:
raise NotFound('Invalid library sectionID: %s' % sectionID) from None
def hubs(self, sectionID=None, identifier=None, **kwargs):
""" Returns a list of :class:`~plexapi.library.Hub` across all library sections.
Parameters:
sectionID (int or str or list, optional):
IDs of the sections to limit results or "playlists".
identifier (str or list, optional):
Names of identifiers to limit results.
Available on `Hub` instances as the `hubIdentifier` attribute.
Examples: 'home.continue' or 'home.ondeck'
"""
if sectionID:
if not isinstance(sectionID, list):
sectionID = [sectionID]
kwargs['contentDirectoryID'] = ",".join(map(str, sectionID))
if identifier:
if not isinstance(identifier, list):
identifier = [identifier]
kwargs['identifier'] = ",".join(identifier)
key = '/hubs%s' % utils.joinArgs(kwargs)
return self.fetchItems(key)
def all(self, **kwargs):
""" Returns a list of all media from all library sections.
This may be a very large dataset to retrieve.
@ -169,7 +191,7 @@ class Library(PlexObject):
name (str): Name of the library
agent (str): Example com.plexapp.agents.imdb
type (str): movie, show, # check me
location (str): /path/to/files
location (str or list): /path/to/files, ["/path/to/files", "/path/to/morefiles"]
language (str): Two letter language fx en
kwargs (dict): Advanced options should be passed as a dict. where the id is the key.
@ -308,8 +330,16 @@ class Library(PlexObject):
40:South Africa, 41:Spain, 42:Sweden, 43:Switzerland, 44:Taiwan, 45:Trinidad,
46:United Kingdom, 47:United States, 48:Uruguay, 49:Venezuela.
"""
part = '/library/sections?name=%s&type=%s&agent=%s&scanner=%s&language=%s&location=%s' % (
quote_plus(name), type, agent, quote_plus(scanner), language, quote_plus(location)) # noqa E126
if isinstance(location, str):
location = [location]
locations = []
for path in location:
if not self._server.isBrowsable(path):
raise BadRequest('Path: %s does not exist.' % path)
locations.append(('location', path))
part = '/library/sections?name=%s&type=%s&agent=%s&scanner=%s&language=%s&%s' % (
quote_plus(name), type, agent, quote_plus(scanner), language, urlencode(locations, doseq=True)) # noqa E126
if kwargs:
part += urlencode(kwargs)
return self._server.query(part, method=self._server._session.post)
@ -486,16 +516,76 @@ class LibrarySection(PlexObject):
return self
def edit(self, agent=None, **kwargs):
""" Edit a library (Note: agent is required). See :class:`~plexapi.library.Library` for example usage.
""" Edit a library. See :class:`~plexapi.library.Library` for example usage.
Parameters:
agent (str, optional): The library agent.
kwargs (dict): Dict of settings to edit.
"""
if not agent:
agent = self.agent
part = '/library/sections/%s?agent=%s&%s' % (self.key, agent, urlencode(kwargs))
locations = []
if kwargs.get('location'):
if isinstance(kwargs['location'], str):
kwargs['location'] = [kwargs['location']]
for path in kwargs.pop('location'):
if not self._server.isBrowsable(path):
raise BadRequest('Path: %s does not exist.' % path)
locations.append(('location', path))
params = list(kwargs.items()) + locations
part = '/library/sections/%s?agent=%s&%s' % (self.key, agent, urlencode(params, doseq=True))
self._server.query(part, method=self._server._session.put)
def addLocations(self, location):
""" Add a location to a library.
Parameters:
location (str or list): A single folder path, list of paths.
Example:
.. code-block:: python
LibrarySection.addLocations('/path/1')
LibrarySection.addLocations(['/path/1', 'path/2', '/path/3'])
"""
locations = self.locations
if isinstance(location, str):
location = [location]
for path in location:
if not self._server.isBrowsable(path):
raise BadRequest('Path: %s does not exist.' % path)
locations.append(path)
self.edit(location=locations)
def removeLocations(self, location):
""" Remove a location from a library.
Parameters:
location (str or list): A single folder path, list of paths.
Example:
.. code-block:: python
LibrarySection.removeLocations('/path/1')
LibrarySection.removeLocations(['/path/1', 'path/2', '/path/3'])
"""
locations = self.locations
if isinstance(location, str):
location = [location]
for path in location:
if path in locations:
locations.remove(path)
else:
raise BadRequest('Path: %s does not exist in the library.' % location)
if len(locations) == 0:
raise BadRequest('You are unable to remove all locations from a library.')
self.edit(location=locations)
def get(self, title):
""" Returns the media item with the specified title.
@ -510,9 +600,7 @@ class LibrarySection(PlexObject):
def getGuid(self, guid):
""" Returns the media item with the specified external IMDB, TMDB, or TVDB ID.
Note: This search uses a PlexAPI operator so performance may be slow. All items from the
entire Plex library need to be retrieved for each guid search. It is recommended to create
your own lookup dictionary if you are searching for a lot of external guids.
Note: Only available for the Plex Movie and Plex TV Series agents.
Parameters:
guid (str): The external guid of the item to return.
@ -525,20 +613,23 @@ class LibrarySection(PlexObject):
.. code-block:: python
# This will retrieve all items in the entire library 3 times
result1 = library.getGuid('imdb://tt0944947')
result2 = library.getGuid('tmdb://1399')
result3 = library.getGuid('tvdb://121361')
# This will only retrieve all items in the library once to create a lookup dictionary
# Alternatively, create your own guid lookup dictionary for faster performance
guidLookup = {guid.id: item for item in library.all() for guid in item.guids}
result1 = guidLookup['imdb://tt0944947']
result2 = guidLookup['tmdb://1399']
result3 = guidLookup['tvdb://121361']
"""
key = '/library/sections/%s/all?includeGuids=1' % self.key
return self.fetchItem(key, Guid__id__iexact=guid)
try:
dummy = self.search(maxresults=1)[0]
match = dummy.matches(agent=self.agent, title=guid.replace('://', '-'))
return self.search(guid=match[0].guid)[0]
except IndexError:
raise NotFound("Guid '%s' is not found in the library" % guid) from None
def all(self, libtype=None, **kwargs):
""" Returns a list of all items from this library section.
@ -556,13 +647,13 @@ class LibrarySection(PlexObject):
def hubs(self):
""" Returns a list of available :class:`~plexapi.library.Hub` for this library section.
"""
key = '/hubs/sections/%s' % self.key
key = '/hubs/sections/%s?includeStations=1' % self.key
return self.fetchItems(key)
def agents(self):
""" Returns a list of available :class:`~plexapi.media.Agent` for this library section.
"""
return self._server.agents(utils.searchType(self.type))
return self._server.agents(self.type)
def settings(self):
""" Returns a list of all library settings. """
@ -606,6 +697,36 @@ class LibrarySection(PlexObject):
self.edit(**data)
def _lockUnlockAllField(self, field, libtype=None, locked=True):
""" Lock or unlock a field for all items in the library. """
libtype = libtype or self.TYPE
args = {
'type': utils.searchType(libtype),
'%s.locked' % field: int(locked)
}
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
self._server.query(key, method=self._server._session.put)
def lockAllField(self, field, libtype=None):
""" Lock a field for all items in the library.
Parameters:
field (str): The field to lock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode,
artist, album, track, photoalbum, photo). Default is the main library type.
"""
self._lockUnlockAllField(field, libtype=libtype, locked=True)
def unlockAllField(self, field, libtype=None):
""" Unlock a field for all items in the library.
Parameters:
field (str): The field to unlock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode,
artist, album, track, photoalbum, photo). Default is the main library type.
"""
self._lockUnlockAllField(field, libtype=libtype, locked=False)
def timeline(self):
""" Returns a timeline query for this library section. """
key = '/library/sections/%s/timeline' % self.key
@ -1718,9 +1839,8 @@ class MusicSection(LibrarySection):
return self.fetchItems(key)
def stations(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
key = '/hubs/sections/%s?includeStations=1' % self.key
return self.fetchItems(key, cls=Station)
""" Returns a list of :class:`~plexapi.playlist.Playlist` stations in this section. """
return next((hub.items for hub in self.hubs() if hub.context == 'hub.music.stations'), None)
def searchArtists(self, **kwargs):
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search` for usage. """
@ -1934,6 +2054,7 @@ class Hub(PlexObject):
context (str): The context of the hub.
hubKey (str): API URL for these specific hub items.
hubIdentifier (str): The identifier of the hub.
items (list): List of items in the hub.
key (str): API URL for the hub.
more (bool): True if there are more items to load (call reload() to fetch all items).
size (int): The number of items in the hub.
@ -2086,39 +2207,6 @@ class Place(HubMediaTag):
TAGTYPE = 400
@utils.registerPlexObject
class Station(PlexObject):
""" Represents the Station area in the MusicSection.
Attributes:
TITLE (str): 'Stations'
TYPE (str): 'station'
hubIdentifier (str): Unknown.
size (int): Number of items found.
title (str): Title of this Hub.
type (str): Type of items in the Hub.
more (str): Unknown.
style (str): Unknown
items (str): List of items in the Hub.
"""
TITLE = 'Stations'
TYPE = 'station'
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._data = data
self.hubIdentifier = data.attrib.get('hubIdentifier')
self.size = utils.cast(int, data.attrib.get('size'))
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
self.more = data.attrib.get('more')
self.style = data.attrib.get('style')
self.items = self.findItems(data)
def __len__(self):
return self.size
class FilteringType(PlexObject):
""" Represents a single filtering Type object for a library.

View file

@ -6,6 +6,7 @@ from urllib.parse import quote_plus
from plexapi import log, settings, utils
from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest
from plexapi.utils import deprecated
@utils.registerPlexObject
@ -1058,31 +1059,50 @@ class Agent(PlexObject):
self.hasAttribution = data.attrib.get('hasAttribution')
self.hasPrefs = data.attrib.get('hasPrefs')
self.identifier = data.attrib.get('identifier')
self.name = data.attrib.get('name')
self.primary = data.attrib.get('primary')
self.shortIdentifier = self.identifier.rsplit('.', 1)[1]
if 'mediaType' in self._initpath:
self.name = data.attrib.get('name')
self.languageCode = []
for code in data:
self.languageCode += [code.attrib.get('code')]
else:
self.mediaTypes = [AgentMediaType(server=self._server, data=d) for d in data]
def _settings(self):
if 'mediaType' in self._initpath:
self.languageCodes = self.listAttrs(data, 'code', etag='Language')
self.mediaTypes = []
else:
self.languageCodes = []
self.mediaTypes = self.findItems(data, cls=AgentMediaType)
@property
@deprecated('use "languageCodes" instead')
def languageCode(self):
return self.languageCodes
def settings(self):
key = '/:/plugins/%s/prefs' % self.identifier
data = self._server.query(key)
return self.findItems(data, cls=settings.Setting)
@deprecated('use "settings" instead')
def _settings(self):
return self.settings()
class AgentMediaType(Agent):
""" Represents a single Agent MediaType.
Attributes:
TAG (str): 'MediaType'
"""
TAG = 'MediaType'
def __repr__(self):
uid = self._clean(self.firstAttr('name'))
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid] if p])
def _loadData(self, data):
self.languageCodes = self.listAttrs(data, 'code', etag='Language')
self.mediaType = utils.cast(int, data.attrib.get('mediaType'))
self.name = data.attrib.get('name')
self.languageCode = []
for code in data:
self.languageCode += [code.attrib.get('code')]
@property
@deprecated('use "languageCodes" instead')
def languageCode(self):
return self.languageCodes

View file

@ -29,7 +29,11 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
icon (str): Icon URI string for smart playlists.
key (str): API URL (/playlist/<ratingkey>).
leafCount (int): Number of items in the playlist view.
librarySectionID (int): Library section identifier (radio only)
librarySectionKey (str): Library section key (radio only)
librarySectionTitle (str): Library section title (radio only)
playlistType (str): 'audio', 'video', or 'photo'
radio (bool): If this playlist represents a radio station
ratingKey (int): Unique key identifying the playlist.
smart (bool): True if the playlist is a smart playlist.
summary (str): Summary of the playlist.
@ -54,7 +58,11 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
self.icon = data.attrib.get('icon')
self.key = data.attrib.get('key', '').replace('/items', '') # FIX_BUG_50
self.leafCount = utils.cast(int, data.attrib.get('leafCount'))
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
self.librarySectionKey = data.attrib.get('librarySectionKey')
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
self.playlistType = data.attrib.get('playlistType')
self.radio = utils.cast(bool, data.attrib.get('radio', 0))
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.smart = utils.cast(bool, data.attrib.get('smart'))
self.summary = data.attrib.get('summary')
@ -169,6 +177,8 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin, SmartFilterMi
def items(self):
""" Returns a list of all items in the playlist. """
if self.radio:
return []
if self._items is None:
key = '%s/items' % self.key
items = self.fetchItems(key)

View file

@ -175,8 +175,11 @@ class PlayQueue(PlexObject):
args["uri"] = "library:///directory/{uri_args}".format(uri_args=uri_args)
args["type"] = items[0].listType
elif items.type == "playlist":
args["playlistID"] = items.ratingKey
args["type"] = items.playlistType
if items.radio:
args["uri"] = f"server://{server.machineIdentifier}/{server.library.identifier}{items.key}"
else:
args["playlistID"] = items.ratingKey
else:
uuid = items.section().uuid
args["type"] = items.listType
@ -192,6 +195,42 @@ class PlayQueue(PlexObject):
c._server = server
return c
@classmethod
def fromStationKey(cls, server, key):
"""Create and return a new :class:`~plexapi.playqueue.PlayQueue`.
This is a convenience method to create a `PlayQueue` for
radio stations when only the `key` string is available.
Parameters:
server (:class:`~plexapi.server.PlexServer`): Server you are connected to.
key (str): A station key as provided by :func:`~plexapi.library.LibrarySection.hubs()`
or :func:`~plexapi.audio.Artist.station()`
Example:
.. code-block:: python
from plexapi.playqueue import PlayQueue
music = server.library.section("Music")
artist = music.get("Artist Name")
station = artist.station()
key = station.key # "/library/metadata/12855/station/8bd39616-dbdb-459e-b8da-f46d0b170af4?type=10"
pq = PlayQueue.fromStationKey(server, key)
client = server.clients()[0]
client.playMedia(pq)
"""
args = {
"type": "audio",
"uri": f"server://{server.machineIdentifier}/{server.library.identifier}{key}"
}
path = f"/playQueues{utils.joinArgs(args)}"
data = server.query(path, method=server._session.post)
c = cls(server, data, initpath=path)
c.playQueueType = args["type"]
c._server = server
return c
def addItem(self, item, playNext=False, refresh=True):
"""
Append the provided item to the "Up Next" section of the PlayQueue.

View file

@ -3,6 +3,7 @@ from urllib.parse import urlencode
from xml.etree import ElementTree
import requests
import os
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE, log,
logfilter)
from plexapi import utils
@ -228,10 +229,10 @@ class PlexServer(PlexObject):
return activities
def agents(self, mediaType=None):
""" Returns the :class:`~plexapi.media.Agent` objects this server has available. """
""" Returns a list of :class:`~plexapi.media.Agent` objects this server has available. """
key = '/system/agents'
if mediaType:
key += '?mediaType=%s' % mediaType
key += '?mediaType=%s' % utils.searchType(mediaType)
return self.fetchItems(key)
def createToken(self, type='delegation', scope='all'):
@ -384,6 +385,18 @@ class PlexServer(PlexObject):
for path, paths, files in self.walk(_path):
yield path, paths, files
def isBrowsable(self, path):
""" Returns True if the Plex server can browse the given path.
Parameters:
path (:class:`~plexapi.library.Path` or str): Full path to browse.
"""
if isinstance(path, Path):
path = path.path
path = os.path.normpath(path)
paths = [p.path for p in self.browse(os.path.dirname(path), includeFiles=False)]
return path in paths
def clients(self):
""" Returns list of all :class:`~plexapi.client.PlexClient` objects connected to server. """
items = []

View file

@ -113,17 +113,19 @@ class Setting(PlexObject):
def _loadData(self, data):
""" Load attribute values from Plex XML response. """
self._setValue = None
self.type = data.attrib.get('type')
self.advanced = utils.cast(bool, data.attrib.get('advanced'))
self.default = self._cast(data.attrib.get('default'))
self.enumValues = self._getEnumValues(data)
self.group = data.attrib.get('group')
self.hidden = utils.cast(bool, data.attrib.get('hidden'))
self.id = data.attrib.get('id')
self.label = data.attrib.get('label')
self.option = data.attrib.get('option')
self.secure = utils.cast(bool, data.attrib.get('secure'))
self.summary = data.attrib.get('summary')
self.type = data.attrib.get('type')
self.default = self._cast(data.attrib.get('default'))
self.value = self._cast(data.attrib.get('value'))
self.hidden = utils.cast(bool, data.attrib.get('hidden'))
self.advanced = utils.cast(bool, data.attrib.get('advanced'))
self.group = data.attrib.get('group')
self.enumValues = self._getEnumValues(data)
self._setValue = None
def _cast(self, value):
""" Cast the specific value to the type of this setting. """
@ -132,8 +134,8 @@ class Setting(PlexObject):
return value
def _getEnumValues(self, data):
""" Returns a list of dictionary of valis value for this setting. """
enumstr = data.attrib.get('enumValues')
""" Returns a list or dictionary of values for this setting. """
enumstr = data.attrib.get('enumValues') or data.attrib.get('values')
if not enumstr:
return None
if ':' in enumstr:

View file

@ -27,7 +27,7 @@ musicbrainzngs==0.7.1
oauthlib==3.1.1
packaging==21.3
paho-mqtt==1.6.1
plexapi==4.8.0
plexapi==4.9.1
portend==3.1.0
profilehooks==1.12.0
PyJWT==2.3.0