Release v1.0.5

This commit is contained in:
Stein Magnus Jodal 2015-05-19 22:07:22 +02:00
commit 2ae56ed8a6
9 changed files with 102 additions and 217 deletions

View File

@ -5,6 +5,18 @@ Changelog
This changelog is used to track all major changes to Mopidy.
v1.0.5 (2015-05-19)
===================
Bug fix release.
- Core: Add workaround for playlist providers that do not support
creating playlists. (Fixes: :issue:`1162`, PR :issue:`1165`)
- M3U: Fix encoding error when saving playlists with non-ASCII track
titles. (Fixes: :issue:`1175`, PR :issue:`1176`)
v1.0.4 (2015-04-30)
===================

View File

@ -12,38 +12,8 @@ http://mpd.wikia.com/wiki/Clients.
:local:
Test procedure
==============
In some cases, we've used the following test procedure to compare the feature
completeness of clients:
#. Connect to Mopidy
#. Search for "foo", with search type "any" if it can be selected
#. Add "The Pretender" from the search results to the current playlist
#. Start playback
#. Pause and resume playback
#. Adjust volume
#. Find a playlist and append it to the current playlist
#. Skip to next track
#. Skip to previous track
#. Select the last track from the current playlist
#. Turn on repeat mode
#. Seek to 10 seconds or so before the end of the track
#. Wait for the end of the track and confirm that playback continues at the
start of the playlist
#. Turn off repeat mode
#. Turn on random mode
#. Skip to next track and confirm that it random mode works
#. Turn off random mode
#. Stop playback
#. Check if the app got support for single mode and consume mode
#. Kill Mopidy and confirm that the app handles it without crashing
Console clients
===============
MPD console clients
===================
ncmpcpp
-------
@ -83,8 +53,8 @@ A command line client. Version 0.16 and upwards seems to work nicely with
Mopidy.
Graphical clients
=================
MPD graphical clients
=====================
GMPC
----
@ -132,22 +102,12 @@ client for OS X. It is unmaintained, but generally works well with Mopidy.
.. _android_mpd_clients:
Android clients
===============
We've tested all five MPD clients we could find for Android with Mopidy 0.8.1
on a Samsung Galaxy Nexus with Android 4.1.2, using our standard test
procedure.
MPD Android clients
===================
MPDroid
-------
Test date:
2012-11-06
Tested version:
1.03.1 (released 2012-10-16)
.. image:: mpd-client-mpdroid.jpg
:width: 288
:height: 512
@ -155,128 +115,17 @@ Tested version:
You can get `MPDroid from Google Play
<https://play.google.com/store/apps/details?id=com.namelessdev.mpdroid>`_.
- MPDroid started out as a fork of PMix, and is now much better.
- MPDroid's user interface looks nice.
- Everything in the test procedure works.
- In contrast to all other Android clients, MPDroid does support single mode or
consume mode.
- When Mopidy is killed, MPDroid handles it gracefully and asks if you want to
try to reconnect.
MPDroid is a good MPD client, and really the only one we can recommend.
BitMPC
------
Test date:
2012-11-06
Tested version:
1.0.0 (released 2010-04-12)
You can get `BitMPC from Google Play
<https://play.google.com/store/apps/details?id=bitendian.bitmpc>`_.
- The user interface lacks some finishing touches. E.g. you can't enter a
hostname for the server. Only IPv4 addresses are allowed.
- When we last tested the same version of BitMPC using Android 2.1:
- All features exercised in the test procedure worked.
- BitMPC lacked support for single mode and consume mode.
- BitMPC crashed if Mopidy was killed or crashed.
- When we tried to test using Android 4.1.1, BitMPC started and connected to
Mopidy without problems, but the app crashed as soon as we fired off our
search, and continued to crash on startup after that.
In conclusion, BitMPC is usable if you got an older Android phone and don't
care about looks. For newer Android versions, BitMPC will probably not work as
it hasn't been maintained for 2.5 years.
Droid MPD Client
----------------
Test date:
2012-11-06
Tested version:
1.4.0 (released 2011-12-20)
You can get `Droid MPD Client from Google Play
<https://play.google.com/store/apps/details?id=com.soreha.droidmpdclient>`_.
- No intutive way to ask the app to connect to the server after adding the
server hostname to the settings.
- To find the search functionality, you have to select the menu,
then "Playlist manager", then the search tab. I do not understand why search
is hidden inside "Playlist manager".
- The tabs "Artists" and "Albums" did not contain anything, and did not cause
any requests.
- The tab "Folders" showed a spinner and said "Updating data..." but did not
send any requests.
- Searching for "foo" did nothing. No request was sent to the server.
- Droid MPD client does not support single mode or consume mode.
- Not able to complete the test procedure, due to the above problems.
In conclusion, not a client we can recommend.
PMix
----
Test date:
2012-11-06
Tested version:
0.4.0 (released 2010-03-06)
You can get `PMix from Google Play
<https://play.google.com/store/apps/details?id=org.pmix.ui>`_.
PMix haven't been updated for 2.5 years, and has less working features than
it's fork MPDroid. Ignore PMix and use MPDroid instead.
MPD Remote
----------
Test date:
2012-11-06
Tested version:
1.0 (released 2012-05-01)
You can get `MPD Remote from Google Play
<https://play.google.com/store/apps/details?id=fr.mildlyusefulsoftware.mpdremote>`_.
This app looks terrible in the screen shots, got just 100+ downloads, and got a
terrible rating. I honestly didn't take the time to test it.
.. _ios_mpd_clients:
iOS clients
===========
MPD iOS clients
===============
MPoD
----
Test date:
2012-11-06
Tested version:
1.7.1
.. image:: mpd-client-mpod.jpg
:width: 320
:height: 480
@ -285,26 +134,10 @@ The `MPoD <http://www.katoemba.net/makesnosenseatall/mpod/>`_ iPhone/iPod Touch
app can be installed from `MPoD at iTunes Store
<https://itunes.apple.com/us/app/mpod/id285063020>`_.
- The user interface looks nice.
- All features exercised in the test procedure worked with MPaD, except seek,
which I didn't figure out to do.
- Search only works in the "Browse" tab, and not under in the "Artist",
"Album", or "Song" tabs. For the tabs where search doesn't work, no queries
are sent to Mopidy when searching.
- Single mode and consume mode is supported.
MPaD
----
Test date:
2012-11-06
Tested version:
1.7.1
.. image:: mpd-client-mpad.jpg
:width: 480
:height: 360
@ -313,25 +146,11 @@ The `MPaD <http://www.katoemba.net/makesnosenseatall/mpad/>`_ iPad app can be
purchased from `MPaD at iTunes Store
<https://itunes.apple.com/us/app/mpad/id423097706>`_
- The user interface looks nice, though I would like to be able to view the
current playlist in the large part of the split view.
- All features exercised in the test procedure worked with MPaD.
- Search only works in the "Browse" tab, and not under in the "Artist",
"Album", or "Song" tabs. For the tabs where search doesn't work, no queries
are sent to Mopidy when searching.
- Single mode and consume mode is supported.
- The server menu can be very slow top open, and there is no visible feedback
when waiting for the connection to a server to succeed.
.. _mpd-web-clients:
Web clients
===========
MPD web clients
===============
The following web clients use the MPD protocol to communicate with Mopidy. For
other web clients, see :ref:`http-clients`.

