Merge pull request #643 from jodal/feature/mpd-listall

Add "listall" and "listallinfo" MPD commands
This commit is contained in:
Thomas Adamcik 2014-01-15 14:44:15 -08:00
commit fe1b01df4d
4 changed files with 124 additions and 15 deletions

View File

@ -18,7 +18,7 @@ v0.18.0 (UNRELEASED)
- Add :meth:`mopidy.core.LibraryController.browse` method for browsing a - Add :meth:`mopidy.core.LibraryController.browse` method for browsing a
virtual file system of tracks. Backends can implement support for this by virtual file system of tracks. Backends can implement support for this by
implementing :meth:`mopidy.backends.base.BaseLibraryController.browse`. implementing :meth:`mopidy.backend.LibraryProvider.browse`.
- Events emitted on play/stop, pause/resume, next/previous and on end of track - Events emitted on play/stop, pause/resume, next/previous and on end of track
has been cleaned up to work consistenly. See the message of has been cleaned up to work consistenly. See the message of
@ -46,6 +46,10 @@ v0.18.0 (UNRELEASED)
Imports from the old locations still works, but are deprecated. Imports from the old locations still works, but are deprecated.
- Add :meth:`mopidy.backend.LibraryProvider.browse`, which can be implemented
by backends that wants to expose directories of tracks in Mopidy's virtual
file system.
**Commands** **Commands**
- Reduce amount of logging from dependencies when using :option:`mopidy -v`. - Reduce amount of logging from dependencies when using :option:`mopidy -v`.
@ -85,7 +89,7 @@ v0.18.0 (UNRELEASED)
**Local backend** **Local backend**
- Added support for browsing local directories. - Added support for browsing local directories in Mopidy's virtual file system.
- Finished the work on creating pluggable libraries. Users can now - Finished the work on creating pluggable libraries. Users can now
reconfigure Mopidy to use alternate library providers of their choosing for reconfigure Mopidy to use alternate library providers of their choosing for
@ -125,9 +129,8 @@ v0.18.0 (UNRELEASED)
**MPD frontend** **MPD frontend**
- Make the ``lsinfo`` command support browsing of Mopidy's virtual file - Make the ``lsinfo``, ``listall``, and ``listallinfo`` commands support
system. Note that the related ``listall`` and ``listallinfo`` commands are browsing of Mopidy's virtual file system. (Fixes: :issue:`145`)
still not implemented.
- Empty commands now return a ``ACK [5@0] {} No command given`` error instead - Empty commands now return a ``ACK [5@0] {} No command given`` error instead
of ``OK``. This is consistent with the original MPD server implementation. of ``OK``. This is consistent with the original MPD server implementation.

View File

@ -47,7 +47,6 @@ near future:
- Modifying stored playlists is not supported - Modifying stored playlists is not supported
- ``tagtypes`` is not supported - ``tagtypes`` is not supported
- Browsing the file system is not supported
- Live update of the music database is not supported - Live update of the music database is not supported

View File

@ -6,7 +6,7 @@ import re
from mopidy.models import Ref, Track from mopidy.models import Ref, Track
from mopidy.mpd import translator from mopidy.mpd import translator
from mopidy.mpd.exceptions import MpdArgError, MpdNotImplemented from mopidy.mpd.exceptions import MpdArgError, MpdNoExistError
from mopidy.mpd.protocol import handle_request, stored_playlists from mopidy.mpd.protocol import handle_request, stored_playlists
@ -417,7 +417,25 @@ def listall(context, uri=None):
Lists all songs and directories in ``URI``. Lists all songs and directories in ``URI``.
""" """
raise MpdNotImplemented # TODO if uri is None:
uri = '/'
if not uri.startswith('/'):
uri = '/%s' % uri
result = []
browse_futures = [context.core.library.browse(uri)]
while browse_futures:
for ref in browse_futures.pop().get():
if ref.type == Ref.DIRECTORY:
result.append(('directory', ref.uri))
browse_futures.append(context.core.library.browse(ref.uri))
elif ref.type == Ref.TRACK:
result.append(('file', ref.uri))
if not result:
raise MpdNoExistError('Not found', command='listall')
return [('directory', uri)] + result
@handle_request(r'listallinfo$') @handle_request(r'listallinfo$')
@ -431,7 +449,34 @@ 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``.
""" """
raise MpdNotImplemented # TODO if uri is None:
uri = '/'
if not uri.startswith('/'):
uri = '/%s' % uri
dirs_and_futures = []
browse_futures = [context.core.library.browse(uri)]
while browse_futures:
for ref in browse_futures.pop().get():
if ref.type == Ref.DIRECTORY:
dirs_and_futures.append(('directory', ref.uri))
browse_futures.append(context.core.library.browse(ref.uri))
elif ref.type == Ref.TRACK:
# TODO Lookup tracks in batch for better performance
dirs_and_futures.append(context.core.library.lookup(ref.uri))
result = []
for obj in dirs_and_futures:
if hasattr(obj, 'get'):
for track in obj.get():
result.extend(translator.track_to_mpd_format(track))
else:
result.append(obj)
if not result:
raise MpdNoExistError('Not found', command='listallinfo')
return [('directory', uri)] + result
@handle_request(r'lsinfo$') @handle_request(r'lsinfo$')

View File

@ -123,20 +123,82 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listall_without_uri(self): def test_listall_without_uri(self):
tracks = [Track(uri='dummy:/a', name='a'),
Track(uri='dummy:/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')]}
self.sendRequest('listall') self.sendRequest('listall')
self.assertEqualResponse('ACK [0@0] {} Not implemented')
self.assertInResponse('file: dummy:/a')
self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b')
self.assertInResponse('OK')
def test_listall_with_uri(self): def test_listall_with_uri(self):
self.sendRequest('listall "file:///dev/urandom"') tracks = [Track(uri='dummy:/a', name='a'),
self.assertEqualResponse('ACK [0@0] {} Not implemented') Track(uri='dummy:/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')]}
self.sendRequest('listall "/dummy/foo"')
self.assertNotInResponse('file: dummy:/a')
self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b')
self.assertInResponse('OK')
def test_listall_with_unknown_uri(self):
self.sendRequest('listall "/unknown"')
self.assertEqualResponse('ACK [50@0] {listall} Not found')
def test_listallinfo_without_uri(self): def test_listallinfo_without_uri(self):
tracks = [Track(uri='dummy:/a', name='a'),
Track(uri='dummy:/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')]}
self.sendRequest('listallinfo') self.sendRequest('listallinfo')
self.assertEqualResponse('ACK [0@0] {} Not implemented')
self.assertInResponse('file: dummy:/a')
self.assertInResponse('Title: a')
self.assertInResponse('directory: /dummy/foo')
self.assertInResponse('file: dummy:/b')
self.assertInResponse('Title: b')
self.assertInResponse('OK')
def test_listallinfo_with_uri(self): def test_listallinfo_with_uri(self):
self.sendRequest('listallinfo "file:///dev/urandom"') tracks = [Track(uri='dummy:/a', name='a'),
self.assertEqualResponse('ACK [0@0] {} Not implemented') Track(uri='dummy:/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')]}
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('Title: b')
self.assertInResponse('OK')
def test_listallinfo_with_unknown_uri(self):
self.sendRequest('listallinfo "/unknown"')
self.assertEqualResponse('ACK [50@0] {listallinfo} Not found')
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)