mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-10 14:14:19 +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.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '')
|
self.key = data.attrib.get('key', '')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'audio'
|
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
|
it only returns those items. By default we convert the xml elements
|
||||||
with the best guess PlexObjects based on tag and type attrs.
|
with the best guess PlexObjects based on tag and type attrs.
|
||||||
etag (str): Only fetch items with the specified tag.
|
etag (str): Only fetch items with the specified tag.
|
||||||
**kwargs (dict): Optionally add attribute filters on the items to fetch. For
|
**kwargs (dict): Optionally add XML attribute to filter the items.
|
||||||
example, passing in viewCount=0 will only return matching items. Filtering
|
See :func:`~plexapi.base.PlexObject.fetchItems` for more details
|
||||||
is done before the Python objects are built to help keep things speedy.
|
on how this is used.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
if ekey is None:
|
if ekey is None:
|
||||||
raise BadRequest('ekey was not provided')
|
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):
|
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
|
""" 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
|
and attrs.
|
||||||
on how this is used.
|
|
||||||
|
|
||||||
Parameters:
|
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_start (None, int): offset to get a subset of the data
|
||||||
container_size (None, int): How many items in 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 = {}
|
url_kw = {}
|
||||||
|
@ -204,7 +243,7 @@ class PlexObject(object):
|
||||||
data = self._server.query(ekey, params=url_kw)
|
data = self._server.query(ekey, params=url_kw)
|
||||||
items = self.findItems(data, cls, ekey, **kwargs)
|
items = self.findItems(data, cls, ekey, **kwargs)
|
||||||
|
|
||||||
librarySectionID = data.attrib.get('librarySectionID')
|
librarySectionID = utils.cast(int, data.attrib.get('librarySectionID'))
|
||||||
if librarySectionID:
|
if librarySectionID:
|
||||||
for item in items:
|
for item in items:
|
||||||
item.librarySectionID = librarySectionID
|
item.librarySectionID = librarySectionID
|
||||||
|
|
|
@ -59,7 +59,7 @@ class Collections(PlexPartialObject, ArtMixin, PosterMixin, LabelMixin):
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
||||||
self.labels = self.findItems(data, media.Label)
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.maxYear = utils.cast(int, data.attrib.get('maxYear'))
|
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.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '').replace('/children', '') # FIX_BUG_50
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'photo'
|
self.listType = 'photo'
|
||||||
|
@ -186,7 +186,7 @@ class Photo(PlexPartialObject, Playable, ArtUrlMixin, PosterUrlMixin, TagMixin):
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.index = utils.cast(int, data.attrib.get('index'))
|
self.index = utils.cast(int, data.attrib.get('index'))
|
||||||
self.key = data.attrib.get('key', '')
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'photo'
|
self.listType = 'photo'
|
||||||
|
|
|
@ -237,7 +237,7 @@ class Playlist(PlexPartialObject, Playable, ArtMixin, PosterMixin):
|
||||||
uri = uri + '&limit=%s' % str(limit)
|
uri = uri + '&limit=%s' % str(limit)
|
||||||
|
|
||||||
for category, value in kwargs.items():
|
for category, value in kwargs.items():
|
||||||
sectionChoices = section.listChoices(category)
|
sectionChoices = section.listFilterChoices(category)
|
||||||
for choice in sectionChoices:
|
for choice in sectionChoices:
|
||||||
if str(choice.title).lower() == str(value).lower():
|
if str(choice.title).lower() == str(value).lower():
|
||||||
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
uri = uri + '&%s=%s' % (category.lower(), str(choice.key))
|
||||||
|
|
|
@ -512,7 +512,7 @@ class PlexServer(PlexObject):
|
||||||
data = response.text.encode('utf8')
|
data = response.text.encode('utf8')
|
||||||
return ElementTree.fromstring(data) if data.strip() else None
|
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
|
""" 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/>`_
|
`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,
|
against all items in your Plex library. This searches genres, actors, directors,
|
||||||
|
@ -526,10 +526,11 @@ class PlexServer(PlexObject):
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
query (str): Query to use when searching your library.
|
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,
|
actor, album, artist, autotag, collection, director, episode, game, genre,
|
||||||
movie, photo, photoalbum, place, playlist, shared, show, tag, track
|
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 = []
|
results = []
|
||||||
params = {
|
params = {
|
||||||
|
@ -538,6 +539,8 @@ class PlexServer(PlexObject):
|
||||||
'includeExternalMedia': 1}
|
'includeExternalMedia': 1}
|
||||||
if limit:
|
if limit:
|
||||||
params['limit'] = limit
|
params['limit'] = limit
|
||||||
|
if sectionId:
|
||||||
|
params['sectionId'] = sectionId
|
||||||
key = '/hubs/search?%s' % urlencode(params)
|
key = '/hubs/search?%s' % urlencode(params)
|
||||||
for hub in self.fetchItems(key, Hub):
|
for hub in self.fetchItems(key, Hub):
|
||||||
if mediatype:
|
if mediatype:
|
||||||
|
|
|
@ -48,7 +48,7 @@ class Video(PlexPartialObject):
|
||||||
self.guid = data.attrib.get('guid')
|
self.guid = data.attrib.get('guid')
|
||||||
self.key = data.attrib.get('key', '')
|
self.key = data.attrib.get('key', '')
|
||||||
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt'))
|
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.librarySectionKey = data.attrib.get('librarySectionKey')
|
||||||
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
self.librarySectionTitle = data.attrib.get('librarySectionTitle')
|
||||||
self.listType = 'video'
|
self.listType = 'video'
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from datetime import datetime
|
from collections import namedtuple
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import pytest
|
import pytest
|
||||||
from plexapi.exceptions import NotFound
|
from plexapi.exceptions import BadRequest, NotFound
|
||||||
|
|
||||||
from . import conftest as utils
|
from . import conftest as utils
|
||||||
|
|
||||||
|
@ -25,13 +26,13 @@ def test_library_sectionByID_with_attrs(plex, movies):
|
||||||
assert movies.agent == "tv.plex.agents.movie"
|
assert movies.agent == "tv.plex.agents.movie"
|
||||||
# This seems to fail for some reason.
|
# This seems to fail for some reason.
|
||||||
# my account alloew of sync, didnt find any about settings about the library.
|
# 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 movies.art == "/:/resources/movie-fanart.jpg"
|
||||||
assert utils.is_metadata(
|
assert utils.is_metadata(
|
||||||
movies.composite, prefix="/library/sections/", contains="/composite/"
|
movies.composite, prefix="/library/sections/", contains="/composite/"
|
||||||
)
|
)
|
||||||
assert utils.is_datetime(movies.createdAt)
|
assert utils.is_datetime(movies.createdAt)
|
||||||
assert movies.filters == "1"
|
assert movies.filters is True
|
||||||
assert movies._initpath == "/library/sections"
|
assert movies._initpath == "/library/sections"
|
||||||
assert utils.is_int(movies.key)
|
assert utils.is_int(movies.key)
|
||||||
assert movies.language == "en-US"
|
assert movies.language == "en-US"
|
||||||
|
@ -47,8 +48,8 @@ def test_library_sectionByID_with_attrs(plex, movies):
|
||||||
assert len(movies.uuid) == 36
|
assert len(movies.uuid) == 36
|
||||||
|
|
||||||
|
|
||||||
def test_library_section_get_movie(plex):
|
def test_library_section_get_movie(movies):
|
||||||
assert plex.library.section("Movies").get("Sita Sings the Blues")
|
assert movies.get("Sita Sings the Blues")
|
||||||
|
|
||||||
|
|
||||||
def test_library_section_movies_all(movies):
|
def test_library_section_movies_all(movies):
|
||||||
|
@ -143,10 +144,6 @@ def test_library_MovieSection_update_path(movies):
|
||||||
movies.update(path=movies.locations[0])
|
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):
|
def test_library_MovieSection_refresh(movies, patched_http_call):
|
||||||
movies.refresh()
|
movies.refresh()
|
||||||
|
|
||||||
|
@ -173,6 +170,10 @@ def test_library_MovieSection_onDeck(movie, movies, tvshows, episode):
|
||||||
episode.markUnwatched()
|
episode.markUnwatched()
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_MovieSection_searchMovies(movies):
|
||||||
|
assert movies.searchMovies(title="Elephants Dream")
|
||||||
|
|
||||||
|
|
||||||
def test_library_MovieSection_recentlyAdded(movies):
|
def test_library_MovieSection_recentlyAdded(movies):
|
||||||
assert len(movies.recentlyAdded())
|
assert len(movies.recentlyAdded())
|
||||||
|
|
||||||
|
@ -185,10 +186,18 @@ def test_library_MovieSection_collections(movies, collection):
|
||||||
assert len(movies.collections())
|
assert len(movies.collections())
|
||||||
|
|
||||||
|
|
||||||
|
def test_library_ShowSection_all(tvshows):
|
||||||
|
assert len(tvshows.all(title__iexact="The 100"))
|
||||||
|
|
||||||
|
|
||||||
def test_library_ShowSection_searchShows(tvshows):
|
def test_library_ShowSection_searchShows(tvshows):
|
||||||
assert tvshows.searchShows(title="The 100")
|
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):
|
def test_library_ShowSection_searchEpisodes(tvshows):
|
||||||
assert tvshows.searchEpisodes(title="Winter Is Coming")
|
assert tvshows.searchEpisodes(title="Winter Is Coming")
|
||||||
|
|
||||||
|
@ -229,10 +238,10 @@ def test_library_PhotoSection_searchPhotos(photos, photoalbum):
|
||||||
assert len(photos.searchPhotos(title))
|
assert len(photos.searchPhotos(title))
|
||||||
|
|
||||||
|
|
||||||
def test_library_and_section_search_for_movie(plex):
|
def test_library_and_section_search_for_movie(plex, movies):
|
||||||
find = "16 blocks"
|
find = "Elephants Dream"
|
||||||
l_search = plex.library.search(find)
|
l_search = plex.library.search(find)
|
||||||
s_search = plex.library.section("Movies").search(find)
|
s_search = movies.search(find)
|
||||||
assert l_search == s_search
|
assert l_search == s_search
|
||||||
|
|
||||||
|
|
||||||
|
@ -244,12 +253,12 @@ def test_library_settings(movies):
|
||||||
def test_library_editAdvanced_default(movies):
|
def test_library_editAdvanced_default(movies):
|
||||||
movies.editAdvanced(hidden=2)
|
movies.editAdvanced(hidden=2)
|
||||||
for setting in movies.settings():
|
for setting in movies.settings():
|
||||||
if setting.id == 'hidden':
|
if setting.id == "hidden":
|
||||||
assert int(setting.value) == 2
|
assert int(setting.value) == 2
|
||||||
|
|
||||||
movies.editAdvanced(collectionMode=0)
|
movies.editAdvanced(collectionMode=0)
|
||||||
for setting in movies.settings():
|
for setting in movies.settings():
|
||||||
if setting.id == 'collectionMode':
|
if setting.id == "collectionMode":
|
||||||
assert int(setting.value) == 0
|
assert int(setting.value) == 0
|
||||||
|
|
||||||
movies.reload()
|
movies.reload()
|
||||||
|
@ -258,17 +267,16 @@ def test_library_editAdvanced_default(movies):
|
||||||
assert str(setting.value) == str(setting.default)
|
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"
|
ep_title = "Coup de Grâce"
|
||||||
result_root = plex.search(ep_title)
|
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_root
|
||||||
assert result_shows
|
assert result_shows
|
||||||
assert result_root == result_shows
|
assert result_root == result_shows
|
||||||
|
|
||||||
|
|
||||||
def test_crazy_search(plex, movie):
|
def test_crazy_search(plex, movies, movie):
|
||||||
movies = plex.library.section("Movies")
|
|
||||||
assert movie in movies.search(
|
assert movie in movies.search(
|
||||||
actor=movie.actors[0], sort="titleSort"
|
actor=movie.actors[0], sort="titleSort"
|
||||||
), "Unable to search movie by actor."
|
), "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
|
assert len(movies.search(container_start=2, container_size=1)) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_library_section_timeline(plex):
|
def test_library_section_timeline(plex, movies):
|
||||||
movies = plex.library.section("Movies")
|
|
||||||
tl = movies.timeline()
|
tl = movies.timeline()
|
||||||
assert tl.TAG == "LibraryTimeline"
|
assert tl.TAG == "LibraryTimeline"
|
||||||
assert tl.size > 0
|
assert tl.size > 0
|
||||||
|
@ -304,3 +311,174 @@ def test_library_section_timeline(plex):
|
||||||
assert utils.is_int(tl.updateQueueSize, gte=0)
|
assert utils.is_int(tl.updateQueueSize, gte=0)
|
||||||
assert tl.viewGroup == "secondary"
|
assert tl.viewGroup == "secondary"
|
||||||
assert tl.viewMode == 65592
|
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
|
cnt = 1
|
||||||
if entry["type"] == SEARCHTYPES["show"]:
|
if entry["type"] == SEARCHTYPES["show"]:
|
||||||
show = server.library.sectionByID(
|
show = server.library.sectionByID(
|
||||||
str(entry["sectionID"])
|
entry["sectionID"]
|
||||||
).get(entry["title"])
|
).get(entry["title"])
|
||||||
cnt = show.leafCount
|
cnt = show.leafCount
|
||||||
bar.update(cnt)
|
bar.update(cnt)
|
||||||
|
|
Loading…
Reference in a new issue