View File

@ -30,4 +30,4 @@ except ImportError:
warnings.filterwarnings('ignore', 'could not open display')
__version__ = '1.0.4'
__version__ = '1.0.5'

View File

@ -118,13 +118,19 @@ class PlaylistsController(object):
:rtype: :class:`mopidy.models.Playlist`
"""
if uri_scheme in self.backends.with_playlists:
backend = self.backends.with_playlists[uri_scheme]
backends = [self.backends.with_playlists[uri_scheme]]
else:
# TODO: this fallback looks suspicious
backend = list(self.backends.with_playlists.values())[0]
playlist = backend.playlists.create(name).get()
listener.CoreListener.send('playlist_changed', playlist=playlist)
return playlist
backends = self.backends.with_playlists.values()
for backend in backends:
try:
playlist = backend.playlists.create(name).get()
except Exception:
playlist = None
# Workaround for playlist providers that return None from create()
if not playlist:
continue
listener.CoreListener.send('playlist_changed', playlist=playlist)
return playlist
def delete(self, uri):
"""

View File

@ -88,11 +88,6 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider):
self._playlists[playlist.uri] = playlist
return playlist
def _write_m3u_extinf(self, file_handle, track):
title = track.name.encode('latin-1', 'replace')
runtime = track.length // 1000 if track.length else -1
file_handle.write('#EXTINF:' + str(runtime) + ',' + title + '\n')
def _sanitize_m3u_name(self, name, encoding=sys.getfilesystemencoding()):
name = self._invalid_filename_chars.sub('|', name.strip())
# make sure we end up with a valid path segment
@ -113,15 +108,6 @@ class M3UPlaylistsProvider(backend.PlaylistsProvider):
name, _ = os.path.splitext(os.path.basename(path).decode(encoding))
else:
raise ValueError('M3U playlist needs name or URI')
extended = any(track.name for track in playlist.tracks)
with open(path, 'w') as file_handle:
if extended:
file_handle.write('#EXTM3U\n')
for track in playlist.tracks:
if extended and track.name:
self._write_m3u_extinf(file_handle, track)
file_handle.write(track.uri + '\n')
translator.save_m3u(path, playlist.tracks, 'latin1')
# assert playlist name matches file name/uri
return playlist.copy(uri=uri, name=name)

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals
import codecs
import logging
import os
import re
@ -108,3 +109,18 @@ def parse_m3u(file_path, media_dir=None):
track = Track()
return tracks
def save_m3u(filename, tracks, encoding='latin1', errors='replace'):
extended = any(track.name for track in tracks)
# codecs.open() always uses binary mode, just being explicit here
with codecs.open(filename, 'wb', encoding, errors) as m3u:
if extended:
m3u.write('#EXTM3U' + os.linesep)
for track in tracks:
if extended and track.name:
m3u.write('#EXTINF:%d,%s%s' % (
track.length // 1000 if track.length else -1,
track.name,
os.linesep))
m3u.write(track.uri + os.linesep)

