diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 2e73e0db..d1eb430a 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -5,6 +5,8 @@ import urlparse import pykka +from mopidy.models import Ref + class LibraryController(object): pykka_traversable = True @@ -29,6 +31,63 @@ class LibraryController(object): (b, None) for b in self.backends.with_library.values()]) return backends_to_uris + def browse(self, path): + """ + Browse directories and tracks at the given ``path``. + + ``path`` is a string that always starts with "/". It points to a + directory in Mopidy's virtual file system. + + Returns a list of :class:`mopidy.models.Ref` objects for the + directories and tracks at the given ``path``. + + The :class:`~mopidy.models.Ref` objects representing tracks keeps the + track's original URI. A matching pair of objects can look like this:: + + Track(uri='dummy:/foo.mp3', name='foo', artists=..., album=...) + Ref(uri='dummy:/foo.mp3', name='foo', type='track') + + The :class:`~mopidy.models.Ref` objects representing directories has + plain paths, not including any URI schema. For example, the dummy + library's ``/bar`` directory is returned like this:: + + Ref(uri='/dummy/bar', name='bar', type='directory') + + Note to backend implementors: The ``/dummy`` part of the URI is added + by Mopidy core, not the individual backends. + + :param path: path to browse + :type path: string + :rtype: list of :class:`mopidy.models.Ref` + """ + if not path.startswith('/'): + return [] + if path == '/': + library_names = [ + backend.library.name.get() + for backend in self.backends.with_library.values() + if backend.library.browse('/').get()] + return [ + Ref(uri='/%s' % name, name=name, type='directory') + for name in library_names] + uri_scheme = path.split('/', 2)[1] + backend = self.backends.with_library.get(uri_scheme, None) + if backend: + backend_path = path.replace('/%s' % uri_scheme, '') + if not backend_path.startswith('/'): + backend_path = '/%s' % backend_path + refs = backend.library.browse(backend_path).get() + result = [] + for ref in refs: + if ref.type == 'directory': + result.append( + ref.copy(uri='/%s%s' % (uri_scheme, ref.uri))) + else: + result.append(ref) + return result + else: + return [] + def find_exact(self, query=None, uris=None, **kwargs): """ Search the library for tracks where ``field`` is ``values``. diff --git a/tests/core/library_test.py b/tests/core/library_test.py index f4028d2f..3734af41 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -5,7 +5,7 @@ import unittest from mopidy.backends import base from mopidy.core import Core -from mopidy.models import SearchResult, Track +from mopidy.models import Ref, SearchResult, Track class CoreLibraryTest(unittest.TestCase): @@ -13,11 +13,13 @@ class CoreLibraryTest(unittest.TestCase): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.library1 = mock.Mock(spec=base.BaseLibraryProvider) + self.library1.name.get.return_value = 'dummy1' self.backend1.library = self.library1 self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.library2 = mock.Mock(spec=base.BaseLibraryProvider) + self.library2.name.get.return_value = 'dummy2' self.backend2.library = self.library2 # A backend without the optional library provider @@ -28,6 +30,72 @@ class CoreLibraryTest(unittest.TestCase): self.core = Core(audio=None, backends=[ self.backend1, self.backend2, self.backend3]) + def test_browse_root_returns_dir_ref_for_each_library_with_content(self): + result1 = [ + Ref(uri='/foo/bar', name='bar', type='directory'), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + ] + self.library1.browse().get.return_value = result1 + self.library1.browse.reset_mock() + self.library2.browse().get.return_value = [] + self.library2.browse.reset_mock() + + result = self.core.library.browse('/') + + self.assertEqual(result, [ + Ref(uri='/dummy1', name='dummy1', type='directory'), + ]) + self.assertTrue(self.library1.browse.called) + self.assertTrue(self.library2.browse.called) + self.assertFalse(self.backend3.library.browse.called) + + def test_browse_empty_string_returns_nothing(self): + result = self.core.library.browse('') + + self.assertEqual(result, []) + self.assertFalse(self.library1.browse.called) + self.assertFalse(self.library2.browse.called) + + def test_browse_dummy1_selects_dummy1_backend(self): + self.library1.browse().get.return_value = [] + self.library1.browse.reset_mock() + + self.core.library.browse('/dummy1/foo') + + self.library1.browse.assert_called_once_with('/foo') + self.assertFalse(self.library2.browse.called) + + def test_browse_dummy2_selects_dummy2_backend(self): + self.library2.browse().get.return_value = [] + self.library2.browse.reset_mock() + + self.core.library.browse('/dummy2/bar') + + self.assertFalse(self.library1.browse.called) + self.library2.browse.assert_called_once_with('/bar') + + def test_browse_dummy3_returns_nothing(self): + result = self.core.library.browse('/dummy3') + + self.assertEqual(result, []) + self.assertFalse(self.library1.browse.called) + self.assertFalse(self.library2.browse.called) + + def test_browse_dir_returns_subdirs_and_tracks(self): + result1 = [ + Ref(uri='/foo/bar', name='bar', type='directory'), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + ] + self.library1.browse().get.return_value = result1 + self.library1.browse.reset_mock() + + result = self.core.library.browse('/dummy1/foo') + + self.assertEqual(result, [ + Ref(uri='/dummy1/foo/bar', name='bar', type='directory'), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + ]) + def test_lookup_selects_dummy1_backend(self): self.core.library.lookup('dummy1:a')