diff --git a/plexapi/library.py b/plexapi/library.py index 006fda706..30d2b14f0 100644 --- a/plexapi/library.py +++ b/plexapi/library.py @@ -847,12 +847,6 @@ def _validateFieldValue(self, filterField, values, libtype=None): values = [values] fieldType = self.getFieldType(filterField.type) - choiceTypes = {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'} - if fieldType.type in choiceTypes: - filterChoices = self.listFilterChoices(filterField.key, libtype) - else: - filterChoices = [] - results = [] try: @@ -865,11 +859,8 @@ def _validateFieldValue(self, filterField, values, libtype=None): value = float(value) if '.' in str(value) else int(value) elif fieldType.type == 'string': value = str(value) - elif fieldType.type in choiceTypes: - value = str((value.id or value.tag) if isinstance(value, media.MediaTag) else value) - matchValue = value.lower() - value = next((f.key for f in filterChoices - if matchValue in {f.key.lower(), f.title.lower()}), value) + elif fieldType.type in {'tag', 'subtitleLanguage', 'audioLanguage', 'resolution'}: + value = self._validateFieldValueTag(value, filterField, libtype) results.append(str(value)) except (ValueError, AttributeError): raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s' @@ -888,24 +879,47 @@ def _validateFieldValueDate(self, value): else: return int(utils.toDatetime(value, '%Y-%m-%d').timestamp()) + def _validateFieldValueTag(self, value, filterField, libtype): + """ Validates a filter tag value. A filter tag value can be a :class:`~plexapi.library.FilterChoice` object, + a :class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*), + or the exact id :attr:`MediaTag.id` (*int*). + """ + if isinstance(value, FilterChoice): + return value.key + if isinstance(value, media.MediaTag): + value = str(value.id or value.tag) + else: + value = str(value) + filterChoices = self.listFilterChoices(filterField.key, libtype) + matchValue = value.lower() + return next((f.key for f in filterChoices if matchValue in {f.key.lower(), f.title.lower()}), value) + def _validateSortFields(self, sort, libtype=None): - """ Validates a list of filter sort fields is available for the library. + """ Validates a list of filter sort fields is available for the library. Sort fields can be a + list of :class:`~plexapi.library.FilteringSort` objects, or a comma separated string. Returns the validated comma separated sort fields string. """ if isinstance(sort, str): sort = sort.split(',') + if not isinstance(sort, (list, tuple)): + sort = [sort] + validatedSorts = [] for _sort in sort: - validatedSorts.append(self._validateSortField(_sort.strip(), libtype)) + validatedSorts.append(self._validateSortField(_sort, libtype)) return ','.join(validatedSorts) def _validateSortField(self, sort, libtype=None): - """ Validates a filter sort field is available for the library. + """ Validates a filter sort field is available for the library. A sort field can be a + :class:`~plexapi.library.FilteringSort` object, or a string. Returns the validated sort field string. """ - match = re.match(r'(?:([a-zA-Z]*)\.)?([a-zA-Z]+):?([a-zA-Z]*)', sort) + if isinstance(sort, FilteringSort): + return '%s.%s:%s' % (libtype or self.TYPE, sort.key, sort.defaultDirection) + + match = re.match(r'(?:([a-zA-Z]*)\.)?([a-zA-Z]+):?([a-zA-Z]*)', sort.strip()) if not match: raise BadRequest('Invalid filter sort: %s' % sort) _libtype, sortField, sortDir = match.groups() @@ -1009,9 +1023,8 @@ def search(self, title=None, sort=None, maxresults=None, libtype=None, Parameters: title (str, optional): General string query to search for. Partial string matches are allowed. - sort (str or list, optional): A string of comma separated sort fields or a list of sort fields - in the format ``column:dir``. - See :func:`~plexapi.library.LibrarySection.listSorts` to get a list of available sort fields. + sort (:class:`~plexapi.library.FilteringSort` or str or list, optional): A field to sort the results. + See the details below for more info. maxresults (int, optional): Only return the specified number of results. libtype (str, optional): Return results of a specific type (movie, show, season, episode, artist, album, track, photoalbum, photo, collection) (e.g. ``libtype='episode'`` will only @@ -1027,6 +1040,28 @@ def search(self, title=None, sort=None, maxresults=None, libtype=None, :exc:`~plexapi.exceptions.BadRequest`: When the sort or filter is invalid. :exc:`~plexapi.exceptions.NotFound`: When applying an unknown sort or filter. + **Sorting Results** + + The search results can be sorted by including the ``sort`` parameter. + + * See :func:`~plexapi.library.LibrarySection.listSorts` to get a list of available sort fields. + + The ``sort`` parameter can be a :class:`~plexapi.library.FilteringSort` object or a sort string in the + format ``field:dir``. The sort direction ``dir`` can be ``asc``, ``desc``, or ``nullsLast``. Omitting the + sort direction or using a :class:`~plexapi.library.FilteringSort` object will sort the results in the default + direction of the field. Multi-sorting on multiple fields can be achieved by using a comma separated list of + sort strings, or a list of :class:`~plexapi.library.FilteringSort` object or strings. + + Examples: + + .. code-block:: python + + library.search(sort="titleSort:desc") # Sort title in descending order + library.search(sort="titleSort") # Sort title in the default order + # Multi-sort by year in descending order, then by audience rating in descending order + library.search(sort="year:desc,audienceRating:desc") + library.search(sort=["year:desc", "audienceRating:desc"]) + **Using Plex Filters** Any of the available custom filters can be applied to the search results @@ -1065,8 +1100,9 @@ def search(self, title=None, sort=None, maxresults=None, libtype=None, * **writer** (:class:`~plexapi.media.MediaTag`): Search for the name of a writer. * **year** (*int*): Search for a specific year. - Tag type filter values can be a :class:`~plexapi.media.MediaTag` object, the exact name - :attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*). + Tag type filter values can be a :class:`~plexapi.library.FilterChoice` object, + :class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*), + or the exact id :attr:`MediaTag.id` (*int*). Date type filter values can be a ``datetime`` object, a relative date using a one of the available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format. diff --git a/tests/test_library.py b/tests/test_library.py index dde3d78ed..f743acd35 100644 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -370,6 +370,13 @@ def test_library_MovieSection_search(movies, movie, collection): _test_library_search(movies, collection) +def test_library_MovieSection_search_FilterChoice(movies, collection): + filterChoice = next(c for c in movies.listFilterChoices("collection") if c.title == collection.title) + results = movies.search(filters={'collection': filterChoice}) + movie = collection.items()[0] + assert movie in results + + def test_library_MovieSection_advancedSearch(movies, movie): advancedFilters = { 'and': [ @@ -483,7 +490,13 @@ def test_library_MovieSection_search_sort(movies): results_multi_list = movies.search(sort=["year:desc", "titleSort:desc"]) titleSort_multi_list = [(r.year, r.titleSort) for r in results_multi_list] - assert titleSort_multi_list == sorted(titleSort_multi_str, reverse=True) + assert titleSort_multi_list == sorted(titleSort_multi_list, reverse=True) + + # Test sort using FilteringSort object + sortObj = next(s for s in movies.listSorts() if s.key == "year") + results_sortObj = movies.search(sort=sortObj) + sortObj_list = [r.year for r in results_sortObj] + assert sortObj_list == sorted(sortObj_list, reverse=True) def test_library_ShowSection_search_sort(tvshows):