From 636639a201bd9cfea36af53e7e4ec2c72d2d479f Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Wed, 6 May 2015 14:50:21 +0200 Subject: [PATCH 01/10] Fix #1162: Ignore None results and exceptions from PlaylistsProvider.create(). --- docs/changelog.rst | 9 +++++++++ mopidy/core/playlists.py | 18 ++++++++++++------ tests/core/test_playlists.py | 26 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 10c413ec..00cee7b8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,15 @@ Changelog This changelog is used to track all major changes to Mopidy. +v1.0.5 (UNRELEASED) +=================== + +Bug fix release. + +- Core: Add workaround for playlist providers that do not support + creating playlists. (Fixes: :issue:`1162`, PR :issue:`1165`) + + v1.0.4 (2015-04-30) =================== diff --git a/mopidy/core/playlists.py b/mopidy/core/playlists.py index 669e1f35..1b4c2692 100644 --- a/mopidy/core/playlists.py +++ b/mopidy/core/playlists.py @@ -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): """ diff --git a/tests/core/test_playlists.py b/tests/core/test_playlists.py index 081f73e6..e02f6204 100644 --- a/tests/core/test_playlists.py +++ b/tests/core/test_playlists.py @@ -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 From ea5dff109ec8a8ea1d3b3dd086a2b8242bdf0bcb Mon Sep 17 00:00:00 2001 From: Thomas Kemmer Date: Sun, 10 May 2015 20:57:39 +0200 Subject: [PATCH 02/10] m3u: Fix encoding error when saving playlists with non-ASCII track titles. --- docs/changelog.rst | 3 +++ mopidy/m3u/playlists.py | 16 +--------------- mopidy/m3u/translator.py | 16 ++++++++++++++++ tests/m3u/test_playlists.py | 23 +++++++++++++++++++++-- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 00cee7b8..d87b16a9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,9 @@ 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) =================== diff --git a/mopidy/m3u/playlists.py b/mopidy/m3u/playlists.py index c09eccdf..8800e468 100644 --- a/mopidy/m3u/playlists.py +++ b/mopidy/m3u/playlists.py @@ -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) diff --git a/mopidy/m3u/translator.py b/mopidy/m3u/translator.py index 4eefce9d..a6e006b1 100644 --- a/mopidy/m3u/translator.py +++ b/mopidy/m3u/translator.py @@ -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) diff --git a/tests/m3u/test_playlists.py b/tests/m3u/test_playlists.py index 355aabf5..b7ac827f 100644 --- a/tests/m3u/test_playlists.py +++ b/tests/m3u/test_playlists.py @@ -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') From f8c99b5daba2a3796952ff2bccb6ee7367324112 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:35:11 +0200 Subject: [PATCH 03/10] docs: Remove PMix from MPD client list It is no longer available on Google Play, and we didn't recommend it 2.5y ago when it was. (cherry picked from commit d37b76f6c94a781f5b48a0e6719e08a6b69a6513) --- docs/clients/mpd.rst | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 91d0f8db..9ff888bc 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -234,21 +234,6 @@ You can get `Droid MPD Client from Google Play 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 -`_. - -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 ---------- From 2ed019b69fff20994c3c26f42db75a2f7229f82e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:36:24 +0200 Subject: [PATCH 04/10] docs: Remove MPD Remote from MPD client list It looked terrible 2.5y ago and I didn't care to test it. It has seen no updates since. (cherry picked from commit c95b3016965c6c1f4f1b49ad697435938caccb5b) --- docs/clients/mpd.rst | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 9ff888bc..760d2f6e 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -234,21 +234,6 @@ You can get `Droid MPD Client from Google Play In conclusion, not a client we can recommend. -MPD Remote ----------- - -Test date: - 2012-11-06 -Tested version: - 1.0 (released 2012-05-01) - -You can get `MPD Remote from Google Play -`_. - -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 From 2f05c1cff34db41d7cf8b529624e3d2b5d39864e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:37:51 +0200 Subject: [PATCH 05/10] docs: Remove bitMPC from MPD client list It looked bad and only worked on Android 2.x when tested 2.5y ago. It has seen no updates since 2010. (cherry picked from commit 0b3b17e3a66e6ae6a1771a5ee4a92d594ee5f1b8) --- docs/clients/mpd.rst | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 760d2f6e..4d7a8881 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -170,37 +170,6 @@ You can get `MPDroid from Google Play 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 -`_. - -- 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 ---------------- From 85d85739ce45be88b492496533af7f90b2f5470c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:39:44 +0200 Subject: [PATCH 06/10] docs: Remove Droid MPD Client from MPD client list We couldn't recommend it 2.5y ago, and it has seen no updates since. (cherry picked from commit 0273b14c70f7ca63cfaf020a55225172134aad0b) --- docs/clients/mpd.rst | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 4d7a8881..36af7b84 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -170,39 +170,6 @@ You can get `MPDroid from Google Play MPDroid is a good MPD client, and really the only one we can recommend. -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 -`_. - -- 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. - - .. _ios_mpd_clients: iOS clients From 8027befe9c995b5224f9bb8184bad9cd473551d1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:43:25 +0200 Subject: [PATCH 07/10] docs: Include 'MPD' in the subsection headers (cherry picked from commit b03d3a8a1c61f3f5074501da1910d0f6337d8441) --- docs/clients/mpd.rst | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 36af7b84..ed1df581 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -41,9 +41,8 @@ completeness of clients: #. Kill Mopidy and confirm that the app handles it without crashing - -Console clients -=============== +MPD console clients +=================== ncmpcpp ------- @@ -83,8 +82,8 @@ A command line client. Version 0.16 and upwards seems to work nicely with Mopidy. -Graphical clients -================= +MPD graphical clients +===================== GMPC ---- @@ -132,8 +131,8 @@ client for OS X. It is unmaintained, but generally works well with Mopidy. .. _android_mpd_clients: -Android clients -=============== +MPD 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 @@ -172,8 +171,8 @@ MPDroid is a good MPD client, and really the only one we can recommend. .. _ios_mpd_clients: -iOS clients -=========== +MPD iOS clients +=============== MPoD ---- @@ -236,8 +235,8 @@ purchased from `MPaD at iTunes Store .. _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`. From 14b9c12d093912cf0cc4c6441d8f8e929d970138 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 May 2015 21:48:19 +0200 Subject: [PATCH 08/10] docs: Remove MPD client test procedure and outdated results (cherry picked from commit 4ede30436a32e65461053cf686c1f3606e5389a2) --- docs/clients/mpd.rst | 86 -------------------------------------------- 1 file changed, 86 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index ed1df581..fe7ef21d 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -12,35 +12,6 @@ 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 - - MPD console clients =================== @@ -134,19 +105,9 @@ client for OS X. It is unmaintained, but generally works well with Mopidy. MPD 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. - - MPDroid ------- -Test date: - 2012-11-06 -Tested version: - 1.03.1 (released 2012-10-16) - .. image:: mpd-client-mpdroid.jpg :width: 288 :height: 512 @@ -154,18 +115,6 @@ Tested version: You can get `MPDroid from Google Play `_. -- 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. @@ -177,11 +126,6 @@ MPD iOS clients MPoD ---- -Test date: - 2012-11-06 -Tested version: - 1.7.1 - .. image:: mpd-client-mpod.jpg :width: 320 :height: 480 @@ -190,26 +134,10 @@ The `MPoD `_ iPhone/iPod Touch app can be installed from `MPoD at iTunes Store `_. -- 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 @@ -218,20 +146,6 @@ The `MPaD `_ iPad app can be purchased from `MPaD at iTunes Store `_ -- 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: From 7023bcded6b3b41f704071ba5d6eaae3d337f449 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 19 May 2015 22:04:37 +0200 Subject: [PATCH 09/10] docs: Update changelog for v1.0.5 --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d87b16a9..4aad8690 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog This changelog is used to track all major changes to Mopidy. -v1.0.5 (UNRELEASED) +v1.0.5 (2015-05-19) =================== Bug fix release. From b0a776114d245a4f6117c6fdc83816841d46cdab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 19 May 2015 22:05:20 +0200 Subject: [PATCH 10/10] Bump version to 1.0.5 --- mopidy/__init__.py | 2 +- tests/test_version.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 0bc5410e..802a44d4 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -30,4 +30,4 @@ except ImportError: warnings.filterwarnings('ignore', 'could not open display') -__version__ = '1.0.4' +__version__ = '1.0.5' diff --git a/tests/test_version.py b/tests/test_version.py index 37d0b459..ed413cc1 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -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')