mirror of
https://github.com/pkkid/python-plexapi
synced 2024-09-20 22:01:57 +00:00
Merge pull request #693 from JonnyWong16/feature/library_search
Fix and update library searching
This commit is contained in:
commit
1beee642fb
13 changed files with 958 additions and 306 deletions
BIN
docs/_static/images/LibrarySection.listFilters.png
vendored
Normal file
BIN
docs/_static/images/LibrarySection.listFilters.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
BIN
docs/_static/images/LibrarySection.listSorts.png
vendored
Normal file
BIN
docs/_static/images/LibrarySection.listSorts.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 KiB |
BIN
docs/_static/images/LibrarySection.search.png
vendored
Normal file
BIN
docs/_static/images/LibrarySection.search.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 KiB |
|
@ -52,7 +52,7 @@ class Audio(PlexPartialObject):
|
|||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'audio'
|
||||
|
|
101
plexapi/base.py
101
plexapi/base.py
|
@ -144,34 +144,9 @@ class PlexObject(object):
|
|||
it only returns those items. By default we convert the xml elements
|
||||
with the best guess PlexObjects based on tag and type attrs.
|
||||
etag (str): Only fetch items with the specified tag.
|
||||
**kwargs (dict): Optionally add attribute filters on the items to fetch. For
|
||||
example, passing in viewCount=0 will only return matching items. Filtering
|
||||
is done before the Python objects are built to help keep things speedy.
|
||||
Note: Because some attribute names are already used as arguments to this
|
||||
function, such as 'tag', you may still reference the attr tag byappending
|
||||
an underscore. For example, passing in _tag='foobar' will return all items
|
||||
where tag='foobar'. Also Note: Case very much matters when specifying kwargs
|
||||
-- Optionally, operators can be specified by append it
|
||||
to the end of the attribute name for more complex lookups. For example,
|
||||
passing in viewCount__gte=0 will return all items where viewCount >= 0.
|
||||
Available operations include:
|
||||
|
||||
* __contains: Value contains specified arg.
|
||||
* __endswith: Value ends with specified arg.
|
||||
* __exact: Value matches specified arg.
|
||||
* __exists (bool): Value is or is not present in the attrs.
|
||||
* __gt: Value is greater than specified arg.
|
||||
* __gte: Value is greater than or equal to specified arg.
|
||||
* __icontains: Case insensative value contains specified arg.
|
||||
* __iendswith: Case insensative value ends with specified arg.
|
||||
* __iexact: Case insensative value matches specified arg.
|
||||
* __in: Value is in a specified list or tuple.
|
||||
* __iregex: Case insensative value matches the specified regular expression.
|
||||
* __istartswith: Case insensative value starts with specified arg.
|
||||
* __lt: Value is less than specified arg.
|
||||
* __lte: Value is less than or equal to specified arg.
|
||||
* __regex: Value matches the specified regular expression.
|
||||
* __startswith: Value starts with specified arg.
|
||||
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||
See :func:`~plexapi.base.PlexObject.fetchItems` for more details
|
||||
on how this is used.
|
||||
"""
|
||||
if ekey is None:
|
||||
raise BadRequest('ekey was not provided')
|
||||
|
@ -185,12 +160,76 @@ class PlexObject(object):
|
|||
|
||||
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
|
||||
""" Load the specified key to find and build all items with the specified tag
|
||||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
on how this is used.
|
||||
and attrs.
|
||||
|
||||
Parameters:
|
||||
ekey (str): API URL path in Plex to fetch items from.
|
||||
cls (:class:`~plexapi.base.PlexObject`): If you know the class of the
|
||||
items to be fetched, passing this in will help the parser ensure
|
||||
it only returns those items. By default we convert the xml elements
|
||||
with the best guess PlexObjects based on tag and type attrs.
|
||||
etag (str): Only fetch items with the specified tag.
|
||||
container_start (None, int): offset to get a subset of the data
|
||||
container_size (None, int): How many items in data
|
||||
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||
See the details below for more info.
|
||||
|
||||
**Filtering XML Attributes**
|
||||
|
||||
Any XML attribute can be filtered when fetching results. Filtering is done before
|
||||
the Python objects are built to help keep things speedy. For example, passing in
|
||||
``viewCount=0`` will only return matching items where the view count is ``0``.
|
||||
Note that case matters when specifying attributes. Attributes futher down in the XML
|
||||
tree can be filtered by *prepending* the attribute with each element tag ``Tag__``.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fetchItem(ekey, viewCount=0)
|
||||
fetchItem(ekey, contentRating="PG")
|
||||
fetchItem(ekey, Genre__tag="Animation")
|
||||
fetchItem(ekey, Media__videoCodec="h265")
|
||||
fetchItem(ekey, Media__Part__container="mp4)
|
||||
|
||||
Note that because some attribute names are already used as arguments to this
|
||||
function, such as ``tag``, you may still reference the attr tag by prepending an
|
||||
underscore. For example, passing in ``_tag='foobar'`` will return all items where
|
||||
``tag='foobar'``.
|
||||
|
||||
**Using PlexAPI Operators**
|
||||
|
||||
Optionally, PlexAPI operators can be specified by *appending* it to the end of the
|
||||
attribute for more complex lookups. For example, passing in ``viewCount__gte=0``
|
||||
will return all items where ``viewCount >= 0``.
|
||||
|
||||
List of Available Operators:
|
||||
|
||||
* ``__contains``: Value contains specified arg.
|
||||
* ``__endswith``: Value ends with specified arg.
|
||||
* ``__exact``: Value matches specified arg.
|
||||
* ``__exists`` (*bool*): Value is or is not present in the attrs.
|
||||
* ``__gt``: Value is greater than specified arg.
|
||||
* ``__gte``: Value is greater than or equal to specified arg.
|
||||
* ``__icontains``: Case insensative value contains specified arg.
|
||||
* ``__iendswith``: Case insensative value ends with specified arg.
|
||||
* ``__iexact``: Case insensative value matches specified arg.
|
||||
* ``__in``: Value is in a specified list or tuple.
|
||||
* ``__iregex``: Case insensative value matches the specified regular expression.
|
||||
* ``__istartswith``: Case insensative value starts with specified arg.
|
||||
* ``__lt``: Value is less than specified arg.
|
||||
* ``__lte``: Value is less than or equal to specified arg.
|
||||
* ``__regex``: Value matches the specified regular expression.
|
||||
* ``__startswith``: Value starts with specified arg.
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fetchItem(ekey, viewCount__gte=0)
|
||||
fetchItem(ekey, Media__container__in=["mp4", "mkv"])
|
||||
fetchItem(ekey, guid__iregex=r"(imdb:\/\/|themoviedb:\/\/)")
|
||||
fetchItem(ekey, Media__Part__file__startswith="D:\\Movies")
|
||||
|
||||
"""
|
||||
url_kw = {}
|
||||
|
@ -204,7 +243,7 @@ class PlexObject(object):
|
|||
data = self._server.query(ekey, params=url_kw)
|
||||
items = self.findItems(data, cls, ekey, **kwargs)
|
||||
|
||||
librarySectionID = data.attrib.get('librarySectionID')
|
||||
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
if librarySectionID:
|
||||
for item in items:
|
||||
item.librarySectionID = librarySectionID
|
||||
|
|
|
@ -59,7 +59,7 @@ class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
|
|||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.labels = self.findItems(data, media.Label)
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -46,7 +46,7 @@ class Photoalbum(PlexPartialObject, ArtMixin, PosterMixin):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'photo'
|
||||
|
@ -186,7 +186,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.index = utils.cast(int, data.attrib.get('index'))
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'photo'
|
||||
|
|
|
@ -237,7 +237,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
|||
uri = uri + '&limit=%s' % str(limit)
|
||||
|
||||
for category, value in kwargs.items():
|
||||
sectionChoices = section.listChoices(category)
|
||||
sectionChoices = section.listFilterChoices(category)
|
||||
for choice in sectionChoices:
|
||||
if str(choice.title).lower() == str(value).lower():
|
||||
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
||||
|
|
|
@ -512,7 +512,7 @@ class PlexServer(PlexObject):
|
|||
data = response.text.encode('utf8')
|
||||
return ElementTree.fromstring(data) if data.strip() else None
|
||||
|
||||
def search(self, query, mediatype=None, limit=None):
|
||||
def search(self, query, mediatype=None, limit=None, sectionId=None):
|
||||
""" Returns a list of media items or filter categories from the resulting
|
||||
`Hub Search <https://www.plex.tv/blog/seek-plex-shall-find-leveling-web-app/>`_
|
||||
against all items in your Plex library. This searches genres, actors, directors,
|
||||
|
@ -526,10 +526,11 @@ class PlexServer(PlexObject):
|
|||
|
||||
Parameters:
|
||||
query (str): Query to use when searching your library.
|
||||
mediatype (str): Optionally limit your search to the specified media type.
|
||||
mediatype (str, optional): Limit your search to the specified media type.
|
||||
actor, album, artist, autotag, collection, director, episode, game, genre,
|
||||
movie, photo, photoalbum, place, playlist, shared, show, tag, track
|
||||
limit (int): Optionally limit to the specified number of results per Hub.
|
||||
limit (int, optional): Limit to the specified number of results per Hub.
|
||||
sectionId (int, optional): The section ID (key) of the library to search within.
|
||||
"""
|
||||
results = []
|
||||
params = {
|
||||
|
@ -538,6 +539,8 @@ class PlexServer(PlexObject):
|
|||
'includeExternalMedia': 1}
|
||||
if limit:
|
||||
params['limit'] = limit
|
||||
if sectionId:
|
||||
params['sectionId'] = sectionId
|
||||
key = '/hubs/search?%s' % urlencode(params)
|
||||
for hub in self.fetchItems(key, Hub):
|
||||
if mediatype:
|
||||
|
|
|
@ -48,7 +48,7 @@ class Video(PlexPartialObject):
|
|||
self.guid = data.attrib.get('guid')
|
||||
self.key = data.attrib.get('key', '')
|
||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
||||
self.librarySectionID = data.attrib.get('librarySectionID')
|
||||
self.librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||
self.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||
self.listType = 'video'
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
import pytest
|
||||
from plexapi.exceptions import NotFound
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
from . import conftest as utils
|
||||
|
||||
|
@ -25,13 +26,13 @@ def test_library_sectionByID_with_attrs(plex, movies):
|
|||
assert movies.agent == "tv.plex.agents.movie"
|
||||
# This seems to fail for some reason.
|
||||
# my account alloew of sync, didnt find any about settings about the library.
|
||||
# assert movies.allowSync is ('sync' in plex.ownerFeatures)
|
||||
# assert movies.allowSync is ("sync" in plex.ownerFeatures)
|
||||
assert movies.art == "/:/resources/movie-fanart.jpg"
|
||||
assert utils.is_metadata(
|
||||
movies.composite, prefix="/library/sections/", contains="/composite/"
|
||||
)
|
||||
assert utils.is_datetime(movies.createdAt)
|
||||
assert movies.filters == "1"
|
||||
assert movies.filters is True
|
||||
assert movies._initpath == "/library/sections"
|
||||
assert utils.is_int(movies.key)
|
||||
assert movies.language == "en-US"
|
||||
|
@ -47,8 +48,8 @@ def test_library_sectionByID_with_attrs(plex, movies):
|
|||
assert len(movies.uuid) == 36
|
||||
|
||||
|
||||
def test_library_section_get_movie(plex):
|
||||
assert plex.library.section("Movies").get("Sita Sings the Blues")
|
||||
def test_library_section_get_movie(movies):
|
||||
assert movies.get("Sita Sings the Blues")
|
||||
|
||||
|
||||
def test_library_section_movies_all(movies):
|
||||
|
@ -143,10 +144,6 @@ def test_library_MovieSection_update_path(movies):
|
|||
movies.update(path=movies.locations[0])
|
||||
|
||||
|
||||
def test_library_ShowSection_all(tvshows):
|
||||
assert len(tvshows.all(title__iexact="The 100"))
|
||||
|
||||
|
||||
def test_library_MovieSection_refresh(movies, patched_http_call):
|
||||
movies.refresh()
|
||||
|
||||
|
@ -173,6 +170,10 @@ def test_library_MovieSection_onDeck(movie, movies, tvshows, episode):
|
|||
episode.markUnwatched()
|
||||
|
||||
|
||||
def test_library_MovieSection_searchMovies(movies):
|
||||
assert movies.searchMovies(title="Elephants Dream")
|
||||
|
||||
|
||||
def test_library_MovieSection_recentlyAdded(movies):
|
||||
assert len(movies.recentlyAdded())
|
||||
|
||||
|
@ -185,10 +186,18 @@ def test_library_MovieSection_collections(movies, collection):
|
|||
assert len(movies.collections())
|
||||
|
||||
|
||||
def test_library_ShowSection_all(tvshows):
|
||||
assert len(tvshows.all(title__iexact="The 100"))
|
||||
|
||||
|
||||
def test_library_ShowSection_searchShows(tvshows):
|
||||
assert tvshows.searchShows(title="The 100")
|
||||
|
||||
|
||||
def test_library_ShowSection_searchSseasons(tvshows):
|
||||
assert tvshows.searchSeasons(**{"show.title": "The 100"})
|
||||
|
||||
|
||||
def test_library_ShowSection_searchEpisodes(tvshows):
|
||||
assert tvshows.searchEpisodes(title="Winter Is Coming")
|
||||
|
||||
|
@ -229,10 +238,10 @@ def test_library_PhotoSection_searchPhotos(photos, photoalbum):
|
|||
assert len(photos.searchPhotos(title))
|
||||
|
||||
|
||||
def test_library_and_section_search_for_movie(plex):
|
||||
find = "16 blocks"
|
||||
def test_library_and_section_search_for_movie(plex, movies):
|
||||
find = "Elephants Dream"
|
||||
l_search = plex.library.search(find)
|
||||
s_search = plex.library.section("Movies").search(find)
|
||||
s_search = movies.search(find)
|
||||
assert l_search == s_search
|
||||
|
||||
|
||||
|
@ -244,12 +253,12 @@ def test_library_settings(movies):
|
|||
def test_library_editAdvanced_default(movies):
|
||||
movies.editAdvanced(hidden=2)
|
||||
for setting in movies.settings():
|
||||
if setting.id == 'hidden':
|
||||
if setting.id == "hidden":
|
||||
assert int(setting.value) == 2
|
||||
|
||||
movies.editAdvanced(collectionMode=0)
|
||||
for setting in movies.settings():
|
||||
if setting.id == 'collectionMode':
|
||||
if setting.id == "collectionMode":
|
||||
assert int(setting.value) == 0
|
||||
|
||||
movies.reload()
|
||||
|
@ -258,17 +267,16 @@ def test_library_editAdvanced_default(movies):
|
|||
assert str(setting.value) == str(setting.default)
|
||||
|
||||
|
||||
def test_search_with_weird_a(plex):
|
||||
def test_search_with_weird_a(plex, tvshows):
|
||||
ep_title = "Coup de Grâce"
|
||||
result_root = plex.search(ep_title)
|
||||
result_shows = plex.library.section("TV Shows").searchEpisodes(title=ep_title)
|
||||
result_shows = tvshows.searchEpisodes(title=ep_title)
|
||||
assert result_root
|
||||
assert result_shows
|
||||
assert result_root == result_shows
|
||||
|
||||
|
||||
def test_crazy_search(plex, movie):
|
||||
movies = plex.library.section("Movies")
|
||||
def test_crazy_search(plex, movies, movie):
|
||||
assert movie in movies.search(
|
||||
actor=movie.actors[0], sort="titleSort"
|
||||
), "Unable to search movie by actor."
|
||||
|
@ -287,8 +295,7 @@ def test_crazy_search(plex, movie):
|
|||
assert len(movies.search(container_start=2, container_size=1)) == 2
|
||||
|
||||
|
||||
def test_library_section_timeline(plex):
|
||||
movies = plex.library.section("Movies")
|
||||
def test_library_section_timeline(plex, movies):
|
||||
tl = movies.timeline()
|
||||
assert tl.TAG == "LibraryTimeline"
|
||||
assert tl.size > 0
|
||||
|
@ -304,3 +311,174 @@ def test_library_section_timeline(plex):
|
|||
assert utils.is_int(tl.updateQueueSize, gte=0)
|
||||
assert tl.viewGroup == "secondary"
|
||||
assert tl.viewMode == 65592
|
||||
|
||||
|
||||
def test_library_MovieSection_hubSearch(movies):
|
||||
assert movies.hubSearch("Elephants Dream")
|
||||
|
||||
|
||||
def test_library_MovieSection_search(movies, movie):
|
||||
movie.addLabel("test_search")
|
||||
movie.addCollection("test_search")
|
||||
_test_library_search(movies, movie)
|
||||
movie.removeLabel("test_search", locked=False)
|
||||
movie.removeCollection("test_search", locked=False)
|
||||
|
||||
|
||||
def test_library_ShowSection_search(tvshows, show):
|
||||
show.addLabel("test_search")
|
||||
show.addCollection("test_search")
|
||||
_test_library_search(tvshows, show)
|
||||
show.removeLabel("test_search", locked=False)
|
||||
show.removeCollection("test_search", locked=False)
|
||||
|
||||
season = show.season(season=1)
|
||||
_test_library_search(tvshows, season)
|
||||
|
||||
episode = season.episode(episode=1)
|
||||
_test_library_search(tvshows, episode)
|
||||
|
||||
# Additional test for mapping field to the correct libtype
|
||||
assert tvshows.search(unwatched=True) # equal to episode.unwatched=True
|
||||
|
||||
|
||||
def test_library_MusicSection_search(music, artist):
|
||||
artist.addGenre("test_search")
|
||||
artist.addStyle("test_search")
|
||||
artist.addMood("test_search")
|
||||
artist.addCollection("test_search")
|
||||
_test_library_search(music, artist)
|
||||
artist.removeGenre("test_search", locked=False)
|
||||
artist.removeStyle("test_search", locked=False)
|
||||
artist.removeMood("test_search", locked=False)
|
||||
artist.removeCollection("test_search", locked=False)
|
||||
|
||||
album = artist.album("Layers")
|
||||
album.addGenre("test_search")
|
||||
album.addStyle("test_search")
|
||||
album.addMood("test_search")
|
||||
album.addCollection("test_search")
|
||||
album.addLabel("test_search")
|
||||
_test_library_search(music, album)
|
||||
album.removeGenre("test_search", locked=False)
|
||||
album.removeStyle("test_search", locked=False)
|
||||
album.removeMood("test_search", locked=False)
|
||||
album.removeCollection("test_search", locked=False)
|
||||
album.removeLabel("test_search", locked=False)
|
||||
|
||||
track = album.track(track=1)
|
||||
track.addMood("test_search")
|
||||
_test_library_search(music, track)
|
||||
track.removeMood("test_search", locked=False)
|
||||
|
||||
|
||||
def test_library_PhotoSection_search(photos, photoalbum):
|
||||
photo = photoalbum.photo("photo1")
|
||||
photo.addTag("test_search")
|
||||
_test_library_search(photos, photo)
|
||||
photo.removeTag("test_search")
|
||||
|
||||
|
||||
def test_library_MovieSection_search_sort(movies):
|
||||
results = movies.search(sort="titleSort")
|
||||
titleSort = [r.titleSort for r in results]
|
||||
assert titleSort == sorted(titleSort)
|
||||
results_asc = movies.search(sort="titleSort:asc")
|
||||
titleSort_asc = [r.titleSort for r in results_asc]
|
||||
assert titleSort == titleSort_asc
|
||||
results_desc = movies.search(sort="titleSort:desc")
|
||||
titleSort_desc = [r.titleSort for r in results_desc]
|
||||
assert titleSort_desc == sorted(titleSort_desc, reverse=True)
|
||||
|
||||
|
||||
def test_library_search_exceptions(movies):
|
||||
with pytest.raises(BadRequest):
|
||||
movies.listFilterChoices(field="123abc.title")
|
||||
with pytest.raises(BadRequest):
|
||||
movies.search(**{"123abc": True})
|
||||
with pytest.raises(BadRequest):
|
||||
movies.search(year="123abc")
|
||||
with pytest.raises(BadRequest):
|
||||
movies.search(sort="123abc")
|
||||
with pytest.raises(NotFound):
|
||||
movies.getFilterType(libtype='show')
|
||||
with pytest.raises(NotFound):
|
||||
movies.getFieldType(fieldType="unknown")
|
||||
with pytest.raises(NotFound):
|
||||
movies.listFilterChoices(field="unknown")
|
||||
with pytest.raises(NotFound):
|
||||
movies.search(unknown="unknown")
|
||||
with pytest.raises(NotFound):
|
||||
movies.search(**{"title<>!=": "unknown"})
|
||||
with pytest.raises(NotFound):
|
||||
movies.search(sort="unknown")
|
||||
with pytest.raises(NotFound):
|
||||
movies.search(sort="titleSort:bad")
|
||||
|
||||
|
||||
def _test_library_search(library, obj):
|
||||
# Create & operator
|
||||
AndOperator = namedtuple('AndOperator', ['key', 'title'])
|
||||
andOp = AndOperator('&=', 'and')
|
||||
|
||||
fields = library.listFields(obj.type)
|
||||
for field in fields:
|
||||
fieldAttr = field.key.split(".")[-1]
|
||||
operators = library.listOperators(field.type)
|
||||
if field.type in {'tag', 'string'}:
|
||||
operators += [andOp]
|
||||
|
||||
for operator in operators:
|
||||
if fieldAttr == "unmatched" and operator.key == "!=" or fieldAttr == 'userRating':
|
||||
continue
|
||||
|
||||
value = getattr(obj, fieldAttr, None)
|
||||
|
||||
if field.type == "boolean" and value is None:
|
||||
value = fieldAttr.startswith("unwatched")
|
||||
if field.type == "tag" and isinstance(value, list) and value and operator.title != 'and':
|
||||
value = value[0]
|
||||
elif value is None:
|
||||
continue
|
||||
|
||||
if operator.title == "begins with":
|
||||
searchValue = value[:3]
|
||||
elif operator.title == "ends with":
|
||||
searchValue = value[-3:]
|
||||
elif "contain" in operator.title:
|
||||
searchValue = value.split(" ")[0]
|
||||
elif operator.title == "is less than":
|
||||
searchValue = value + 1
|
||||
elif operator.title == "is greater than":
|
||||
searchValue = max(value - 1, 0)
|
||||
elif operator.title == "is before":
|
||||
searchValue = value + timedelta(days=1)
|
||||
elif operator.title == "is after":
|
||||
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
|
||||
|
||||
# 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")
|
||||
|
||||
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
|
||||
|
|
|
@ -295,7 +295,7 @@ def create_section(server, section, opts):
|
|||
cnt = 1
|
||||
if entry["type"] == SEARCHTYPES["show"]:
|
||||
show = server.library.sectionByID(
|
||||
str(entry["sectionID"])
|
||||
entry["sectionID"]
|
||||
).get(entry["title"])
|
||||
cnt = show.leafCount
|
||||
bar.update(cnt)
|
||||
|
|
Loading…
Reference in a new issue