diff --git a/mopidy/backend.py b/mopidy/backend.py index 9ada95c5..6f895985 100644 --- a/mopidy/backend.py +++ b/mopidy/backend.py @@ -37,6 +37,9 @@ class Backend(object): def has_library(self): return self.library is not None + def has_library_browse(self): + return self.has_library() and self.library.root_directory is not None + def has_playback(self): return self.playback is not None @@ -52,9 +55,12 @@ class LibraryProvider(object): pykka_traversable = True - root_directory_name = None + root_directory = None """ - Name of the library's root directory in Mopidy's virtual file system. + :class:`models.Ref.directory` instance with a URI and name set + representing the root of this library's browse tree. URIs must + use one of the schemes supported by the backend, and name should + be set to a human friendly value. *MUST be set by any class that implements :meth:`LibraryProvider.browse`.* """ diff --git a/mopidy/core/actor.py b/mopidy/core/actor.py index 0f152436..b27bb3cc 100644 --- a/mopidy/core/actor.py +++ b/mopidy/core/actor.py @@ -88,7 +88,7 @@ class Backends(list): super(Backends, self).__init__(backends) self.with_library = collections.OrderedDict() - self.with_browsable_library = collections.OrderedDict() + self.with_library_browse = collections.OrderedDict() self.with_playback = collections.OrderedDict() self.with_playlists = collections.OrderedDict() @@ -97,6 +97,7 @@ class Backends(list): for backend in backends: has_library = backend.has_library().get() + has_library_browse = backend.has_library_browse().get() has_playback = backend.has_playback().get() has_playlists = backend.has_playlists().get() @@ -109,12 +110,9 @@ class Backends(list): if has_library: self.with_library[scheme] = backend + if has_library_browse: + self.with_library_browse[scheme] = backend if has_playback: self.with_playback[scheme] = backend if has_playlists: self.with_playlists[scheme] = backend - - if has_library: - root_dir_name = backend.library.root_directory_name.get() - if root_dir_name is not None: - self.with_browsable_library[root_dir_name] = backend diff --git a/mopidy/core/library.py b/mopidy/core/library.py index cea21b10..ce92cced 100644 --- a/mopidy/core/library.py +++ b/mopidy/core/library.py @@ -1,13 +1,10 @@ from __future__ import unicode_literals import collections -import re import urlparse import pykka -from mopidy.models import Ref - class LibraryController(object): pykka_traversable = True @@ -32,15 +29,16 @@ class LibraryController(object): (b, None) for b in self.backends.with_library.values()]) return backends_to_uris - def browse(self, path): + def browse(self, uri): """ - Browse directories and tracks at the given ``path``. + Browse directories and tracks at the given ``uri``. - ``path`` is a string that always starts with "/". It points to a - directory in Mopidy's virtual file system. + ``uri`` is a sring which represents some directory belonging to a + backend. To get the intial root directories for backends pass None as + the URI. Returns a list of :class:`mopidy.models.Ref` objects for the - directories and tracks at the given ``path``. + directories and tracks at the given ``uri``. The :class:`~mopidy.models.Ref` objects representing tracks keep the track's original URI. A matching pair of objects can look like this:: @@ -49,45 +47,28 @@ class LibraryController(object): Ref.track(uri='dummy:/foo.mp3', name='foo') The :class:`~mopidy.models.Ref` objects representing directories have - plain paths, not including any URI schema. For example, the dummy - library's ``/bar`` directory is returned like this:: + backend specific URIs. These are opaque values, so no one but the + backend that created them should try and derive any meaning from them. + The only valid exception to this is checking the scheme, as it is used + to route browse requests to the correct backend. - Ref.directory(uri='/dummy/bar', name='bar') + For example, the dummy library's ``/bar`` directory could be returned + like this:: - Note to backend implementors: The ``/dummy`` part of the URI is added - by Mopidy core, not the individual backends. + Ref.directory(uri='dummy:directory:/bar', name='bar') - :param path: path to browse - :type path: string + :param string uri: URI to browse :rtype: list of :class:`mopidy.models.Ref` """ - if not path.startswith('/'): - return [] + if uri is None: + backends = self.backends.with_library_browse.values() + return [b.library.root_directory.get() for b in backends] - if path == '/': - return [ - Ref.directory(uri='/%s' % name, name=name) - for name in self.backends.with_browsable_library.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 = self.backends.with_browsable_library.get(library_name, None) + scheme = urlparse.urlparse(uri).scheme + backend = self.backends.with_library_browse.get(scheme) if not backend: return [] - - refs = backend.library.browse(backend_path).get() - result = [] - for ref in refs: - if ref.type == Ref.DIRECTORY: - uri = '/'.join(['', library_name, ref.uri.lstrip('/')]) - result.append(ref.copy(uri=uri)) - else: - result.append(ref) - return result + return backend.library.browse(uri).get() def find_exact(self, query=None, uris=None, **kwargs): """ diff --git a/mopidy/local/json.py b/mopidy/local/json.py index ce11f058..b87c5bce 100644 --- a/mopidy/local/json.py +++ b/mopidy/local/json.py @@ -49,41 +49,31 @@ class _BrowseCache(object): splitpath_re = re.compile(r'([^/]+)') def __init__(self, uris): - """Create a dictionary tree for quick browsing. + # {parent_uri: {uri: ref}} + self._cache = {} - {'foo': {'bar': {None: [ref1, ref2]}, - 'baz': {}, - None: [ref3]}} - """ - self._root = collections.OrderedDict() - - for uri in uris: - path = translator.local_track_uri_to_path(uri, b'/') + for track_uri in uris: + path = translator.local_track_uri_to_path(track_uri, b'/') parts = self.splitpath_re.findall( path.decode(self.encoding, 'replace')) - filename = parts.pop() - node = self._root - for part in parts: - node = node.setdefault(part, collections.OrderedDict()) - ref = models.Ref.track(uri=uri, name=filename) - node.setdefault(None, []).append(ref) + track_ref = models.Ref.track(uri=track_uri, name=parts.pop()) - def lookup(self, path): - results = [] - node = self._root + parent = 'local:directory' + for i in range(len(parts)): + self._cache.setdefault(parent, collections.OrderedDict()) - for part in self.splitpath_re.findall(path): - node = node.get(part, {}) + directory = '/'.join(parts[:i+1]) + dir_uri = translator.path_to_local_directory_uri(directory) + dir_ref = models.Ref.directory(uri=dir_uri, name=parts[i]) + self._cache[parent][dir_uri] = dir_ref - for key, value in node.items(): - if key is not None: - uri = os.path.join(path, key) - results.append(models.Ref.directory(uri=uri, name=key)) + parent = dir_uri - # Get tracks afterwards to ensure ordering. - results.extend(node.get(None, [])) + self._cache.setdefault(parent, collections.OrderedDict()) + self._cache[parent][track_uri] = track_ref - return results + def lookup(self, uri): + return self._cache.get(uri, {}).values() class JsonLibrary(local.Library): diff --git a/mopidy/local/library.py b/mopidy/local/library.py index dc068457..a626f566 100644 --- a/mopidy/local/library.py +++ b/mopidy/local/library.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import logging -from mopidy import backend +from mopidy import backend, models logger = logging.getLogger(__name__) @@ -10,7 +10,8 @@ logger = logging.getLogger(__name__) class LocalLibraryProvider(backend.LibraryProvider): """Proxy library that delegates work to our active local library.""" - root_directory_name = 'local' + root_directory = models.Ref.directory(uri=b'local:directory', + name='Local media') def __init__(self, backend, library): super(LocalLibraryProvider, self).__init__(backend) diff --git a/mopidy/local/translator.py b/mopidy/local/translator.py index 7ec6d3fe..c3f9874b 100644 --- a/mopidy/local/translator.py +++ b/mopidy/local/translator.py @@ -33,6 +33,13 @@ def path_to_local_track_uri(relpath): return b'local:track:%s' % urllib.quote(relpath) +def path_to_local_directory_uri(relpath): + """Convert path relative to :confval:`local/media_dir` directory URI.""" + if isinstance(relpath, unicode): + relpath = relpath.encode('utf-8') + return b'local:directory:%s' % urllib.quote(relpath) + + def m3u_extinf_to_track(line): """Convert extended M3U directive to track template.""" m = M3U_EXTINF_RE.match(line) diff --git a/mopidy/mpd/dispatcher.py b/mopidy/mpd/dispatcher.py index a601f13e..6aeace9d 100644 --- a/mopidy/mpd/dispatcher.py +++ b/mopidy/mpd/dispatcher.py @@ -297,3 +297,19 @@ class MpdContext(object): if uri not in self._playlist_name_from_uri: self.refresh_playlists_mapping() return self._playlist_name_from_uri[uri] + + # TODO: consider making context.browse(path) which uses this internally. + # advantage would be that all browse requests then go through the same code + # and we could prebuild/cache path->uri relationships instead of having to + # look them up all the time. + def directory_path_to_uri(self, path): + parts = re.findall(r'[^/]+', path) + uri = None + for part in parts: + for ref in self.core.library.browse(uri).get(): + if ref.type == ref.DIRECTORY and ref.name == part: + uri = ref.uri + break + else: + raise exceptions.MpdNoExistError() + return uri diff --git a/mopidy/mpd/protocol/current_playlist.py b/mopidy/mpd/protocol/current_playlist.py index 6263e2e8..de8721d3 100644 --- a/mopidy/mpd/protocol/current_playlist.py +++ b/mopidy/mpd/protocol/current_playlist.py @@ -27,8 +27,12 @@ def add(context, uri): if tl_tracks: return - if not uri.startswith('/'): - uri = '/%s' % uri + try: + uri = context.directory_path_to_uri(translator.normalize_path(uri)) + except MpdNoExistError as e: + e.command = 'add' + e.message = 'directory or file not found' + raise browse_futures = [context.core.library.browse(uri)] lookup_futures = [] diff --git a/mopidy/mpd/protocol/music_db.py b/mopidy/mpd/protocol/music_db.py index 774ec383..58681557 100644 --- a/mopidy/mpd/protocol/music_db.py +++ b/mopidy/mpd/protocol/music_db.py @@ -417,25 +417,33 @@ def listall(context, uri=None): Lists all songs and directories in ``URI``. """ - if uri is None: - uri = '/' - if not uri.startswith('/'): - uri = '/%s' % uri - result = [] - browse_futures = [context.core.library.browse(uri)] + root_path = translator.normalize_path(uri) + # TODO: doesn't the dispatcher._call_handler have enough info to catch + # the error this can produce, set the command and then 'raise'? + try: + uri = context.directory_path_to_uri(root_path) + except MpdNoExistError as e: + e.command = 'listall' + e.message = 'Not found' + raise + browse_futures = [(root_path, context.core.library.browse(uri))] + while browse_futures: - for ref in browse_futures.pop().get(): + base_path, future = browse_futures.pop() + for ref in future.get(): if ref.type == Ref.DIRECTORY: - result.append(('directory', ref.uri)) - browse_futures.append(context.core.library.browse(ref.uri)) + path = '/'.join([base_path, ref.name.replace('/', '')]) + result.append(('directory', path)) + browse_futures.append( + (path, context.core.library.browse(ref.uri))) elif ref.type == Ref.TRACK: result.append(('file', ref.uri)) if not result: raise MpdNoExistError('Not found') - return [('directory', uri)] + result + return [('directory', root_path)] + result @handle_request(r'listallinfo$') @@ -449,18 +457,25 @@ def listallinfo(context, uri=None): Same as ``listall``, except it also returns metadata info in the same format as ``lsinfo``. """ - if uri is None: - uri = '/' - if not uri.startswith('/'): - uri = '/%s' % uri - dirs_and_futures = [] - browse_futures = [context.core.library.browse(uri)] + result = [] + root_path = translator.normalize_path(uri) + try: + uri = context.directory_path_to_uri(root_path) + except MpdNoExistError as e: + e.command = 'listallinfo' + e.message = 'Not found' + raise + browse_futures = [(root_path, context.core.library.browse(uri))] + while browse_futures: - for ref in browse_futures.pop().get(): + base_path, future = browse_futures.pop() + for ref in future.get(): if ref.type == Ref.DIRECTORY: - dirs_and_futures.append(('directory', ref.uri)) - browse_futures.append(context.core.library.browse(ref.uri)) + path = '/'.join([base_path, ref.name.replace('/', '')]) + future = context.core.library.browse(ref.uri) + browse_futures.append((path, future)) + dirs_and_futures.append(('directory', path)) elif ref.type == Ref.TRACK: # TODO Lookup tracks in batch for better performance dirs_and_futures.append(context.core.library.lookup(ref.uri)) @@ -476,7 +491,7 @@ def listallinfo(context, uri=None): if not result: raise MpdNoExistError('Not found') - return [('directory', uri)] + result + return [('directory', root_path)] + result @handle_request(r'lsinfo$') @@ -498,16 +513,21 @@ def lsinfo(context, uri=None): ""``, and ``lsinfo "/"``. """ result = [] - if uri is None or uri == '/' or uri == '': + root_path = translator.normalize_path(uri, relative=True) + try: + uri = context.directory_path_to_uri(root_path) + except MpdNoExistError as e: + e.command = 'lsinfo' + e.message = 'Not found' + raise + + if uri is None: 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 == Ref.DIRECTORY: - assert ref.uri.startswith('/'), ( - 'Directory URIs must start with /: %r' % ref) - result.append(('directory', ref.uri[1:])) + path = '/'.join([root_path, ref.name.replace('/', '')]) + result.append(('directory', path.lstrip('/'))) elif ref.type == Ref.TRACK: # TODO Lookup tracks in batch for better performance tracks = context.core.library.lookup(ref.uri).get() diff --git a/mopidy/mpd/translator.py b/mopidy/mpd/translator.py index 49ebce35..520e9ac8 100644 --- a/mopidy/mpd/translator.py +++ b/mopidy/mpd/translator.py @@ -1,11 +1,20 @@ from __future__ import unicode_literals +import re import shlex from mopidy.mpd.exceptions import MpdArgError from mopidy.models import TlTrack # TODO: special handling of local:// uri scheme +normalize_path_re = re.compile(r'[^/]+') + + +def normalize_path(path, relative=False): + parts = normalize_path_re.findall(path or '') + if not relative: + parts.insert(0, '') + return '/'.join(parts) def track_to_mpd_format(track, position=None): diff --git a/tests/core/test_library.py b/tests/core/test_library.py index 836a434e..7a40194d 100644 --- a/tests/core/test_library.py +++ b/tests/core/test_library.py @@ -9,32 +9,35 @@ from mopidy.models import Ref, SearchResult, Track class CoreLibraryTest(unittest.TestCase): def setUp(self): + dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1') self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.library1 = mock.Mock(spec=backend.LibraryProvider) - self.library1.root_directory_name.get.return_value = 'dummy1' + self.library1.root_directory.get.return_value = dummy1_root self.backend1.library = self.library1 + dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2') self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.library2 = mock.Mock(spec=backend.LibraryProvider) - self.library2.root_directory_name.get.return_value = 'dummy2' + self.library2.root_directory.get.return_value = dummy2_root self.backend2.library = self.library2 # A backend without the optional library provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.has_library().get.return_value = False + self.backend3.has_library_browse().get.return_value = False self.core = core.Core(audio=None, backends=[ self.backend1, self.backend2, self.backend3]) def test_browse_root_returns_dir_ref_for_each_lib_with_root_dir_name(self): - result = self.core.library.browse('/') + result = self.core.library.browse(None) self.assertEqual(result, [ - Ref.directory(uri='/dummy1', name='dummy1'), - Ref.directory(uri='/dummy2', name='dummy2'), + Ref.directory(uri='dummy1:directory', name='dummy1'), + Ref.directory(uri='dummy2:directory', name='dummy2'), ]) self.assertFalse(self.library1.browse.called) self.assertFalse(self.library2.browse.called) @@ -49,32 +52,32 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dummy1_selects_dummy1_backend(self): self.library1.browse().get.return_value = [ - Ref.directory(uri='/foo/bar', name='bar'), - Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), + Ref.directory(uri='dummy1:directory:/foo/bar', name='bar'), + Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'), ] self.library1.browse.reset_mock() - self.core.library.browse('/dummy1/foo') + self.core.library.browse('dummy1:directory:/foo') self.assertEqual(self.library1.browse.call_count, 1) self.assertEqual(self.library2.browse.call_count, 0) - self.library1.browse.assert_called_with('/foo') + self.library1.browse.assert_called_with('dummy1:directory:/foo') def test_browse_dummy2_selects_dummy2_backend(self): self.library2.browse().get.return_value = [ - Ref.directory(uri='/bar/quux', name='quux'), - Ref.track(uri='dummy2:/foo/baz.mp3', name='Baz'), + Ref.directory(uri='dummy2:directory:/bar/baz', name='quux'), + Ref.track(uri='dummy2:track:/bar/foo.mp3', name='Baz'), ] self.library2.browse.reset_mock() - self.core.library.browse('/dummy2/bar') + self.core.library.browse('dummy2:directory:/bar') self.assertEqual(self.library1.browse.call_count, 0) self.assertEqual(self.library2.browse.call_count, 1) - self.library2.browse.assert_called_with('/bar') + self.library2.browse.assert_called_with('dummy2:directory:/bar') def test_browse_dummy3_returns_nothing(self): - result = self.core.library.browse('/dummy3') + result = self.core.library.browse('dummy3:test') self.assertEqual(result, []) self.assertEqual(self.library1.browse.call_count, 0) @@ -82,16 +85,15 @@ class CoreLibraryTest(unittest.TestCase): def test_browse_dir_returns_subdirs_and_tracks(self): self.library1.browse().get.return_value = [ - Ref.directory(uri='/foo/bar', name='bar'), - Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), + Ref.directory(uri='dummy1:directory:/foo/bar', name='Bar'), + Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'), ] self.library1.browse.reset_mock() - result = self.core.library.browse('/dummy1/foo') - + result = self.core.library.browse('dummy1:directory:/foo') self.assertEqual(result, [ - Ref.directory(uri='/dummy1/foo/bar', name='bar'), - Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), + Ref.directory(uri='dummy1:directory:/foo/bar', name='Bar'), + Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'), ]) def test_lookup_selects_dummy1_backend(self): diff --git a/tests/dummy_backend.py b/tests/dummy_backend.py index 258340b9..94b01433 100644 --- a/tests/dummy_backend.py +++ b/tests/dummy_backend.py @@ -9,7 +9,7 @@ from __future__ import unicode_literals import pykka from mopidy import backend -from mopidy.models import Playlist, SearchResult +from mopidy.models import Playlist, Ref, SearchResult def create_dummy_backend_proxy(config=None, audio=None): @@ -28,7 +28,7 @@ class DummyBackend(pykka.ThreadingActor, backend.Backend): class DummyLibraryProvider(backend.LibraryProvider): - root_directory_name = 'dummy' + root_directory = Ref.directory(uri='dummy:/', name='dummy') def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) diff --git a/tests/local/json_test.py b/tests/local/json_test.py index af606c05..9c8686e9 100644 --- a/tests/local/json_test.py +++ b/tests/local/json_test.py @@ -14,23 +14,19 @@ class BrowseCacheTest(unittest.TestCase): self.cache = json._BrowseCache(self.uris) def test_lookup_root(self): - expected = [Ref.directory(uri='/foo', name='foo')] - self.assertEqual(expected, self.cache.lookup('/')) + expected = [Ref.directory(uri='local:directory:foo', name='foo')] + self.assertEqual(expected, self.cache.lookup('local:directory')) def test_lookup_foo(self): - expected = [Ref.directory(uri='/foo/bar', name='bar'), + expected = [Ref.directory(uri='local:directory:foo/bar', name='bar'), Ref.track(uri=self.uris[2], name='song3')] - self.assertEqual(expected, self.cache.lookup('/foo')) + self.assertEqual(expected, self.cache.lookup('local:directory:foo')) def test_lookup_foo_bar(self): expected = [Ref.track(uri=self.uris[0], name='song1'), Ref.track(uri=self.uris[1], name='song2')] - self.assertEqual(expected, self.cache.lookup('/foo/bar')) + self.assertEqual( + expected, self.cache.lookup('local:directory:foo/bar')) def test_lookup_foo_baz(self): - self.assertEqual([], self.cache.lookup('/foo/baz')) - - def test_lookup_normalize_slashes(self): - expected = [Ref.track(uri=self.uris[0], name='song1'), - Ref.track(uri=self.uris[1], name='song2')] - self.assertEqual(expected, self.cache.lookup('/foo//bar/')) + self.assertEqual([], self.cache.lookup('local:directory:foo/baz')) diff --git a/tests/mpd/protocol/test_current_playlist.py b/tests/mpd/protocol/test_current_playlist.py index dbb77d08..e2db8b05 100644 --- a/tests/mpd/protocol/test_current_playlist.py +++ b/tests/mpd/protocol/test_current_playlist.py @@ -27,7 +27,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add_with_empty_uri_should_not_add_anything_and_ok(self): self.backend.library.dummy_library = [Track(uri='dummy:/a', name='a')] self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a')]} self.sendRequest('add ""') self.assertEqual(len(self.core.tracklist.tracks.get()), 0) @@ -35,13 +35,13 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add_with_library_should_recurse(self): tracks = [Track(uri='dummy:/a', name='a'), - Track(uri='dummy:/b', name='b')] + Track(uri='dummy:/foo/b', name='b')] self.backend.library.dummy_library = tracks self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo')], - '/foo': [Ref.track(uri='dummy:/b', name='b')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')], + 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]} self.sendRequest('add "/dummy"') self.assertEqual(self.core.tracklist.tracks.get(), tracks) @@ -50,7 +50,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add_root_should_not_add_anything_and_ok(self): self.backend.library.dummy_library = [Track(uri='dummy:/a', name='a')] self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a')]} self.sendRequest('add "/"') self.assertEqual(len(self.core.tracklist.tracks.get()), 0) diff --git a/tests/mpd/protocol/test_music_db.py b/tests/mpd/protocol/test_music_db.py index d2ecd66c..8d74fb95 100644 --- a/tests/mpd/protocol/test_music_db.py +++ b/tests/mpd/protocol/test_music_db.py @@ -124,34 +124,34 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_listall_without_uri(self): tracks = [Track(uri='dummy:/a', name='a'), - Track(uri='dummy:/b', name='b')] + Track(uri='dummy:/foo/b', name='b')] self.backend.library.dummy_library = tracks self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo')], - '/foo': [Ref.track(uri='dummy:/b', name='b')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')], + 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]} self.sendRequest('listall') self.assertInResponse('file: dummy:/a') self.assertInResponse('directory: /dummy/foo') - self.assertInResponse('file: dummy:/b') + self.assertInResponse('file: dummy:/foo/b') self.assertInResponse('OK') def test_listall_with_uri(self): tracks = [Track(uri='dummy:/a', name='a'), - Track(uri='dummy:/b', name='b')] + Track(uri='dummy:/foo/b', name='b')] self.backend.library.dummy_library = tracks self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo')], - '/foo': [Ref.track(uri='dummy:/b', name='b')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')], + 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]} self.sendRequest('listall "/dummy/foo"') self.assertNotInResponse('file: dummy:/a') self.assertInResponse('directory: /dummy/foo') - self.assertInResponse('file: dummy:/b') + self.assertInResponse('file: dummy:/foo/b') self.assertInResponse('OK') def test_listall_with_unknown_uri(self): @@ -159,39 +159,57 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [50@0] {listall} Not found') + def test_listall_for_dir_with_and_without_leading_slash_is_the_same(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} + + response1 = self.sendRequest('listall "dummy"') + response2 = self.sendRequest('listall "/dummy"') + self.assertEqual(response1, response2) + + def test_listall_for_dir_with_and_without_trailing_slash_is_the_same(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} + + response1 = self.sendRequest('listall "dummy"') + response2 = self.sendRequest('listall "dummy/"') + self.assertEqual(response1, response2) + def test_listallinfo_without_uri(self): tracks = [Track(uri='dummy:/a', name='a'), - Track(uri='dummy:/b', name='b')] + Track(uri='dummy:/foo/b', name='b')] self.backend.library.dummy_library = tracks self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo')], - '/foo': [Ref.track(uri='dummy:/b', name='b')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')], + 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]} self.sendRequest('listallinfo') self.assertInResponse('file: dummy:/a') self.assertInResponse('Title: a') self.assertInResponse('directory: /dummy/foo') - self.assertInResponse('file: dummy:/b') + self.assertInResponse('file: dummy:/foo/b') self.assertInResponse('Title: b') self.assertInResponse('OK') def test_listallinfo_with_uri(self): tracks = [Track(uri='dummy:/a', name='a'), - Track(uri='dummy:/b', name='b')] + Track(uri='dummy:/foo/b', name='b')] self.backend.library.dummy_library = tracks self.backend.library.dummy_browse_result = { - '/': [Ref.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo')], - '/foo': [Ref.track(uri='dummy:/b', name='b')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')], + 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]} self.sendRequest('listallinfo "/dummy/foo"') self.assertNotInResponse('file: dummy:/a') self.assertNotInResponse('Title: a') self.assertInResponse('directory: /dummy/foo') - self.assertInResponse('file: dummy:/b') + self.assertInResponse('file: dummy:/foo/b') self.assertInResponse('Title: b') self.assertInResponse('OK') @@ -200,6 +218,24 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): self.assertEqualResponse('ACK [50@0] {listallinfo} Not found') + def test_listallinfo_for_dir_with_and_without_leading_slash_is_same(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} + + response1 = self.sendRequest('listallinfo "dummy"') + response2 = self.sendRequest('listallinfo "/dummy"') + self.assertEqual(response1, response2) + + def test_listallinfo_for_dir_with_and_without_trailing_slash_is_same(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} + + response1 = self.sendRequest('listallinfo "dummy"') + response2 = self.sendRequest('listallinfo "dummy/"') + self.assertEqual(response1, response2) + 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 = [ @@ -231,8 +267,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.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo', name='foo')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} self.sendRequest('lsinfo "/"') self.assertInResponse('directory: dummy') @@ -240,19 +276,28 @@ 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.track(uri='dummy:/a', name='a'), - Ref.directory(uri='/foo', name='foo')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} response1 = self.sendRequest('lsinfo "dummy"') response2 = self.sendRequest('lsinfo "/dummy"') self.assertEqual(response1, response2) + def test_lsinfo_for_dir_with_and_without_trailing_slash_is_the_same(self): + self.backend.library.dummy_browse_result = { + 'dummy:/': [Ref.track(uri='dummy:/a', name='a'), + Ref.directory(uri='dummy:/foo', name='foo')]} + + 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.track(uri='dummy:/a', name='a')]} + 'dummy:/': [Ref.track(uri='dummy:/a', name='a')]} self.sendRequest('lsinfo "/dummy"') self.assertInResponse('file: dummy:/a') @@ -261,7 +306,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_lsinfo_for_dir_includes_subdirs(self): self.backend.library.dummy_browse_result = { - '/': [Ref.directory(uri='/foo', name='foo')]} + 'dummy:/': [Ref.directory(uri='/foo', name='foo')]} self.sendRequest('lsinfo "/dummy"') self.assertInResponse('directory: dummy/foo')