Skip to content

Commit

Permalink
Add search kwargs to LibrarySection.get() (#1191)
Browse files Browse the repository at this point in the history
* Optimize methods to use library get/search

* Revert methods

* Fix reloading of episodes for missing parentRatingKey

* Add tests for LibrarySection.get with kwargs
  • Loading branch information
JonnyWong16 authored Jul 28, 2023
1 parent 58e279b commit 8298a61
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 36 deletions.
41 changes: 23 additions & 18 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject, PlexHistory, PlexSession
from plexapi.exceptions import BadRequest, NotFound
from plexapi.exceptions import BadRequest
from plexapi.mixins import (
AdvancedSettingsMixin, SplitMergeMixin, UnmatchMatchMixin, ExtrasMixin, HubsMixin, PlayedUnplayedMixin, RatingMixin,
ArtUrlMixin, ArtMixin, PosterUrlMixin, PosterMixin, ThemeMixin, ThemeUrlMixin,
Expand Down Expand Up @@ -178,14 +178,19 @@ def album(self, title):
Parameters:
title (str): Title of the album to return.
"""
try:
return self.section().search(title, libtype='album', filters={'artist.id': self.ratingKey})[0]
except IndexError:
raise NotFound(f"Unable to find album '{title}'") from None
return self.section().get(
title=title,
libtype='album',
filters={'artist.id': self.ratingKey}
)

def albums(self, **kwargs):
""" Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """
return self.section().search(libtype='album', filters={'artist.id': self.ratingKey}, **kwargs)
return self.section().search(
libtype='album',
filters={'artist.id': self.ratingKey},
**kwargs
)

def track(self, title=None, album=None, track=None):
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
Expand Down Expand Up @@ -430,18 +435,6 @@ def _loadData(self, data):
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year'))

def _prettyfilename(self):
""" Returns a filename for use in download. """
return f'{self.grandparentTitle} - {self.parentTitle} - {str(self.trackNumber).zfill(2)} - {self.title}'

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

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

@property
def locations(self):
""" This does not exist in plex xml response but is added to have a common
Expand All @@ -457,6 +450,18 @@ def trackNumber(self):
""" Returns the track number. """
return self.index

def _prettyfilename(self):
""" Returns a filename for use in download. """
return f'{self.grandparentTitle} - {self.parentTitle} - {str(self.trackNumber).zfill(2)} - {self.title}'

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

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

def _defaultSyncTitle(self):
""" Returns str, default title for a new syncItem. """
return f'{self.grandparentTitle} - {self.parentTitle} - {self.title}'
Expand Down
13 changes: 9 additions & 4 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,19 +588,24 @@ def removeLocations(self, location):
raise BadRequest('You are unable to remove all locations from a library.')
return self.edit(location=locations)

def get(self, title):
""" Returns the media item with the specified title.
def get(self, title, **kwargs):
""" Returns the media item with the specified title and kwargs.
Parameters:
title (str): Title of the item to return.
kwargs (dict): Additional search parameters.
See :func:`~plexapi.library.LibrarySection.search` for more info.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The title is not found in the library.
"""
try:
return self.search(title)[0]
return self.search(title, limit=1, **kwargs)[0]
except IndexError:
raise NotFound(f"Unable to find item '{title}'") from None
msg = f"Unable to find item with title '{title}'"
if kwargs:
msg += f" and kwargs {kwargs}"
raise NotFound(msg) from None

def getGuid(self, guid):
""" Returns the media item with the specified external Plex, IMDB, TMDB, or TVDB ID.
Expand Down
29 changes: 16 additions & 13 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ class Movie(
duration (int): Duration of the movie in milliseconds.
editionTitle (str): The edition title of the movie (e.g. Director's Cut, Extended Edition, etc.).
enableCreditsMarkerGeneration (int): Setting that indicates if credits markers detection is enabled.
(-1 = Library default, 0 = Disabled)
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
guids (List<:class:`~plexapi.media.Guid`>): List of guid objects.
labels (List<:class:`~plexapi.media.Label`>): List of label objects.
Expand Down Expand Up @@ -473,6 +474,7 @@ class Show(
contentRating (str) Content rating (PG-13; NR; TV-G).
duration (int): Typical duration of the show episodes in milliseconds.
enableCreditsMarkerGeneration (int): Setting that indicates if credits markers detection is enabled.
(-1 = Library default, 0 = Disabled).
episodeSort (int): Setting that indicates how episodes are sorted for the show
(-1 = Library default, 0 = Oldest first, 1 = Newest first).
flattenSeasons (int): Setting that indicates if seasons are set to hidden for the show
Expand All @@ -494,7 +496,8 @@ class Show(
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
seasonCount (int): Number of seasons (excluding Specials) in the show.
showOrdering (str): Setting that indicates the episode ordering for the show
(None = Library default).
(None = Library default, tmdbAiring = The Movie Database (Aired),
aired = TheTVDB (Aired), dvd = TheTVDB (DVD), absolute = TheTVDB (Absolute)).
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment).
subtitleLanguage (str): Setting that indicates the preferred subtitle language.
Expand Down Expand Up @@ -738,10 +741,12 @@ def seasonNumber(self):
""" Returns the season number. """
return self.index

def episodes(self, **kwargs):
""" Returns a list of :class:`~plexapi.video.Episode` objects in the season. """
key = f'{self.key}/children'
return self.fetchItems(key, Episode, **kwargs)
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.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)

def episode(self, title=None, episode=None):
""" Returns the episode with the given title or number.
Expand All @@ -764,17 +769,15 @@ def episode(self, title=None, episode=None):
return self.fetchItem(key, Episode, parentIndex=self.index, index=index)
raise BadRequest('Missing argument: title or episode is required')

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

def get(self, title=None, episode=None):
""" Alias to :func:`~plexapi.video.Season.episode`. """
return self.episode(title, episode)

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.
"""
data = self._server.query(self._details_key)
return next(iter(self.findItems(data, rtag='OnDeck')), None)

def show(self):
""" Return the season's :class:`~plexapi.video.Show`. """
return self.fetchItem(self.parentKey)
Expand Down Expand Up @@ -903,7 +906,7 @@ def _loadData(self, data):

# 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:
if self.skipParent and data.attrib.get('parentRatingKey') is None:
# Parse the parentRatingKey from the parentThumb
if self.parentThumb and self.parentThumb.startswith('/library/metadata/'):
self.parentRatingKey = utils.cast(int, self.parentThumb.split('/')[3])
Expand Down
3 changes: 3 additions & 0 deletions tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ def test_library_sectionByID_with_attrs(plex, movies):

def test_library_section_get_movie(movies):
assert movies.get("Sita Sings the Blues")
assert movies.get(None, filters={"title": "Big Buck Bunny", "year": 2008})
with pytest.raises(NotFound):
movies.get("invalid title")


def test_library_MovieSection_getGuid(movies, movie):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def test_video_Movie_mixins_fields(movie):
test_mixins.edit_user_rating(movie)


@pytest.mark.anonymous
@pytest.mark.anonymously
def test_video_Movie_mixins_fields_edition(movie):
with pytest.raises(BadRequest):
test_mixins.edit_edition_title(movie)
Expand Down

0 comments on commit 8298a61

Please sign in to comment.