Add search kwargs to LibrarySection.get() (#1191)

* Optimize methods to use library get/search

* Revert methods

* Fix reloading of episodes for missing parentRatingKey

* Add tests for LibrarySection.get with kwargs
This commit is contained in:
JonnyWong16 2023-07-27 18:02:23 -07:00 committed by GitHub
parent 58e279b837
commit 8298a61b64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 52 additions and 36 deletions

View file

@ -4,7 +4,7 @@ from urllib.parse import quote_plus
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,
@ -178,14 +178,19 @@ class Artist(
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.
@ -430,18 +435,6 @@ class Track(
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
@ -457,6 +450,18 @@ class Track(
""" 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}'

View file

@ -588,19 +588,24 @@ class LibrarySection(PlexObject):
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.

View file

@ -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.
@ -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
@ -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.
@ -738,10 +741,12 @@ class Season(
""" 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.
@ -764,17 +769,15 @@ class Season(
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)
@ -903,7 +906,7 @@ class Episode(
# 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])

View file

@ -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):

View file

@ -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)