Skip to content

Commit 437eea8

Browse files
authored
Merge pull request #466 from pkkid/batchsearch
Make it easier to set a container size using search
2 parents b8135d5 + 1e77e76 commit 437eea8

File tree

3 files changed

+83
-18
lines changed

3 files changed

+83
-18
lines changed

plexapi/base.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,21 @@ def fetchItem(self, ekey, cls=None, **kwargs):
142142
clsname = cls.__name__ if cls else 'None'
143143
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
144144

145-
def fetchItems(self, ekey, cls=None, **kwargs):
145+
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
146146
""" Load the specified key to find and build all items with the specified tag
147147
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
148148
on how this is used.
149149
150-
Use container_start and container_size for pagination.
150+
Parameters:
151+
container_start (None, int): offset to get a subset of the data
152+
container_size (None, int): How many items in data
153+
151154
"""
152155
url_kw = {}
153-
for key, value in dict(kwargs).items():
154-
if key == "container_start":
155-
url_kw["X-Plex-Container-Start"] = kwargs.pop(key)
156-
if key == "container_size":
157-
url_kw["X-Plex-Container-Size"] = kwargs.pop(key)
156+
if container_start is not None:
157+
url_kw["X-Plex-Container-Start"] = container_start
158+
if container_size is not None:
159+
url_kw["X-Plex-Container-Size"] = container_size
158160

159161
if ekey is None:
160162
raise BadRequest('ekey was not provided')

plexapi/library.py

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,40 @@ def _loadData(self, data):
360360
# Private attrs as we dont want a reload.
361361
self._total_size = None
362362

363+
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
364+
""" Load the specified key to find and build all items with the specified tag
365+
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
366+
on how this is used.
367+
368+
Parameters:
369+
container_start (None, int): offset to get a subset of the data
370+
container_size (None, int): How many items in data
371+
372+
"""
373+
url_kw = {}
374+
if container_start is not None:
375+
url_kw["X-Plex-Container-Start"] = container_start
376+
if container_size is not None:
377+
url_kw["X-Plex-Container-Size"] = container_size
378+
379+
if ekey is None:
380+
raise BadRequest('ekey was not provided')
381+
data = self._server.query(ekey, params=url_kw)
382+
383+
if '/all' in ekey:
384+
# totalSize is only included in the xml response
385+
# if container size is used.
386+
total_size = data.attrib.get("totalSize") or data.attrib.get("size")
387+
self._total_size = utils.cast(int, total_size)
388+
389+
items = self.findItems(data, cls, ekey, **kwargs)
390+
391+
librarySectionID = data.attrib.get('librarySectionID')
392+
if librarySectionID:
393+
for item in items:
394+
item.librarySectionID = librarySectionID
395+
return items
396+
363397
@property
364398
def totalSize(self):
365399
if self._total_size is None:
@@ -505,9 +539,9 @@ def listChoices(self, category, libtype=None, **kwargs):
505539
key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
506540
return self.fetchItems(key, cls=FilterChoice)
507541

508-
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
509-
""" Search the library. If there are many results, they will be fetched from the server
510-
in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first <num>
542+
def search(self, title=None, sort=None, maxresults=None,
543+
libtype=None, container_start=0, container_size=X_PLEX_CONTAINER_SIZE, **kwargs):
544+
""" Search the library. The http requests will be batched in container_size. If you're only looking for the first <num>
511545
results, it would be wise to set the maxresults option to that amount so this functions
512546
doesn't iterate over all results on the server.
513547
@@ -518,6 +552,8 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg
518552
maxresults (int): Only return the specified number of results (optional).
519553
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
520554
album, track; optional).
555+
container_start (int): default 0
556+
container_size (int): default X_PLEX_CONTAINER_SIZE in your config file.
521557
**kwargs (dict): Any of the available filters for the current library section. Partial string
522558
matches allowed. Multiple matches OR together. Negative filtering also possible, just add an
523559
exclamation mark to the end of filter name, e.g. `resolution!=1x1`.
@@ -549,15 +585,37 @@ def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwarg
549585
args['sort'] = self._cleanSearchSort(sort)
550586
if libtype is not None:
551587
args['type'] = utils.searchType(libtype)
552-
# iterate over the results
553-
results, subresults = [], '_init'
554-
args['X-Plex-Container-Start'] = 0
555-
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
556-
while subresults and maxresults > len(results):
588+
589+
results = []
590+
subresults = []
591+
offset = container_start
592+
593+
if maxresults is not None:
594+
container_size = min(container_size, maxresults)
595+
while True:
557596
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
558-
subresults = self.fetchItems(key)
559-
results += subresults[:maxresults - len(results)]
560-
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
597+
subresults = self.fetchItems(key, container_start=container_start,
598+
container_size=container_size)
599+
if not len(subresults):
600+
if offset > self.totalSize:
601+
log.info("container_start is higher then the number of items in the library")
602+
break
603+
604+
results.extend(subresults)
605+
606+
# self.totalSize is not used as a condition in the while loop as
607+
# this require a additional http request.
608+
# self.totalSize is updated from .fetchItems
609+
wanted_number_of_items = self.totalSize - offset
610+
if maxresults is not None:
611+
wanted_number_of_items = min(maxresults, wanted_number_of_items)
612+
container_size = min(container_size, maxresults - len(results))
613+
614+
if wanted_number_of_items <= len(results):
615+
break
616+
617+
container_start += container_size
618+
561619
return results
562620

563621
def _cleanSearchFilter(self, category, value, libtype=None):

tests/test_library.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,8 @@ def test_crazy_search(plex, movie):
260260
), "Unable to search movie by year."
261261
assert movie not in movies.search(year=2007), "Unable to filter movie by year."
262262
assert movie in movies.search(actor=movie.actors[0].tag)
263+
assert len(movies.search(container_start=2, maxresults=1)) == 1
264+
assert len(movies.search(container_size=None)) == 4
265+
assert len(movies.search(container_size=1)) == 4
266+
assert len(movies.search(container_start=9999, container_size=1)) == 0
267+
assert len(movies.search(container_start=2, container_size=1)) == 2

0 commit comments

Comments
 (0)