View File

@ -118,6 +118,32 @@ class PlaylistsTest(unittest.TestCase):
self.sp1.create.assert_called_once_with('foo')
self.assertFalse(self.sp2.create.called)
def test_create_without_uri_scheme_ignores_none_result(self):
playlist = Playlist()
self.sp1.create().get.return_value = None
self.sp1.reset_mock()
self.sp2.create().get.return_value = playlist
self.sp2.reset_mock()
result = self.core.playlists.create('foo')
self.assertEqual(playlist, result)
self.sp1.create.assert_called_once_with('foo')
self.sp2.create.assert_called_once_with('foo')
def test_create_without_uri_scheme_ignores_exception(self):
playlist = Playlist()
self.sp1.create().get.side_effect = Exception
self.sp1.reset_mock()
self.sp2.create().get.return_value = playlist
self.sp2.reset_mock()
result = self.core.playlists.create('foo')
self.assertEqual(playlist, result)
self.sp1.create.assert_called_once_with('foo')
self.sp2.create.assert_called_once_with('foo')
def test_create_with_uri_scheme_selects_the_matching_backend(self):
playlist = Playlist()
self.sp2.create().get.return_value = playlist

View File

@ -107,9 +107,28 @@ class M3UPlaylistsProviderTest(unittest.TestCase):
path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
with open(path) as f:
contents = f.read().splitlines()
m3u = f.read().splitlines()
self.assertEqual(['#EXTM3U', '#EXTINF:60,Test', track.uri], m3u)
self.assertEqual(contents, ['#EXTM3U', '#EXTINF:60,Test', track.uri])
def test_latin1_playlist_contents_is_written_to_disk(self):
track = Track(uri=generate_song(1), name='Test\x9f', length=60000)
playlist = self.core.playlists.create('test')
playlist = self.core.playlists.save(playlist.copy(tracks=[track]))
path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
with open(path, 'rb') as f:
m3u = f.read().splitlines()
self.assertEqual([b'#EXTM3U', b'#EXTINF:60,Test\x9f', track.uri], m3u)
def test_utf8_playlist_contents_is_replaced_and_written_to_disk(self):
track = Track(uri=generate_song(1), name='Test\u07b4', length=60000)
playlist = self.core.playlists.create('test')
playlist = self.core.playlists.save(playlist.copy(tracks=[track]))
path = playlist_uri_to_path(playlist.uri, self.playlists_dir)
with open(path, 'rb') as f:
m3u = f.read().splitlines()
self.assertEqual([b'#EXTM3U', b'#EXTINF:60,Test?', track.uri], m3u)
def test_playlists_are_loaded_at_startup(self):
track = Track(uri='dummy:track:path2')

View File

@ -59,5 +59,6 @@ class VersionTest(unittest.TestCase):
self.assertVersionLess('1.0.0', '1.0.1')
self.assertVersionLess('1.0.1', '1.0.2')
self.assertVersionLess('1.0.2', '1.0.3')
self.assertVersionLess('1.0.3', __version__)
self.assertVersionLess(__version__, '1.0.5')
self.assertVersionLess('1.0.3', '1.0.4')
self.assertVersionLess('1.0.4', __version__)
self.assertVersionLess(__version__, '1.0.6')