From a3731c8187c46ba851083e80d0d7bad59e793d49 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 2 Jan 2014 22:06:32 +0100 Subject: [PATCH 01/11] backend: Add library.browse() --- mopidy/backends/base.py | 19 +++++++++++++++++++ mopidy/backends/dummy.py | 6 ++++++ mopidy/backends/local/json/library.py | 2 ++ tests/backends/local/library_test.py | 4 ++++ 4 files changed, 31 insertions(+) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 6b980f06..3dc644ee 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -50,9 +50,28 @@ class BaseLibraryProvider(object): pykka_traversable = True + name = None + """ + Name of the library. + + Used as the library directory name in Mopidy's virtual file system. + + *MUST be set by any class that implements :meth:`browse`.* + """ + def __init__(self, backend): self.backend = backend + def browse(self, path): + """ + See :meth:`mopidy.core.LibraryController.browse`. + + If you implement this method, make sure to also set :attr:`name`. + + *MAY be implemented by subclass.* + """ + return [] + # TODO: replace with search(query, exact=True, ...) def find_exact(self, query=None, uris=None): """ diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index 65477ea2..f16c457a 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -38,12 +38,18 @@ class DummyBackend(pykka.ThreadingActor, base.Backend): class DummyLibraryProvider(base.BaseLibraryProvider): + name = 'dummy' + def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) self.dummy_library = [] + self.dummy_browse_result = [] self.dummy_find_exact_result = SearchResult() self.dummy_search_result = SearchResult() + def browse(self, path): + return self.dummy_browse_result + def find_exact(self, **query): return self.dummy_find_exact_result diff --git a/mopidy/backends/local/json/library.py b/mopidy/backends/local/json/library.py index 99640543..c3dacec4 100644 --- a/mopidy/backends/local/json/library.py +++ b/mopidy/backends/local/json/library.py @@ -42,6 +42,8 @@ def write_library(json_file, data): class LocalJsonLibraryProvider(base.BaseLibraryProvider): + name = 'local' + def __init__(self, *args, **kwargs): super(LocalJsonLibraryProvider, self).__init__(*args, **kwargs) self._uri_mapping = {} diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index e4c00570..92d615ba 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -110,6 +110,10 @@ class LocalLibraryProviderTest(unittest.TestCase): result = backend.library.lookup(self.tracks[0].uri) self.assertEqual(result, []) + @unittest.SkipTest + def test_browse(self): + pass # TODO + def test_lookup(self): tracks = self.library.lookup(self.tracks[0].uri) self.assertEqual(tracks, self.tracks[0:1]) From 6027ed1fac7002b0fc2b971f1512202227c689fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 2 Jan 2014 22:06:33 +0100 Subject: [PATCH 02/11] core: Add library.browse() --- mopidy/core/library.py | 59 ++++++++++++++++++++++++++++++++ tests/core/library_test.py | 70 +++++++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) 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') From 7dba0dafa519d172fd4225aa50eadbed57f427ef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 2 Jan 2014 22:06:33 +0100 Subject: [PATCH 03/11] mpd: Include dirs and files in lsinfo response --- mopidy/mpd/protocol/music_db.py | 18 ++++++- tests/mpd/protocol/music_db_test.py | 83 ++++++++++++++++++++++++----- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/mopidy/mpd/protocol/music_db.py b/mopidy/mpd/protocol/music_db.py index 6e7e3956..ae18957d 100644 --- a/mopidy/mpd/protocol/music_db.py +++ b/mopidy/mpd/protocol/music_db.py @@ -452,9 +452,23 @@ def lsinfo(context, uri=None): directories located at the root level, for both ``lsinfo``, ``lsinfo ""``, and ``lsinfo "/"``. """ + result = [] if uri is None or uri == '/' or uri == '': - return stored_playlists.listplaylists(context) - raise MpdNotImplemented # TODO + result.extend(stored_playlists.listplaylists(context)) + uri = '/' + if not uri.startswith('/'): + uri = '/%s' % uri + for ref in context.core.library.browse(uri).get(): + if ref.type == 'directory': + assert ref.uri.startswith('/'), ( + 'Directory URIs must start with /: %r' % ref) + result.append(('directory', ref.uri[1:])) + elif ref.type == 'track': + # TODO Lookup tracks in batch for better performance + tracks = context.core.library.lookup(ref.uri).get() + if tracks: + result.extend(translator.track_to_mpd_format(tracks[0])) + return result @handle_request(r'rescan$') diff --git a/tests/mpd/protocol/music_db_test.py b/tests/mpd/protocol/music_db_test.py index 09db53ae..eabc2064 100644 --- a/tests/mpd/protocol/music_db_test.py +++ b/tests/mpd/protocol/music_db_test.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +import datetime import unittest from mopidy.mpd.protocol import music_db -from mopidy.models import Album, Artist, SearchResult, Track +from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track from tests.mpd import protocol @@ -137,20 +138,76 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.sendRequest('listallinfo "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') - def test_lsinfo_without_path_returns_same_as_listplaylists(self): - lsinfo_response = self.sendRequest('lsinfo') - listplaylists_response = self.sendRequest('listplaylists') - self.assertEqual(lsinfo_response, listplaylists_response) + def test_lsinfo_without_path_returns_same_as_for_root(self): + last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + self.backend.playlists.playlists = [ + Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] - def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): - lsinfo_response = self.sendRequest('lsinfo ""') - listplaylists_response = self.sendRequest('listplaylists') - self.assertEqual(lsinfo_response, listplaylists_response) + response1 = self.sendRequest('lsinfo') + response2 = self.sendRequest('lsinfo "/"') + self.assertEqual(response1, response2) - def test_lsinfo_for_root_returns_same_as_listplaylists(self): - lsinfo_response = self.sendRequest('lsinfo "/"') - listplaylists_response = self.sendRequest('listplaylists') - self.assertEqual(lsinfo_response, listplaylists_response) + def test_lsinfo_with_empty_path_returns_same_as_for_root(self): + last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + self.backend.playlists.playlists = [ + Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] + + response1 = self.sendRequest('lsinfo ""') + response2 = self.sendRequest('lsinfo "/"') + self.assertEqual(response1, response2) + + def test_lsinfo_for_root_includes_playlists(self): + last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) + self.backend.playlists.playlists = [ + Playlist(name='a', uri='dummy:/a', last_modified=last_modified)] + + self.sendRequest('lsinfo "/"') + self.assertInResponse('playlist: a') + # Date without microseconds and with time zone information + self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z') + self.assertInResponse('OK') + + def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self): + self.backend.library.dummy_browse_result = [ + Ref(uri='dummy:/a', name='a', type='track'), + Ref(uri='/foo', name='foo', type='directory'), + ] + + self.sendRequest('lsinfo "/"') + self.assertInResponse('directory: dummy') + self.assertInResponse('OK') + + def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(self): + self.backend.library.dummy_browse_result = [ + Ref(uri='dummy:/a', name='a', type='track'), + Ref(uri='/foo', name='foo', type='directory'), + ] + + response1 = self.sendRequest('lsinfo "dummy"') + response2 = self.sendRequest('lsinfo "/dummy"') + self.assertEqual(response1, response2) + + def test_lsinfo_for_dir_includes_tracks(self): + self.backend.library.dummy_library = [ + Track(uri='dummy:/a', name='a'), + ] + self.backend.library.dummy_browse_result = [ + Ref(uri='dummy:/a', name='a', type='track'), + ] + + self.sendRequest('lsinfo "/dummy"') + self.assertInResponse('file: dummy:/a') + self.assertInResponse('Title: a') + self.assertInResponse('OK') + + def test_lsinfo_for_dir_includes_subdirs(self): + self.backend.library.dummy_browse_result = [ + Ref(uri='/foo', name='foo', type='directory'), + ] + + self.sendRequest('lsinfo "/dummy"') + self.assertInResponse('directory: dummy/foo') + self.assertInResponse('OK') def test_update_without_uri(self): self.sendRequest('update') From 048c3bb544f19bcba4a62c27a2896f62b38362aa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 2 Jan 2014 23:10:09 +0100 Subject: [PATCH 04/11] core: Use library name instead of URI scheme in browse() --- mopidy/core/library.py | 48 +++++++++++++++++++++----------------- tests/core/library_test.py | 27 +++++++++++++-------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index d1eb430a..015661d3 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import collections +import re import urlparse import pykka @@ -62,32 +63,37 @@ class LibraryController(object): """ if not path.startswith('/'): return [] + + backends = { + backend.library.name.get(): backend + for backend in self.backends.with_library.values() + if backend.library.browse('/').get()} + 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: + for name in backends.keys()] + + groups = re.match('/(?P[^/]+)(?P.*)', path).groupdict() + library_name = groups['library'] + backend_path = groups['path'] + if not backend_path.startswith('/'): + backend_path = '/%s' % backend_path + + backend = backends.get(library_name, None) + if not backend: return [] + refs = backend.library.browse(backend_path).get() + result = [] + for ref in refs: + if ref.type == 'directory': + result.append( + ref.copy(uri='/%s%s' % (library_name, ref.uri))) + else: + result.append(ref) + return result + 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 3734af41..26bef60a 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -31,11 +31,10 @@ class CoreLibraryTest(unittest.TestCase): self.backend1, self.backend2, self.backend3]) def test_browse_root_returns_dir_ref_for_each_library_with_content(self): - result1 = [ + self.library1.browse().get.return_value = [ 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() @@ -57,29 +56,37 @@ class CoreLibraryTest(unittest.TestCase): self.assertFalse(self.library2.browse.called) def test_browse_dummy1_selects_dummy1_backend(self): - self.library1.browse().get.return_value = [] + self.library1.browse().get.return_value = [ + Ref(uri='/foo/bar', name='bar', type='directory'), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + ] 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) + self.assertEqual(self.library1.browse.call_count, 2) + self.assertEqual(self.library2.browse.call_count, 1) + self.library1.browse.assert_called_with('/foo') def test_browse_dummy2_selects_dummy2_backend(self): - self.library2.browse().get.return_value = [] + self.library2.browse().get.return_value = [ + Ref(uri='/bar/quux', name='quux', type='directory'), + Ref(uri='dummy2:/foo/baz.mp3', name='Baz', type='track'), + ] 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') + self.assertEqual(self.library1.browse.call_count, 1) + self.assertEqual(self.library2.browse.call_count, 2) + self.library2.browse.assert_called_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) + self.assertEqual(self.library1.browse.call_count, 1) + self.assertEqual(self.library2.browse.call_count, 1) def test_browse_dir_returns_subdirs_and_tracks(self): result1 = [ From af3b0e40fd00263babcfc0aa0ce6c1175442cb81 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 3 Jan 2014 22:16:22 +0100 Subject: [PATCH 05/11] models: Add Ref.type constants --- mopidy/core/library.py | 8 ++++---- mopidy/models.py | 15 +++++++++++++++ tests/core/library_test.py | 22 +++++++++++----------- tests/models_test.py | 7 +++++++ tests/mpd/protocol/music_db_test.py | 6 +++--- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 015661d3..9c1b13a1 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -46,13 +46,13 @@ class LibraryController(object): 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') + Ref(uri='dummy:/foo.mp3', name='foo', type=Ref.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') + Ref(uri='/dummy/bar', name='bar', type=Ref.DIRECTORY) Note to backend implementors: The ``/dummy`` part of the URI is added by Mopidy core, not the individual backends. @@ -71,7 +71,7 @@ class LibraryController(object): if path == '/': return [ - Ref(uri='/%s' % name, name=name, type='directory') + Ref(uri='/%s' % name, name=name, type=Ref.DIRECTORY) for name in backends.keys()] groups = re.match('/(?P[^/]+)(?P.*)', path).groupdict() @@ -87,7 +87,7 @@ class LibraryController(object): refs = backend.library.browse(backend_path).get() result = [] for ref in refs: - if ref.type == 'directory': + if ref.type == Ref.DIRECTORY: result.append( ref.copy(uri='/%s%s' % (library_name, ref.uri))) else: diff --git a/mopidy/models.py b/mopidy/models.py index 0e40a8f6..b3d2a1b8 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -160,6 +160,21 @@ class Ref(ImmutableObject): #: "directory". Read-only. type = None + #: Constant used for comparision with the :attr:`type` field. + ALBUM = 'album' + + #: Constant used for comparision with the :attr:`type` field. + ARTIST = 'artist' + + #: Constant used for comparision with the :attr:`type` field. + DIRECTORY = 'directory' + + #: Constant used for comparision with the :attr:`type` field. + PLAYLIST = 'playlist' + + #: Constant used for comparision with the :attr:`type` field. + TRACK = 'track' + class Artist(ImmutableObject): """ diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 26bef60a..460811ac 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -32,8 +32,8 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_root_returns_dir_ref_for_each_library_with_content(self): self.library1.browse().get.return_value = [ - Ref(uri='/foo/bar', name='bar', type='directory'), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ] self.library1.browse.reset_mock() self.library2.browse().get.return_value = [] @@ -42,7 +42,7 @@ class CoreLibraryTest(unittest.TestCase): result = self.core.library.browse('/') self.assertEqual(result, [ - Ref(uri='/dummy1', name='dummy1', type='directory'), + Ref(uri='/dummy1', name='dummy1', type=Ref.DIRECTORY), ]) self.assertTrue(self.library1.browse.called) self.assertTrue(self.library2.browse.called) @@ -57,8 +57,8 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dummy1_selects_dummy1_backend(self): self.library1.browse().get.return_value = [ - Ref(uri='/foo/bar', name='bar', type='directory'), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type='track'), + Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ] self.library1.browse.reset_mock() @@ -70,8 +70,8 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dummy2_selects_dummy2_backend(self): self.library2.browse().get.return_value = [ - Ref(uri='/bar/quux', name='quux', type='directory'), - Ref(uri='dummy2:/foo/baz.mp3', name='Baz', type='track'), + Ref(uri='/bar/quux', name='quux', type=Ref.DIRECTORY), + Ref(uri='dummy2:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ] self.library2.browse.reset_mock() @@ -90,8 +90,8 @@ class CoreLibraryTest(unittest.TestCase): 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'), + Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ] self.library1.browse().get.return_value = result1 self.library1.browse.reset_mock() @@ -99,8 +99,8 @@ class CoreLibraryTest(unittest.TestCase): 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'), + Ref(uri='/dummy1/foo/bar', name='bar', type=Ref.DIRECTORY), + Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ]) def test_lookup_selects_dummy1_backend(self): diff --git a/tests/models_test.py b/tests/models_test.py index 50faf89e..b2b72ea4 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -87,6 +87,13 @@ class RefTest(unittest.TestCase): ref2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(ref1, ref2) + def test_type_constants(self): + self.assertEqual(Ref.ALBUM, 'album') + self.assertEqual(Ref.ARTIST, 'artist') + self.assertEqual(Ref.DIRECTORY, 'directory') + self.assertEqual(Ref.PLAYLIST, 'playlist') + self.assertEqual(Ref.TRACK, 'track') + class ArtistTest(unittest.TestCase): def test_uri(self): diff --git a/tests/mpd/protocol/music_db_test.py b/tests/mpd/protocol/music_db_test.py index eabc2064..36e36a0a 100644 --- a/tests/mpd/protocol/music_db_test.py +++ b/tests/mpd/protocol/music_db_test.py @@ -170,7 +170,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self): self.backend.library.dummy_browse_result = [ Ref(uri='dummy:/a', name='a', type='track'), - Ref(uri='/foo', name='foo', type='directory'), + Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), ] self.sendRequest('lsinfo "/"') @@ -180,7 +180,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(self): self.backend.library.dummy_browse_result = [ Ref(uri='dummy:/a', name='a', type='track'), - Ref(uri='/foo', name='foo', type='directory'), + Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), ] response1 = self.sendRequest('lsinfo "dummy"') @@ -202,7 +202,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_dir_includes_subdirs(self): self.backend.library.dummy_browse_result = [ - Ref(uri='/foo', name='foo', type='directory'), + Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), ] self.sendRequest('lsinfo "/dummy"') From 69836d2e16c47873e4b2943764a4b9614b7d9e02 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 3 Jan 2014 23:07:31 +0100 Subject: [PATCH 06/11] backend: Rename library.name to library.root_directory_name --- mopidy/backends/base.py | 9 ++++----- mopidy/backends/dummy.py | 2 +- mopidy/core/library.py | 2 +- tests/core/library_test.py | 7 +++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/mopidy/backends/base.py b/mopidy/backends/base.py index 3dc644ee..5a8a23bb 100644 --- a/mopidy/backends/base.py +++ b/mopidy/backends/base.py @@ -50,11 +50,9 @@ class BaseLibraryProvider(object): pykka_traversable = True - name = None + root_directory_name = None """ - Name of the library. - - Used as the library directory name in Mopidy's virtual file system. + Name of the library's root directory in Mopidy's virtual file system. *MUST be set by any class that implements :meth:`browse`.* """ @@ -66,7 +64,8 @@ class BaseLibraryProvider(object): """ See :meth:`mopidy.core.LibraryController.browse`. - If you implement this method, make sure to also set :attr:`name`. + If you implement this method, make sure to also set + :attr:`root_directory_name`. *MAY be implemented by subclass.* """ diff --git a/mopidy/backends/dummy.py b/mopidy/backends/dummy.py index f16c457a..b3be0889 100644 --- a/mopidy/backends/dummy.py +++ b/mopidy/backends/dummy.py @@ -38,7 +38,7 @@ class DummyBackend(pykka.ThreadingActor, base.Backend): class DummyLibraryProvider(base.BaseLibraryProvider): - name = 'dummy' + root_directory_name = 'dummy' def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 9c1b13a1..59993db4 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -65,7 +65,7 @@ class LibraryController(object): return [] backends = { - backend.library.name.get(): backend + backend.library.root_directory_name.get(): backend for backend in self.backends.with_library.values() if backend.library.browse('/').get()} diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 460811ac..dc7ab778 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -13,13 +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.library1.root_directory_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.library2.root_directory_name.get.return_value = 'dummy2' self.backend2.library = self.library2 # A backend without the optional library provider @@ -89,11 +89,10 @@ class CoreLibraryTest(unittest.TestCase): self.assertEqual(self.library2.browse.call_count, 1) def test_browse_dir_returns_subdirs_and_tracks(self): - result1 = [ + self.library1.browse().get.return_value = [ Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), ] - self.library1.browse().get.return_value = result1 self.library1.browse.reset_mock() result = self.core.library.browse('/dummy1/foo') From 252f4792a027a5272b54ed7098e742be4d24ced9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 3 Jan 2014 23:12:03 +0100 Subject: [PATCH 07/11] core: Check if library is browsable at startup --- mopidy/core/actor.py | 8 ++++++++ mopidy/core/library.py | 9 ++------- tests/core/library_test.py | 27 ++++++++++----------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 4924cca2..2055340e 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -88,6 +88,7 @@ class Backends(list): super(Backends, self).__init__(backends) self.with_library = collections.OrderedDict() + self.with_browsable_library = collections.OrderedDict() self.with_playback = collections.OrderedDict() self.with_playlists = collections.OrderedDict() @@ -101,6 +102,13 @@ class Backends(list): self.add(self.with_playback, has_playback, scheme, backend) self.add(self.with_playlists, has_playlists, scheme, backend) + if has_library: + root_dir_name = backend.library.root_directory_name.get() + has_browsable_library = root_dir_name is not None + self.add( + self.with_browsable_library, has_browsable_library, + root_dir_name, backend) + def add(self, registry, supported, uri_scheme, backend): if not supported: return diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 59993db4..6ea3041c 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -64,15 +64,10 @@ class LibraryController(object): if not path.startswith('/'): return [] - backends = { - backend.library.root_directory_name.get(): backend - for backend in self.backends.with_library.values() - if backend.library.browse('/').get()} - if path == '/': return [ Ref(uri='/%s' % name, name=name, type=Ref.DIRECTORY) - for name in backends.keys()] + for name in self.backends.with_browsable_library.keys()] groups = re.match('/(?P[^/]+)(?P.*)', path).groupdict() library_name = groups['library'] @@ -80,7 +75,7 @@ class LibraryController(object): if not backend_path.startswith('/'): backend_path = '/%s' % backend_path - backend = backends.get(library_name, None) + backend = self.backends.with_browsable_library.get(library_name, None) if not backend: return [] diff --git a/tests/core/library_test.py b/tests/core/library_test.py index dc7ab778..59f3faf7 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -30,22 +30,15 @@ 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): - self.library1.browse().get.return_value = [ - Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), - ] - self.library1.browse.reset_mock() - self.library2.browse().get.return_value = [] - self.library2.browse.reset_mock() - + def test_browse_root_returns_dir_ref_for_each_lib_with_root_dir_name(self): result = self.core.library.browse('/') self.assertEqual(result, [ Ref(uri='/dummy1', name='dummy1', type=Ref.DIRECTORY), + Ref(uri='/dummy2', name='dummy2', type=Ref.DIRECTORY), ]) - self.assertTrue(self.library1.browse.called) - self.assertTrue(self.library2.browse.called) + self.assertFalse(self.library1.browse.called) + self.assertFalse(self.library2.browse.called) self.assertFalse(self.backend3.library.browse.called) def test_browse_empty_string_returns_nothing(self): @@ -64,8 +57,8 @@ class CoreLibraryTest(unittest.TestCase): self.core.library.browse('/dummy1/foo') - self.assertEqual(self.library1.browse.call_count, 2) - self.assertEqual(self.library2.browse.call_count, 1) + self.assertEqual(self.library1.browse.call_count, 1) + self.assertEqual(self.library2.browse.call_count, 0) self.library1.browse.assert_called_with('/foo') def test_browse_dummy2_selects_dummy2_backend(self): @@ -77,16 +70,16 @@ class CoreLibraryTest(unittest.TestCase): self.core.library.browse('/dummy2/bar') - self.assertEqual(self.library1.browse.call_count, 1) - self.assertEqual(self.library2.browse.call_count, 2) + self.assertEqual(self.library1.browse.call_count, 0) + self.assertEqual(self.library2.browse.call_count, 1) self.library2.browse.assert_called_with('/bar') def test_browse_dummy3_returns_nothing(self): result = self.core.library.browse('/dummy3') self.assertEqual(result, []) - self.assertEqual(self.library1.browse.call_count, 1) - self.assertEqual(self.library2.browse.call_count, 1) + self.assertEqual(self.library1.browse.call_count, 0) + self.assertEqual(self.library2.browse.call_count, 0) def test_browse_dir_returns_subdirs_and_tracks(self): self.library1.browse().get.return_value = [ From dcd912580f6dc349390ae07a2cd8180f5d83e884 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 3 Jan 2014 23:14:01 +0100 Subject: [PATCH 08/11] local: Change library.{name => root_directory_name} --- mopidy/backends/local/json/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/local/json/library.py b/mopidy/backends/local/json/library.py index c3dacec4..5aa0d647 100644 --- a/mopidy/backends/local/json/library.py +++ b/mopidy/backends/local/json/library.py @@ -42,7 +42,7 @@ def write_library(json_file, data): class LocalJsonLibraryProvider(base.BaseLibraryProvider): - name = 'local' + root_directory_name = 'local' def __init__(self, *args, **kwargs): super(LocalJsonLibraryProvider, self).__init__(*args, **kwargs) From 9da5ccbb79a05cec6e1dcd4f6058e85451630488 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jan 2014 08:50:26 +0100 Subject: [PATCH 09/11] models: Add type specific constructors to Ref --- mopidy/models.py | 30 ++++++++++++++++++++++++++++++ tests/models_test.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/mopidy/models.py b/mopidy/models.py index b3d2a1b8..53083eb7 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -175,6 +175,36 @@ class Ref(ImmutableObject): #: Constant used for comparision with the :attr:`type` field. TRACK = 'track' + @classmethod + def album(cls, **kwargs): + """Create a :class:`Ref` with ``type`` :attr:`ALBUM`.""" + kwargs['type'] = Ref.ALBUM + return cls(**kwargs) + + @classmethod + def artist(cls, **kwargs): + """Create a :class:`Ref` with ``type`` :attr:`ARTIST`.""" + kwargs['type'] = Ref.ARTIST + return cls(**kwargs) + + @classmethod + def directory(cls, **kwargs): + """Create a :class:`Ref` with ``type`` :attr:`DIRECTORY`.""" + kwargs['type'] = Ref.DIRECTORY + return cls(**kwargs) + + @classmethod + def playlist(cls, **kwargs): + """Create a :class:`Ref` with ``type`` :attr:`PLAYLIST`.""" + kwargs['type'] = Ref.PLAYLIST + return cls(**kwargs) + + @classmethod + def track(cls, **kwargs): + """Create a :class:`Ref` with ``type`` :attr:`TRACK`.""" + kwargs['type'] = Ref.TRACK + return cls(**kwargs) + class Artist(ImmutableObject): """ diff --git a/tests/models_test.py b/tests/models_test.py index b2b72ea4..02cba8f4 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -94,6 +94,36 @@ class RefTest(unittest.TestCase): self.assertEqual(Ref.PLAYLIST, 'playlist') self.assertEqual(Ref.TRACK, 'track') + def test_album_constructor(self): + ref = Ref.album(uri='foo', name='bar') + self.assertEqual(ref.uri, 'foo') + self.assertEqual(ref.name, 'bar') + self.assertEqual(ref.type, Ref.ALBUM) + + def test_artist_constructor(self): + ref = Ref.artist(uri='foo', name='bar') + self.assertEqual(ref.uri, 'foo') + self.assertEqual(ref.name, 'bar') + self.assertEqual(ref.type, Ref.ARTIST) + + def test_directory_constructor(self): + ref = Ref.directory(uri='foo', name='bar') + self.assertEqual(ref.uri, 'foo') + self.assertEqual(ref.name, 'bar') + self.assertEqual(ref.type, Ref.DIRECTORY) + + def test_playlist_constructor(self): + ref = Ref.playlist(uri='foo', name='bar') + self.assertEqual(ref.uri, 'foo') + self.assertEqual(ref.name, 'bar') + self.assertEqual(ref.type, Ref.PLAYLIST) + + def test_track_constructor(self): + ref = Ref.track(uri='foo', name='bar') + self.assertEqual(ref.uri, 'foo') + self.assertEqual(ref.name, 'bar') + self.assertEqual(ref.type, Ref.TRACK) + class ArtistTest(unittest.TestCase): def test_uri(self): From fe283113248b17bcca28d86d6f879602a06bed68 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jan 2014 08:54:03 +0100 Subject: [PATCH 10/11] models: Use new Ref constructors --- mopidy/core/library.py | 6 +++--- tests/core/library_test.py | 20 ++++++++++---------- tests/mpd/protocol/music_db_test.py | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/mopidy/core/library.py b/mopidy/core/library.py index 6ea3041c..ac1aad14 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -46,13 +46,13 @@ class LibraryController(object): 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=Ref.TRACK) + Ref.track(uri='dummy:/foo.mp3', name='foo') 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=Ref.DIRECTORY) + Ref.directory(uri='/dummy/bar', name='bar') Note to backend implementors: The ``/dummy`` part of the URI is added by Mopidy core, not the individual backends. @@ -66,7 +66,7 @@ class LibraryController(object): if path == '/': return [ - Ref(uri='/%s' % name, name=name, type=Ref.DIRECTORY) + Ref.directory(uri='/%s' % name, name=name) for name in self.backends.with_browsable_library.keys()] groups = re.match('/(?P[^/]+)(?P.*)', path).groupdict() diff --git a/tests/core/library_test.py b/tests/core/library_test.py index 59f3faf7..44c5e3f1 100644 --- a/tests/core/library_test.py +++ b/tests/core/library_test.py @@ -34,8 +34,8 @@ class CoreLibraryTest(unittest.TestCase): result = self.core.library.browse('/') self.assertEqual(result, [ - Ref(uri='/dummy1', name='dummy1', type=Ref.DIRECTORY), - Ref(uri='/dummy2', name='dummy2', type=Ref.DIRECTORY), + Ref.directory(uri='/dummy1', name='dummy1'), + Ref.directory(uri='/dummy2', name='dummy2'), ]) self.assertFalse(self.library1.browse.called) self.assertFalse(self.library2.browse.called) @@ -50,8 +50,8 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dummy1_selects_dummy1_backend(self): self.library1.browse().get.return_value = [ - Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), + Ref.directory(uri='/foo/bar', name='bar'), + Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), ] self.library1.browse.reset_mock() @@ -63,8 +63,8 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dummy2_selects_dummy2_backend(self): self.library2.browse().get.return_value = [ - Ref(uri='/bar/quux', name='quux', type=Ref.DIRECTORY), - Ref(uri='dummy2:/foo/baz.mp3', name='Baz', type=Ref.TRACK), + Ref.directory(uri='/bar/quux', name='quux'), + Ref.track(uri='dummy2:/foo/baz.mp3', name='Baz'), ] self.library2.browse.reset_mock() @@ -83,16 +83,16 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dir_returns_subdirs_and_tracks(self): self.library1.browse().get.return_value = [ - Ref(uri='/foo/bar', name='bar', type=Ref.DIRECTORY), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), + Ref.directory(uri='/foo/bar', name='bar'), + Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), ] self.library1.browse.reset_mock() result = self.core.library.browse('/dummy1/foo') self.assertEqual(result, [ - Ref(uri='/dummy1/foo/bar', name='bar', type=Ref.DIRECTORY), - Ref(uri='dummy1:/foo/baz.mp3', name='Baz', type=Ref.TRACK), + Ref.directory(uri='/dummy1/foo/bar', name='bar'), + Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), ]) def test_lookup_selects_dummy1_backend(self): diff --git a/tests/mpd/protocol/music_db_test.py b/tests/mpd/protocol/music_db_test.py index 36e36a0a..25feab27 100644 --- a/tests/mpd/protocol/music_db_test.py +++ b/tests/mpd/protocol/music_db_test.py @@ -169,8 +169,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self): self.backend.library.dummy_browse_result = [ - Ref(uri='dummy:/a', name='a', type='track'), - Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), + Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='/foo', name='foo'), ] self.sendRequest('lsinfo "/"') @@ -179,8 +179,8 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(self): self.backend.library.dummy_browse_result = [ - Ref(uri='dummy:/a', name='a', type='track'), - Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), + Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='/foo', name='foo'), ] response1 = self.sendRequest('lsinfo "dummy"') @@ -192,7 +192,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): Track(uri='dummy:/a', name='a'), ] self.backend.library.dummy_browse_result = [ - Ref(uri='dummy:/a', name='a', type='track'), + Ref.track(uri='dummy:/a', name='a'), ] self.sendRequest('lsinfo "/dummy"') @@ -202,7 +202,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_dir_includes_subdirs(self): self.backend.library.dummy_browse_result = [ - Ref(uri='/foo', name='foo', type=Ref.DIRECTORY), + Ref.directory(uri='/foo', name='foo'), ] self.sendRequest('lsinfo "/dummy"') From 8d6f9ee8079fa6c60d0243ce31a3aeb943fb5e46 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Jan 2014 08:57:32 +0100 Subject: [PATCH 11/11] mpd: Use Ref type constants --- mopidy/mpd/protocol/music_db.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/mpd/protocol/music_db.py b/mopidy/mpd/protocol/music_db.py index ae18957d..b31d295b 100644 --- a/mopidy/mpd/protocol/music_db.py +++ b/mopidy/mpd/protocol/music_db.py @@ -4,7 +4,7 @@ import functools import itertools import re -from mopidy.models import Track +from mopidy.models import Ref, Track from mopidy.mpd import translator from mopidy.mpd.exceptions import MpdArgError, MpdNotImplemented from mopidy.mpd.protocol import handle_request, stored_playlists @@ -459,11 +459,11 @@ def lsinfo(context, uri=None): if not uri.startswith('/'): uri = '/%s' % uri for ref in context.core.library.browse(uri).get(): - if ref.type == 'directory': + if ref.type == Ref.DIRECTORY: assert ref.uri.startswith('/'), ( 'Directory URIs must start with /: %r' % ref) result.append(('directory', ref.uri[1:])) - elif ref.type == 'track': + elif ref.type == Ref.TRACK: # TODO Lookup tracks in batch for better performance tracks = context.core.library.lookup(ref.uri).get() if tracks: