mpd: Faster playlist listing

This commit is contained in:
Stein Magnus Jodal 2015-08-06 01:00:00 +02:00
parent 2faf6689c1
commit c8d31e94b7
4 changed files with 51 additions and 28 deletions

View File

@ -119,6 +119,14 @@ MPD frontend
- Implement protocol extensions to output Album URIs and Album Images when - Implement protocol extensions to output Album URIs and Album Images when
outputting track data to clients. (PR: :issue:`1230`) outputting track data to clients. (PR: :issue:`1230`)
- The MPD commands ``lsinfo`` and ``listplaylists`` are now implemented using
the :meth:`~mopidy.core.PlaylistsProvider.as_list` method, which retrieves a
lot less data and is thus much faster than the deprecated
:meth:`~mopidy.core.PlaylistsProvider.get_playlists`. The drawback is that
the ``Last-Modified`` timestamp is not available through this method, and the
timestamps in the MPD command responses are now always set to the current
time.
Stream backend Stream backend
-------------- --------------

View File

@ -75,29 +75,29 @@ def listplaylists(context):
- ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must - ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must
ignore playlists without names, which isn't very useful anyway. ignore playlists without names, which isn't very useful anyway.
""" """
last_modified = _get_last_modified()
result = [] result = []
for playlist in context.core.playlists.get_playlists().get(): for playlist_ref in context.core.playlists.as_list().get():
if not playlist.name: if not playlist_ref.name:
continue continue
name = context.lookup_playlist_name_from_uri(playlist.uri) name = context.lookup_playlist_name_from_uri(playlist_ref.uri)
result.append(('playlist', name)) result.append(('playlist', name))
result.append(('Last-Modified', _get_last_modified(playlist))) result.append(('Last-Modified', last_modified))
return result return result
# TODO: move to translators? # TODO: move to translators?
def _get_last_modified(playlist): def _get_last_modified(last_modified=None):
"""Formats last modified timestamp of a playlist for MPD. """Formats last modified timestamp of a playlist for MPD.
Time in UTC with second precision, formatted in the ISO 8601 format, with Time in UTC with second precision, formatted in the ISO 8601 format, with
the "Z" time zone marker for UTC. For example, "1970-01-01T00:00:00Z". the "Z" time zone marker for UTC. For example, "1970-01-01T00:00:00Z".
""" """
if playlist.last_modified is None: if last_modified is None:
# If unknown, assume the playlist is modified # If unknown, assume the playlist is modified
dt = datetime.datetime.utcnow() dt = datetime.datetime.utcnow()
else: else:
dt = datetime.datetime.utcfromtimestamp( dt = datetime.datetime.utcfromtimestamp(last_modified / 1000.0)
playlist.last_modified / 1000.0)
dt = dt.replace(microsecond=0) dt = dt.replace(microsecond=0)
return '%sZ' % dt.isoformat() return '%sZ' % dt.isoformat()

View File

