Release v1.0.5
This commit is contained in:
commit
2ae56ed8a6
@ -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)
|
||||
===================
|
||||
|
||||
|
||||
@ -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`.
|
||||
|
||||
@ -30,4 +30,4 @@ except ImportError:
|
||||
warnings.filterwarnings('ignore', 'could not open display')
|
||||
|
||||
|
||||
__version__ = '1.0.4'
|
||||
__version__ = '1.0.5'
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user