Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,12 @@ def sonicallySimilar(
Returns:
List[:class:`~plexapi.audio.Audio`]: list of sonically similar audio items.
"""

key = f"{self.key}/nearest"
params: Dict[str, Any] = {}
if limit is not None:
params['limit'] = limit
if maxDistance is not None:
params['maxDistance'] = maxDistance
key += utils.joinArgs(params)
key = self._buildQueryKey(f"{self.key}/nearest", **params)

return self.fetchItems(
key,
Expand Down Expand Up @@ -280,7 +278,7 @@ def track(self, title=None, album=None, track=None):
Raises:
:exc:`~plexapi.exceptions.BadRequest`: If title or album and track parameters are missing.
"""
key = f'{self.key}/allLeaves'
key = self._buildQueryKey(f'{self.key}/allLeaves')
if title is not None:
return self.fetchItem(key, Track, title__iexact=title)
elif album is not None and track is not None:
Expand All @@ -289,7 +287,7 @@ def track(self, title=None, album=None, track=None):

def tracks(self, **kwargs):
""" Returns a list of :class:`~plexapi.audio.Track` objects by the artist. """
key = f'{self.key}/allLeaves'
key = self._buildQueryKey(f"{self.key}/allLeaves")
return self.fetchItems(key, Track, **kwargs)

def get(self, title=None, album=None, track=None):
Expand Down Expand Up @@ -329,7 +327,7 @@ def popularTracks(self):

def station(self):
""" Returns a :class:`~plexapi.playlist.Playlist` artist radio station or `None`. """
key = f'{self.key}?includeStations=1'
key = self._buildQueryKey(f'{self.key}', includeStations=1)
return next(iter(self.fetchItems(key, cls=Playlist, rtag="Stations")), None)

@property
Expand Down Expand Up @@ -440,7 +438,7 @@ def track(self, title=None, track=None):
Raises:
:exc:`~plexapi.exceptions.BadRequest`: If title or track parameter is missing.
"""
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
if title is not None and not isinstance(title, int):
return self.fetchItem(key, Track, title__iexact=title)
elif track is not None or isinstance(title, int):
Expand All @@ -453,7 +451,7 @@ def track(self, title=None, track=None):

def tracks(self, **kwargs):
""" Returns a list of :class:`~plexapi.audio.Track` objects in the album. """
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItems(key, Track, **kwargs)

def get(self, title=None, track=None):
Expand All @@ -462,7 +460,8 @@ def get(self, title=None, track=None):

def artist(self):
""" Return the album's :class:`~plexapi.audio.Artist`. """
return self.fetchItem(self.parentKey)
key = self._buildQueryKey(self.parentKey)
return self.fetchItem(key)

def download(self, savepath=None, keep_original_name=False, **kwargs):
""" Download all tracks from the album. See :func:`~plexapi.base.Playable.download` for details.
Expand Down Expand Up @@ -609,11 +608,13 @@ def _prettyfilename(self):

def album(self):
""" Return the track's :class:`~plexapi.audio.Album`. """
return self.fetchItem(self.parentKey)
key = self._buildQueryKey(self.parentKey)
return self.fetchItem(key)

def artist(self):
""" Return the track's :class:`~plexapi.audio.Artist`. """
return self.fetchItem(self.grandparentKey)
key = self._buildQueryKey(self.grandparentKey)
return self.fetchItem(key)

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
Expand Down
5 changes: 3 additions & 2 deletions plexapi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,10 @@ def _buildQueryKey(self, key, **kwargs):
return None

args = {'includeGuids': 1, **kwargs}
params = utils.joinArgs(args)
params = utils.joinArgs(args).lstrip('?')
delim = '&' if '?' in key else '?'

return f"{key}{params}"
return f"{key}{delim}{params}"

def _isChildOf(self, **kwargs):
""" Returns True if this object is a child of the given attributes.
Expand Down
2 changes: 1 addition & 1 deletion plexapi/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def item(self, title):
@cached_data_property
def _items(self):
""" Cache for the items. """
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItems(key)

def items(self):
Expand Down
31 changes: 18 additions & 13 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def hubs(self, sectionID=None, identifier=None, **kwargs):
if not isinstance(identifier, list):
identifier = [identifier]
kwargs['identifier'] = ",".join(identifier)
key = f'/hubs{utils.joinArgs(kwargs)}'
key = self._buildQueryKey('/hubs', **kwargs)
return self.fetchItems(key)

def all(self, **kwargs):
Expand All @@ -151,11 +151,13 @@ def all(self, **kwargs):

def onDeck(self):
""" Returns a list of all media items on deck. """
return self.fetchItems('/library/onDeck')
key = self._buildQueryKey('/library/onDeck')
return self.fetchItems(key)

def recentlyAdded(self):
""" Returns a list of all media items recently added. """
return self.fetchItems('/library/recentlyAdded')
key = self._buildQueryKey('/library/recentlyAdded')
return self.fetchItems(key)

def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain
Expand All @@ -173,7 +175,7 @@ def search(self, title=None, libtype=None, **kwargs):
args['type'] = utils.searchType(libtype)
for attr, value in kwargs.items():
args[attr] = value
key = f'/library/all{utils.joinArgs(args)}'
key = self._buildQueryKey('/library/all', **args)
return self.fetchItems(key)

def cleanBundles(self):
Expand Down Expand Up @@ -711,7 +713,7 @@ def resetManagedHubs(self):
def hubs(self):
""" Returns a list of available :class:`~plexapi.library.Hub` for this library section.
"""
key = f'/hubs/sections/{self.key}?includeStations=1'
key = self._buildQueryKey(f'/hubs/sections/{self.key}', includeStations=1)
return self.fetchItems(key)

def agents(self):
Expand Down Expand Up @@ -800,12 +802,12 @@ def timeline(self):

def onDeck(self):
""" Returns a list of media items on deck from this library section. """
key = f'/library/sections/{self.key}/onDeck'
key = self._buildQueryKey(f'/library/sections/{self.key}/onDeck')
return self.fetchItems(key)

def continueWatching(self):
""" Return a list of media items in the library's Continue Watching hub. """
key = f'/hubs/sections/{self.key}/continueWatching/items'
key = self._buildQueryKey(f'/hubs/sections/{self.key}/continueWatching/items')
return self.fetchItems(key)

def recentlyAdded(self, maxresults=50, libtype=None):
Expand Down Expand Up @@ -1955,7 +1957,7 @@ class MusicSection(LibrarySection, ArtistEditMixins, AlbumEditMixins, TrackEditM

def albums(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
key = f'/library/sections/{self.key}/albums'
key = self._buildQueryKey(f'/library/sections/{self.key}/albums')
return self.fetchItems(key)

def stations(self):
Expand Down Expand Up @@ -2054,7 +2056,7 @@ def sonicAdventure(
startID = start if isinstance(start, int) else start.ratingKey
endID = end if isinstance(end, int) else end.ratingKey

key = f"/library/sections/{self.key}/computePath?startID={startID}&endID={endID}"
key = self._buildQueryKey(f"/library/sections/{self.key}/computePath", startID=startID, endID=endID)
return self.fetchItems(key, **kwargs)


Expand Down Expand Up @@ -2233,7 +2235,8 @@ def _partialItems(self):
def _items(self):
""" Cache for items. """
if self.more and self.key: # If there are more items to load, fetch them
items = self.fetchItems(self.key)
key = self._buildQueryKey(self.key)
items = self.fetchItems(key)
self.more = False
self.size = len(items)
return items
Expand Down Expand Up @@ -2309,11 +2312,12 @@ def _loadData(self, data):
self.tagValue = utils.cast(int, data.attrib.get('tagValue'))
self.thumb = data.attrib.get('thumb')

def items(self, *args, **kwargs):
def items(self):
""" Return the list of items within this tag. """
if not self.key:
raise BadRequest(f'Key is not defined for this tag: {self.tag}')
return self.fetchItems(self.key)
key = self._buildQueryKey(self.key)
return self.fetchItems(key)


@utils.registerPlexObject
Expand Down Expand Up @@ -2997,7 +3001,8 @@ def _loadData(self, data):

def items(self):
""" Returns a list of items for this filter choice. """
return self.fetchItems(self.fastKey)
key = self._buildQueryKey(self.fastKey)
return self.fetchItems(key)


class ManagedHub(PlexObject):
Expand Down
3 changes: 2 additions & 1 deletion plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,8 @@ def items(self):
""" Return the list of items within this tag. """
if not self.key:
raise BadRequest(f'Key is not defined for this tag: {self.tag}. Reload the parent object.')
return self.fetchItems(self.key)
key = self._buildQueryKey(self.key)
return self.fetchItems(key)


@utils.registerPlexObject
Expand Down
4 changes: 2 additions & 2 deletions plexapi/mixins/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class ExtrasMixin:
def extras(self):
""" Returns a list of :class:`~plexapi.video.Extra` objects. """
from plexapi.video import Extra
key = f'{self.key}/extras'
key = self._buildQueryKey(f'{self.key}/extras')
return self.fetchItems(key, cls=Extra)


Expand All @@ -14,5 +14,5 @@ class HubsMixin:
def hubs(self):
""" Returns a list of :class:`~plexapi.library.Hub` objects. """
from plexapi.library import Hub
key = f'{self.key}/related'
key = self._buildQueryKey(f'{self.key}/related')
return self.fetchItems(key, cls=Hub)
15 changes: 8 additions & 7 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ def album(self, title):
Parameters:
title (str): Title of the photo album to return.
"""
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItem(key, Photoalbum, title__iexact=title)

def albums(self, **kwargs):
""" Returns a list of :class:`~plexapi.photo.Photoalbum` objects in the album. """
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItems(key, Photoalbum, **kwargs)

def photo(self, title):
Expand All @@ -93,12 +93,12 @@ def photo(self, title):
Parameters:
title (str): Title of the photo to return.
"""
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItem(key, Photo, title__iexact=title)

def photos(self, **kwargs):
""" Returns a list of :class:`~plexapi.photo.Photo` objects in the album. """
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItems(key, Photo, **kwargs)

def clip(self, title):
Expand All @@ -107,12 +107,12 @@ def clip(self, title):
Parameters:
title (str): Title of the clip to return.
"""
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItem(key, video.Clip, title__iexact=title)

def clips(self, **kwargs):
""" Returns a list of :class:`~plexapi.video.Clip` objects in the album. """
key = f'{self.key}/children'
key = self._buildQueryKey(f'{self.key}/children')
return self.fetchItems(key, video.Clip, **kwargs)

def get(self, title):
Expand Down Expand Up @@ -250,7 +250,8 @@ def _prettyfilename(self):

def photoalbum(self):
""" Return the photo's :class:`~plexapi.photo.Photoalbum`. """
return self.fetchItem(self.parentKey)
key = self._buildQueryKey(self.parentKey)
return self.fetchItem(key)

def section(self):
""" Returns the :class:`~plexapi.library.LibrarySection` the item belongs to. """
Expand Down
2 changes: 1 addition & 1 deletion plexapi/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def _items(self):
if self.radio:
return []

key = f'{self.key}/items'
key = self._buildQueryKey(f'{self.key}/items')
items = self.fetchItems(key)

# Cache server connections to avoid reconnecting for each item
Expand Down
13 changes: 8 additions & 5 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ def onDeck(self):
""" Returns show's On Deck :class:`~plexapi.video.Video` object or `None`.
If show is unwatched, return will likely be the first episode.
"""
key = f'{self.key}?includeOnDeck=1'
key = self._buildQueryKey(f'{self.key}', includeOnDeck=1)
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)

def season(self, title=None, season=None):
Expand Down Expand Up @@ -893,7 +893,7 @@ def onDeck(self):
""" Returns season's On Deck :class:`~plexapi.video.Video` object or `None`.
Will only return a match if the show's On Deck episode is in this season.
"""
key = f'{self.key}?includeOnDeck=1'
key = self._buildQueryKey(f'{self.key}', includeOnDeck=1)
return next(iter(self.fetchItems(key, cls=Episode, rtag='OnDeck')), None)

def episode(self, title=None, episode=None):
Expand Down Expand Up @@ -928,7 +928,8 @@ def get(self, title=None, episode=None):

def show(self):
""" Return the season's :class:`~plexapi.video.Show`. """
return self.fetchItem(self._buildQueryKey(self.parentKey))
key = self._buildQueryKey(self.parentKey)
return self.fetchItem(key)

def watched(self):
""" Returns list of watched :class:`~plexapi.video.Episode` objects. """
Expand Down Expand Up @@ -1218,11 +1219,13 @@ def hasPreviewThumbnails(self):

def season(self):
"""" Return the episode's :class:`~plexapi.video.Season`. """
return self.fetchItem(self._buildQueryKey(self.parentKey))
key = self._buildQueryKey(self.parentKey)
return self.fetchItem(key)

def show(self):
"""" Return the episode's :class:`~plexapi.video.Show`. """
return self.fetchItem(self._buildQueryKey(self.grandparentKey))
key = self._buildQueryKey(self.grandparentKey)
return self.fetchItem(key)

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
Expand Down
22 changes: 22 additions & 0 deletions tests/test_fetch_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,25 @@ def test_find_items_empty_data(plex):
assert len(result) == 0
result = plex.findItems(Element("MediaContainer"))
assert isinstance(result, MediaContainer)


def test_build_query_key(plex):
key = '/test/key'
key_with_query = f'{key}?foo=bar'
kwargs = {'index': 1, 'type': 2}

query_key = plex._buildQueryKey(key)
assert query_key.startswith(key)
assert '?includeGuids=1' in query_key

query_key = plex._buildQueryKey(key, **kwargs)
query_params = []
for k, v in kwargs.items():
query_param = f'{k}={v}'
assert query_param in query_key
query_params.append(query_param)

query_key = plex._buildQueryKey(key_with_query, **kwargs)
assert query_key.startswith(key_with_query)
assert '&includeGuids=1' in query_key
assert f'&{"&".join(query_params)}' in query_key
Loading