@ -2,8 +2,10 @@ from __future__ import absolute_import, unicode_literals
import unittest import unittest
import mock
from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track
from mopidy.mpd.protocol import music_db from mopidy.mpd.protocol import music_db, stored_playlists
from tests.mpd import protocol from tests.mpd import protocol
@ -299,33 +301,37 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.send_request('listfiles') self.send_request('listfiles')
self.assertEqualResponse('ACK [0@0] {listfiles} Not implemented') self.assertEqualResponse('ACK [0@0] {listfiles} Not implemented')
def test_lsinfo_without_path_returns_same_as_for_root(self): @mock.patch.object(stored_playlists, '_get_last_modified')
last_modified = 1390942873222 def test_lsinfo_without_path_returns_same_as_for_root(
self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.playlists.set_dummy_playlists([ self.backend.playlists.set_dummy_playlists([
Playlist(name='a', uri='dummy:/a', last_modified=last_modified)]) Playlist(name='a', uri='dummy:/a')])
response1 = self.send_request('lsinfo') response1 = self.send_request('lsinfo')
response2 = self.send_request('lsinfo "/"') response2 = self.send_request('lsinfo "/"')
self.assertEqual(response1, response2) self.assertEqual(response1, response2)
def test_lsinfo_with_empty_path_returns_same_as_for_root(self): @mock.patch.object(stored_playlists, '_get_last_modified')
last_modified = 1390942873222 def test_lsinfo_with_empty_path_returns_same_as_for_root(
self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.playlists.set_dummy_playlists([ self.backend.playlists.set_dummy_playlists([
Playlist(name='a', uri='dummy:/a', last_modified=last_modified)]) Playlist(name='a', uri='dummy:/a')])
response1 = self.send_request('lsinfo ""') response1 = self.send_request('lsinfo ""')
response2 = self.send_request('lsinfo "/"') response2 = self.send_request('lsinfo "/"')
self.assertEqual(response1, response2) self.assertEqual(response1, response2)
def test_lsinfo_for_root_includes_playlists(self): @mock.patch.object(stored_playlists, '_get_last_modified')
last_modified = 1390942873222 def test_lsinfo_for_root_includes_playlists(self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.playlists.set_dummy_playlists([ self.backend.playlists.set_dummy_playlists([
Playlist(name='a', uri='dummy:/a', last_modified=last_modified)]) Playlist(name='a', uri='dummy:/a')])
self.send_request('lsinfo "/"') self.send_request('lsinfo "/"')
self.assertInResponse('playlist: a') self.assertInResponse('playlist: a')
# Date without milliseconds and with time zone information self.assertInResponse('Last-Modified: 2015-08-05T22:51:06Z')
self.assertInResponse('Last-Modified: 2014-01-28T21:01:13Z')
self.assertInResponse('OK') self.assertInResponse('OK')
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):
@ -337,7 +343,10 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('directory: dummy') self.assertInResponse('directory: dummy')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(self): @mock.patch.object(stored_playlists, '_get_last_modified')
def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(
self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'dummy:/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='dummy:/foo', name='foo')]} Ref.directory(uri='dummy:/foo', name='foo')]}
@ -346,7 +355,10 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
response2 = self.send_request('lsinfo "/dummy"') response2 = self.send_request('lsinfo "/dummy"')
self.assertEqual(response1, response2) self.assertEqual(response1, response2)
def test_lsinfo_for_dir_with_and_without_trailing_slash_is_the_same(self): @mock.patch.object(stored_playlists, '_get_last_modified')
def test_lsinfo_for_dir_with_and_without_trailing_slash_is_the_same(
self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'dummy:/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='dummy:/foo', name='foo')]} Ref.directory(uri='dummy:/foo', name='foo')]}
@ -404,12 +416,11 @@ class MusicDatabaseHandlerTest(protocol.BaseTestCase):
self.assertInResponse('OK') self.assertInResponse('OK')
def test_lsinfo_for_root_returns_browse_result_before_playlists(self): def test_lsinfo_for_root_returns_browse_result_before_playlists(self):
last_modified = 1390942873222
self.backend.library.dummy_browse_result = { self.backend.library.dummy_browse_result = {
'dummy:/': [Ref.track(uri='dummy:/a', name='a'), 'dummy:/': [Ref.track(uri='dummy:/a', name='a'),
Ref.directory(uri='dummy:/foo', name='foo')]} Ref.directory(uri='dummy:/foo', name='foo')]}
self.backend.playlists.set_dummy_playlists([ self.backend.playlists.set_dummy_playlists([
Playlist(name='a', uri='dummy:/a', last_modified=last_modified)]) Playlist(name='a', uri='dummy:/a')])
response = self.send_request('lsinfo "/"') response = self.send_request('lsinfo "/"')
self.assertLess(response.index('directory: dummy'), self.assertLess(response.index('directory: dummy'),

View File

@ -1,6 +1,9 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import mock
from mopidy.models import Playlist, Track from mopidy.models import Playlist, Track
from mopidy.mpd.protocol import stored_playlists
from tests.mpd import protocol from tests.mpd import protocol
@ -76,15 +79,16 @@ class PlaylistsHandlerTest(protocol.BaseTestCase):
self.assertNotInResponse('Pos: 0') self.assertNotInResponse('Pos: 0')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listplaylists(self): @mock.patch.object(stored_playlists, '_get_last_modified')
last_modified = 1390942873222 def test_listplaylists(self, last_modified_mock):
last_modified_mock.return_value = '2015-08-05T22:51:06Z'
self.backend.playlists.set_dummy_playlists([ self.backend.playlists.set_dummy_playlists([
Playlist(name='a', uri='dummy:a', last_modified=last_modified)]) Playlist(name='a', uri='dummy:a')])
self.send_request('listplaylists') self.send_request('listplaylists')
self.assertInResponse('playlist: a') self.assertInResponse('playlist: a')
# Date without milliseconds and with time zone information # Date without milliseconds and with time zone information
self.assertInResponse('Last-Modified: 2014-01-28T21:01:13Z') self.assertInResponse('Last-Modified: 2015-08-05T22:51:06Z')
self.assertInResponse('OK') self.assertInResponse('OK')
def test_listplaylists_duplicate(self): def test_listplaylists_duplicate(self):