core: Limit search to backends matching URI roots

Fixes #337
This commit is contained in:
Stein Magnus Jodal 2013-03-30 22:20:12 +01:00
parent c54db3298f
commit 2abce2af38
2 changed files with 96 additions and 23 deletions

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import defaultdict
import urlparse import urlparse
import pykka import pykka
@ -16,35 +17,60 @@ class LibraryController(object):
uri_scheme = urlparse.urlparse(uri).scheme uri_scheme = urlparse.urlparse(uri).scheme
return self.backends.with_library_by_uri_scheme.get(uri_scheme, None) return self.backends.with_library_by_uri_scheme.get(uri_scheme, None)
def find_exact(self, query=None, **kwargs): def _get_backends_to_uris(self, uris):
if uris:
backends_to_uris = defaultdict(list)
for uri in uris:
backend = self._get_backend(uri)
if backend is not None:
backends_to_uris[backend].append(uri)
else:
backends_to_uris = dict([
(b, None) for b in self.backends.with_library])
return backends_to_uris
def find_exact(self, query=None, uris=None, **kwargs):
""" """
Search the library for tracks where ``field`` is ``values``. Search the library for tracks where ``field`` is ``values``.
If the query is empty, and the backend can support it, all available If the query is empty, and the backend can support it, all available
tracks are returned. tracks are returned.
If ``uris`` is given, the search is limited to results from within the
URI roots. For example passing ``uris=['file:']`` will limit the search
to the local backend.
Examples:: Examples::
# Returns results matching 'a' # Returns results matching 'a' from any backend
find_exact({'any': ['a']}) find_exact({'any': ['a']})
find_exact(any=['a']) find_exact(any=['a'])
# Returns results matching artist 'xyz' # Returns results matching artist 'xyz' from any backend
find_exact({'artist': ['xyz']}) find_exact({'artist': ['xyz']})
find_exact(artist=['xyz']) find_exact(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz' # Returns results matching 'a' and 'b' and artist 'xyz' from any
# backend
find_exact({'any': ['a', 'b'], 'artist': ['xyz']}) find_exact({'any': ['a', 'b'], 'artist': ['xyz']})
find_exact(any=['a', 'b'], artist=['xyz']) find_exact(any=['a', 'b'], artist=['xyz'])
# Returns results matching 'a' if within the given URI roots
# "file:///media/music" and "spotify:"
find_exact(
{'any': ['a']}, uris=['file:///media/music', 'spotify:'])
find_exact(any=['a'], uris=['file:///media/music', 'spotify:'])
:param query: one or more queries to search for :param query: one or more queries to search for
:type query: dict :type query: dict
:param uris: zero or more URI roots to limit the search to
:type uris: list of strings or :class:`None`
:rtype: list of :class:`mopidy.models.SearchResult` :rtype: list of :class:`mopidy.models.SearchResult`
""" """
query = query or kwargs query = query or kwargs
futures = [ futures = [
b.library.find_exact(query=query) backend.library.find_exact(query=query, uris=uris)
for b in self.backends.with_library] for (backend, uris) in self._get_backends_to_uris(uris).items()]
return [result for result in pykka.get_all(futures) if result] return [result for result in pykka.get_all(futures) if result]
def lookup(self, uri): def lookup(self, uri):
@ -80,32 +106,45 @@ class LibraryController(object):
b.library.refresh(uri) for b in self.backends.with_library] b.library.refresh(uri) for b in self.backends.with_library]
pykka.get_all(futures) pykka.get_all(futures)
def search(self, query=None, **kwargs): def search(self, query=None, uris=None, **kwargs):
""" """
Search the library for tracks where ``field`` contains ``values``. Search the library for tracks where ``field`` contains ``values``.
If the query is empty, and the backend can support it, all available If the query is empty, and the backend can support it, all available
tracks are returned. tracks are returned.
If ``uris`` is given, the search is limited to results from within the
URI roots. For example passing ``uris=['file:']`` will limit the search
to the local backend.
Examples:: Examples::
# Returns results matching 'a' # Returns results matching 'a' in any backend
search({'any': ['a']}) search({'any': ['a']})
search(any=['a']) search(any=['a'])
# Returns results matching artist 'xyz' # Returns results matching artist 'xyz' in any backend
search({'artist': ['xyz']}) search({'artist': ['xyz']})
search(artist=['xyz']) search(artist=['xyz'])
# Returns results matching 'a' and 'b' and artist 'xyz' # Returns results matching 'a' and 'b' and artist 'xyz' in any
# backend
search({'any': ['a', 'b'], 'artist': ['xyz']}) search({'any': ['a', 'b'], 'artist': ['xyz']})
search(any=['a', 'b'], artist=['xyz']) search(any=['a', 'b'], artist=['xyz'])
# Returns results matching 'a' if within the given URI roots
# "file:///media/music" and "spotify:"
search({'any': ['a']}, uris=['file:///media/music', 'spotify:'])
search(any=['a'], uris=['file:///media/music', 'spotify:'])
:param query: one or more queries to search for :param query: one or more queries to search for
:type query: dict :type query: dict
:param uris: zero or more URI roots to limit the search to
:type uris: list of strings or :class:`None`
:rtype: list of :class:`mopidy.models.SearchResult` :rtype: list of :class:`mopidy.models.SearchResult`
""" """
query = query or kwargs query = query or kwargs
futures = [ futures = [
b.library.search(query=query) for b in self.backends.with_library] backend.library.search(query=query, uris=uris)
for (backend, uris) in self._get_backends_to_uris(uris).items()]
return [result for result in pykka.get_all(futures) if result] return [result for result in pykka.get_all(futures) if result]

View File

@ -88,9 +88,26 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.find_exact.assert_called_once_with( self.library1.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.find_exact.assert_called_once_with( self.library2.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
def test_find_exact_with_uris_selects_dummy1_backend(self):
self.core.library.find_exact(
any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy3:'])
self.library1.find_exact.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
self.assertFalse(self.library2.find_exact.called)
def test_find_exact_with_uris_selects_both_backends(self):
self.core.library.find_exact(
any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy2:'])
self.library1.find_exact.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
self.library2.find_exact.assert_called_once_with(
query=dict(any=['a']), uris=['dummy2:'])
def test_find_exact_filters_out_none(self): def test_find_exact_filters_out_none(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -106,9 +123,9 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertNotIn(None, result) self.assertNotIn(None, result)
self.library1.find_exact.assert_called_once_with( self.library1.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.find_exact.assert_called_once_with( self.library2.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
def test_find_accepts_query_dict_instead_of_kwargs(self): def test_find_accepts_query_dict_instead_of_kwargs(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -126,9 +143,9 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.find_exact.assert_called_once_with( self.library1.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.find_exact.assert_called_once_with( self.library2.find_exact.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
def test_search_combines_results_from_all_backends(self): def test_search_combines_results_from_all_backends(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -146,9 +163,26 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
def test_search_with_uris_selects_dummy1_backend(self):
self.core.library.search(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:'])
self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
self.assertFalse(self.library2.search.called)
def test_search_with_uris_selects_both_backends(self):
self.core.library.search(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy2:'])
self.library1.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo'])
self.library2.search.assert_called_once_with(
query=dict(any=['a']), uris=['dummy2:'])
def test_search_filters_out_none(self): def test_search_filters_out_none(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -164,9 +198,9 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertNotIn(None, result) self.assertNotIn(None, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
def test_search_accepts_query_dict_instead_of_kwargs(self): def test_search_accepts_query_dict_instead_of_kwargs(self):
track1 = Track(uri='dummy1:a') track1 = Track(uri='dummy1:a')
@ -184,6 +218,6 @@ class CoreLibraryTest(unittest.TestCase):
self.assertIn(result1, result) self.assertIn(result1, result)
self.assertIn(result2, result) self.assertIn(result2, result)
self.library1.search.assert_called_once_with( self.library1.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)
self.library2.search.assert_called_once_with( self.library2.search.assert_called_once_with(
query=dict(any=['a'])) query=dict(any=['a']), uris=None)