Cleanup search a bit; Get existing tests passing and add a few new search tests

This commit is contained in:
Michael Shepanski 2016-03-31 18:36:54 -04:00
parent b10faf8560
commit d63339bd24
3 changed files with 63 additions and 43 deletions

View file

@ -1,33 +1,10 @@
# -*- coding: utf-8 -*-
"""
PlexLibrary
# --- SEARCH ---
# type=1
# sort=column:[asc|desc]
# -> column in {addedAt,originallyAvailableAt,lastViewedAt,titleSort,rating,mediaHeight,duration}
# unwatched=1
# duplicate=1
# year=yyyy,yyyy,yyyy
# decade=<key>,<key>
# genre=<id>,<id>
# contentRating=<key>,<key>
# collection=<id>,<id>
# director=<id>,<id>
# actor=<id>,<id>
# studio=<key>,<key>
# resolution=720,480,sd ??
# X-Plex-Container-Start=0
# X-Plex-Container-Size=0
# --- CANNED ---
# /library/sections/1/onDeck
# /library/sections/1/recentlyViewed
# /library/sections/1/all?sort=addedAt:desc
"""
from plexapi import log, utils
from plexapi import X_PLEX_CONTAINER_SIZE
from plexapi.media import MediaTag
from plexapi.exceptions import BadRequest, NotFound
@ -83,7 +60,8 @@ class Library(object):
""" Searching within a library section is much more powerful. It seems certain attributes on
the media objects can be targeted to filter this search down a bit, but I havent found the
documentation for it. For example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu"
all work.
all work. Other items such as actor=<id> seem to work, but require you already know the id
of the actor.
"""
# TODO: FIGURE THIS OUT!
args = {}
@ -148,7 +126,7 @@ class LibrarySection(object):
args[category] = self._cleanSearchFilter(subcategory, value)
if libtype is not None: args['type'] = utils.searchType(libtype)
query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
return utils.listItems(self.server, query)
return utils.listItems(self.server, query, bytag=True)
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
""" Search the library. If there are many results, they will be fetched from the server
@ -205,26 +183,28 @@ class LibrarySection(object):
self.server.query('/library/sections/%s/refresh' % self.key)
def _cleanSearchFilter(self, category, value, libtype=None):
result = set()
# check a few things before we begin
if category not in self.ALLOWED_FILTERS:
raise BadRequest('Unknown filter category: %s' % category)
if category in self.BOOLEAN_FILTERS:
return '1' if value else '0'
if not isinstance(value, (list, tuple)):
value = [value]
# convert list of values to list of keys or ids
result = set()
choices = self.listChoices(category, libtype)
lookup = {c.title.lower():c.key for c in choices}
allowed = set(c.key for c in choices)
for dirtykey in value:
dirtykey = str(dirtykey).lower()
if dirtykey in allowed:
result.add(dirtykey); continue
if dirtykey in lookup:
result.add(lookup[dirtykey]); continue
for key in [k for t,k in lookup.items() if dirtykey in t]:
result.add(key)
if not result:
log.warning('No known filter values: %s; Will probably yield no results.' % ', '.join(value))
for item in value:
item = str(item.id if isinstance(item, MediaTag) else item).lower()
# find most logical choice(s) to use in url
if item in allowed: result.add(item); continue
if item in lookup: result.add(lookup[item]); continue
matches = [k for t,k in lookup.items() if item in t]
if matches: map(result.add, matches); continue
# nothing matched; use raw item value
log.warning('Filter value not listed, using raw item value: %s' % item)
result.add(item)
return ','.join(result)
def _cleanSearchSort(self, sort):
@ -282,7 +262,9 @@ class FilterChoice(object):
self.initpath = initpath
self.fastKey = data.attrib.get('fastKey')
self.key = data.attrib.get('key')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.type = data.attrib.get('type')
def __repr__(self):
title = self.title.replace(' ','.')[0:20]

View file

@ -47,7 +47,7 @@ class PlexPartialObject(object):
def __repr__(self):
title = self.title.replace(' ','.')[0:20]
return '<%s:%s:%s>' % (self.__class__.__name__, self.key, title.encode('utf8'))
return '<%s:%s:%s>' % (self.__class__.__name__, self.ratingKey, title.encode('utf8'))
def __getattr__(self, attr):
if self.isPartialObject():
@ -144,8 +144,8 @@ class PlexPartialObject(object):
self._loadData(data[0])
def buildItem(server, elem, initpath):
libtype = elem.attrib.get('type') or elem.tag
def buildItem(server, elem, initpath, bytag=False):
libtype = elem.tag if bytag else elem.attrib.get('type')
if libtype in LIBRARY_TYPES:
cls = LIBRARY_TYPES[libtype]
return cls(server, elem, initpath)
@ -182,6 +182,14 @@ def findItem(server, path, title):
raise NotFound('Unable to find item: %s' % title)
def isInt(string):
try:
int(string)
return True
except ValueError:
return False
def joinArgs(args):
if not args: return ''
arglist = []
@ -195,14 +203,14 @@ def listChoices(server, path):
return {c.attrib['title']:c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None):
def listItems(server, path, libtype=None, watched=None, bytag=False):
items = []
for elem in server.query(path):
if libtype and elem.attrib.get('type') != libtype: continue
if watched is True and elem.attrib.get('viewCount', 0) == 0: continue
if watched is False and elem.attrib.get('viewCount', 0) >= 1: continue
try:
items.append(buildItem(server, elem, path))
items.append(buildItem(server, elem, path, bytag))
except UnknownType:
pass
return items

View file

@ -109,10 +109,40 @@ def test_search_audio(plex, user=None):
assert result_server == result_library == result_music, 'Audio searches not consistent.'
@register('search,audio')
def test_search_related(plex, user=None):
movies = plex.library.section(MOVIE_SECTION)
movie = movies.get(MOVIE_TITLE)
related_by_actors = movies.search(actor=movie.actors, maxresults=3)
log(2, u'Actors: %s..' % movie.actors)
log(2, u'Related by Actors: %s..' % related_by_actors)
assert related_by_actors, 'No related movies found by actor.'
related_by_genre = movies.search(genre=movie.genres, maxresults=3)
log(2, u'Genres: %s..' % movie.genres)
log(2, u'Related by Genre: %s..' % related_by_genre)
assert related_by_genre, 'No related movies found by genre.'
related_by_director = movies.search(director=movie.directors, maxresults=3)
log(2, 'Directors: %s..' % movie.directors)
log(2, 'Related by Director: %s..' % related_by_director)
assert related_by_director, 'No related movies found by director.'
@register('search,show')
def test_crazy_search(plex, user=None):
movies = plex.library.section(MOVIE_SECTION)
print(movies.search(duplicate=True))
movie = movies.get('Jurassic World')
log(2, u'Search by Actor: "Chris Pratt"')
assert movie in movies.search(actor='Chris Pratt'), 'Unable to search movie by actor.'
log(2, u'Search by Director: ["Trevorrow"]')
assert movie in movies.search(director=['Trevorrow']), 'Unable to search movie by director.'
log(2, u'Search by Year: ["2014", "2015"]')
assert movie in movies.search(year=['2014', '2015']), 'Unable to search movie by year.'
log(2, u'Filter by Year: 2014')
assert movie not in movies.search(year=2014), 'Unable to filter movie by year.'
judy = [a for a in movie.actors if 'Judy' in a.tag][0]
log(2, u'Search by Unpopular Actor: %s' % judy)
assert movie in movies.search(actor=judy.id), 'Unable to filter movie by year.'
#-----------------------