Skip to content

Commit 19fa6c1

Browse files
authored
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
1 parent 5584ef1 commit 19fa6c1

File tree

2 files changed

+57
-31
lines changed

2 files changed

+57
-31
lines changed

plexapi/library.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -852,10 +852,7 @@ def _validateFieldValue(self, filterField, values, libtype=None):
852852
if fieldType.type == 'boolean':
853853
value = int(bool(value))
854854
elif fieldType.type == 'date':
855-
if isinstance(value, datetime):
856-
value = int(value.timestamp())
857-
else:
858-
value = int(utils.toDatetime(value, '%Y-%m-%d').timestamp())
855+
value = self._validateFieldValueDate(value)
859856
elif fieldType.type == 'integer':
860857
value = int(value)
861858
elif fieldType.type == 'string':
@@ -866,12 +863,23 @@ def _validateFieldValue(self, filterField, values, libtype=None):
866863
value = next((f.key for f in filterChoices
867864
if matchValue in {f.key.lower(), f.title.lower()}), value)
868865
results.append(str(value))
869-
except ValueError:
866+
except (ValueError, AttributeError):
870867
raise BadRequest('Invalid value "%s" for filter field "%s", value should be type %s'
871868
% (value, filterField.key, fieldType.type)) from None
872869

873870
return results
874871

872+
def _validateFieldValueDate(self, value):
873+
""" Validates a filter date value. A filter date value can be a datetime object,
874+
a relative date (e.g. -30d), or a date in YYYY-MM-DD format.
875+
"""
876+
if isinstance(value, datetime):
877+
return int(value.timestamp())
878+
elif re.match(r'^-?\d+(mon|[smhdwy])$', value):
879+
return '-' + value.lstrip('-')
880+
else:
881+
return int(utils.toDatetime(value, '%Y-%m-%d').timestamp())
882+
875883
def _validateSortField(self, sort, libtype=None):
876884
""" Validates a filter sort field is available for the library.
877885
Returns the validated sort field string.
@@ -942,10 +950,7 @@ def search(self, title=None, sort=None, maxresults=None,
942950
* See :func:`~plexapi.library.LibrarySection.listFilterChoices` to get a list of all available filter values.
943951
944952
The following filter fields are just some examples of the possible filters. The list is not exaustive,
945-
and not all filters apply to all library types. For tag type filters, a :class:`~plexapi.media.MediaTag`
946-
object, the exact name :attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*) can be
947-
provided. For date type filters, either a ``datetime`` object or a date in ``YYYY-MM-DD`` (*str*) format
948-
can be provided. Multiple values can be ``OR`` together by providing a list of values.
953+
and not all filters apply to all library types.
949954
950955
* **actor** (:class:`~plexapi.media.MediaTag`): Search for the name of an actor.
951956
* **addedAt** (*datetime*): Search for items added before or after a date. See operators below.
@@ -973,6 +978,24 @@ def search(self, title=None, sort=None, maxresults=None,
973978
* **writer** (:class:`~plexapi.media.MediaTag`): Search for the name of a writer.
974979
* **year** (*int*): Search for a specific year.
975980
981+
Tag type filter values can be a :class:`~plexapi.media.MediaTag` object, the exact name
982+
:attr:`MediaTag.tag` (*str*), or the exact id :attr:`MediaTag.id` (*int*).
983+
984+
Date type filter values can be a ``datetime`` object, a relative date using a one of the
985+
available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format.
986+
987+
Relative date suffixes:
988+
989+
* ``s``: ``seconds``
990+
* ``m``: ``minutes``
991+
* ``h``: ``hours``
992+
* ``d``: ``days``
993+
* ``w``: ``weeks``
994+
* ``mon``: ``months``
995+
* ``y``: ``years``
996+
997+
Multiple values can be ``OR`` together by providing a list of values.
998+
976999
Examples:
9771000
9781001
.. code-block:: python
@@ -1071,6 +1094,9 @@ def search(self, title=None, sort=None, maxresults=None,
10711094
# Title starts with Marvel and added before 2021-01-01
10721095
library.search(**{"title<": "Marvel", "addedAt<<": "2021-01-01"})
10731096
1097+
# Added in the last 30 days using relative dates
1098+
library.search(**{"addedAt>>": "30d"})
1099+
10741100
# Collection is James Bond and user rating is greater than 8
10751101
library.search(**{"collection": "James Bond", "userRating>>": 8})
10761102

tests/test_library.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -469,28 +469,28 @@ def _test_library_search(library, obj):
469469
searchValue = value - timedelta(days=1)
470470
else:
471471
searchValue = value
472-
473-
searchFilter = {field.key + operator.key[:-1]: searchValue}
474-
results = library.search(libtype=obj.type, **searchFilter)
475472

476-
if operator.key.startswith("!") or operator.key.startswith(">>") and searchValue == 0:
477-
assert obj not in results
478-
else:
479-
assert obj in results
473+
_do_test_library_search(library, obj, field, operator, searchValue)
480474

481475
# Test search again using string tag and date
482-
if field.type in {"tag", "date"}:
483-
if field.type == "tag" and fieldAttr != 'contentRating':
484-
if not isinstance(searchValue, list):
485-
searchValue = [searchValue]
486-
searchValue = [v.tag for v in searchValue]
487-
elif field.type == "date":
488-
searchValue = searchValue.strftime("%Y-%m-%d")
489-
490-
searchFilter = {field.key + operator.key[:-1]: searchValue}
491-
results = library.search(libtype=obj.type, **searchFilter)
492-
493-
if operator.key.startswith("!") or operator.key.startswith(">>") and searchValue == 0:
494-
assert obj not in results
495-
else:
496-
assert obj in results
476+
if field.type == "tag" and fieldAttr != "contentRating":
477+
if not isinstance(searchValue, list):
478+
searchValue = [searchValue]
479+
searchValue = [v.tag for v in searchValue]
480+
_do_test_library_search(library, obj, field, operator, searchValue)
481+
482+
elif field.type == "date":
483+
searchValue = searchValue.strftime("%Y-%m-%d")
484+
_do_test_library_search(library, obj, field, operator, searchValue)
485+
searchValue = "1s"
486+
_do_test_library_search(library, obj, field, operator, searchValue)
487+
488+
489+
def _do_test_library_search(library, obj, field, operator, searchValue):
490+
searchFilter = {field.key + operator.key[:-1]: searchValue}
491+
results = library.search(libtype=obj.type, **searchFilter)
492+
493+
if operator.key.startswith("!") or operator.key.startswith(">>") and (searchValue == 0 or searchValue == '1s'):
494+
assert obj not in results
495+
else:
496+
assert obj in results

0 commit comments

Comments
 (0)