Allow relative dates for search filters (#717)

* Add validation for relative date search filter values

* Update search doc strings for searching using relative dates

* Update library search tests for relative dates

* Fix relative date search test

* Automatically format negative sign in relative dates

* Fix relative date search test
This commit is contained in:
JonnyWong16 2021-04-05 13:46:26 -07:00 committed by GitHub
parent 5584ef1d4f
commit 19fa6c1e50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 55 additions and 29 deletions

View file

@ -852,10 +852,7 @@ class LibrarySection(PlexObject):
if fieldType.type == 'boolean':
value = int(bool(value))
elif fieldType.type == 'date':
if isinstance(value, datetime):
value = int(value.timestamp())
else:
value = int(utils.toDatetime(value, '%Y-%m-%d').timestamp())
value = self._validateFieldValueDate(value)
elif fieldType.type == 'integer':
value = int(value)
elif fieldType.type == 'string':
@ -866,12 +863,23 @@ class LibrarySection(PlexObject):
value = next((f.key for f in filterChoices
if matchValue in {f.key.lower(), f.title.lower()}), value)
results.append(str(value))
except ValueError:
except (ValueError, AttributeError):
raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s'
% (value, filterField.key, fieldType.type)) from None
return results
def _validateFieldValueDate(self, value):
""" Validates a filter date value. A filter date value can be a datetime object,
a relative date (e.g. -30d), or a date in YYYY-MM-DD format.
"""
if isinstance(value, datetime):
return int(value.timestamp())
elif re.match(r'^-?\d+(mon|[smhdwy])$', value):
return '-' + value.lstrip('-')
else:
return int(utils.toDatetime(value, '%Y-%m-%d').timestamp())
def _validateSortField(self, sort, libtype=None):
""" Validates a filter sort field is available for the library.
Returns the validated sort field string.
@ -942,10 +950,7 @@ class LibrarySection(PlexObject):
* See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values.
The following filter fields are just some examples of the possible filters. The list is not exaustive,
and not all filters apply to all library types. For tag type filters, a :class:`~plexapi.media.MediaTag`
object, the exact name :attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*) can be
provided. For date type filters, either a ``datetime`` object or a date in ``YYYY-MM-DD`` (*str*) format
can be provided. Multiple values can be ``OR`` together by providing a list of values.
and not all filters apply to all library types.
* **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor.
* **addedAt** (*datetime*): Search for items added before or after a date. See operators below.
@ -973,6 +978,24 @@ class LibrarySection(PlexObject):
* **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*).
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.
Relative date suffixes:
* ``s``: ``seconds``
* ``m``: ``minutes``
* ``h``: ``hours``
* ``d``: ``days``
* ``w``: ``weeks``
* ``mon``: ``months``
* ``y``: ``years``
Multiple values can be ``OR`` together by providing a list of values.
Examples:
.. code-block:: python
@ -1071,6 +1094,9 @@ class LibrarySection(PlexObject):
# Title starts with Marvel and added before 2021-01-01
library.search(**{"title<": "Marvel", "addedAt<<": "2021-01-01"})
# Added in the last 30 days using relative dates
library.search(**{"addedAt>>": "30d"})
# Collection is James Bond and user rating is greater than 8
library.search(**{"collection": "James Bond", "userRating>>": 8})

View file

@ -469,28 +469,28 @@ def _test_library_search(library, obj):
searchValue = value - timedelta(days=1)
else:
searchValue = value
searchFilter = {field.key + operator.key[:-1]: searchValue}
results = library.search(libtype=obj.type, **searchFilter)
if operator.key.startswith("!") or operator.key.startswith(">>") and searchValue == 0:
assert obj not in results
else:
assert obj in results
_do_test_library_search(library, obj, field, operator, searchValue)
# Test search again using string tag and date
if field.type in {"tag", "date"}:
if field.type == "tag" and fieldAttr != 'contentRating':
if not isinstance(searchValue, list):
searchValue = [searchValue]
searchValue = [v.tag for v in searchValue]
elif field.type == "date":
searchValue = searchValue.strftime("%Y-%m-%d")
if field.type == "tag" and fieldAttr != "contentRating":
if not isinstance(searchValue, list):
searchValue = [searchValue]
searchValue = [v.tag for v in searchValue]
_do_test_library_search(library, obj, field, operator, searchValue)
searchFilter = {field.key + operator.key[:-1]: searchValue}
results = library.search(libtype=obj.type, **searchFilter)
elif field.type == "date":
searchValue = searchValue.strftime("%Y-%m-%d")
_do_test_library_search(library, obj, field, operator, searchValue)
searchValue = "1s"
_do_test_library_search(library, obj, field, operator, searchValue)
if operator.key.startswith("!") or operator.key.startswith(">>") and searchValue == 0:
assert obj not in results
else:
assert obj in results
def _do_test_library_search(library, obj, field, operator, searchValue):
searchFilter = {field.key + operator.key[:-1]: searchValue}
results = library.search(libtype=obj.type, **searchFilter)
if operator.key.startswith("!") or operator.key.startswith(">>") and (searchValue == 0 or searchValue == '1s'):
assert obj not in results
else:
assert obj in results