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):
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`.*
"""

View File

@ -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

View File

@ -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<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)
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):
"""

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 = []

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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'))

View File

@ -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)

View File

@ -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')