Skip to content
Merged
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
76 changes: 56 additions & 20 deletions plexapi/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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'
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 14 additions & 1 deletion tests/test_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': [
Expand Down Expand Up @@ -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):
Expand Down