Merge pull request #647 from adamcik/feature/browse-by-uri

Convert browse API to be fully URI based.
This commit is contained in:
Stein Magnus Jodal 2014-01-17 15:58:39 -08:00
commit da71d7fb14
15 changed files with 245 additions and 170 deletions

View File

@ -37,6 +37,9 @@ class Backend(object):
def has_library(self): def has_library(self):
return self.library is not None 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): def has_playback(self):
return self.playback is not None return self.playback is not None
@ -52,9 +55,12 @@ class LibraryProvider(object):
pykka_traversable = True 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`.* *MUST be set by any class that implements :meth:`LibraryProvider.browse`.*
""" """

View File

@ -88,7 +88,7 @@ class Backends(list):
super(Backends, self).__init__(backends) super(Backends, self).__init__(backends)
self.with_library = collections.OrderedDict() self.with_library = collections.OrderedDict()
self.with_browsable_library = collections.OrderedDict() self.with_library_browse = collections.OrderedDict()
self.with_playback = collections.OrderedDict() self.with_playback = collections.OrderedDict()
self.with_playlists = collections.OrderedDict() self.with_playlists = collections.OrderedDict()
@ -97,6 +97,7 @@ class Backends(list):
for backend in backends: for backend in backends:
has_library = backend.has_library().get() has_library = backend.has_library().get()
has_library_browse = backend.has_library_browse().get()
has_playback = backend.has_playback().get() has_playback = backend.has_playback().get()
has_playlists = backend.has_playlists().get() has_playlists = backend.has_playlists().get()
@ -109,12 +110,9 @@ class Backends(list):
if has_library: if has_library:
self.with_library[scheme] = backend self.with_library[scheme] = backend
if has_library_browse:
self.with_library_browse[scheme] = backend
if has_playback: if has_playback:
self.with_playback[scheme] = backend self.with_playback[scheme] = backend
if has_playlists: if has_playlists:
self.with_playlists[scheme] = backend 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

View File

@ -1,13 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import collections import collections
import re
import urlparse import urlparse
import pykka import pykka
from mopidy.models import Ref
class LibraryController(object): class LibraryController(object):
pykka_traversable = True pykka_traversable = True
@ -32,15 +29,16 @@ class LibraryController(object):
(b, None) for b in self.backends.with_library.values()]) (b, None) for b in self.backends.with_library.values()])
return backends_to_uris 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 ``uri`` is a sring which represents some directory belonging to a
directory in Mopidy's virtual file system. 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 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 The :class:`~mopidy.models.Ref` objects representing tracks keep the
track's original URI. A matching pair of objects can look like this:: 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') Ref.track(uri='dummy:/foo.mp3', name='foo')
The :class:`~mopidy.models.Ref` objects representing directories have The :class:`~mopidy.models.Ref` objects representing directories have
plain paths, not including any URI schema. For example, the dummy backend specific URIs. These are opaque values, so no one but the
library's ``/bar`` directory is returned like this:: 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 Ref.directory(uri='dummy:directory:/bar', name='bar')
by Mopidy core, not the individual backends.
:param path: path to browse :param string uri: URI to browse
:type path: string
:rtype: list of :class:`mopidy.models.Ref` :rtype: list of :class:`mopidy.models.Ref`
""" """
if not path.startswith('/'): if uri is None:
return [] backends = self.backends.with_library_browse.values()
return [b.library.root_directory.get() for b in backends]
if path == '/': scheme = urlparse.urlparse(uri).scheme
return [ backend = self.backends.with_library_browse.get(scheme)
Ref.directory(uri='/%s' % name, name=name)
for name in self.backends.with_browsable_library.keys()]
groups = re.match('/(?P<library>[^/]+)(?P<path>.*)', 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)
if not backend: if not backend:
return [] return []
return backend.library.browse(uri).get()
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
def find_exact(self, query=None, uris=None, **kwargs): def find_exact(self, query=None, uris=None, **kwargs):
""" """

View File

@ -49,41 +49,31 @@ class _BrowseCache(object):
splitpath_re = re.compile(r'([^/]+)') splitpath_re = re.compile(r'([^/]+)')
def __init__(self, uris): def __init__(self, uris):
"""Create a dictionary tree for quick browsing. # {parent_uri: {uri: ref}}
self._cache = {}
{'foo': {'bar': {None: [ref1, ref2]}, for track_uri in uris:
'baz': {}, path = translator.local_track_uri_to_path(track_uri, b'/')
None: [ref3]}}
"""
self._root = collections.OrderedDict()
for uri in uris:
path = translator.local_track_uri_to_path(uri, b'/')
parts = self.splitpath_re.findall( parts = self.splitpath_re.findall(
path.decode(self.encoding, 'replace')) path.decode(self.encoding, 'replace'))
filename = parts.pop() track_ref = models.Ref.track(uri=track_uri, name=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)
def lookup(self, path): parent = 'local:directory'
results = [] for i in range(len(parts)):
node = self._root self._cache.setdefault(parent, collections.OrderedDict())
for part in self.splitpath_re.findall(path): directory = '/'.join(parts[:i+1])
node = node.get(part, {}) 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(): parent = dir_uri
if key is not None:
uri = os.path.join(path, key)
results.append(models.Ref.directory(uri=uri, name=key))
# Get tracks afterwards to ensure ordering. self._cache.setdefault(parent, collections.OrderedDict())
results.extend(node.get(None, [])) self._cache[parent][track_uri] = track_ref
return results def lookup(self, uri):
return self._cache.get(uri, {}).values()
class JsonLibrary(local.Library): class JsonLibrary(local.Library):

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
import logging import logging
from mopidy import backend from mopidy import backend, models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -10,7 +10,8 @@ logger = logging.getLogger(__name__)
class LocalLibraryProvider(backend.LibraryProvider): class LocalLibraryProvider(backend.LibraryProvider):
"""Proxy library that delegates work to our active local library.""" """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): def __init__(self, backend, library):
super(LocalLibraryProvider, self).__init__(backend) super(LocalLibraryProvider, self).__init__(backend)

View File

@ -33,6 +33,13 @@ def path_to_local_track_uri(relpath):
return b'local:track:%s' % urllib.quote(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): def m3u_extinf_to_track(line):
"""Convert extended M3U directive to track template.""" """Convert extended M3U directive to track template."""
m = M3U_EXTINF_RE.match(line) m = M3U_EXTINF_RE.match(line)

View File

@ -297,3 +297,19 @@ class MpdContext(object):
if uri not in self._playlist_name_from_uri: if uri not in self._playlist_name_from_uri:
self.refresh_playlists_mapping() self.refresh_playlists_mapping()
return self._playlist_name_from_uri[uri] 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

View File

@ -27,8 +27,12 @@ def add(context, uri):
if tl_tracks: if tl_tracks:
return return
if not uri.startswith('/'): try:
uri = '/%s' % uri 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)] browse_futures = [context.core.library.browse(uri)]
lookup_futures = [] lookup_futures = []

View File

@ -417,25 +417,33 @@ def listall(context, uri=None):
Lists all songs and directories in ``URI``. Lists all songs and directories in ``URI``.
""" """
if uri is None:
uri = '/'
if not uri.startswith('/'):
uri = '/%s' % uri
result = [] 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: 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: if ref.type == Ref.DIRECTORY:
result.append(('directory', ref.uri)) path = '/'.join([base_path, ref.name.replace('/', '')])
browse_futures.append(context.core.library.browse(ref.uri)) result.append(('directory', path))
browse_futures.append(
(path, context.core.library.browse(ref.uri)))
elif ref.type == Ref.TRACK: elif ref.type == Ref.TRACK:
result.append(('file', ref.uri)) result.append(('file', ref.uri))
if not result: if not result:
raise MpdNoExistError('Not found') raise MpdNoExistError('Not found')
return [('directory', uri)] + result return [('directory', root_path)] + result
@handle_request(r'listallinfo$') @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 as ``listall``, except it also returns metadata info in the
same format as ``lsinfo``. same format as ``lsinfo``.
""" """
if uri is None:
uri = '/'
if not uri.startswith('/'):
uri = '/%s' % uri
dirs_and_futures = [] 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: 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: if ref.type == Ref.DIRECTORY:
dirs_and_futures.append(('directory', ref.uri)) path = '/'.join([base_path, ref.name.replace('/', '')])
browse_futures.append(context.core.library.browse(ref.uri)) future = context.core.library.browse(ref.uri)
browse_futures.append((path, future))
dirs_and_futures.append(('directory', path))
elif ref.type == Ref.TRACK: elif ref.type == Ref.TRACK:
# TODO Lookup tracks in batch for better performance # TODO Lookup tracks in batch for better performance
dirs_and_futures.append(context.core.library.lookup(ref.uri)) dirs_and_futures.append(context.core.library.lookup(ref.uri))
@ -476,7 +491,7 @@ def listallinfo(context, uri=None):
if not result: if not result:
raise MpdNoExistError('Not found') raise MpdNoExistError('Not found')
return [('directory', uri)] + result return [('directory', root_path)] + result
@handle_request(r'lsinfo$') @handle_request(r'lsinfo$')
@ -498,16 +513,21 @@ def lsinfo(context, uri=None):
""``, and ``lsinfo "/"``. ""``, and ``lsinfo "/"``.
""" """
result = [] 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)) result.extend(stored_playlists.listplaylists(context))
uri = '/'
if not uri.startswith('/'):
uri = '/%s' % uri
for ref in context.core.library.browse(uri).get(): for ref in context.core.library.browse(uri).get():
if ref.type == Ref.DIRECTORY: if ref.type == Ref.DIRECTORY:
assert ref.uri.startswith('/'), ( path = '/'.join([root_path, ref.name.replace('/', '')])
'Directory URIs must start with /: %r' % ref) result.append(('directory', path.lstrip('/')))
result.append(('directory', ref.uri[1:]))
elif ref.type == Ref.TRACK: elif ref.type == Ref.TRACK:
# TODO Lookup tracks in batch for better performance # TODO Lookup tracks in batch for better performance
tracks = context.core.library.lookup(ref.uri).get() tracks = context.core.library.lookup(ref.uri).get()

View File

@ -1,11 +1,20 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
import shlex import shlex
from mopidy.mpd.exceptions import MpdArgError from mopidy.mpd.exceptions import MpdArgError
from mopidy.models import TlTrack from mopidy.models import TlTrack
# TODO: special handling of local:// uri scheme # 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): def track_to_mpd_format(track, position=None):

View File

@ -9,32 +9,35 @@ from mopidy.models import Ref, SearchResult, Track
class CoreLibraryTest(unittest.TestCase): class CoreLibraryTest(unittest.TestCase):
def setUp(self): def setUp(self):
dummy1_root = Ref.directory(uri='dummy1:directory', name='dummy1')
self.backend1 = mock.Mock() self.backend1 = mock.Mock()
self.backend1.uri_schemes.get.return_value = ['dummy1'] self.backend1.uri_schemes.get.return_value = ['dummy1']
self.library1 = mock.Mock(spec=backend.LibraryProvider) 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 self.backend1.library = self.library1
dummy2_root = Ref.directory(uri='dummy2:directory', name='dummy2')
self.backend2 = mock.Mock() self.backend2 = mock.Mock()
self.backend2.uri_schemes.get.return_value = ['dummy2'] self.backend2.uri_schemes.get.return_value = ['dummy2']
self.library2 = mock.Mock(spec=backend.LibraryProvider) 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 self.backend2.library = self.library2
# A backend without the optional library provider # A backend without the optional library provider
self.backend3 = mock.Mock() self.backend3 = mock.Mock()
self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.uri_schemes.get.return_value = ['dummy3']
self.backend3.has_library().get.return_value = False 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.core = core.Core(audio=None, backends=[
self.backend1, self.backend2, self.backend3]) self.backend1, self.backend2, self.backend3])
def test_browse_root_returns_dir_ref_for_each_lib_with_root_dir_name(self): 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, [ self.assertEqual(result, [
Ref.directory(uri='/dummy1', name='dummy1'), Ref.directory(uri='dummy1:directory', name='dummy1'),
Ref.directory(uri='/dummy2', name='dummy2'), Ref.directory(uri='dummy2:directory', name='dummy2'),
]) ])
self.assertFalse(self.library1.browse.called) self.assertFalse(self.library1.browse.called)
self.assertFalse(self.library2.browse.called) self.assertFalse(self.library2.browse.called)
@ -49,32 +52,32 @@ class CoreLibraryTest(unittest.TestCase):
def test_browse_dummy1_selects_dummy1_backend(self): def test_browse_dummy1_selects_dummy1_backend(self):
self.library1.browse().get.return_value = [ self.library1.browse().get.return_value = [
Ref.directory(uri='/foo/bar', name='bar'), Ref.directory(uri='dummy1:directory:/foo/bar', name='bar'),
Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
] ]
self.library1.browse.reset_mock() 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.library1.browse.call_count, 1)
self.assertEqual(self.library2.browse.call_count, 0) 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): def test_browse_dummy2_selects_dummy2_backend(self):
self.library2.browse().get.return_value = [ self.library2.browse().get.return_value = [
Ref.directory(uri='/bar/quux', name='quux'), Ref.directory(uri='dummy2:directory:/bar/baz', name='quux'),
Ref.track(uri='dummy2:/foo/baz.mp3', name='Baz'), Ref.track(uri='dummy2:track:/bar/foo.mp3', name='Baz'),
] ]
self.library2.browse.reset_mock() 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.library1.browse.call_count, 0)
self.assertEqual(self.library2.browse.call_count, 1) 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): 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(result, [])
self.assertEqual(self.library1.browse.call_count, 0) 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): def test_browse_dir_returns_subdirs_and_tracks(self):
self.library1.browse().get.return_value = [ self.library1.browse().get.return_value = [
Ref.directory(uri='/foo/bar', name='bar'), Ref.directory(uri='dummy1:directory:/foo/bar', name='Bar'),
Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
] ]
self.library1.browse.reset_mock() self.library1.browse.reset_mock()
result = self.core.library.browse('/dummy1/foo') result = self.core.library.browse('dummy1:directory:/foo')
self.assertEqual(result, [ self.assertEqual(result, [
Ref.directory(uri='/dummy1/foo/bar', name='bar'), Ref.directory(uri='dummy1:directory:/foo/bar', name='Bar'),
Ref.track(uri='dummy1:/foo/baz.mp3', name='Baz'), Ref.track(uri='dummy1:track:/foo/baz.mp3', name='Baz'),
]) ])
def test_lookup_selects_dummy1_backend(self): def test_lookup_selects_dummy1_backend(self):

View File

@ -9,7 +9,7 @@ from __future__ import unicode_literals
import pykka import pykka
from mopidy import backend 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): def create_dummy_backend_proxy(config=None, audio=None):
@ -28,7 +28,7 @@ class DummyBackend(pykka.ThreadingActor, backend.Backend):
class DummyLibraryProvider(backend.LibraryProvider): class DummyLibraryProvider(backend.LibraryProvider):
root_directory_name = 'dummy' root_directory = Ref.directory(uri='dummy:/', name='dummy')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(DummyLibraryProvider, self).__init__(*args, **kwargs) super(DummyLibraryProvider, self).__init__(*args, **kwargs)

View File

@ -14,23 +14,19 @@ class BrowseCacheTest(unittest.TestCase):
self.cache = json._BrowseCache(self.uris) self.cache = json._BrowseCache(self.uris)
def test_lookup_root(self): def test_lookup_root(self):
expected = [Ref.directory(uri='/foo', name='foo')] expected = [Ref.directory(uri='local:directory:foo', name='foo')]
self.assertEqual(expected, self.cache.lookup('/')) self.assertEqual(expected, self.cache.lookup('local:directory'))
def test_lookup_foo(self): 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')] 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): def test_lookup_foo_bar(self):
expected = [Ref.track(uri=self.uris[0], name='song1'), expected = [Ref.track(uri=self.uris[0], name='song1'),
Ref.track(uri=self.uris[1], name='song2')] 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): def test_lookup_foo_baz(self):
self.assertEqual([], self.cache.lookup('/foo/baz')) self.assertEqual([], self.cache.lookup('local:directory: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/'))

View File

@ -27,7 +27,7 @@ class CurrentPlaylistHandlerTest(protocol.BaseTestCase):
def test_add_with_empty_uri_should_not_add_anything_and_ok(self): 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_library = [Track(uri='dummy:/a', name='a')]
self.backend.library.dummy_browse_result = { 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.sendRequest('add ""')
self.assertEqual(len(self.core.tracklist.tracks.get()), 0) 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): def test_add_with_library_should_recurse(self):
tracks = [Track(uri='dummy:/a', name='a'), 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_library = tracks
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo')], Ref.directory(uri='dummy:/foo', name='foo')],
'/foo': [Ref.track(uri='dummy:/b', name='b')]} 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]}
self.sendRequest('add "/dummy"') self.sendRequest('add "/dummy"')
self.assertEqual(self.core.tracklist.tracks.get(), tracks) 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): 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_library = [Track(uri='dummy:/a', name='a')]
self.backend.library.dummy_browse_result = { 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.sendRequest('add "/"')
self.assertEqual(len(self.core.tracklist.tracks.get()), 0) self.assertEqual(len(self.core.tracklist.tracks.get()), 0)

View File

@ -124,34 +124,34 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_listall_without_uri(self): def test_listall_without_uri(self):
tracks = [Track(uri='dummy:/a', name='a'), 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_library = tracks
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo')], Ref.directory(uri='dummy:/foo', name='foo')],
'/foo': [Ref.track(uri='dummy:/b', name='b')]} 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]}
self.sendRequest('listall') self.sendRequest('listall')
self.assertInResponse('file: dummy:/a') self.assertInResponse('file: dummy:/a')
self.assertInResponse('directory: /dummy/foo') self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b') self.assertInResponse('file: dummy:/foo/b')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listall_with_uri(self): def test_listall_with_uri(self):
tracks = [Track(uri='dummy:/a', name='a'), 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_library = tracks
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo')], Ref.directory(uri='dummy:/foo', name='foo')],
'/foo': [Ref.track(uri='dummy:/b', name='b')]} 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]}
self.sendRequest('listall "/dummy/foo"') self.sendRequest('listall "/dummy/foo"')
self.assertNotInResponse('file: dummy:/a') self.assertNotInResponse('file: dummy:/a')
self.assertInResponse('directory: /dummy/foo') self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b') self.assertInResponse('file: dummy:/foo/b')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listall_with_unknown_uri(self): def test_listall_with_unknown_uri(self):
@ -159,39 +159,57 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertEqualResponse('ACK [50@0] {listall} Not found') 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): def test_listallinfo_without_uri(self):
tracks = [Track(uri='dummy:/a', name='a'), 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_library = tracks
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo')], Ref.directory(uri='dummy:/foo', name='foo')],
'/foo': [Ref.track(uri='dummy:/b', name='b')]} 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]}
self.sendRequest('listallinfo') self.sendRequest('listallinfo')
self.assertInResponse('file: dummy:/a') self.assertInResponse('file: dummy:/a')
self.assertInResponse('Title: a') self.assertInResponse('Title: a')
self.assertInResponse('directory: /dummy/foo') self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b') self.assertInResponse('file: dummy:/foo/b')
self.assertInResponse('Title: b') self.assertInResponse('Title: b')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listallinfo_with_uri(self): def test_listallinfo_with_uri(self):
tracks = [Track(uri='dummy:/a', name='a'), 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_library = tracks
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo')], Ref.directory(uri='dummy:/foo', name='foo')],
'/foo': [Ref.track(uri='dummy:/b', name='b')]} 'dummy:/foo': [Ref.track(uri='dummy:/foo/b', name='b')]}
self.sendRequest('listallinfo "/dummy/foo"') self.sendRequest('listallinfo "/dummy/foo"')
self.assertNotInResponse('file: dummy:/a') self.assertNotInResponse('file: dummy:/a')
self.assertNotInResponse('Title: a') self.assertNotInResponse('Title: a')
self.assertInResponse('directory: /dummy/foo') self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b') self.assertInResponse('file: dummy:/foo/b')
self.assertInResponse('Title: b') self.assertInResponse('Title: b')
self.assertInResponse('OK') self.assertInResponse('OK')
@ -200,6 +218,24 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertEqualResponse('ACK [50@0] {listallinfo} Not found') 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): def test_lsinfo_without_path_returns_same_as_for_root(self):
last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345)
self.backend.playlists.playlists = [ 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): def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self):
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo', name='foo')]} Ref.directory(uri='dummy:/foo', name='foo')]}
self.sendRequest('lsinfo "/"') self.sendRequest('lsinfo "/"')
self.assertInResponse('directory: dummy') 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): def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(self):
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='/foo', name='foo')]} Ref.directory(uri='dummy:/foo', name='foo')]}
response1 = self.sendRequest('lsinfo "dummy"') response1 = self.sendRequest('lsinfo "dummy"')
response2 = self.sendRequest('lsinfo "/dummy"') response2 = self.sendRequest('lsinfo "/dummy"')
self.assertEqual(response1, response2) 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): def test_lsinfo_for_dir_includes_tracks(self):
self.backend.library.dummy_library = [ self.backend.library.dummy_library = [
Track(uri='dummy:/a', name='a'), Track(uri='dummy:/a', name='a'),
] ]
self.backend.library.dummy_browse_result = { 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.sendRequest('lsinfo "/dummy"')
self.assertInResponse('file: dummy:/a') self.assertInResponse('file: dummy:/a')
@ -261,7 +306,7 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_lsinfo_for_dir_includes_subdirs(self): def test_lsinfo_for_dir_includes_subdirs(self):
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'/': [Ref.directory(uri='/foo', name='foo')]} 'dummy:/': [Ref.directory(uri='/foo', name='foo')]}
self.sendRequest('lsinfo "/dummy"') self.sendRequest('lsinfo "/dummy"')
self.assertInResponse('directory: dummy/foo') self.assertInResponse('directory: dummy/foo')