From 0e66ffe6a5a7f227fd08ac94a98097939ddef4ab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Aug 2012 23:12:04 +0200 Subject: [PATCH 1/7] Add locale_decode util function that decodes bytestrings using the current locale's encoding --- mopidy/utils/__init__.py | 7 +++++++ tests/utils/decode_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/utils/decode_test.py diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index 9d7532a0..00129cdd 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -1,3 +1,4 @@ +import locale import logging import os import sys @@ -29,3 +30,9 @@ def get_class(name): except (ImportError, AttributeError): raise ImportError("Couldn't load: %s" % name) return class_object + +def locale_decode(bytestr): + try: + return unicode(bytestr) + except UnicodeError: + return str(bytestr).decode(locale.getpreferredencoding()) diff --git a/tests/utils/decode_test.py b/tests/utils/decode_test.py new file mode 100644 index 00000000..edbfe651 --- /dev/null +++ b/tests/utils/decode_test.py @@ -0,0 +1,38 @@ +import mock + +from mopidy.utils import locale_decode + +from tests import unittest + + +@mock.patch('mopidy.utils.locale.getpreferredencoding') +class LocaleDecodeTest(unittest.TestCase): + def test_can_decode_utf8_strings_with_french_content(self, mock): + mock.return_value = 'UTF-8' + + result = locale_decode( + '[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') + + self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + + def test_can_decode_an_ioerror_with_french_content(self, mock): + mock.return_value = 'UTF-8' + + error = IOError(98, 'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') + result = locale_decode(error) + + self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + + def test_does_not_use_locale_to_decode_unicode_strings(self, mock): + mock.return_value = 'UTF-8' + + locale_decode(u'abc') + + self.assertFalse(mock.called) + + def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock): + mock.return_value = 'UTF-8' + + locale_decode('abc') + + self.assertFalse(mock.called) From d6f17b4cf00868ceaac59d1867ba9ccfabcc8ede Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Aug 2012 23:17:41 +0200 Subject: [PATCH 2/7] Decode messages from IOError before logging them IOError messages are bytestrings, often in the language of the system, so they may include non-ASCII characters. Thus, we must decode them using the locale's preferred encoding to get Unicode objects we safely can pass on for logging the IOError. --- mopidy/backends/local/translator.py | 9 +++++---- mopidy/frontends/mpd/__init__.py | 6 +++--- mopidy/utils/network.py | 6 ++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index be7ab8a8..3b610a94 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -4,6 +4,7 @@ import os logger = logging.getLogger('mopidy.backends.local.translator') from mopidy.models import Track, Artist, Album +from mopidy.utils import locale_decode from mopidy.utils.path import path_to_uri def parse_m3u(file_path): @@ -33,8 +34,8 @@ def parse_m3u(file_path): try: with open(file_path) as m3u: contents = m3u.readlines() - except IOError, e: - logger.error('Couldn\'t open m3u: %s', e) + except IOError as error: + logger.error('Couldn\'t open m3u: %s', locale_decode(error)) return uris for line in contents: @@ -61,8 +62,8 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''): try: with open(tag_cache) as library: contents = library.read() - except IOError, e: - logger.error('Could not open tag cache: %s', e) + except IOError as error: + logger.error('Could not open tag cache: %s', locale_decode(error)) return tracks current = {} diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 99134012..e8b2aabe 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -5,7 +5,7 @@ from pykka import registry, actor from mopidy import listeners, settings from mopidy.frontends.mpd import dispatcher, protocol -from mopidy.utils import network, process, log +from mopidy.utils import locale_decode, log, network, process logger = logging.getLogger('mopidy.frontends.mpd') @@ -32,8 +32,8 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): try: network.Server(hostname, port, protocol=MpdSession, max_connections=settings.MPD_SERVER_MAX_CONNECTIONS) - except IOError, e: - logger.error(u'MPD server startup failed: %s', e) + except IOError as error: + logger.error(u'MPD server startup failed: %s', locale_decode(error)) sys.exit(1) logger.info(u'MPD server running at [%s]:%s', hostname, port) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 0a0928ce..4b8a9ac9 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -9,6 +9,8 @@ from pykka import ActorDeadError from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry +from mopidy.utils import locale_decode + logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): @@ -21,9 +23,9 @@ def try_ipv6_socket(): try: socket.socket(socket.AF_INET6).close() return True - except IOError, e: + except IOError as error: logger.debug(u'Platform supports IPv6, but socket ' - 'creation failed, disabling: %s', e) + 'creation failed, disabling: %s', locale_decode(error)) return False #: Boolean value that indicates if creating an IPv6 socket will succeed. From f392a7cccb485ce16f64d5b2109e710838351a5a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 00:29:04 +0200 Subject: [PATCH 3/7] Update changelog --- docs/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 84f5ffca..1e0900d4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,14 @@ Changes This change log is used to track all major changes to Mopidy. +v0.7.3 (in development) +======================= + +**Changes** + +- Fixed crash when logging :exc:`IOError` exceptions on systems using languages + with non-ASCII characters, like French. + v0.7.2 (2012-05-07) =================== From 436fd7815d6f967aa0fc53dbe10f7f516e7cd64a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 00:31:58 +0200 Subject: [PATCH 4/7] Move the Spotify cache to a subdir of the Mopidy cache --- docs/changes.rst | 4 ++++ mopidy/backends/spotify/session_manager.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1e0900d4..a93369ea 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,10 @@ v0.7.3 (in development) - Fixed crash when logging :exc:`IOError` exceptions on systems using languages with non-ASCII characters, like French. +- Move the default location of the Spotify cache from `~/.cache/mopidy` to + `~/.cache/mopidy/spotify`. You can change this by setting + :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`. + v0.7.2 (2012-05-07) =================== diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 2ae4ed2d..3794513c 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -23,8 +23,9 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager') class SpotifySessionManager(BaseThread, PyspotifySessionManager): - cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH - settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH + cache_location = (settings.SPOTIFY_CACHE_PATH + or os.path.join(CACHE_PATH, 'spotify')) + settings_location = cache_location appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() From d78d62c68ce4c88cd3ac7e6fa89f9936e4ef7521 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:52:44 +0100 Subject: [PATCH 5/7] Hack to speed up Spotify backend startup with clean cache from 35s to 12s The time Improvement is probably a magnitude or two larger on outdated caches. --- docs/changes.rst | 4 ++++ mopidy/backends/spotify/session_manager.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index a93369ea..7b923e1a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -16,6 +16,10 @@ v0.7.3 (in development) `~/.cache/mopidy/spotify`. You can change this by setting :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`. +- Reduce time required to update the Spotify cache on startup. One one + system/Spotify account, the time from clean cache to ready for use was + reduced from 35s to 12s. + v0.7.2 (2012-05-07) =================== diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 3794513c..481f7a94 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -43,6 +43,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.container_manager = None self.playlist_manager = None + self._initial_data_receive_completed = False + def run_inside_try(self): self.setup() self.connect() @@ -126,6 +128,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def log_message(self, session, data): """Callback used by pyspotify""" logger.debug(u'System message: %s' % data.strip()) + if 'offline-mgr' in data and 'files unlocked' in data: + # XXX This is a very very fragile and ugly hack, but we get no + # proper event when libspotify is done with initial data loading. + # We delay the expensive refresh of Mopidy's stored playlists until + # this message arrives. This way, we avoid doing the refresh once + # for every playlist or other change. This reduces the time from + # startup until the Spotify backend is ready from 35s to 12s in one + # test with clean Spotify cache. In cases with an outdated cache + # the time improvements should be a lot better. + self._initial_data_receive_completed = True + self.refresh_stored_playlists() def end_of_track(self, session): """Callback used by pyspotify""" @@ -135,6 +148,9 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def refresh_stored_playlists(self): """Refresh the stored playlists in the backend with fresh meta data from Spotify""" + if not self._initial_data_receive_completed: + logger.debug(u'Still getting data; skipped refresh of playlists') + return playlists = map(SpotifyTranslator.to_mopidy_playlist, self.session.playlist_container()) playlists = filter(None, playlists) From 52e242cbe1055ce65a760d37472b99c21c30de28 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 03:37:55 +0200 Subject: [PATCH 6/7] Update version number to 0.7.3 --- mopidy/__init__.py | 2 +- tests/version_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 8a2b469e..11293446 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ from subprocess import PIPE, Popen import glib -__version__ = '0.7.2' +__version__ = '0.7.3' DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index b1c0b90e..26045ac1 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -26,7 +26,8 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.6.0') < SV('0.6.1')) self.assert_(SV('0.6.1') < SV('0.7.0')) self.assert_(SV('0.7.0') < SV('0.7.1')) - self.assert_(SV('0.7.1') < SV(__version__)) + self.assert_(SV('0.7.1') < SV('0.7.2')) + self.assert_(SV('0.7.2') < SV(__version__)) self.assert_(SV(__version__) < SV('0.8.0')) def test_get_platform_contains_platform(self): From 90490375575b7406fb117237b166e46812f16377 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 03:38:07 +0200 Subject: [PATCH 7/7] Update changelog for v0.7.3 --- docs/changes.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7b923e1a..a4aae058 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,8 +4,11 @@ Changes This change log is used to track all major changes to Mopidy. -v0.7.3 (in development) -======================= +v0.7.3 (2012-08-11) +=================== + +A small maintenance release to fix a crash affecting a few users, and a couple +of small adjustments to the Spotify backend. **Changes**