From f6a66604b8841254069f0b86c0474e2e7a943102 Mon Sep 17 00:00:00 2001 From: Antoine Pierlot-Garcin Date: Tue, 7 Jun 2011 23:46:28 -0400 Subject: [PATCH 01/25] pyspotify now returns unicode objects --- mopidy/backends/spotify/translator.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 21abdf78..0ab4def9 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,7 +4,6 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.spotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.translator') @@ -16,7 +15,7 @@ class SpotifyTranslator(object): return Artist(name=u'[loading...]') return Artist( uri=str(Link.from_artist(spotify_artist)), - name=spotify_artist.name().decode(ENCODING, 'replace'), + name=spotify_artist.name() ) @classmethod @@ -24,7 +23,7 @@ class SpotifyTranslator(object): if spotify_album is None or not spotify_album.is_loaded(): return Album(name=u'[loading...]') # TODO pyspotify got much more data on albums than this - return Album(name=spotify_album.name().decode(ENCODING, 'replace')) + return Album(name=spotify_album.name()) @classmethod def to_mopidy_track(cls, spotify_track): @@ -38,7 +37,7 @@ class SpotifyTranslator(object): date = None return Track( uri=uri, - name=spotify_track.name().decode(ENCODING, 'replace'), + name=spotify_track.name(), artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()], album=cls.to_mopidy_album(spotify_track.album()), track_no=spotify_track.index(), @@ -57,7 +56,7 @@ class SpotifyTranslator(object): try: return Playlist( uri=str(Link.from_playlist(spotify_playlist)), - name=spotify_playlist.name().decode(ENCODING, 'replace'), + name=spotify_playlist.name(), # FIXME if check on link is a hackish workaround for is_local tracks=[cls.to_mopidy_track(t) for t in spotify_playlist if str(Link.from_track(t, 0))], From d46c22dca380f301115d053e9ba8506f54e522f2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 13:46:50 +0200 Subject: [PATCH 02/25] Make Spotify backend fail early if settings is incomplete --- mopidy/backends/spotify/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 87997059..774273e3 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -72,19 +72,22 @@ class SpotifyBackend(ThreadingActor, Backend): self.gstreamer = None self.spotify = None + # Fail early if settings are not present + self.username = settings.SPOTIFY_USERNAME + self.password = settings.SPOTIFY_PASSWORD + def on_start(self): gstreamer_refs = ActorRegistry.get_by_class(GStreamer) assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.' self.gstreamer = gstreamer_refs[0].proxy() + logger.info(u'Mopidy uses SPOTIFY(R) CORE') self.spotify = self._connect() def _connect(self): from .session_manager import SpotifySessionManager - logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.debug(u'Connecting to Spotify') - spotify = SpotifySessionManager( - settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD) + spotify = SpotifySessionManager(self.username, self.password) spotify.start() return spotify From ce8cc55f7932092b0153359d5effd0ab0e8c57f3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 13:48:50 +0200 Subject: [PATCH 03/25] Log GStreamer output addition at debug level --- mopidy/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index f52292d2..166c487e 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -298,7 +298,7 @@ class GStreamer(ThreadingActor): output.sync_state_with_parent() # Required to add to running pipe gst.element_link_many(self._tee, output) self._outputs.append(output) - logger.info('Added %s', output.get_name()) + logger.debug('GStreamer added %s', output.get_name()) def list_outputs(self): """ From fbc47a041a75d10cebd35576abe98a10005e3054 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 14:15:17 +0200 Subject: [PATCH 04/25] Add more debug logging to stop_all_actors --- mopidy/utils/process.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 5b09148d..2e18280e 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -22,9 +22,13 @@ def exit_handler(signum, frame): def stop_all_actors(): num_actors = len(ActorRegistry.get_all()) while num_actors: + logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s', + num_actors, threading.active_count() - num_actors, + ', '.join([t.name for t in threading.enumerate()])) logger.debug(u'Stopping %d actor(s)...', num_actors) ActorRegistry.stop_all() num_actors = len(ActorRegistry.get_all()) + logger.debug(u'All actors stopped.') class BaseThread(threading.Thread): def __init__(self): From 2453e6826f93301a0003b869e46d4cb41aeb81b7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 14:17:29 +0200 Subject: [PATCH 05/25] Add exit_process() function for shutting down Mopidy instead of ActorRegistry.stop_all() --- mopidy/core.py | 5 ++--- mopidy/utils/process.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index b89a5456..8138f198 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -40,11 +40,10 @@ def main(): setup_mixer() setup_backend() setup_frontends() - while ActorRegistry.get_all(): + while True: time.sleep(1) - logger.info(u'No actors left. Exiting...') except KeyboardInterrupt: - logger.info(u'User interrupt. Exiting...') + logger.info(u'Interrupted. Exiting...') stop_all_actors() def parse_options(): diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 2e18280e..c1d1c9f5 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -1,5 +1,6 @@ import logging import signal +import thread import threading import gobject @@ -12,12 +13,17 @@ from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') +def exit_process(): + logger.debug(u'Interrupting main...') + thread.interrupt_main() + logger.debug(u'Interrupted main') + def exit_handler(signum, frame): """A :mod:`signal` handler which will exit the program on signal.""" signals = dict((k, v) for v, k in signal.__dict__.iteritems() if v.startswith('SIG') and not v.startswith('SIG_')) - logger.info(u'Got %s. Exiting...', signals[signum]) - stop_all_actors() + logger.info(u'Got %s signal', signals[signum]) + exit_process() def stop_all_actors(): num_actors = len(ActorRegistry.get_all()) From 51e43487c37faafe236ac67234d99cc0591d0862 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 14:18:09 +0200 Subject: [PATCH 06/25] Log traceback for exceptions popping up to main() --- mopidy/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/core.py b/mopidy/core.py index 8138f198..8c6e9833 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -44,6 +44,9 @@ def main(): time.sleep(1) except KeyboardInterrupt: logger.info(u'Interrupted. Exiting...') + except Exception as e: + logger.exception(e) + finally: stop_all_actors() def parse_options(): From 8e983c337f4569547c70da9cb7a5f82554d693d9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 14:18:38 +0200 Subject: [PATCH 07/25] Only log the error message for SettingsError --- mopidy/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/core.py b/mopidy/core.py index 8c6e9833..65472a29 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -42,6 +42,8 @@ def main(): setup_frontends() while True: time.sleep(1) + except SettingsError as e: + logger.error(e.message) except KeyboardInterrupt: logger.info(u'Interrupted. Exiting...') except Exception as e: From f311dd1e77b0aa16433effc1ff2f3f7046117a37 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 15:14:34 +0200 Subject: [PATCH 08/25] Do not refresh playlists on every metadata update, but just when the playlist container is loaded --- mopidy/backends/spotify/session_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 4b6abe85..04398751 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -74,7 +74,6 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def metadata_updated(self, session): """Callback used by pyspotify""" logger.debug(u'Metadata updated') - self.refresh_stored_playlists() def connection_error(self, session, error): """Callback used by pyspotify""" From 98d410545fd6d7ffff40f88198afde57b6729c6e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 12 Jun 2011 01:39:57 +0200 Subject: [PATCH 09/25] Add MANIFEST.in to MANIFEST.in --- MANIFEST.in | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 033c51f2..f3723ecd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,10 @@ -include LICENSE pylintrc *.rst *.ini data/mopidy.desktop +include *.ini +include *.rst +include LICENSE +include MANIFEST.in +include data/mopidy.desktop include mopidy/backends/spotify/spotify_appkey.key +include pylintrc recursive-include docs * prune docs/_build recursive-include requirements * From 8107c77bcf805ff3ec7e3a64068d13d5bea6287b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 12 Jun 2011 02:25:51 +0200 Subject: [PATCH 10/25] Require pyspotify 1.3 --- docs/changes.rst | 2 +- mopidy/backends/spotify/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 2c240bfa..2a36fbbc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,7 +18,7 @@ Please note that 0.5.0 requires some updated dependencies, as listed under **Important changes** - If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and - pyspotify 1.2. If you install from APT, libspotify and pyspotify will + pyspotify 1.3. If you install from APT, libspotify and pyspotify will automatically be upgraded. If you are not installing from APT, follow the instructions at :doc:`/installation/libspotify/`. diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 774273e3..66bcffd4 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -33,7 +33,7 @@ class SpotifyBackend(ThreadingActor, Backend): **Dependencies:** - libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com) - - pyspotify == 1.2 (python-spotify package from apt.mopidy.com) + - pyspotify == 1.3 (python-spotify package from apt.mopidy.com) **Settings:** From 4090c14ae18246d2b14fcd80a31ffd67c563c503 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 12 Jun 2011 11:37:15 +0200 Subject: [PATCH 11/25] pyspotify 1.3 adds autolinking of tracks. Fixes #34. --- docs/changes.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 2a36fbbc..c93e0ee8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -45,8 +45,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under workaround of searching and reconnecting to make the playlists appear are no longer necessary. (Fixes: :issue:`59`) - - Replace not decodable characters returned from Spotify instead of throwing - an exception, as we won't try to figure out the encoding of non-UTF-8-data. + - Track's that are no longer available in Spotify's archives are now + "autolinked" to corresponding tracks in other albums, just like the + official Spotify clients do. (Fixes: :issue:`34`) - MPD frontend: From a6c85710050556dd6f3a2893d0a9a42230c0e81c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 12 Jun 2011 14:19:42 +0200 Subject: [PATCH 12/25] Fix error/reconnect during retrieval of command list. MpdDispatcher returns [] instead of None after the filter refactoring --- mopidy/frontends/mpd/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 53f4cab7..ce5d3be7 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -49,7 +49,7 @@ class MpdSession(asynchat.async_chat): Format a response from the MPD command handlers and send it to the client. """ - if response is not None: + if response: response = LINE_TERMINATOR.join(response) logger.debug(u'Response to [%s]:%s: %s', self.client_address, self.client_port, indent(response)) From cd5886cc7a3f042365077ac0d218be3dcf688fa1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Jun 2011 16:50:17 +0200 Subject: [PATCH 13/25] Rename Spotify thread to simply 'SpotifyThread' --- mopidy/backends/spotify/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 04398751..ba7782b2 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -29,7 +29,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def __init__(self, username, password): PyspotifySessionManager.__init__(self, username, password) BaseThread.__init__(self) - self.name = 'SpotifySMThread' + self.name = 'SpotifyThread' self.gstreamer = None self.backend = None From 174e0082689c8461483f4ebfeabff97b1c78a3b1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Jun 2011 16:51:02 +0200 Subject: [PATCH 14/25] Change wording of Spotify's 'no error' error so it makes sense without a preceeding error message --- mopidy/backends/spotify/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index ba7782b2..552fa2a2 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -78,7 +78,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def connection_error(self, session, error): """Callback used by pyspotify""" if error is None: - logger.info(u'Spotify connection error resolved') + logger.info(u'Spotify connection OK') else: logger.error(u'Spotify connection error: %s', error) self.backend.playback.pause() From 6f4117729ab83e7a254c3515e0d9b7f5dd98a2fe Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 00:00:38 +0200 Subject: [PATCH 15/25] Formatting --- mopidy/backends/spotify/container_manager.py | 4 ++-- mopidy/backends/spotify/session_manager.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py index 29360d79..9ae524e1 100644 --- a/mopidy/backends/spotify/container_manager.py +++ b/mopidy/backends/spotify/container_manager.py @@ -1,11 +1,11 @@ import logging -from spotify.manager import SpotifyContainerManager as PyspotifyContainerManager +from spotify.manager import SpotifyContainerManager as \ + PyspotifyContainerManager logger = logging.getLogger('mopidy.backends.spotify.container_manager') class SpotifyContainerManager(PyspotifyContainerManager): - def __init__(self, session_manager): PyspotifyContainerManager.__init__(self) self.session_manager = session_manager diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 552fa2a2..d581c7c1 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -45,7 +45,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def setup(self): gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.' + assert len(gstreamer_refs) == 1, \ + 'Expected exactly one running gstreamer.' self.gstreamer = gstreamer_refs[0].proxy() backend_refs = ActorRegistry.get_by_class(Backend) @@ -57,14 +58,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): if error: logger.error(u'Spotify login error: %s', error) return + logger.info(u'Connected to Spotify') self.session = session - logger.debug(u'Preferred Spotify bitrate is %s kbps.', settings.SPOTIFY_BITRATE) + logger.debug(u'Preferred Spotify bitrate is %s kbps', + settings.SPOTIFY_BITRATE) self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE]) self.container_manager = SpotifyContainerManager(self) self.container_manager.watch(self.session.playlist_container()) + self.connected.set() def logged_out(self, session): From 371fb9b90d452f8893446e4968659d2e1ff58676 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 00:03:23 +0200 Subject: [PATCH 16/25] Add missing container callbacks with debug log statements --- mopidy/backends/spotify/container_manager.py | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py index 9ae524e1..bd9035aa 100644 --- a/mopidy/backends/spotify/container_manager.py +++ b/mopidy/backends/spotify/container_manager.py @@ -11,6 +11,30 @@ class SpotifyContainerManager(PyspotifyContainerManager): self.session_manager = session_manager def container_loaded(self, container, userdata): - """Callback used by pyspotify.""" - logger.debug(u'Container loaded') + """Callback used by pyspotify""" + logger.debug(u'Callback called: playlist container loaded') self.session_manager.refresh_stored_playlists() + + def playlist_added(self, container, playlist, position, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: playlist "%s" added at position %d', + playlist.name(), position) + # container_loaded() is called after this callback, so we do not need + # to handle this callback. + + def playlist_moved(self, container, playlist, old_position, new_position, + userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: playlist "%s" moved from position %d to %d', + playlist.name(), old_position, new_position) + # container_loaded() is called after this callback, so we do not need + # to handle this callback. + + def playlist_removed(self, container, playlist, position, userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: playlist "%s" removed from position %d', + playlist.name(), position) + # container_loaded() is called after this callback, so we do not need + # to handle this callback. From 82ba04408c606c915f8df5795ee7cc3f4451adcb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 00:04:08 +0200 Subject: [PATCH 17/25] Add playlist callbacks with debug log statements --- mopidy/backends/spotify/container_manager.py | 5 ++ mopidy/backends/spotify/playlist_manager.py | 94 ++++++++++++++++++++ mopidy/backends/spotify/session_manager.py | 8 +- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 mopidy/backends/spotify/playlist_manager.py diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py index bd9035aa..5166cacb 100644 --- a/mopidy/backends/spotify/container_manager.py +++ b/mopidy/backends/spotify/container_manager.py @@ -15,6 +15,11 @@ class SpotifyContainerManager(PyspotifyContainerManager): logger.debug(u'Callback called: playlist container loaded') self.session_manager.refresh_stored_playlists() + playlist_container = self.session_manager.session.playlist_container() + for playlist in playlist_container: + self.session_manager.playlist_manager.watch(playlist) + logger.debug(u'Watching %d playlist(s) for changes', len(playlist_container)) + def playlist_added(self, container, playlist, position, userdata): """Callback used by pyspotify""" logger.debug(u'Callback called: playlist "%s" added at position %d', diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py new file mode 100644 index 00000000..5f4f1fd7 --- /dev/null +++ b/mopidy/backends/spotify/playlist_manager.py @@ -0,0 +1,94 @@ +import datetime +import logging + +from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager + +logger = logging.getLogger('mopidy.backends.spotify.playlist_manager') + +class SpotifyPlaylistManager(PyspotifyPlaylistManager): + def __init__(self, session_manager): + PyspotifyPlaylistManager.__init__(self) + self.session_manager = session_manager + + def tracks_added(self, playlist, tracks, position, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: ' + u'%d track(s) added to position %d in playlist "%s"', + len(tracks), position, playlist.name()) + # TODO Partially update stored playlists? + + def tracks_moved(self, playlist, tracks, new_position, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: ' + u'%d track(s) moved to position %d in playlist "%s"', + len(tracks), new_position, playlist.name()) + # TODO Partially update stored playlists? + + def tracks_removed(self, playlist, tracks, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: ' + u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name()) + # TODO Partially update stored playlists? + + def playlist_renamed(self, playlist, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: Playlist renamed to "%s"', + playlist.name()) + # TODO Partially update stored playlists? + + def playlist_state_changed(self, playlist, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: The state of playlist "%s" changed', + playlist.name()) + + def playlist_update_in_progress(self, playlist, done, userdata): + """Callback used by pyspotify""" + if done: + logger.debug(u'Callback called: ' + u'Update of playlist "%s" done', playlist.name()) + else: + logger.debug(u'Callback called: ' + u'Update of playlist "%s" in progress', playlist.name()) + + def playlist_metadata_updated(self, playlist, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: Metadata updated for playlist "%s"', + playlist.name()) + # TODO Update stored playlists? + + def track_created_changed(self, playlist, position, user, when, userdata): + """Callback used by pyspotify""" + when = datetime.datetime.fromtimestamp(when) + logger.debug( + u'Callback called: Created by/when for track %d in playlist ' + u'"%s" changed to user "N/A" and time "%s"', + position, playlist.name(), when) + + def track_message_changed(self, playlist, position, message, userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: Message for track %d in playlist ' + u'"%s" changed to "%s"', position, playlist.name(), message) + + def track_seen_changed(self, playlist, position, seen, userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: Seen attribute for track %d in playlist ' + u'"%s" changed to "%s"', position, playlist.name(), seen) + + def description_changed(self, playlist, description, userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: Description changed for playlist "%s" to "%s"', + playlist.name(), description) + + def subscribers_changed(self, playlist, userdata): + """Callback used by pyspotify""" + logger.debug( + u'Callback called: Subscribers changed for playlist "%s"', + playlist.name()) + + def image_changed(self, playlist, image, userdata): + """Callback used by pyspotify""" + logger.debug(u'Callback called: Image changed for playlist "%s"', + playlist.name()) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index d581c7c1..fd71d861 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -9,11 +9,12 @@ from pykka.registry import ActorRegistry from mopidy import get_version, settings from mopidy.backends.base import Backend from mopidy.backends.spotify import BITRATES +from mopidy.backends.spotify.container_manager import SpotifyContainerManager +from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist from mopidy.gstreamer import GStreamer from mopidy.utils.process import BaseThread -from mopidy.backends.spotify.container_manager import SpotifyContainerManager logger = logging.getLogger('mopidy.backends.spotify.session_manager') @@ -38,6 +39,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.session = None self.container_manager = None + self.playlist_manager = None def run_inside_try(self): self.setup() @@ -67,6 +69,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE]) self.container_manager = SpotifyContainerManager(self) + self.playlist_manager = SpotifyPlaylistManager(self) + self.container_manager.watch(self.session.playlist_container()) self.connected.set() @@ -77,7 +81,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def metadata_updated(self, session): """Callback used by pyspotify""" - logger.debug(u'Metadata updated') + logger.debug(u'Callback called: Metadata updated') def connection_error(self, session, error): """Callback used by pyspotify""" From 3c68c8f9ead9a8c04e7cefa022c2ce453efa2b26 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 00:19:59 +0200 Subject: [PATCH 18/25] The playlist name is not available when playlist_added is called --- mopidy/backends/spotify/container_manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/spotify/container_manager.py b/mopidy/backends/spotify/container_manager.py index 5166cacb..520cfb68 100644 --- a/mopidy/backends/spotify/container_manager.py +++ b/mopidy/backends/spotify/container_manager.py @@ -18,12 +18,13 @@ class SpotifyContainerManager(PyspotifyContainerManager): playlist_container = self.session_manager.session.playlist_container() for playlist in playlist_container: self.session_manager.playlist_manager.watch(playlist) - logger.debug(u'Watching %d playlist(s) for changes', len(playlist_container)) + logger.debug(u'Watching %d playlist(s) for changes', + len(playlist_container)) def playlist_added(self, container, playlist, position, userdata): """Callback used by pyspotify""" - logger.debug(u'Callback called: playlist "%s" added at position %d', - playlist.name(), position) + logger.debug(u'Callback called: playlist added at position %d', + position) # container_loaded() is called after this callback, so we do not need # to handle this callback. From 4516767372f9026d84c1a9b8a562a5d0f9f61f47 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 00:24:54 +0200 Subject: [PATCH 19/25] Refresh stored playlists when tracks are added to, moved in, or removed from playlists --- mopidy/backends/spotify/playlist_manager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/playlist_manager.py b/mopidy/backends/spotify/playlist_manager.py index 5f4f1fd7..f72ac4ca 100644 --- a/mopidy/backends/spotify/playlist_manager.py +++ b/mopidy/backends/spotify/playlist_manager.py @@ -15,26 +15,26 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager): logger.debug(u'Callback called: ' u'%d track(s) added to position %d in playlist "%s"', len(tracks), position, playlist.name()) - # TODO Partially update stored playlists? + self.session_manager.refresh_stored_playlists() def tracks_moved(self, playlist, tracks, new_position, userdata): """Callback used by pyspotify""" logger.debug(u'Callback called: ' u'%d track(s) moved to position %d in playlist "%s"', len(tracks), new_position, playlist.name()) - # TODO Partially update stored playlists? + self.session_manager.refresh_stored_playlists() def tracks_removed(self, playlist, tracks, userdata): """Callback used by pyspotify""" logger.debug(u'Callback called: ' u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name()) - # TODO Partially update stored playlists? + self.session_manager.refresh_stored_playlists() def playlist_renamed(self, playlist, userdata): """Callback used by pyspotify""" logger.debug(u'Callback called: Playlist renamed to "%s"', playlist.name()) - # TODO Partially update stored playlists? + self.session_manager.refresh_stored_playlists() def playlist_state_changed(self, playlist, userdata): """Callback used by pyspotify""" @@ -54,7 +54,6 @@ class SpotifyPlaylistManager(PyspotifyPlaylistManager): """Callback used by pyspotify""" logger.debug(u'Callback called: Metadata updated for playlist "%s"', playlist.name()) - # TODO Update stored playlists? def track_created_changed(self, playlist, position, user, when, userdata): """Callback used by pyspotify""" From 30a8fd5e857f449d36a55dba89350783e1ed5a29 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 14 Jun 2011 14:56:25 +0200 Subject: [PATCH 20/25] Remove source file for outdated and removed sequence diagram --- docs/_static/thread_communication.txt | 37 --------------------------- 1 file changed, 37 deletions(-) delete mode 100644 docs/_static/thread_communication.txt diff --git a/docs/_static/thread_communication.txt b/docs/_static/thread_communication.txt deleted file mode 100644 index 4119004e..00000000 --- a/docs/_static/thread_communication.txt +++ /dev/null @@ -1,37 +0,0 @@ -Script for use with www.websequencediagrams.com -=============================================== - -Main -> Core: create -activate Core -note over Core: create NadMixer -Core -> NadTalker: create -activate NadTalker -note over NadTalker: calibrate device -note over Core: create DespotifyBackend -Core -> despotify: connect to Spotify -activate despotify -note over Core: create MpdFrontend -Main -> Server: create -activate Server -note over Server: open port -Client -> Server: connect -note over Server: open session -Client -> Server: play 1 -Server -> Core: play 1 -Core -> despotify: play first track -Client -> Server: setvol 50 -Server -> Core: setvol 50 -Core -> NadTalker: volume = 50 -Client -> Server: status -Server -> Core: status -Core -> NadTalker: volume? -NadTalker -> Core: volume = 50 -Core -> Server: status response -Server -> Client: status response -despotify -> Core: end of track callback -Core -> despotify: play second track -Client -> Server: stop -Server -> Core: stop -Core -> despotify: stop -Client -> Server: disconnect -note over Server: close session From 462f1bd89335b98af20506fa73b7e7c2f433af5e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 15 Jun 2011 22:17:35 +0200 Subject: [PATCH 21/25] Make tox work by allowing the use of global site-packages --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 8b91c6b7..48676e46 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py26,py27,docs [testenv] deps = nose commands = nosetests [] +sitepackages = True [testenv:docs] basepython = python From 6a779229058eb67fd42d51b680b0b3c119219288 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 15 Jun 2011 22:19:41 +0200 Subject: [PATCH 22/25] Ask about missing settings in alphabetic order --- mopidy/utils/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 500477e2..cab94089 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -73,7 +73,7 @@ class SettingsProxy(object): raise SettingsError(u'Settings validation failed.') def _read_missing_settings_from_stdin(self, current, runtime): - for setting, value in current.iteritems(): + for setting, value in sorted(current.iteritems()): if isinstance(value, basestring) and len(value) == 0: runtime[setting] = self._read_from_stdin(setting + u': ') From e6e2ab58553516f7a9698bb8a67d44bd7bb00575 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 15 Jun 2011 22:31:54 +0200 Subject: [PATCH 23/25] Add shutdown procedure improvements to changelog --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index c93e0ee8..63f7d336 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -74,6 +74,9 @@ Please note that 0.5.0 requires some updated dependencies, as listed under - Added :option:`--interactive` for reading missing local settings from ``stdin``. (Fixes: :issue:`96`) + - Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``, + which initiates the same shutdown procedure as CTRL+C does. + - Tag cache generator: - Made it possible to abort :command:`mopidy-scan` with CTRL+C. From d30ee6a9688d070a5c7d75271cc06d8ef49f28f0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 15 Jun 2011 22:32:57 +0200 Subject: [PATCH 24/25] Update changelog for v0.5.0 release --- docs/changes.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 63f7d336..4ccf62c9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,12 +5,12 @@ Changes This change log is used to track all major changes to Mopidy. -v0.5.0 (in development) -======================= +v0.5.0 (2011-06-15) +=================== Since last time we've added support for audio streaming to SHOUTcast servers and fixed the longstanding playlist loading issue in the Spotify backend. As -always the release has a bunch of bug fixes. +always the release has a bunch of bug fixes and minor improvements. Please note that 0.5.0 requires some updated dependencies, as listed under *Important changes* below. From 653890c037df528a8edd2c087ae854d9f88cd393 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 15 Jun 2011 23:10:51 +0200 Subject: [PATCH 25/25] Ready for v0.6 development --- docs/changes.rst | 8 ++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4ccf62c9..4125b788 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,14 @@ Changes This change log is used to track all major changes to Mopidy. +v0.6.0 (in development) +======================= + +**Changes** + +- None yet + + v0.5.0 (2011-06-15) =================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 79a0aa29..7b25c525 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -5,7 +5,7 @@ if not (2, 6) <= sys.version_info < (3,): from subprocess import PIPE, Popen -VERSION = (0, 5, 0) +VERSION = (0, 6, 0) def get_version(): try: diff --git a/tests/version_test.py b/tests/version_test.py index 7bfb540e..9b53c63f 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -19,8 +19,9 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.3.0') < SV('0.3.1')) self.assert_(SV('0.3.1') < SV('0.4.0')) self.assert_(SV('0.4.0') < SV('0.4.1')) - self.assert_(SV('0.4.1') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.5.1')) + self.assert_(SV('0.4.1') < SV('0.5.0')) + self.assert_(SV('0.5.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.6.1')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform())