diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 26216846..50d75144 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from collections import defaultdict import urlparse import pykka @@ -16,35 +17,60 @@ class LibraryController(object): uri_scheme = urlparse.urlparse(uri).scheme 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``. If the query is empty, and the backend can support it, all available 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:: - # Returns results matching 'a' + # Returns results matching 'a' from any backend 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']) - # 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']) + # 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 :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` """ query = query or kwargs futures = [ - b.library.find_exact(query=query) - for b in self.backends.with_library] + backend.library.find_exact(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] def lookup(self, uri): @@ -80,32 +106,45 @@ class LibraryController(object): b.library.refresh(uri) for b in self.backends.with_library] 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``. If the query is empty, and the backend can support it, all available 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:: - # Returns results matching 'a' + # Returns results matching 'a' in any backend 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']) - # 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']) + # 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 :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` """ query = query or kwargs 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] diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 603a8109..6e9d240a 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -88,9 +88,26 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) 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( - 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): track1 = Track(uri='dummy1:a') @@ -106,9 +123,9 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertNotIn(None, result) 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( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None) def test_find_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') @@ -126,9 +143,9 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) 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( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None) def test_search_combines_results_from_all_backends(self): track1 = Track(uri='dummy1:a') @@ -146,9 +163,26 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) self.library1.search.assert_called_once_with( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None) 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): track1 = Track(uri='dummy1:a') @@ -164,9 +198,9 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertNotIn(None, result) self.library1.search.assert_called_once_with( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None) 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): track1 = Track(uri='dummy1:a') @@ -184,6 +218,6 @@ class CoreLibraryTest(unittest.TestCase): self.assertIn(result1, result) self.assertIn(result2, result) self.library1.search.assert_called_once_with( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None) self.library2.search.assert_called_once_with( - query=dict(any=['a'])) + query=dict(any=['a']), uris=None)