mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-25 13:10:17 +00:00
Fix searching and browsing Discover results (#970)
* Fix discover search results * Plex API response changed to separate "Free on Demand" and "More Ways To Watch" * Add test for searchDiscover * Manually set watchlist and discover result to online metadata objects * Replace all key format ratingKey with key * Add libtype argument to searchDiscover * Update tests for searchDiscover libtype * Add includeUserState=1 parameter to Discover results details key * Add method to return UserState object
This commit is contained in:
parent
117af58897
commit
3107e25a0e
6 changed files with 118 additions and 41 deletions
|
@ -201,7 +201,7 @@ class Artist(
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
key = f'{self.key}/allLeaves'
|
||||||
if title is not None:
|
if title is not None:
|
||||||
return self.fetchItem(key, Track, title__iexact=title)
|
return self.fetchItem(key, Track, title__iexact=title)
|
||||||
elif album is not None and track is not None:
|
elif album is not None and track is not None:
|
||||||
|
@ -210,7 +210,7 @@ class Artist(
|
||||||
|
|
||||||
def tracks(self, **kwargs):
|
def tracks(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """
|
""" Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """
|
||||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
key = f'{self.key}/allLeaves'
|
||||||
return self.fetchItems(key, Track, **kwargs)
|
return self.fetchItems(key, Track, **kwargs)
|
||||||
|
|
||||||
def get(self, title=None, album=None, track=None):
|
def get(self, title=None, album=None, track=None):
|
||||||
|
@ -316,7 +316,7 @@ class Album(
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
if title is not None and not isinstance(title, int):
|
if title is not None and not isinstance(title, int):
|
||||||
return self.fetchItem(key, Track, title__iexact=title)
|
return self.fetchItem(key, Track, title__iexact=title)
|
||||||
elif track is not None or isinstance(title, int):
|
elif track is not None or isinstance(title, int):
|
||||||
|
@ -329,7 +329,7 @@ class Album(
|
||||||
|
|
||||||
def tracks(self, **kwargs):
|
def tracks(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.audio.Track` objects in the album. """
|
""" Returns a list of :class:`~plexapi.audio.Track` objects in the album. """
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItems(key, Track, **kwargs)
|
return self.fetchItems(key, Track, **kwargs)
|
||||||
|
|
||||||
def get(self, title=None, track=None):
|
def get(self, title=None, track=None):
|
||||||
|
|
|
@ -125,7 +125,7 @@ class SplitMergeMixin:
|
||||||
|
|
||||||
def split(self):
|
def split(self):
|
||||||
""" Split duplicated Plex object into separate objects. """
|
""" Split duplicated Plex object into separate objects. """
|
||||||
key = '/library/metadata/%s/split' % self.ratingKey
|
key = f'{self.key}/split'
|
||||||
return self._server.query(key, method=self._server._session.put)
|
return self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
def merge(self, ratingKeys):
|
def merge(self, ratingKeys):
|
||||||
|
@ -146,7 +146,7 @@ class UnmatchMatchMixin:
|
||||||
|
|
||||||
def unmatch(self):
|
def unmatch(self):
|
||||||
""" Unmatches metadata match from object. """
|
""" Unmatches metadata match from object. """
|
||||||
key = '/library/metadata/%s/unmatch' % self.ratingKey
|
key = f'{self.key}/unmatch'
|
||||||
self._server.query(key, method=self._server._session.put)
|
self._server.query(key, method=self._server._session.put)
|
||||||
|
|
||||||
def matches(self, agent=None, title=None, year=None, language=None):
|
def matches(self, agent=None, title=None, year=None, language=None):
|
||||||
|
@ -177,7 +177,7 @@ class UnmatchMatchMixin:
|
||||||
|
|
||||||
For 2 to 7, the agent and language is automatically filled in
|
For 2 to 7, the agent and language is automatically filled in
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/matches' % self.ratingKey
|
key = f'{self.key}/matches'
|
||||||
params = {'manual': 1}
|
params = {'manual': 1}
|
||||||
|
|
||||||
if agent and not any([title, year, language]):
|
if agent and not any([title, year, language]):
|
||||||
|
@ -216,7 +216,7 @@ class UnmatchMatchMixin:
|
||||||
~plexapi.base.matches()
|
~plexapi.base.matches()
|
||||||
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/match' % self.ratingKey
|
key = f'{self.key}/match'
|
||||||
if auto:
|
if auto:
|
||||||
autoMatch = self.matches(agent=agent)
|
autoMatch = self.matches(agent=agent)
|
||||||
if autoMatch:
|
if autoMatch:
|
||||||
|
@ -289,7 +289,7 @@ class ArtMixin(ArtUrlMixin):
|
||||||
|
|
||||||
def arts(self):
|
def arts(self):
|
||||||
""" Returns list of available :class:`~plexapi.media.Art` objects. """
|
""" Returns list of available :class:`~plexapi.media.Art` objects. """
|
||||||
return self.fetchItems('/library/metadata/%s/arts' % self.ratingKey, cls=media.Art)
|
return self.fetchItems(f'{self.key}/arts', cls=media.Art)
|
||||||
|
|
||||||
def uploadArt(self, url=None, filepath=None):
|
def uploadArt(self, url=None, filepath=None):
|
||||||
""" Upload a background artwork from a url or filepath.
|
""" Upload a background artwork from a url or filepath.
|
||||||
|
@ -299,10 +299,10 @@ class ArtMixin(ArtUrlMixin):
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = '/library/metadata/%s/arts?url=%s' % (self.ratingKey, quote_plus(url))
|
key = f'{self.key}/arts?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = '/library/metadata/%s/arts?' % self.ratingKey
|
key = f'{self.key}/arts'
|
||||||
data = open(filepath, 'rb').read()
|
data = open(filepath, 'rb').read()
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
@ -338,7 +338,7 @@ class BannerMixin(BannerUrlMixin):
|
||||||
|
|
||||||
def banners(self):
|
def banners(self):
|
||||||
""" Returns list of available :class:`~plexapi.media.Banner` objects. """
|
""" Returns list of available :class:`~plexapi.media.Banner` objects. """
|
||||||
return self.fetchItems('/library/metadata/%s/banners' % self.ratingKey, cls=media.Banner)
|
return self.fetchItems(f'{self.key}/banners', cls=media.Banner)
|
||||||
|
|
||||||
def uploadBanner(self, url=None, filepath=None):
|
def uploadBanner(self, url=None, filepath=None):
|
||||||
""" Upload a banner from a url or filepath.
|
""" Upload a banner from a url or filepath.
|
||||||
|
@ -348,10 +348,10 @@ class BannerMixin(BannerUrlMixin):
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = '/library/metadata/%s/banners?url=%s' % (self.ratingKey, quote_plus(url))
|
key = f'{self.key}/banners?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = '/library/metadata/%s/banners?' % self.ratingKey
|
key = f'{self.key}/banners'
|
||||||
data = open(filepath, 'rb').read()
|
data = open(filepath, 'rb').read()
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
@ -392,7 +392,7 @@ class PosterMixin(PosterUrlMixin):
|
||||||
|
|
||||||
def posters(self):
|
def posters(self):
|
||||||
""" Returns list of available :class:`~plexapi.media.Poster` objects. """
|
""" Returns list of available :class:`~plexapi.media.Poster` objects. """
|
||||||
return self.fetchItems('/library/metadata/%s/posters' % self.ratingKey, cls=media.Poster)
|
return self.fetchItems(f'{self.key}/posters', cls=media.Poster)
|
||||||
|
|
||||||
def uploadPoster(self, url=None, filepath=None):
|
def uploadPoster(self, url=None, filepath=None):
|
||||||
""" Upload a poster from a url or filepath.
|
""" Upload a poster from a url or filepath.
|
||||||
|
@ -402,10 +402,10 @@ class PosterMixin(PosterUrlMixin):
|
||||||
filepath (str): The full file path the the image to upload.
|
filepath (str): The full file path the the image to upload.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = '/library/metadata/%s/posters?url=%s' % (self.ratingKey, quote_plus(url))
|
key = f'{self.key}/posters?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = '/library/metadata/%s/posters?' % self.ratingKey
|
key = f'{self.key}/posters'
|
||||||
data = open(filepath, 'rb').read()
|
data = open(filepath, 'rb').read()
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
@ -441,7 +441,7 @@ class ThemeMixin(ThemeUrlMixin):
|
||||||
|
|
||||||
def themes(self):
|
def themes(self):
|
||||||
""" Returns list of available :class:`~plexapi.media.Theme` objects. """
|
""" Returns list of available :class:`~plexapi.media.Theme` objects. """
|
||||||
return self.fetchItems('/library/metadata/%s/themes' % self.ratingKey, cls=media.Theme)
|
return self.fetchItems(f'{self.key}/themes', cls=media.Theme)
|
||||||
|
|
||||||
def uploadTheme(self, url=None, filepath=None):
|
def uploadTheme(self, url=None, filepath=None):
|
||||||
""" Upload a theme from url or filepath.
|
""" Upload a theme from url or filepath.
|
||||||
|
@ -453,10 +453,10 @@ class ThemeMixin(ThemeUrlMixin):
|
||||||
filepath (str): The full file path to the theme to upload.
|
filepath (str): The full file path to the theme to upload.
|
||||||
"""
|
"""
|
||||||
if url:
|
if url:
|
||||||
key = '/library/metadata/%s/themes?url=%s' % (self.ratingKey, quote_plus(url))
|
key = f'{self.key}/themes?url={quote_plus(url)}'
|
||||||
self._server.query(key, method=self._server._session.post)
|
self._server.query(key, method=self._server._session.post)
|
||||||
elif filepath:
|
elif filepath:
|
||||||
key = '/library/metadata/%s/themes?' % self.ratingKey
|
key = f'{self.key}/themes'
|
||||||
data = open(filepath, 'rb').read()
|
data = open(filepath, 'rb').read()
|
||||||
self._server.query(key, method=self._server._session.post, data=data)
|
self._server.query(key, method=self._server._session.post, data=data)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import copy
|
||||||
import html
|
import html
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
@ -800,7 +801,7 @@ class MyPlexAccount(PlexObject):
|
||||||
|
|
||||||
params.update(kwargs)
|
params.update(kwargs)
|
||||||
data = self.query(f'{self.METADATA}/library/sections/watchlist/{filter}', params=params)
|
data = self.query(f'{self.METADATA}/library/sections/watchlist/{filter}', params=params)
|
||||||
return self.findItems(data)
|
return self._toOnlineMetadata(self.findItems(data))
|
||||||
|
|
||||||
def onWatchlist(self, item):
|
def onWatchlist(self, item):
|
||||||
""" Returns True if the item is on the user's watchlist.
|
""" Returns True if the item is on the user's watchlist.
|
||||||
|
@ -809,9 +810,7 @@ class MyPlexAccount(PlexObject):
|
||||||
item (:class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`): Item to check
|
item (:class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`): Item to check
|
||||||
if it is on the user's watchlist.
|
if it is on the user's watchlist.
|
||||||
"""
|
"""
|
||||||
ratingKey = item.guid.rsplit('/', 1)[-1]
|
return bool(self.userState(item).watchlistedAt)
|
||||||
data = self.query(f"{self.METADATA}/library/metadata/{ratingKey}/userState")
|
|
||||||
return bool(data.find('UserState').attrib.get('watchlistedAt'))
|
|
||||||
|
|
||||||
def addToWatchlist(self, items):
|
def addToWatchlist(self, items):
|
||||||
""" Add media items to the user's watchlist
|
""" Add media items to the user's watchlist
|
||||||
|
@ -853,29 +852,45 @@ class MyPlexAccount(PlexObject):
|
||||||
ratingKey = item.guid.rsplit('/', 1)[-1]
|
ratingKey = item.guid.rsplit('/', 1)[-1]
|
||||||
self.query(f'{self.METADATA}/actions/removeFromWatchlist?ratingKey={ratingKey}', method=self._session.put)
|
self.query(f'{self.METADATA}/actions/removeFromWatchlist?ratingKey={ratingKey}', method=self._session.put)
|
||||||
|
|
||||||
def searchDiscover(self, query, limit=30):
|
def userState(self, item):
|
||||||
|
""" Returns a :class:`~plexapi.myplex.UserState` object for the specified item.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
item (:class:`~plexapi.video.Movie` or :class:`~plexapi.video.Show`): Item to return the user state.
|
||||||
|
"""
|
||||||
|
ratingKey = item.guid.rsplit('/', 1)[-1]
|
||||||
|
data = self.query(f"{self.METADATA}/library/metadata/{ratingKey}/userState")
|
||||||
|
# TODO: change to findItem after PR#931 is merged
|
||||||
|
return self.findItems(data, cls=UserState)[0]
|
||||||
|
|
||||||
|
def searchDiscover(self, query, limit=30, libtype=None):
|
||||||
""" Search for movies and TV shows in Discover.
|
""" Search for movies and TV shows in Discover.
|
||||||
Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` objects.
|
Returns a list of :class:`~plexapi.video.Movie` and :class:`~plexapi.video.Show` objects.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
query (str): Search query.
|
query (str): Search query.
|
||||||
limit (int, optional): Limit to the specified number of results. Default 30.
|
limit (int, optional): Limit to the specified number of results. Default 30.
|
||||||
|
libtype (str, optional): 'movie' or 'show' to only return movies or shows, otherwise return all items.
|
||||||
"""
|
"""
|
||||||
|
libtypes = {'movie': 'movies', 'show': 'tv'}
|
||||||
|
libtype = libtypes.get(libtype, 'movies,tv')
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
params = {
|
params = {
|
||||||
'query': query,
|
'query': query,
|
||||||
'limit ': limit,
|
'limit ': limit,
|
||||||
'searchTypes': 'movies,tv',
|
'searchTypes': libtype,
|
||||||
'includeMetadata': 1
|
'includeMetadata': 1
|
||||||
}
|
}
|
||||||
|
|
||||||
data = self.query(f'{self.METADATA}/library/search', headers=headers, params=params)
|
data = self.query(f'{self.METADATA}/library/search', headers=headers, params=params)
|
||||||
searchResults = data['MediaContainer'].get('SearchResult', [])
|
searchResults = data['MediaContainer'].get('SearchResults', [])
|
||||||
|
searchResult = next((s['SearchResult'] for s in searchResults if s.get('id') == 'external'), [])
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for result in searchResults:
|
for result in searchResult:
|
||||||
metadata = result['Metadata']
|
metadata = result['Metadata']
|
||||||
type = metadata['type']
|
type = metadata['type']
|
||||||
if type == 'movie':
|
if type == 'movie':
|
||||||
|
@ -888,7 +903,7 @@ class MyPlexAccount(PlexObject):
|
||||||
xml = f'<{tag} {attrs}/>'
|
xml = f'<{tag} {attrs}/>'
|
||||||
results.append(self._manuallyLoadXML(xml))
|
results.append(self._manuallyLoadXML(xml))
|
||||||
|
|
||||||
return results
|
return self._toOnlineMetadata(results)
|
||||||
|
|
||||||
def link(self, pin):
|
def link(self, pin):
|
||||||
""" Link a device to the account using a pin code.
|
""" Link a device to the account using a pin code.
|
||||||
|
@ -903,6 +918,24 @@ class MyPlexAccount(PlexObject):
|
||||||
data = {'code': pin}
|
data = {'code': pin}
|
||||||
self.query(self.LINK, self._session.put, headers=headers, data=data)
|
self.query(self.LINK, self._session.put, headers=headers, data=data)
|
||||||
|
|
||||||
|
def _toOnlineMetadata(self, objs):
|
||||||
|
""" Convert a list of media objects to online metadata objects. """
|
||||||
|
# TODO: Add proper support for metadata.provider.plex.tv
|
||||||
|
# Temporary workaround to allow reloading and browsing of online media objects
|
||||||
|
if not isinstance(objs, list):
|
||||||
|
objs = [objs]
|
||||||
|
for obj in objs:
|
||||||
|
obj._server = PlexServer(self.METADATA, self._token)
|
||||||
|
|
||||||
|
# Parse details key to modify query string
|
||||||
|
url = urlsplit(obj._details_key)
|
||||||
|
query = dict(parse_qsl(url.query))
|
||||||
|
query['includeUserState'] = 1
|
||||||
|
query.pop('includeFields', None)
|
||||||
|
obj._details_key = urlunsplit((url.scheme, url.netloc, url.path, urlencode(query), url.fragment))
|
||||||
|
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
class MyPlexUser(PlexObject):
|
class MyPlexUser(PlexObject):
|
||||||
""" This object represents non-signed in users such as friends and linked
|
""" This object represents non-signed in users such as friends and linked
|
||||||
|
@ -1645,3 +1678,33 @@ class AccountOptOut(PlexObject):
|
||||||
if self.key == 'tv.plex.provider.music':
|
if self.key == 'tv.plex.provider.music':
|
||||||
raise BadRequest('%s does not have the option to opt out managed users.' % self.key)
|
raise BadRequest('%s does not have the option to opt out managed users.' % self.key)
|
||||||
self._updateOptOut('opt_out_managed')
|
self._updateOptOut('opt_out_managed')
|
||||||
|
|
||||||
|
|
||||||
|
class UserState(PlexObject):
|
||||||
|
""" Represents a single UserState
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
TAG (str): UserState
|
||||||
|
lastViewedAt (datetime): Datetime the item was last played.
|
||||||
|
ratingKey (str): Unique key identifying the item.
|
||||||
|
type (str): The media type of the item.
|
||||||
|
viewCount (int): Count of times the item was played.
|
||||||
|
viewedLeafCount (int): Number of items marked as played in the show/season.
|
||||||
|
viewOffset (int): Time offset in milliseconds from the start of the content
|
||||||
|
viewState (bool): True or False if the item has been played.
|
||||||
|
watchlistedAt (datetime): Datetime the item was added to the watchlist.
|
||||||
|
"""
|
||||||
|
TAG = 'UserState'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<{self.__class__.__name__}:{self.ratingKey}>'
|
||||||
|
|
||||||
|
def _loadData(self, data):
|
||||||
|
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||||
|
self.ratingKey = data.attrib.get('ratingKey')
|
||||||
|
self.type = data.attrib.get('type')
|
||||||
|
self.viewCount = utils.cast(int, data.attrib.get('viewCount', 0))
|
||||||
|
self.viewedLeafCount = utils.cast(int, data.attrib.get('viewedLeafCount', 0))
|
||||||
|
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
|
||||||
|
self.viewState = data.attrib.get('viewState') == 'complete'
|
||||||
|
self.watchlistedAt = utils.toDatetime(data.attrib.get('watchlistedAt'))
|
||||||
|
|
|
@ -79,12 +79,12 @@ class Photoalbum(
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the photo album to return.
|
title (str): Title of the photo album to return.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItem(key, Photoalbum, title__iexact=title)
|
return self.fetchItem(key, Photoalbum, title__iexact=title)
|
||||||
|
|
||||||
def albums(self, **kwargs):
|
def albums(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """
|
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItems(key, Photoalbum, **kwargs)
|
return self.fetchItems(key, Photoalbum, **kwargs)
|
||||||
|
|
||||||
def photo(self, title):
|
def photo(self, title):
|
||||||
|
@ -93,12 +93,12 @@ class Photoalbum(
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the photo to return.
|
title (str): Title of the photo to return.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItem(key, Photo, title__iexact=title)
|
return self.fetchItem(key, Photo, title__iexact=title)
|
||||||
|
|
||||||
def photos(self, **kwargs):
|
def photos(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """
|
""" Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItems(key, Photo, **kwargs)
|
return self.fetchItems(key, Photo, **kwargs)
|
||||||
|
|
||||||
def clip(self, title):
|
def clip(self, title):
|
||||||
|
@ -107,12 +107,12 @@ class Photoalbum(
|
||||||
Parameters:
|
Parameters:
|
||||||
title (str): Title of the clip to return.
|
title (str): Title of the clip to return.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItem(key, video.Clip, title__iexact=title)
|
return self.fetchItem(key, video.Clip, title__iexact=title)
|
||||||
|
|
||||||
def clips(self, **kwargs):
|
def clips(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.video.Clip` objects in the album. """
|
""" Returns a list of :class:`~plexapi.video.Clip` objects in the album. """
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItems(key, video.Clip, **kwargs)
|
return self.fetchItems(key, video.Clip, **kwargs)
|
||||||
|
|
||||||
def get(self, title):
|
def get(self, title):
|
||||||
|
|
|
@ -520,7 +520,7 @@ class Show(
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or season parameter is missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
key = f'{self.key}/children?excludeAllLeaves=1'
|
||||||
if title is not None and not isinstance(title, int):
|
if title is not None and not isinstance(title, int):
|
||||||
return self.fetchItem(key, Season, title__iexact=title)
|
return self.fetchItem(key, Season, title__iexact=title)
|
||||||
elif season is not None or isinstance(title, int):
|
elif season is not None or isinstance(title, int):
|
||||||
|
@ -533,7 +533,7 @@ class Show(
|
||||||
|
|
||||||
def seasons(self, **kwargs):
|
def seasons(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.video.Season` objects in the show. """
|
""" Returns a list of :class:`~plexapi.video.Season` objects in the show. """
|
||||||
key = '/library/metadata/%s/children?excludeAllLeaves=1' % self.ratingKey
|
key = f'{self.key}/children?excludeAllLeaves=1'
|
||||||
return self.fetchItems(key, Season, **kwargs)
|
return self.fetchItems(key, Season, **kwargs)
|
||||||
|
|
||||||
def episode(self, title=None, season=None, episode=None):
|
def episode(self, title=None, season=None, episode=None):
|
||||||
|
@ -547,7 +547,7 @@ class Show(
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or season and episode parameters are missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
key = f'{self.key}/allLeaves'
|
||||||
if title is not None:
|
if title is not None:
|
||||||
return self.fetchItem(key, Episode, title__iexact=title)
|
return self.fetchItem(key, Episode, title__iexact=title)
|
||||||
elif season is not None and episode is not None:
|
elif season is not None and episode is not None:
|
||||||
|
@ -556,7 +556,7 @@ class Show(
|
||||||
|
|
||||||
def episodes(self, **kwargs):
|
def episodes(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the show. """
|
""" Returns a list of :class:`~plexapi.video.Episode` objects in the show. """
|
||||||
key = '/library/metadata/%s/allLeaves' % self.ratingKey
|
key = f'{self.key}/allLeaves'
|
||||||
return self.fetchItems(key, Episode, **kwargs)
|
return self.fetchItems(key, Episode, **kwargs)
|
||||||
|
|
||||||
def get(self, title=None, season=None, episode=None):
|
def get(self, title=None, season=None, episode=None):
|
||||||
|
@ -665,7 +665,7 @@ class Season(
|
||||||
|
|
||||||
def episodes(self, **kwargs):
|
def episodes(self, **kwargs):
|
||||||
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
|
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
return self.fetchItems(key, Episode, **kwargs)
|
return self.fetchItems(key, Episode, **kwargs)
|
||||||
|
|
||||||
def episode(self, title=None, episode=None):
|
def episode(self, title=None, episode=None):
|
||||||
|
@ -678,7 +678,7 @@ class Season(
|
||||||
Raises:
|
Raises:
|
||||||
:exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing.
|
:exc:`~plexapi.exceptions.BadRequest`: If title or episode parameter is missing.
|
||||||
"""
|
"""
|
||||||
key = '/library/metadata/%s/children' % self.ratingKey
|
key = f'{self.key}/children'
|
||||||
if title is not None and not isinstance(title, int):
|
if title is not None and not isinstance(title, int):
|
||||||
return self.fetchItem(key, Episode, title__iexact=title)
|
return self.fetchItem(key, Episode, title__iexact=title)
|
||||||
elif episode is not None or isinstance(title, int):
|
elif episode is not None or isinstance(title, int):
|
||||||
|
|
|
@ -304,3 +304,17 @@ def test_myplex_watchlist(account, movie, show, artist):
|
||||||
# Test adding invalid item to watchlist
|
# Test adding invalid item to watchlist
|
||||||
with pytest.raises(BadRequest):
|
with pytest.raises(BadRequest):
|
||||||
account.addToWatchlist(artist)
|
account.addToWatchlist(artist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_myplex_searchDiscover(account, movie, show):
|
||||||
|
guids = lambda x: [r.guid for r in x]
|
||||||
|
|
||||||
|
results = account.searchDiscover(movie.title)
|
||||||
|
assert movie.guid in guids(results)
|
||||||
|
results = account.searchDiscover(movie.title, libtype="show")
|
||||||
|
assert movie.guid not in guids(results)
|
||||||
|
|
||||||
|
results = account.searchDiscover(show.title)
|
||||||
|
assert show.guid in [r.guid for r in results]
|
||||||
|
results = account.searchDiscover(show.title, libtype="movie")
|
||||||
|
assert show.guid not in guids(results)
|
||||||
|
|
Loading…
Reference in a new issue