From 31ccf37dd82b7e7c0d9480fd1dc259ebd5a75398 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 Aug 2010 01:42:07 +0200 Subject: [PATCH 01/32] docs: python-alsaaudio is no longer needed for the LibspotifyBackend --- docs/installation/libspotify.rst | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 9dc9689f..3965162d 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -57,22 +57,10 @@ Installing pyspotify Install pyspotify's dependencies. At Debian/Ubuntu systems:: - sudo aptitude install python-dev python-alsaaudio + sudo aptitude install python-dev Check out the pyspotify code, and install it:: git clone git://github.com/jodal/pyspotify.git cd pyspotify/pyspotify/ sudo python setup.py install - - -Testing the installation -======================== - -Apply for an application key at -https://developer.spotify.com/en/libspotify/application-key, download the -binary version, and place the file at ``pyspotify/spotify_appkey.key``. - -Test your libspotify setup:: - - examples/example1.py -u USERNAME -p PASSWORD From f9e0cb5d1a47aca4ca05a16f6f634089eb21186a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 Aug 2010 01:42:34 +0200 Subject: [PATCH 02/32] docs: Require GStreamer >= 0.10, as we have no idea if it will work with lesser versions --- docs/installation/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 73ae62cb..227ae8b9 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -23,7 +23,7 @@ Install dependencies Make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- :doc:`GStreamer ` (>= 0.10 ?) with Python bindings +- :doc:`GStreamer ` >= 0.10, with Python bindings - Dependencies for at least one Mopidy mixer: - :mod:`mopidy.mixers.alsa` (Linux only) From 255d70d1ae105cf5fb9131a84546d7ee16c19e68 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 12:20:14 +0200 Subject: [PATCH 03/32] MPD: Support 'plchanges "-1"' to work better with MPDroid --- docs/changes.rst | 1 + mopidy/frontends/mpd/protocol/current_playlist.py | 6 +++++- tests/frontends/mpd/current_playlist_test.py | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index c8e4c912..6f806425 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,7 @@ Another great release. - Split gigantic protocol implementation into eleven modules. - Search improvements, including support for multi-word search. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty. + - Support ``plchanges "-1"`` to work better with MPDroid. - Backend API: diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index da052fff..76ae62ef 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -257,7 +257,7 @@ def playlistsearch(frontend, tag, needle): """ raise MpdNotImplemented # TODO -@handle_pattern(r'^plchanges "(?P\d+)"$') +@handle_pattern(r'^plchanges "(?P-?\d+)"$') def plchanges(frontend, version): """ *musicpd.org, current playlist section:* @@ -268,6 +268,10 @@ def plchanges(frontend, version): To detect songs that were deleted at the end of the playlist, use ``playlistlength`` returned by status command. + + *MPDroid:* + + - Calls ``plchanges "-1"`` two times per second to get the entire playlist. """ # XXX Naive implementation that returns all tracks as changed if int(version) < frontend.backend.current_playlist.version: diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index ce1e4069..062da6d4 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -321,6 +321,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'Title: c' in result) self.assert_(u'OK' in result) + def test_plchanges_with_minus_one_returns_entire_playlist(self): + self.b.current_playlist.load( + [Track(name='a'), Track(name='b'), Track(name='c')]) + result = self.h.handle_request(u'plchanges "-1"') + self.assert_(u'Title: a' in result) + self.assert_(u'Title: b' in result) + self.assert_(u'Title: c' in result) + self.assert_(u'OK' in result) + def test_plchangesposid(self): self.b.current_playlist.load([Track(), Track(), Track()]) result = self.h.handle_request(u'plchangesposid "0"') From a3fb8a1f72474414876e20d0573210594a77105d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 12:26:34 +0200 Subject: [PATCH 04/32] MPD: Support 'pause' without args to work with MPDroid --- docs/changes.rst | 1 + mopidy/frontends/mpd/protocol/playback.py | 16 ++++++++++++++-- tests/frontends/mpd/playback_test.py | 21 +++++++++++++++------ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6f806425..70d9390f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -24,6 +24,7 @@ Another great release. - Search improvements, including support for multi-word search. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty. - Support ``plchanges "-1"`` to work better with MPDroid. + - Support ``pause`` without arguments to work better with MPDroid. - Backend API: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 719bd8b5..53cc2bbc 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -86,16 +86,28 @@ def next_(frontend): """ return frontend.backend.playback.next() +@handle_pattern(r'^pause$') @handle_pattern(r'^pause "(?P[01])"$') -def pause(frontend, state): +def pause(frontend, state=None): """ *musicpd.org, playback section:* ``pause {PAUSE}`` Toggles pause/resumes playing, ``PAUSE`` is 0 or 1. + + *MPDroid:* + + - Calls ``pause`` without any arguments to toogle pause. """ - if int(state): + if state is None: + if (frontend.backend.playback.state == + frontend.backend.playback.PLAYING): + frontend.backend.playback.pause() + elif (frontend.backend.playback.state == + frontend.backend.playback.PAUSED): + frontend.backend.playback.resume() + elif int(state): frontend.backend.playback.pause() else: frontend.backend.playback.resume() diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index aee05d6c..e76cb9df 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -136,8 +136,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) def test_pause_off(self): - track = Track() - self.b.current_playlist.load([track]) + self.b.current_playlist.load([Track()]) self.h.handle_request(u'play "0"') self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') @@ -145,16 +144,26 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) def test_pause_on(self): - track = Track() - self.b.current_playlist.load([track]) + self.b.current_playlist.load([Track()]) self.h.handle_request(u'play "0"') result = self.h.handle_request(u'pause "1"') self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) + def test_pause_toggle(self): + self.b.current_playlist.load([Track()]) + result = self.h.handle_request(u'play "0"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + result = self.h.handle_request(u'pause') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) + result = self.h.handle_request(u'pause') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + def test_play_without_pos(self): - track = Track() - self.b.current_playlist.load([track]) + self.b.current_playlist.load([Track()]) self.b.playback.state = self.b.playback.PAUSED result = self.h.handle_request(u'play') self.assert_(u'OK' in result) From 539340757199871af92d16fdea03eb49e56c19fa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 13:04:56 +0200 Subject: [PATCH 05/32] MPD: Support 'plchanges' without quotes to work with BitMPC --- docs/changes.rst | 1 + mopidy/frontends/mpd/protocol/current_playlist.py | 1 + tests/frontends/mpd/current_playlist_test.py | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 70d9390f..a59a2b5a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -25,6 +25,7 @@ Another great release. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty. - Support ``plchanges "-1"`` to work better with MPDroid. - Support ``pause`` without arguments to work better with MPDroid. + - Support ``plchanges`` without quotes to work better with BitMPC. - Backend API: diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 76ae62ef..1c1c1764 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -257,6 +257,7 @@ def playlistsearch(frontend, tag, needle): """ raise MpdNotImplemented # TODO +@handle_pattern(r'^plchanges (?P-?\d+)$') @handle_pattern(r'^plchanges "(?P-?\d+)"$') def plchanges(frontend, version): """ diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 062da6d4..0d639f89 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -330,6 +330,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assert_(u'Title: c' in result) self.assert_(u'OK' in result) + def test_plchanges_without_quotes_works(self): + self.b.current_playlist.load( + [Track(name='a'), Track(name='b'), Track(name='c')]) + result = self.h.handle_request(u'plchanges 0') + self.assert_(u'Title: a' in result) + self.assert_(u'Title: b' in result) + self.assert_(u'Title: c' in result) + self.assert_(u'OK' in result) + def test_plchangesposid(self): self.b.current_playlist.load([Track(), Track(), Track()]) result = self.h.handle_request(u'plchangesposid "0"') From 9f71c1533a53bc9769e0fc11b5b938190316580f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 13:09:03 +0200 Subject: [PATCH 06/32] MPD: Support 'play' without quotes to work with BitMPC --- docs/changes.rst | 1 + mopidy/frontends/mpd/protocol/playback.py | 8 ++++++-- tests/frontends/mpd/playback_test.py | 6 ++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a59a2b5a..c8094546 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,6 +26,7 @@ Another great release. - Support ``plchanges "-1"`` to work better with MPDroid. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges`` without quotes to work better with BitMPC. + - Support ``play`` without quotes to work better with BitMPC. - Backend API: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 53cc2bbc..8a7243f6 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -147,8 +147,8 @@ def playid(frontend, cpid): except LookupError: raise MpdNoExistError(u'No such song', command=u'playid') -@handle_pattern(r'^play "(?P\d+)"$') -@handle_pattern(r'^play "(?P-1)"$') +@handle_pattern(r'^play (?P-?\d+)$') +@handle_pattern(r'^play "(?P-?\d+)"$') def playpos(frontend, songpos): """ *musicpd.org, playback section:* @@ -161,6 +161,10 @@ def playpos(frontend, songpos): - issues ``play "-1"`` after playlist replacement to start playback at the first track. + + *BitMPC:* + + - issues ``play 6`` without quotes around the argument. """ songpos = int(songpos) try: diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index e76cb9df..6e69375b 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -175,6 +175,12 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + def test_play_with_pos_without_quotes(self): + self.b.current_playlist.load([Track()]) + result = self.h.handle_request(u'play 0') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + def test_play_with_pos_out_of_bounds(self): self.b.current_playlist.load([]) result = self.h.handle_request(u'play "0"') From 635791cf0e7c65dc152bc085a1ab43a314008685 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 13:33:09 +0200 Subject: [PATCH 07/32] MPD: Support missing quotes for 'consume', 'random', 'repeat', and 'single' to work with BitMPC. --- docs/changes.rst | 4 +-- mopidy/frontends/mpd/protocol/playback.py | 4 +++ tests/frontends/mpd/playback_test.py | 40 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index c8094546..c805b595 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -25,8 +25,8 @@ Another great release. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty. - Support ``plchanges "-1"`` to work better with MPDroid. - Support ``pause`` without arguments to work better with MPDroid. - - Support ``plchanges`` without quotes to work better with BitMPC. - - Support ``play`` without quotes to work better with BitMPC. + - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and + ``single`` without quotes to work better with BitMPC. - Backend API: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 8a7243f6..cf803c6d 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,6 +1,7 @@ from mopidy.frontends.mpd import (handle_pattern, MpdArgError, MpdNoExistError, MpdNotImplemented) +@handle_pattern(r'^consume (?P[01])$') @handle_pattern(r'^consume "(?P[01])"$') def consume(frontend, state): """ @@ -224,6 +225,7 @@ def previous(frontend): """ return frontend.backend.playback.previous() +@handle_pattern(r'^random (?P[01])$') @handle_pattern(r'^random "(?P[01])"$') def random(frontend, state): """ @@ -238,6 +240,7 @@ def random(frontend, state): else: frontend.backend.playback.random = False +@handle_pattern(r'^repeat (?P[01])$') @handle_pattern(r'^repeat "(?P[01])"$') def repeat(frontend, state): """ @@ -319,6 +322,7 @@ def setvol(frontend, volume): volume = 100 frontend.backend.mixer.volume = volume +@handle_pattern(r'^single (?P[01])$') @handle_pattern(r'^single "(?P[01])"$') def single(frontend, state): """ diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 6e69375b..3cf0a11f 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -16,11 +16,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): self.assertFalse(self.b.playback.consume) self.assert_(u'OK' in result) + def test_consume_off_without_quotes(self): + result = self.h.handle_request(u'consume 0') + self.assertFalse(self.b.playback.consume) + self.assert_(u'OK' in result) + def test_consume_on(self): result = self.h.handle_request(u'consume "1"') self.assertTrue(self.b.playback.consume) self.assert_(u'OK' in result) + def test_consume_on_without_quotes(self): + result = self.h.handle_request(u'consume 1') + self.assertTrue(self.b.playback.consume) + self.assert_(u'OK' in result) + def test_crossfade(self): result = self.h.handle_request(u'crossfade "10"') self.assert_(u'ACK [0@0] {} Not implemented' in result) @@ -30,21 +40,41 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): self.assertFalse(self.b.playback.random) self.assert_(u'OK' in result) + def test_random_off_without_quotes(self): + result = self.h.handle_request(u'random 0') + self.assertFalse(self.b.playback.random) + self.assert_(u'OK' in result) + def test_random_on(self): result = self.h.handle_request(u'random "1"') self.assertTrue(self.b.playback.random) self.assert_(u'OK' in result) + def test_random_on_without_quotes(self): + result = self.h.handle_request(u'random 1') + self.assertTrue(self.b.playback.random) + self.assert_(u'OK' in result) + def test_repeat_off(self): result = self.h.handle_request(u'repeat "0"') self.assertFalse(self.b.playback.repeat) self.assert_(u'OK' in result) + def test_repeat_off_without_quotes(self): + result = self.h.handle_request(u'repeat 0') + self.assertFalse(self.b.playback.repeat) + self.assert_(u'OK' in result) + def test_repeat_on(self): result = self.h.handle_request(u'repeat "1"') self.assertTrue(self.b.playback.repeat) self.assert_(u'OK' in result) + def test_repeat_on_without_quotes(self): + result = self.h.handle_request(u'repeat 1') + self.assertTrue(self.b.playback.repeat) + self.assert_(u'OK' in result) + def test_setvol_below_min(self): result = self.h.handle_request(u'setvol "-10"') self.assert_(u'OK' in result) @@ -80,11 +110,21 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): self.assertFalse(self.b.playback.single) self.assert_(u'OK' in result) + def test_single_off_without_quotes(self): + result = self.h.handle_request(u'single 0') + self.assertFalse(self.b.playback.single) + self.assert_(u'OK' in result) + def test_single_on(self): result = self.h.handle_request(u'single "1"') self.assertTrue(self.b.playback.single) self.assert_(u'OK' in result) + def test_single_on_without_quotes(self): + result = self.h.handle_request(u'single 1') + self.assertTrue(self.b.playback.single) + self.assert_(u'OK' in result) + def test_replay_gain_mode_off(self): result = self.h.handle_request(u'replay_gain_mode "off"') self.assert_(u'ACK [0@0] {} Not implemented' in result) From 8fd988e49fe87e4c771bef4946e9448ab4b586ff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 16:56:42 +0200 Subject: [PATCH 08/32] Remove search debug output from libspotify backend --- mopidy/backends/libspotify/__init__.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index c256b55d..974e52df 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -79,15 +79,11 @@ class LibspotifyLibraryController(BaseLibraryController): else: spotify_query.append(u'%s:"%s"' % (field, value)) spotify_query = u' '.join(spotify_query) - logger.debug(u'In search method, search for: %s' % spotify_query) + logger.debug(u'Spotify search query: %s' % spotify_query) my_end, other_end = multiprocessing.Pipe() self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) - logger.debug(u'In Library.search(), waiting for search results') my_end.poll(None) - logger.debug(u'In Library.search(), receiving search results') playlist = my_end.recv() - logger.debug(u'In Library.search(), done receiving search results') - logger.debug(['%s' % t.name for t in playlist.tracks]) return playlist @@ -288,24 +284,10 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def search(self, query, connection): """Search method used by Mopidy backend""" def callback(results, userdata): - logger.debug(u'In SessionManager.search().callback(), ' - 'translating search results') - logger.debug(results.tracks()) # TODO Include results from results.albums(), etc. too playlist = Playlist(tracks=[ LibspotifyTranslator.to_mopidy_track(t) for t in results.tracks()]) - logger.debug(u'In SessionManager.search().callback(), ' - 'sending search results') - logger.debug(['%s' % t.name for t in playlist.tracks]) connection.send(playlist) - logger.debug(u'In SessionManager.search().callback(), ' - 'done sending search results') - logger.debug(u'In SessionManager.search(), ' - 'waiting for Spotify connection') self.connected.wait() - logger.debug(u'In SessionManager.search(), ' - 'sending search query') self.session.search(query, callback) - logger.debug(u'In SessionManager.search(), ' - 'done sending search query') From 382caba05a711c0ae78a3b9bd30b636450f2f4ff Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Fri, 13 Aug 2010 19:56:25 +0200 Subject: [PATCH 09/32] call playback.next if we try to delete current playing track --- mopidy/frontends/mpd/protocol/current_playlist.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 1c1c1764..30acbe89 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -83,6 +83,8 @@ def deleteid(frontend, cpid): """ try: cpid = int(cpid) + if frontend.backend.playback.current_cpid == cpid: + frontend.backend.playback.next() return frontend.backend.current_playlist.remove(cpid=cpid) except LookupError: raise MpdNoExistError(u'No such song', command=u'deleteid') From 01bba501b5b25db91dce65bc1cfd90d7031398a8 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Fri, 13 Aug 2010 20:21:31 +0200 Subject: [PATCH 10/32] changed SERVER_HOSTNAME and SERVER_PORT settings to MPD_SERVER_HOSTNAME and MPD_SERVER_PORT to support other frontends --- docs/api/settings.rst | 2 +- docs/changes.rst | 2 ++ mopidy/frontends/mpd/server.py | 8 ++++---- mopidy/settings.py | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/api/settings.rst b/docs/api/settings.rst index a8ff6446..12d2833f 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -13,7 +13,7 @@ there. A complete ``~/.mopidy/settings.py`` may look like this:: - SERVER_HOSTNAME = u'0.0.0.0' + MPD_SERVER_HOSTNAME = u'0.0.0.0' SPOTIFY_USERNAME = u'alice' SPOTIFY_PASSWORD = u'mysecret' diff --git a/docs/changes.rst b/docs/changes.rst index c805b595..e2c62725 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,6 +17,8 @@ Another great release. - Include Sphinx scripts for building docs, pylintrc, tests and test data in the packages created by ``setup.py`` for i.e. PyPI. - Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`. +- Changed ``SERVER_HOSTNAME`` and ``SERVER_PORT`` settings to + ``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT`` - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 57b6211f..5a124d19 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -29,14 +29,14 @@ class MpdServer(asyncore.dispatcher): else: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() - hostname = self._format_hostname(settings.SERVER_HOSTNAME) - port = settings.SERVER_PORT + hostname = self._format_hostname(settings.MPD_SERVER_HOSTNAME) + port = settings.MPD_SERVER_PORT logger.debug(u'Binding to [%s]:%s', hostname, port) self.bind((hostname, port)) self.listen(1) logger.info(u'MPD server running at [%s]:%s', - self._format_hostname(settings.SERVER_HOSTNAME), - settings.SERVER_PORT) + self._format_hostname(settings.MPD_SERVER_HOSTNAME), + settings.MPD_SERVER_PORT) except IOError, e: sys.exit('MPD server startup failed: %s' % e) diff --git a/mopidy/settings.py b/mopidy/settings.py index 1192c28d..96b95575 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -120,10 +120,10 @@ SERVER = u'mopidy.frontends.mpd.server.MpdServer' #: Listens on all IPv4 interfaces. #: ``::`` #: Listens on all interfaces, both IPv4 and IPv6. -SERVER_HOSTNAME = u'127.0.0.1' +MPD_SERVER_HOSTNAME = u'127.0.0.1' #: Which TCP port Mopidy should listen to. Default: 6600 -SERVER_PORT = 6600 +MPD_SERVER_PORT = 6600 #: Your Spotify Premium username. Used by all Spotify backends. SPOTIFY_USERNAME = u'' From 270a319de4155c19bec6740ee89aba919e7cb6d5 Mon Sep 17 00:00:00 2001 From: Johannes Knutsen Date: Fri, 13 Aug 2010 20:22:09 +0200 Subject: [PATCH 11/32] updated changelog with delete current track bugfix --- docs/changes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changes.rst b/docs/changes.rst index e2c62725..a9c339ac 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -29,6 +29,7 @@ Another great release. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and ``single`` without quotes to work better with BitMPC. + - Fixed delete current playing track from playlist - Backend API: From d4fd4aa221ad097c776ba5cdba47a16770bd6590 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 20:39:50 +0200 Subject: [PATCH 12/32] docs: Elaborate on deleting of the currently playing track. --- docs/changes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index a9c339ac..84561b86 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -29,7 +29,8 @@ Another great release. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and ``single`` without quotes to work better with BitMPC. - - Fixed delete current playing track from playlist + - Fixed delete current playing track from playlist, which crashed several + clients. - Backend API: From bb19dda499fc924874108a2507eda0cb772174ce Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 20:41:40 +0200 Subject: [PATCH 13/32] Source code license is now Apache License v2.0. Documentation license is CC BY-SA 3.0 Unported License. --- COPYING | 339 ---------------------------------------------- LICENSE | 202 +++++++++++++++++++++++++++ docs/changes.rst | 1 + docs/conf.py | 2 +- docs/index.rst | 1 + docs/licenses.rst | 34 +++++ 6 files changed, 239 insertions(+), 340 deletions(-) delete mode 100644 COPYING create mode 100644 LICENSE create mode 100644 docs/licenses.rst diff --git a/COPYING b/COPYING deleted file mode 100644 index d511905c..00000000 --- a/COPYING +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/changes.rst b/docs/changes.rst index 84561b86..63da4dae 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,7 @@ Another great release. **Changes** +- License changed from GPLv2 to Apache License, version 2.0. - GStreamer is now a required dependency. - Exit early if not Python >= 2.6, < 3. - Include Sphinx scripts for building docs, pylintrc, tests and test data in diff --git a/docs/conf.py b/docs/conf.py index b190e8a9..c95c39df 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Mopidy' -copyright = u'2010, Stein Magnus Jodal' +copyright = u'2010, Stein Magnus Jodal and contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/index.rst b/docs/index.rst index 49c41226..7c53572c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ User documentation installation/index changes authors + licenses Reference documentation ======================= diff --git a/docs/licenses.rst b/docs/licenses.rst new file mode 100644 index 00000000..c7bf9433 --- /dev/null +++ b/docs/licenses.rst @@ -0,0 +1,34 @@ +******** +Licenses +******** + +For a list of contributors, see :ref:`authors`. For details on who have +contributed what, please refer to our git repository. + +Source code license +=================== + +Copyright 2009-2010 Stein Magnus Jodal and contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +Documentation license +===================== + +Copyright 2010 Stein Magnus Jodal and contributors + +This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 +Unported License. To view a copy of this license, visit +http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative +Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. From acd043719342f507ca92e9fb6d31605302cdad29 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 20:44:24 +0200 Subject: [PATCH 14/32] Remove despotify backend as library is no longer maintained --- docs/api/backends.rst | 8 - docs/changes.rst | 2 + docs/development/roadmap.rst | 3 +- docs/installation/despotify.rst | 73 ------- docs/installation/index.rst | 7 +- docs/installation/libspotify.rst | 2 +- mopidy/backends/despotify/__init__.py | 209 -------------------- mopidy/settings.py | 5 +- tests/backends/despotify_integrationtest.py | 35 ---- 9 files changed, 7 insertions(+), 337 deletions(-) delete mode 100644 docs/installation/despotify.rst delete mode 100644 mopidy/backends/despotify/__init__.py delete mode 100644 tests/backends/despotify_integrationtest.py diff --git a/docs/api/backends.rst b/docs/api/backends.rst index adb87e56..f675541a 100644 --- a/docs/api/backends.rst +++ b/docs/api/backends.rst @@ -82,14 +82,6 @@ Manages the music library, e.g. searching for tracks to be added to a playlist. :undoc-members: -:mod:`mopidy.backends.despotify` -- Despotify backend -===================================================== - -.. automodule:: mopidy.backends.despotify - :synopsis: Spotify backend using the Despotify library - :members: - - :mod:`mopidy.backends.dummy` -- Dummy backend for testing ========================================================= diff --git a/docs/changes.rst b/docs/changes.rst index 460b7538..4e33125d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -47,6 +47,8 @@ Another great release. - :meth:`mopidy.backends.base.BaseBackend()` now accepts an ``output_queue`` which it can use to send messages (i.e. audio data) to the output process. + - Remove Depsotify backend. + - Libspotify is now the default backend. diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 7dc284df..5544a005 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -11,8 +11,7 @@ Version 0.1 - Core MPD server functionality working. Gracefully handle clients' use of non-supported functionality. -- Read-only support for Spotify through :mod:`mopidy.backends.despotify` and/or - :mod:`mopidy.backends.libspotify`. +- Read-only support for Spotify through :mod:`mopidy.backends.libspotify`. - Initial support for local file playback through :mod:`mopidy.backends.local`. The state of local file playback will not block the release of 0.1. diff --git a/docs/installation/despotify.rst b/docs/installation/despotify.rst deleted file mode 100644 index 6787070d..00000000 --- a/docs/installation/despotify.rst +++ /dev/null @@ -1,73 +0,0 @@ -********************** -Despotify installation -********************** - -To use the `Despotify `_ backend, you first need to -install Despotify and spytify. - -.. warning:: - - This backend requires a Spotify premium account. - - -Installing Despotify on Linux -============================= - -Install Despotify's dependencies. At Debian/Ubuntu systems:: - - sudo aptitude install libssl-dev zlib1g-dev libvorbis-dev \ - libtool libncursesw5-dev libao-dev python-dev - -Check out revision 508 of the Despotify source code:: - - svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508 - -Build and install Despotify:: - - cd despotify/src/ - sudo make install - -When Despotify has been installed, continue with :ref:`spytify_installation`. - - -Installing Despotify on OS X -============================ - -In OS X you need to have `XCode `_ and -`Homebrew `_ installed. Then, to install -Despotify:: - - brew install despotify - -When Despotify has been installed, continue with :ref:`spytify_installation`. - - -.. _spytify_installation: - -Installing spytify -================== - -spytify's source comes bundled with despotify. If you haven't already checkout -out the despotify source, do it now:: - - svn checkout https://despotify.svn.sourceforge.net/svnroot/despotify@508 - -Build and install spytify:: - - cd despotify/src/bindings/python/ - export PKG_CONFIG_PATH=../../lib # Needed on OS X - sudo make install - - -Testing the installation -======================== - -To validate that everything is working, run the ``test.py`` script which is -distributed with spytify:: - - python test.py - -The test script should ask for your username and password (which must be for a -Spotify Premium account), ask for a search query, list all your playlists with -tracks, play 10s from a random song from the search result, pause for two -seconds, play for five more seconds, and quit. diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 227ae8b9..044f2155 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -18,7 +18,6 @@ Install dependencies gstreamer libspotify - despotify Make sure you got the required dependencies installed. @@ -44,10 +43,6 @@ Make sure you got the required dependencies installed. - Dependencies for at least one Mopidy backend: - - :mod:`mopidy.backends.despotify` (Linux and OS X) - - - :doc:`Despotify and spytify ` - - :mod:`mopidy.backends.libspotify` (Linux, OS X, and Windows) - :doc:`libspotify and pyspotify ` @@ -106,7 +101,7 @@ username and password into the file, like this:: SPOTIFY_USERNAME = u'myusername' SPOTIFY_PASSWORD = u'mysecret' -Currently :mod:`mopidy.backends.despotify` is the default +Currently :mod:`mopidy.backends.libspotify` is the default backend. If you want to use :mod:`mopidy.backends.libspotify`, copy the Spotify diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 3965162d..635c0495 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -2,7 +2,7 @@ libspotify installation *********************** -As an alternative to the despotify backend, we are working on a +We are working on a `libspotify `_ backend. To use the libspotify backend you must install libspotify and `pyspotify `_. diff --git a/mopidy/backends/despotify/__init__.py b/mopidy/backends/despotify/__init__.py deleted file mode 100644 index 78c7f774..00000000 --- a/mopidy/backends/despotify/__init__.py +++ /dev/null @@ -1,209 +0,0 @@ -import datetime as dt -import logging -import sys - -import spytify - -from mopidy import settings -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BaseLibraryController, BasePlaybackController, - BaseStoredPlaylistsController) -from mopidy.models import Artist, Album, Track, Playlist - -logger = logging.getLogger('mopidy.backends.despotify') - -ENCODING = 'utf-8' - -class DespotifyBackend(BaseBackend): - """ - A Spotify backend which uses the open source `despotify library - `_. - - `spytify `_ - is the Python bindings for the despotify library. It got litle - documentation, but a couple of examples are available. - - **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-despotify - """ - - def __init__(self, *args, **kwargs): - super(DespotifyBackend, self).__init__(*args, **kwargs) - self.current_playlist = DespotifyCurrentPlaylistController(backend=self) - self.library = DespotifyLibraryController(backend=self) - self.playback = DespotifyPlaybackController(backend=self) - self.stored_playlists = DespotifyStoredPlaylistsController(backend=self) - self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] - self.spotify = self._connect() - self.stored_playlists.refresh() - - def _connect(self): - logger.info(u'Connecting to Spotify') - try: - return DespotifySessionManager( - settings.SPOTIFY_USERNAME.encode(ENCODING), - settings.SPOTIFY_PASSWORD.encode(ENCODING), - core_queue=self.core_queue) - except spytify.SpytifyError as e: - logger.exception(e) - sys.exit(1) - - -class DespotifyCurrentPlaylistController(BaseCurrentPlaylistController): - pass - - -class DespotifyLibraryController(BaseLibraryController): - def find_exact(self, **query): - return self.search(**query) - - def lookup(self, uri): - track = self.backend.spotify.lookup(uri.encode(ENCODING)) - return DespotifyTranslator.to_mopidy_track(track) - - def refresh(self, uri=None): - pass # TODO - - def search(self, **query): - spotify_query = [] - for (field, values) in query.iteritems(): - if not hasattr(values, '__iter__'): - values = [values] - for value in values: - if field == u'track': - field = u'title' - if field == u'any': - spotify_query.append(value) - else: - spotify_query.append(u'%s:"%s"' % (field, value)) - spotify_query = u' '.join(spotify_query) - logger.debug(u'Spotify search query: %s', spotify_query) - result = self.backend.spotify.search(spotify_query.encode(ENCODING)) - if (result is None or result.playlist.tracks[0].get_uri() == - 'spotify:track:0000000000000000000000'): - return Playlist() - return DespotifyTranslator.to_mopidy_playlist(result.playlist) - - -class DespotifyPlaybackController(BasePlaybackController): - def _pause(self): - try: - self.backend.spotify.pause() - return True - except spytify.SpytifyError as e: - logger.error(e) - return False - - def _play(self, track): - try: - self.backend.spotify.play(self.backend.spotify.lookup(track.uri)) - return True - except spytify.SpytifyError as e: - logger.error(e) - return False - - def _resume(self): - try: - self.backend.spotify.resume() - return True - except spytify.SpytifyError as e: - logger.error(e) - return False - - def _seek(self, time_position): - pass # TODO - - def _stop(self): - try: - self.backend.spotify.stop() - return True - except spytify.SpytifyError as e: - logger.error(e) - return False - - -class DespotifyStoredPlaylistsController(BaseStoredPlaylistsController): - def create(self, name): - pass # TODO - - def delete(self, playlist): - pass # TODO - - def lookup(self, uri): - pass # TODO - - def refresh(self): - logger.info(u'Caching stored playlists') - playlists = [] - for spotify_playlist in self.backend.spotify.stored_playlists: - playlists.append( - DespotifyTranslator.to_mopidy_playlist(spotify_playlist)) - self._playlists = playlists - logger.debug(u'Available playlists: %s', - u', '.join([u'<%s>' % p.name for p in self.playlists])) - logger.info(u'Done caching stored playlists') - - def rename(self, playlist, new_name): - pass # TODO - - def save(self, playlist): - pass # TODO - - -class DespotifyTranslator(object): - @classmethod - def to_mopidy_artist(cls, spotify_artist): - return Artist( - uri=spotify_artist.get_uri(), - name=spotify_artist.name.decode(ENCODING) - ) - - @classmethod - def to_mopidy_album(cls, spotify_album_name): - return Album(name=spotify_album_name.decode(ENCODING)) - - @classmethod - def to_mopidy_track(cls, spotify_track): - if spotify_track is None or not spotify_track.has_meta_data(): - return None - if dt.MINYEAR <= int(spotify_track.year) <= dt.MAXYEAR: - date = dt.date(spotify_track.year, 1, 1) - else: - date = None - return Track( - uri=spotify_track.get_uri(), - name=spotify_track.title.decode(ENCODING), - artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists], - album=cls.to_mopidy_album(spotify_track.album), - track_no=spotify_track.tracknumber, - date=date, - length=spotify_track.length, - bitrate=320, - ) - - @classmethod - def to_mopidy_playlist(cls, spotify_playlist): - return Playlist( - uri=spotify_playlist.get_uri(), - name=spotify_playlist.name.decode(ENCODING), - tracks=filter(None, - [cls.to_mopidy_track(t) for t in spotify_playlist.tracks]), - ) - - -class DespotifySessionManager(spytify.Spytify): - DESPOTIFY_NEW_TRACK = 1 - DESPOTIFY_TIME_TELL = 2 - DESPOTIFY_END_OF_PLAYLIST = 3 - DESPOTIFY_TRACK_PLAY_ERROR = 4 - - def __init__(self, *args, **kwargs): - kwargs['callback'] = self.callback - self.core_queue = kwargs.pop('core_queue') - super(DespotifySessionManager, self).__init__(*args, **kwargs) - - def callback(self, signal, data): - if signal == self.DESPOTIFY_END_OF_PLAYLIST: - logger.debug('Despotify signalled end of playlist') - self.core_queue.put({'command': 'end_of_track'}) - elif signal == self.DESPOTIFY_TRACK_PLAY_ERROR: - logger.error('Despotify signalled track play error') diff --git a/mopidy/settings.py b/mopidy/settings.py index 1be09511..d4321685 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -14,13 +14,12 @@ import sys #: List of playback backends to use. See :mod:`mopidy.backends` for all #: available backends. Default:: #: -#: BACKENDS = (u'mopidy.backends.despotify.DespotifyBackend',) +#: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) #: #: .. note:: #: Currently only the first backend in the list is used. BACKENDS = ( - u'mopidy.backends.despotify.DespotifyBackend', - #u'mopidy.backends.libspotify.LibspotifyBackend', + u'mopidy.backends.libspotify.LibspotifyBackend', ) #: The log format used on the console. See diff --git a/tests/backends/despotify_integrationtest.py b/tests/backends/despotify_integrationtest.py deleted file mode 100644 index 4192bf7b..00000000 --- a/tests/backends/despotify_integrationtest.py +++ /dev/null @@ -1,35 +0,0 @@ -# TODO This integration test is work in progress. - -import unittest - -from mopidy.backends.despotify import DespotifyBackend -from mopidy.models import Track - -from tests.backends.base import * - -uris = [ - 'spotify:track:6vqcpVcbI3Zu6sH3ieLDNt', - 'spotify:track:111sulhaZqgsnypz3MkiaW', - 'spotify:track:7t8oznvbeiAPMDRuK0R5ZT', -] - -class DespotifyCurrentPlaylistControllerTest( - BaseCurrentPlaylistControllerTest, unittest.TestCase): - tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)] - backend_class = DespotifyBackend - - -class DespotifyPlaybackControllerTest( - BasePlaybackControllerTest, unittest.TestCase): - tracks = [Track(uri=uri, length=4464) for i, uri in enumerate(uris)] - backend_class = DespotifyBackend - - -class DespotifyStoredPlaylistsControllerTest( - BaseStoredPlaylistsControllerTest, unittest.TestCase): - backend_class = DespotifyBackend - - -class DespotifyLibraryControllerTest( - BaseLibraryControllerTest, unittest.TestCase): - backend_class = DespotifyBackend From 48478968378d93aed68a50452c8b3b0c0eb25eb9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 21:01:31 +0200 Subject: [PATCH 15/32] docs: Cleanup changelog a bit --- docs/changes.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4e33125d..7b154915 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -19,7 +19,10 @@ Another great release. the packages created by ``setup.py`` for i.e. PyPI. - Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`. - Changed ``SERVER_HOSTNAME`` and ``SERVER_PORT`` settings to - ``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT`` + ``MPD_SERVER_HOSTNAME`` and ``MPD_SERVER_PORT``. +- Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained + and the Libspotify backend is working much better. +- :mod:`mopidy.backends.libspotify` is now the default backend. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. @@ -47,8 +50,6 @@ Another great release. - :meth:`mopidy.backends.base.BaseBackend()` now accepts an ``output_queue`` which it can use to send messages (i.e. audio data) to the output process. - - Remove Depsotify backend. - - Libspotify is now the default backend. From 5fce38a7fad9c5285a3b749094d99eaa0fb6e232 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 21:41:47 +0200 Subject: [PATCH 16/32] docs: Update install docs --- docs/installation/index.rst | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 044f2155..d5e76cce 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -2,12 +2,10 @@ Installation ************ -Mopidy itself is a breeze to install, as it just requires a standard Python -installation and the GStreamer library. The libraries we depend on to connect -to the Spotify service is far more tricky to get working for the time being. -Until installation of these libraries are either well documented by their -developers, or the libraries are packaged for various Linux distributions, we -will supply our own installation guides, as linked to below. +To get a basic version of Mopidy running, you need Python and the GStreamer +library. To use Spotify with Mopidy, you also need :doc:`libspotify and +pyspotify `. Mopidy itself can either be installed from the Python +package index, PyPI, or from git. Install dependencies @@ -102,13 +100,8 @@ username and password into the file, like this:: SPOTIFY_PASSWORD = u'mysecret' Currently :mod:`mopidy.backends.libspotify` is the default -backend. - -If you want to use :mod:`mopidy.backends.libspotify`, copy the Spotify -application key to ``~/.mopidy/spotify_appkey.key``, and add the following -setting:: - - BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) +backend. Before you can use :mod:`mopidy.backends.libspotify`, you must copy +the Spotify application key to ``~/.mopidy/spotify_appkey.key``. If you want to use :mod:`mopidy.backends.local`, add the following setting:: From 581d694cb1a660a0ce6dd4399ec37394858e0edd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 21:51:02 +0200 Subject: [PATCH 17/32] Split libspotify backend into one file per class, and thus ensure that spotify depenedices don't fail tests --- mopidy/backends/libspotify/__init__.py | 263 +----------------- mopidy/backends/libspotify/library.py | 41 +++ mopidy/backends/libspotify/playback.py | 51 ++++ mopidy/backends/libspotify/session_manager.py | 106 +++++++ .../backends/libspotify/stored_playlists.py | 20 ++ mopidy/backends/libspotify/translator.py | 53 ++++ 6 files changed, 282 insertions(+), 252 deletions(-) create mode 100644 mopidy/backends/libspotify/library.py create mode 100644 mopidy/backends/libspotify/playback.py create mode 100644 mopidy/backends/libspotify/session_manager.py create mode 100644 mopidy/backends/libspotify/stored_playlists.py create mode 100644 mopidy/backends/libspotify/translator.py diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index 974e52df..ead08c44 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -1,19 +1,7 @@ -import datetime as dt import logging -import os -import multiprocessing -import threading -from spotify import Link, SpotifyError -from spotify.manager import SpotifySessionManager -from spotify.alsahelper import AlsaController - -from mopidy import get_version, settings -from mopidy.backends.base import (BaseBackend, BaseCurrentPlaylistController, - BaseLibraryController, BasePlaybackController, - BaseStoredPlaylistsController) -from mopidy.models import Artist, Album, Track, Playlist -from mopidy.process import pickle_connection +from mopidy import settings +from mopidy.backends.base import BaseBackend, BaseCurrentPlaylistController logger = logging.getLogger('mopidy.backends.libspotify') @@ -35,8 +23,15 @@ class LibspotifyBackend(BaseBackend): **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify """ + # Imports inside methods are to prevent loading of __init__ to fail on + # missing spotify dependencies. def __init__(self, *args, **kwargs): + from .library import LibspotifyLibraryController + from .playback import LibspotifyPlaybackController + from .stored_playlists import LibspotifyStoredPlaylistsController + super(LibspotifyBackend, self).__init__(*args, **kwargs) + self.current_playlist = BaseCurrentPlaylistController(backend=self) self.library = LibspotifyLibraryController(backend=self) self.playback = LibspotifyPlaybackController(backend=self) @@ -46,6 +41,8 @@ class LibspotifyBackend(BaseBackend): self.spotify = self._connect() def _connect(self): + from .session_manager import LibspotifySessionManager + logger.info(u'Connecting to Spotify') spotify = LibspotifySessionManager( settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, @@ -53,241 +50,3 @@ class LibspotifyBackend(BaseBackend): output_queue=self.output_queue) spotify.start() return spotify - - -class LibspotifyLibraryController(BaseLibraryController): - def find_exact(self, **query): - return self.search(**query) - - def lookup(self, uri): - spotify_track = Link.from_string(uri).as_track() - return LibspotifyTranslator.to_mopidy_track(spotify_track) - - def refresh(self, uri=None): - pass # TODO - - def search(self, **query): - spotify_query = [] - for (field, values) in query.iteritems(): - if not hasattr(values, '__iter__'): - values = [values] - for value in values: - if field == u'track': - field = u'title' - if field == u'any': - spotify_query.append(value) - else: - spotify_query.append(u'%s:"%s"' % (field, value)) - spotify_query = u' '.join(spotify_query) - logger.debug(u'Spotify search query: %s' % spotify_query) - my_end, other_end = multiprocessing.Pipe() - self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) - my_end.poll(None) - playlist = my_end.recv() - return playlist - - -class LibspotifyPlaybackController(BasePlaybackController): - def _set_output_state(self, state_name): - logger.debug(u'Setting output state to %s ...', state_name) - (my_end, other_end) = multiprocessing.Pipe() - self.backend.output_queue.put({ - 'command': 'set_state', - 'state': state_name, - 'reply_to': pickle_connection(other_end), - }) - my_end.poll(None) - return my_end.recv() - - def _pause(self): - return self._set_output_state('PAUSED') - - def _play(self, track): - self._set_output_state('READY') - if self.state == self.PLAYING: - self.stop() - if track.uri is None: - return False - try: - self.backend.spotify.session.load( - Link.from_string(track.uri).as_track()) - self.backend.spotify.session.play(1) - self._set_output_state('PLAYING') - return True - except SpotifyError as e: - logger.warning('Play %s failed: %s', track.uri, e) - return False - - def _resume(self): - return self._set_output_state('PLAYING') - - def _seek(self, time_position): - pass # TODO - - def _stop(self): - result = self._set_output_state('READY') - self.backend.spotify.session.play(0) - return result - - -class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): - def create(self, name): - pass # TODO - - def delete(self, playlist): - pass # TODO - - def lookup(self, uri): - pass # TODO - - def refresh(self): - pass # TODO - - def rename(self, playlist, new_name): - pass # TODO - - def save(self, playlist): - pass # TODO - - -class LibspotifyTranslator(object): - @classmethod - def to_mopidy_artist(cls, spotify_artist): - if not spotify_artist.is_loaded(): - return Artist(name=u'[loading...]') - return Artist( - uri=str(Link.from_artist(spotify_artist)), - name=spotify_artist.name().decode(ENCODING), - ) - - @classmethod - def to_mopidy_album(cls, spotify_album): - if 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)) - - @classmethod - def to_mopidy_track(cls, spotify_track): - if not spotify_track.is_loaded(): - return Track(name=u'[loading...]') - uri = str(Link.from_track(spotify_track, 0)) - if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR: - date = dt.date(spotify_track.album().year(), 1, 1) - else: - date = None - return Track( - uri=uri, - name=spotify_track.name().decode(ENCODING), - 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(), - date=date, - length=spotify_track.duration(), - bitrate=320, - ) - - @classmethod - def to_mopidy_playlist(cls, spotify_playlist): - if not spotify_playlist.is_loaded(): - return Playlist(name=u'[loading...]') - return Playlist( - uri=str(Link.from_playlist(spotify_playlist)), - name=spotify_playlist.name().decode(ENCODING), - tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], - ) - -class LibspotifySessionManager(SpotifySessionManager, threading.Thread): - cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) - user_agent = 'Mopidy %s' % get_version() - - def __init__(self, username, password, core_queue, output_queue): - SpotifySessionManager.__init__(self, username, password) - threading.Thread.__init__(self) - self.core_queue = core_queue - self.output_queue = output_queue - self.connected = threading.Event() - self.session = None - - def run(self): - self.connect() - - def logged_in(self, session, error): - """Callback used by pyspotify""" - logger.info('Logged in') - self.session = session - self.connected.set() - - def logged_out(self, session): - """Callback used by pyspotify""" - logger.info('Logged out') - - def metadata_updated(self, session): - """Callback used by pyspotify""" - logger.debug('Metadata updated, refreshing stored playlists') - playlists = [] - for spotify_playlist in session.playlist_container(): - playlists.append( - LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) - self.core_queue.put({ - 'command': 'set_stored_playlists', - 'playlists': playlists, - }) - - def connection_error(self, session, error): - """Callback used by pyspotify""" - logger.error('Connection error: %s', error) - - def message_to_user(self, session, message): - """Callback used by pyspotify""" - logger.info(message) - - def notify_main_thread(self, session): - """Callback used by pyspotify""" - logger.debug('Notify main thread') - - def music_delivery(self, session, frames, frame_size, num_frames, - sample_type, sample_rate, channels): - """Callback used by pyspotify""" - # TODO Base caps_string on arguments - caps_string = """ - audio/x-raw-int, - endianness=(int)1234, - channels=(int)2, - width=(int)16, - depth=(int)16, - signed=True, - rate=(int)44100 - """ - self.output_queue.put({ - 'command': 'deliver_data', - 'caps': caps_string, - 'data': bytes(frames), - }) - - def play_token_lost(self, session): - """Callback used by pyspotify""" - logger.debug('Play token lost') - self.core_queue.put({'command': 'stop_playback'}) - - def log_message(self, session, data): - """Callback used by pyspotify""" - logger.debug(data) - - def end_of_track(self, session): - """Callback used by pyspotify""" - logger.debug('End of data stream.') - self.output_queue.put({'command': 'end_of_data_stream'}) - - def search(self, query, connection): - """Search method used by Mopidy backend""" - def callback(results, userdata): - # TODO Include results from results.albums(), etc. too - playlist = Playlist(tracks=[ - LibspotifyTranslator.to_mopidy_track(t) - for t in results.tracks()]) - connection.send(playlist) - self.connected.wait() - self.session.search(query, callback) diff --git a/mopidy/backends/libspotify/library.py b/mopidy/backends/libspotify/library.py new file mode 100644 index 00000000..c2b70dca --- /dev/null +++ b/mopidy/backends/libspotify/library.py @@ -0,0 +1,41 @@ +import logging +import multiprocessing + +from spotify import Link + +from mopidy.backends.base import BaseLibraryController +from mopidy.backends.libspotify import ENCODING +from mopidy.backends.libspotify.translator import LibspotifyTranslator + +logger = logging.getLogger('mopidy.backends.libspotify.library') + +class LibspotifyLibraryController(BaseLibraryController): + def find_exact(self, **query): + return self.search(**query) + + def lookup(self, uri): + spotify_track = Link.from_string(uri).as_track() + return LibspotifyTranslator.to_mopidy_track(spotify_track) + + def refresh(self, uri=None): + pass # TODO + + def search(self, **query): + spotify_query = [] + for (field, values) in query.iteritems(): + if not hasattr(values, '__iter__'): + values = [values] + for value in values: + if field == u'track': + field = u'title' + if field == u'any': + spotify_query.append(value) + else: + spotify_query.append(u'%s:"%s"' % (field, value)) + spotify_query = u' '.join(spotify_query) + logger.debug(u'Spotify search query: %s' % spotify_query) + my_end, other_end = multiprocessing.Pipe() + self.backend.spotify.search(spotify_query.encode(ENCODING), other_end) + my_end.poll(None) + playlist = my_end.recv() + return playlist diff --git a/mopidy/backends/libspotify/playback.py b/mopidy/backends/libspotify/playback.py new file mode 100644 index 00000000..3ba91d5f --- /dev/null +++ b/mopidy/backends/libspotify/playback.py @@ -0,0 +1,51 @@ +import logging +import multiprocessing + +from spotify import Link, SpotifyError + +from mopidy.backends.base import BasePlaybackController +from mopidy.process import pickle_connection + +logger = logging.getLogger('mopidy.backends.libspotify.playback') + +class LibspotifyPlaybackController(BasePlaybackController): + def _set_output_state(self, state_name): + logger.debug(u'Setting output state to %s ...', state_name) + (my_end, other_end) = multiprocessing.Pipe() + self.backend.output_queue.put({ + 'command': 'set_state', + 'state': state_name, + 'reply_to': pickle_connection(other_end), + }) + my_end.poll(None) + return my_end.recv() + + def _pause(self): + return self._set_output_state('PAUSED') + + def _play(self, track): + self._set_output_state('READY') + if self.state == self.PLAYING: + self.stop() + if track.uri is None: + return False + try: + self.backend.spotify.session.load( + Link.from_string(track.uri).as_track()) + self.backend.spotify.session.play(1) + self._set_output_state('PLAYING') + return True + except SpotifyError as e: + logger.warning('Play %s failed: %s', track.uri, e) + return False + + def _resume(self): + return self._set_output_state('PLAYING') + + def _seek(self, time_position): + pass # TODO + + def _stop(self): + result = self._set_output_state('READY') + self.backend.spotify.session.play(0) + return result diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py new file mode 100644 index 00000000..e286b059 --- /dev/null +++ b/mopidy/backends/libspotify/session_manager.py @@ -0,0 +1,106 @@ +import logging +import os +import threading + +from spotify.manager import SpotifySessionManager + +from mopidy import get_version, settings +from mopidy.models import Playlist +from mopidy.backends.libspotify.translator import LibspotifyTranslator + +logger = logging.getLogger('mopidy.backends.libspotify.session_manager') + +class LibspotifySessionManager(SpotifySessionManager, threading.Thread): + cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) + settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) + appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) + user_agent = 'Mopidy %s' % get_version() + + def __init__(self, username, password, core_queue, output_queue): + SpotifySessionManager.__init__(self, username, password) + threading.Thread.__init__(self) + self.core_queue = core_queue + self.output_queue = output_queue + self.connected = threading.Event() + self.session = None + + def run(self): + self.connect() + + def logged_in(self, session, error): + """Callback used by pyspotify""" + logger.info('Logged in') + self.session = session + self.connected.set() + + def logged_out(self, session): + """Callback used by pyspotify""" + logger.info('Logged out') + + def metadata_updated(self, session): + """Callback used by pyspotify""" + logger.debug('Metadata updated, refreshing stored playlists') + playlists = [] + for spotify_playlist in session.playlist_container(): + playlists.append( + LibspotifyTranslator.to_mopidy_playlist(spotify_playlist)) + self.core_queue.put({ + 'command': 'set_stored_playlists', + 'playlists': playlists, + }) + + def connection_error(self, session, error): + """Callback used by pyspotify""" + logger.error('Connection error: %s', error) + + def message_to_user(self, session, message): + """Callback used by pyspotify""" + logger.info(message) + + def notify_main_thread(self, session): + """Callback used by pyspotify""" + logger.debug('Notify main thread') + + def music_delivery(self, session, frames, frame_size, num_frames, + sample_type, sample_rate, channels): + """Callback used by pyspotify""" + # TODO Base caps_string on arguments + caps_string = """ + audio/x-raw-int, + endianness=(int)1234, + channels=(int)2, + width=(int)16, + depth=(int)16, + signed=True, + rate=(int)44100 + """ + self.output_queue.put({ + 'command': 'deliver_data', + 'caps': caps_string, + 'data': bytes(frames), + }) + + def play_token_lost(self, session): + """Callback used by pyspotify""" + logger.debug('Play token lost') + self.core_queue.put({'command': 'stop_playback'}) + + def log_message(self, session, data): + """Callback used by pyspotify""" + logger.debug(data) + + def end_of_track(self, session): + """Callback used by pyspotify""" + logger.debug('End of data stream.') + self.output_queue.put({'command': 'end_of_data_stream'}) + + def search(self, query, connection): + """Search method used by Mopidy backend""" + def callback(results, userdata): + # TODO Include results from results.albums(), etc. too + playlist = Playlist(tracks=[ + LibspotifyTranslator.to_mopidy_track(t) + for t in results.tracks()]) + connection.send(playlist) + self.connected.wait() + self.session.search(query, callback) diff --git a/mopidy/backends/libspotify/stored_playlists.py b/mopidy/backends/libspotify/stored_playlists.py new file mode 100644 index 00000000..3339578c --- /dev/null +++ b/mopidy/backends/libspotify/stored_playlists.py @@ -0,0 +1,20 @@ +from mopidy.backends.base import BaseStoredPlaylistsController + +class LibspotifyStoredPlaylistsController(BaseStoredPlaylistsController): + def create(self, name): + pass # TODO + + def delete(self, playlist): + pass # TODO + + def lookup(self, uri): + pass # TODO + + def refresh(self): + pass # TODO + + def rename(self, playlist, new_name): + pass # TODO + + def save(self, playlist): + pass # TODO diff --git a/mopidy/backends/libspotify/translator.py b/mopidy/backends/libspotify/translator.py new file mode 100644 index 00000000..3a39aad5 --- /dev/null +++ b/mopidy/backends/libspotify/translator.py @@ -0,0 +1,53 @@ +import datetime as dt + +from spotify import Link + +from mopidy.models import Artist, Album, Track, Playlist +from mopidy.backends.libspotify import ENCODING + +class LibspotifyTranslator(object): + @classmethod + def to_mopidy_artist(cls, spotify_artist): + if not spotify_artist.is_loaded(): + return Artist(name=u'[loading...]') + return Artist( + uri=str(Link.from_artist(spotify_artist)), + name=spotify_artist.name().decode(ENCODING), + ) + + @classmethod + def to_mopidy_album(cls, spotify_album): + if 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)) + + @classmethod + def to_mopidy_track(cls, spotify_track): + if not spotify_track.is_loaded(): + return Track(name=u'[loading...]') + uri = str(Link.from_track(spotify_track, 0)) + if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR: + date = dt.date(spotify_track.album().year(), 1, 1) + else: + date = None + return Track( + uri=uri, + name=spotify_track.name().decode(ENCODING), + 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(), + date=date, + length=spotify_track.duration(), + bitrate=320, + ) + + @classmethod + def to_mopidy_playlist(cls, spotify_playlist): + if not spotify_playlist.is_loaded(): + return Playlist(name=u'[loading...]') + return Playlist( + uri=str(Link.from_playlist(spotify_playlist)), + name=spotify_playlist.name().decode(ENCODING), + tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], + ) From 059f96814d80fc1a3c14e7e9f40428865ceb233a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 22:16:11 +0200 Subject: [PATCH 18/32] Add basic tests for get_class util --- tests/utils_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/utils_test.py b/tests/utils_test.py index d5c98d86..9a8f1129 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -11,6 +11,15 @@ from mopidy.models import Track, Artist, Album from tests import SkipTest, data_folder +class GetClassTest(unittest.TestCase): + def test_loading_class_that_does_not_exist(self): + test = lambda: get_class('foo.bar.Baz') + self.assertRaises(ImportError, test) + + def test_loading_existing_class(self): + cls = get_class('unittest.TestCase') + self.assertEqual(cls.__name__, 'TestCase') + class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): self.parent = tempfile.mkdtemp() From db26a7198da4cf222abd399241700fbfc9554316 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 22:26:13 +0200 Subject: [PATCH 19/32] Remove notes on openspotify, as development has been inactive for six months --- docs/development/roadmap.rst | 4 ---- mopidy/backends/libspotify/__init__.py | 7 ++----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 5544a005..7d97d55b 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -31,10 +31,6 @@ released when we reach the other goal. Stuff we really want to do, but just not right now ================================================== -- Replace libspotify with `openspotify - `_ for - :mod:`mopidy.backends.libspotify`. *Update:* Seems like openspotify - development has stalled. - Create `Debian packages `_ of all our dependencies and Mopidy itself (hosted in our own Debian repo until we get stuff into the various distros) to make Debian/Ubuntu installation a breeze. diff --git a/mopidy/backends/libspotify/__init__.py b/mopidy/backends/libspotify/__init__.py index ead08c44..7a971bc5 100644 --- a/mopidy/backends/libspotify/__init__.py +++ b/mopidy/backends/libspotify/__init__.py @@ -16,15 +16,12 @@ class LibspotifyBackend(BaseBackend): for libspotify. It got no documentation, but multiple examples are available. Like libspotify, pyspotify's calls are mostly asynchronous. - This backend should also work with `openspotify - `_, but we haven't tested - that yet. - **Issues:** http://github.com/jodal/mopidy/issues/labels/backend-libspotify """ - # Imports inside methods are to prevent loading of __init__ to fail on + # Imports inside methods are to prevent loading of __init__.py to fail on # missing spotify dependencies. + def __init__(self, *args, **kwargs): from .library import LibspotifyLibraryController from .playback import LibspotifyPlaybackController From e4bdacbb61a6894a8515bd5bae100cb978514028 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 22:28:02 +0200 Subject: [PATCH 20/32] Add test_import_error_message_contains_complete_class_path test for get_class --- mopidy/utils.py | 5 ++++- tests/utils_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mopidy/utils.py b/mopidy/utils.py index ff032b4e..b8aa574c 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -24,7 +24,10 @@ def get_class(name): module_name = name[:name.rindex('.')] class_name = name[name.rindex('.') + 1:] logger.debug('Loading: %s', name) - module = import_module(module_name) + try: + module = import_module(module_name) + except ImportError: + raise ImportError("Couldn't load: %s" % name) class_object = getattr(module, class_name) return class_object diff --git a/tests/utils_test.py b/tests/utils_test.py index 9a8f1129..d5beade2 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -16,6 +16,12 @@ class GetClassTest(unittest.TestCase): test = lambda: get_class('foo.bar.Baz') self.assertRaises(ImportError, test) + def test_import_error_message_contains_complete_class_path(self): + try: + get_class('foo.bar.Baz') + except ImportError as e: + self.assert_('foo.bar.Baz' in str(e)) + def test_loading_existing_class(self): cls = get_class('unittest.TestCase') self.assertEqual(cls.__name__, 'TestCase') From 710eb91892aeddc79d9e8e542d9e4350a3095989 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 22:29:27 +0200 Subject: [PATCH 21/32] docs: Update Homebrew point on roadmap --- docs/development/roadmap.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/development/roadmap.rst b/docs/development/roadmap.rst index 7d97d55b..243243ab 100644 --- a/docs/development/roadmap.rst +++ b/docs/development/roadmap.rst @@ -31,11 +31,13 @@ released when we reach the other goal. Stuff we really want to do, but just not right now ================================================== +- **[PENDING]** Create `Homebrew `_ recipies + for all our dependencies and Mopidy itself to make OS X installation a + breeze. See `Homebrew's issue #1612 + `_. - Create `Debian packages `_ of all our dependencies and Mopidy itself (hosted in our own Debian repo until we get stuff into the various distros) to make Debian/Ubuntu installation a breeze. -- **[WIP]** Create `Homebrew `_ recipies for - all our dependencies and Mopidy itself to make OS X installation a breeze. - Run frontend tests against a real MPD server to ensure we are in sync. - Start working with MPD client maintainers to get rid of weird assumptions like only searching for first two letters and doing the rest of the filtering From ec67d43fc960d5537fef3fdf5e6fc46c6a354e0f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 22:29:41 +0200 Subject: [PATCH 22/32] Test both case where class and/or module does not exist for get_class --- mopidy/utils.py | 4 ++-- tests/utils_test.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/mopidy/utils.py b/mopidy/utils.py index b8aa574c..bdc0b632 100644 --- a/mopidy/utils.py +++ b/mopidy/utils.py @@ -26,9 +26,9 @@ def get_class(name): logger.debug('Loading: %s', name) try: module = import_module(module_name) - except ImportError: + class_object = getattr(module, class_name) + except (ImportError, AttributeError): raise ImportError("Couldn't load: %s" % name) - class_object = getattr(module, class_name) return class_object def get_or_create_folder(folder): diff --git a/tests/utils_test.py b/tests/utils_test.py index d5beade2..ca44de45 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -12,10 +12,14 @@ from mopidy.models import Track, Artist, Album from tests import SkipTest, data_folder class GetClassTest(unittest.TestCase): - def test_loading_class_that_does_not_exist(self): + def test_loading_module_that_does_not_exist(self): test = lambda: get_class('foo.bar.Baz') self.assertRaises(ImportError, test) + def test_loading_class_that_does_not_exist(self): + test = lambda: get_class('unittest.FooBarBaz') + self.assertRaises(ImportError, test) + def test_import_error_message_contains_complete_class_path(self): try: get_class('foo.bar.Baz') From fa9edf23cf0d3a028095e83737cff4a69fc43dff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 22:29:56 +0200 Subject: [PATCH 23/32] Update MANIFEST.in to include LICENSE instead of COPYING --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index cb752f87..8a73b481 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include COPYING pylintrc *.rst *.txt +include LICENSE pylintrc *.rst *.txt recursive-include docs * prune docs/_build recursive-include tests *.py From 9c11c5ecb9ad5f57307a30d133ccdeb8f1a11f93 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 22:40:38 +0200 Subject: [PATCH 24/32] Log when a process has a problem importing classes and try to exit --- mopidy/process.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/process.py b/mopidy/process.py index 9759c4e6..79638515 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -28,6 +28,9 @@ class BaseProcess(multiprocessing.Process): except SettingsError as e: logger.error(e.message) sys.exit(1) + except ImportError as e: + logger.error(e) + sys.exit(1) def run_inside_try(self): raise NotImplementedError From a05212251bec02740b651eaf9849e190ebd1546e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 22:48:51 +0200 Subject: [PATCH 25/32] Pass output, backend and frontend classes into coreprocess to so that import errors are handeled better --- mopidy/__main__.py | 5 ++++- mopidy/process.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 7c62033b..c92ce1ed 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -22,7 +22,10 @@ def main(): get_or_create_folder('~/.mopidy/') core_queue = multiprocessing.Queue() get_class(settings.SERVER)(core_queue).start() - core = CoreProcess(core_queue) + output_class = get_class(settings.OUTPUT) + backend_class = get_class(settings.BACKENDS[0]) + frontend_class = get_class(settings.FRONTEND) + core = CoreProcess(core_queue, output_class, backend_class, frontend_class) core.start() asyncore.loop() diff --git a/mopidy/process.py b/mopidy/process.py index 79638515..b1cdc8af 100644 --- a/mopidy/process.py +++ b/mopidy/process.py @@ -37,10 +37,14 @@ class BaseProcess(multiprocessing.Process): class CoreProcess(BaseProcess): - def __init__(self, core_queue): + def __init__(self, core_queue, output_class, backend_class, + frontend_class): super(CoreProcess, self).__init__() self.core_queue = core_queue self.output_queue = None + self.output_class = output_class + self.backend_class = backend_class + self.frontend_class = frontend_class self.output = None self.backend = None self.frontend = None @@ -53,11 +57,9 @@ class CoreProcess(BaseProcess): def setup(self): self.output_queue = multiprocessing.Queue() - self.output = get_class(settings.OUTPUT)(self.core_queue, - self.output_queue) - self.backend = get_class(settings.BACKENDS[0])(self.core_queue, - self.output_queue) - self.frontend = get_class(settings.FRONTEND)(self.backend) + self.output = self.output_class(self.core_queue, self.output_queue) + self.backend = self.backend_class(self.core_queue, self.output_queue) + self.frontend = self.frontend_class(self.backend) def process_message(self, message): if message.get('to') == 'output': From 81928b831c354de0b79b4e203f235c8067b535fb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 23:20:17 +0200 Subject: [PATCH 26/32] Strip newline at end of libspotify log messages --- mopidy/backends/libspotify/session_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index e286b059..2de6ae63 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -55,7 +55,7 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def message_to_user(self, session, message): """Callback used by pyspotify""" - logger.info(message) + logger.info(message.strip()) def notify_main_thread(self, session): """Callback used by pyspotify""" @@ -87,7 +87,7 @@ class LibspotifySessionManager(SpotifySessionManager, threading.Thread): def log_message(self, session, data): """Callback used by pyspotify""" - logger.debug(data) + logger.debug(data.strip()) def end_of_track(self, session): """Callback used by pyspotify""" From 5a4d0bd7160ce8741ca876bf626ee0a93959259e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 13 Aug 2010 23:41:31 +0200 Subject: [PATCH 27/32] Freshen up settings docs --- docs/api/settings.rst | 2 +- mopidy/settings.py | 81 ++++++++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/docs/api/settings.rst b/docs/api/settings.rst index 12d2833f..cfc270d6 100644 --- a/docs/api/settings.rst +++ b/docs/api/settings.rst @@ -13,7 +13,7 @@ there. A complete ``~/.mopidy/settings.py`` may look like this:: - MPD_SERVER_HOSTNAME = u'0.0.0.0' + MPD_SERVER_HOSTNAME = u'::' SPOTIFY_USERNAME = u'alice' SPOTIFY_PASSWORD = u'mysecret' diff --git a/mopidy/settings.py b/mopidy/settings.py index d4321685..949b2e06 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -3,16 +3,19 @@ Available settings and their default values. .. warning:: - Do *not* change settings in ``mopidy/settings.py``. Instead, add a file - called ``~/.mopidy/settings.py`` and redefine settings there. + Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a + file called ``~/.mopidy/settings.py`` and redefine settings there. """ +# Absolute import needed to import ~/.mopidy/settings.py and not ourselves from __future__ import absolute_import import os import sys #: List of playback backends to use. See :mod:`mopidy.backends` for all -#: available backends. Default:: +#: available backends. +#: +#: Default:: #: #: BACKENDS = (u'mopidy.backends.libspotify.LibspotifyBackend',) #: @@ -28,32 +31,51 @@ BACKENDS = ( CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \ ' [%(process)d:%(threadName)s] %(name)s\n %(message)s' -#: The log format used for dump logs. Default:: +#: The log format used for dump logs. +#: +#: Default:: #: #: DUMP_LOG_FILENAME = CONSOLE_LOG_FORMAT DUMP_LOG_FORMAT = CONSOLE_LOG_FORMAT -#: The file to dump debug log data to. Default:: +#: The file to dump debug log data to when Mopidy is run with the +#: :option:`--dump` option. +#: +#: Default:: #: #: DUMP_LOG_FILENAME = u'dump.log' DUMP_LOG_FILENAME = u'dump.log' -#: Protocol frontend to use. Default:: +#: Protocol frontend to use. +#: +#: Default:: #: #: FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' FRONTEND = u'mopidy.frontends.mpd.frontend.MpdFrontend' -#: Path to folder with local music. Default:: +#: Path to folder with local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_MUSIC_FOLDER = u'~/music' LOCAL_MUSIC_FOLDER = u'~/music' -#: Path to playlist folder with m3u files for local music. Default:: +#: Path to playlist folder with m3u files for local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' LOCAL_PLAYLIST_FOLDER = u'~/.mopidy/playlists' -#: Path to tag cache for local music. Default:: +#: Path to tag cache for local music. +#: +#: Used by :mod:`mopidy.backends.local`. +#: +#: Default:: #: #: LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' LOCAL_TAG_CACHE = u'~/.mopidy/tag_cache' @@ -86,6 +108,7 @@ MIXER_ALSA_CONTROL = False #: External mixers only. Which port the mixer is connected to. #: #: This must point to the device port like ``/dev/ttyUSB0``. +#: #: Default: :class:`None` MIXER_EXT_PORT = None @@ -104,17 +127,23 @@ MIXER_EXT_SPEAKERS_A = None #: Default: :class:`None`. MIXER_EXT_SPEAKERS_B = None -#: Audio output handler to use. Default:: +#: Audio output handler to use. +#: +#: Default:: #: #: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput' -#: Server to use. Default:: +#: Server to use. +#: +#: Default:: #: #: SERVER = u'mopidy.frontends.mpd.server.MpdServer' SERVER = u'mopidy.frontends.mpd.server.MpdServer' -#: Which address Mopidy should bind to. Examples: +#: Which address Mopidy's MPD server should bind to. +#: +#:Examples: #: #: ``127.0.0.1`` #: Listens only on the IPv4 loopback interface. Default. @@ -126,21 +155,31 @@ SERVER = u'mopidy.frontends.mpd.server.MpdServer' #: Listens on all interfaces, both IPv4 and IPv6. MPD_SERVER_HOSTNAME = u'127.0.0.1' -#: Which TCP port Mopidy should listen to. Default: 6600 +#: Which TCP port Mopidy's MPD server should listen to. +#: +#: Default: 6600 MPD_SERVER_PORT = 6600 -#: Your Spotify Premium username. Used by all Spotify backends. -SPOTIFY_USERNAME = u'' - -#: Your Spotify Premium password. Used by all Spotify backends. -SPOTIFY_PASSWORD = u'' - -#: Path to your libspotify application key. Used by LibspotifyBackend. +#: Path to your libspotify application key. +#: +#: Used by :mod:`mopidy.backends.libspotify`. SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key' -#: Path to the libspotify cache. Used by LibspotifyBackend. +#: Path to the libspotify cache. +#: +#: Used by :mod:`mopidy.backends.libspotify`. SPOTIFY_LIB_CACHE = u'~/.mopidy/libspotify_cache' +#: Your Spotify Premium username. +#: +#: Used by :mod:`mopidy.backends.libspotify`. +SPOTIFY_USERNAME = u'' + +#: Your Spotify Premium password. +#: +#: Used by :mod:`mopidy.backends.libspotify`. +SPOTIFY_PASSWORD = u'' + # Import user specific settings dotdir = os.path.expanduser(u'~/.mopidy/') settings_file = os.path.join(dotdir, u'settings.py') From b777397cceb63bbccce302904e78b51c5991ab17 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 13 Aug 2010 23:52:46 +0200 Subject: [PATCH 28/32] Cleanup pipe creation for GStreamer output --- mopidy/outputs/gstreamer.py | 44 +++++-------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 65b65504..b81fbd0f 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -39,6 +39,8 @@ class GStreamerProcess(BaseProcess): http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html. """ + pipeline_description = 'appsrc name=data ! volume name=volume ! autoaudiosink name=sink' + def __init__(self, core_queue, output_queue): super(GStreamerProcess, self).__init__() self.core_queue = core_queue @@ -65,8 +67,10 @@ class GStreamerProcess(BaseProcess): messages_thread.daemon = True messages_thread.start() - # A pipeline consisting of many elements - self.gst_pipeline = gst.Pipeline("pipeline") + self.gst_pipeline = gst.parse_launch(self.pipeline_description) + self.gst_data_src = self.gst_pipeline.get_by_name('data') + self.gst_volume = self.gst_pipeline.get_by_name('volume') + self.gst_sink = self.gst_pipeline.get_by_name('sink') # Setup bus and message processor self.gst_bus = self.gst_pipeline.get_bus() @@ -74,42 +78,6 @@ class GStreamerProcess(BaseProcess): self.gst_bus_id = self.gst_bus.connect('message', self.process_gst_message) - # Bin for playing audio URIs - #self.gst_uri_src = gst.element_factory_make('uridecodebin', 'uri_src') - #self.gst_pipeline.add(self.gst_uri_src) - - # Bin for playing audio data - self.gst_data_src = gst.element_factory_make('appsrc', 'data_src') - self.gst_pipeline.add(self.gst_data_src) - - # Volume filter - self.gst_volume = gst.element_factory_make('volume', 'volume') - self.gst_pipeline.add(self.gst_volume) - - # Audio output sink - self.gst_sink = gst.element_factory_make('autoaudiosink', 'sink') - self.gst_pipeline.add(self.gst_sink) - - # Add callback that will link uri_src output with volume filter input - # when the output pad is ready. - # See http://stackoverflow.com/questions/2993777 for details. - def on_new_decoded_pad(dbin, pad, is_last): - uri_src = pad.get_parent() - pipeline = uri_src.get_parent() - volume = pipeline.get_by_name('volume') - uri_src.link(volume) - logger.debug("Linked uri_src's new decoded pad to volume filter") - # FIXME uridecodebin got no new-decoded-pad signal, but it's - # subcomponent decodebin2 got that signal. Fixing this is postponed - # till after data_src is up and running perfectly - #self.gst_uri_src.connect('new-decoded-pad', on_new_decoded_pad) - - # Link data source output with volume filter input - self.gst_data_src.link(self.gst_volume) - - # Link volume filter output to audio sink input - self.gst_volume.link(self.gst_sink) - def process_mopidy_message(self, message): """Process messages from the rest of Mopidy.""" if message['command'] == 'play_uri': From 8d19301d41e8bc6f2e81c875671ea8e500a9e3c5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 00:48:15 +0200 Subject: [PATCH 29/32] Update license i PyPI classifiers --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index bbf300f7..33113732 100644 --- a/setup.py +++ b/setup.py @@ -52,14 +52,14 @@ setup( data_files=data_files, scripts=['bin/mopidy'], url='http://www.mopidy.com/', - license='GPLv2', + license='Apache License, Version 2.0', description='MPD server with Spotify support', long_description=open('README.rst').read(), classifiers=[ 'Development Status :: 3 - Alpha', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: GNU General Public License (GPL)', + 'License :: OSI Approved :: Apache Software License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.6', From 4ceb86cad0882a6f0568a58ada154b1a5c9b66da Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 00:48:37 +0200 Subject: [PATCH 30/32] Switch to beta status in PyPI classifiers --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 33113732..5ac94c00 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ setup( description='MPD server with Spotify support', long_description=open('README.rst').read(), classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: Apache Software License', From 63d2e7710e6088cee4162c9987c4ba4179d32965 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 00:50:22 +0200 Subject: [PATCH 31/32] Copy distutils install_data fix from Django --- setup.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5ac94c00..76c38e4b 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,34 @@ +""" +Most of this file is taken from the Django project, which is BSD licensed. +""" + from distutils.core import setup +from distutils.command.install_data import install_data from distutils.command.install import INSTALL_SCHEMES import os +import sys from mopidy import get_version +class osx_install_data(install_data): + # On MacOS, the platform-specific lib dir is + # /System/Library/Framework/Python/.../ which is wrong. Python 2.5 supplied + # with MacOS 10.5 has an Apple-specific fix for this in + # distutils.command.install_data#306. It fixes install_lib but not + # install_data, which is why we roll our own install_data class. + + def finalize_options(self): + # By the time finalize_options is called, install.install_lib is set to + # the fixed directory, so we set the installdir to install_lib. The + # install_data class uses ('install_data', 'install_dir') instead. + self.set_undefined_options('install', ('install_lib', 'install_dir')) + install_data.finalize_options(self) + +if sys.platform == "darwin": + cmdclasses = {'install_data': osx_install_data} +else: + cmdclasses = {'install_data': install_data} + def fullsplit(path, result=None): """ Split a pathname into components (the opposite of os.path.join) in a @@ -20,7 +45,8 @@ def fullsplit(path, result=None): # Tell distutils to put the data_files in platform-specific installation # locations. See here for an explanation: -# http://groups.google.com/group/comp.lang.python/browse_thread/thread/35ec7b2fed36eaec/2105ee4d9e8042cb +# http://groups.google.com/group/comp.lang.python/browse_thread/ +# thread/35ec7b2fed36eaec/2105ee4d9e8042cb for scheme in INSTALL_SCHEMES.values(): scheme['data'] = scheme['purelib'] @@ -49,6 +75,7 @@ setup( author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', packages=packages, + cmdclass=cmdclasses, data_files=data_files, scripts=['bin/mopidy'], url='http://www.mopidy.com/', From 8238e955b815794b5316a4d94f8a2087d9443d97 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 14 Aug 2010 00:55:25 +0200 Subject: [PATCH 32/32] Bundle a Spotify appkey with the libspotify backend --- MANIFEST.in | 1 + docs/changes.rst | 2 ++ mopidy/backends/libspotify/session_manager.py | 2 +- mopidy/backends/libspotify/spotify_appkey.key | Bin 0 -> 321 bytes mopidy/settings.py | 5 ----- setup.py | 1 + 6 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 mopidy/backends/libspotify/spotify_appkey.key diff --git a/MANIFEST.in b/MANIFEST.in index 8a73b481..38819adb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include LICENSE pylintrc *.rst *.txt +include mopidy/backends/libspotify/spotify_appkey.key recursive-include docs * prune docs/_build recursive-include tests *.py diff --git a/docs/changes.rst b/docs/changes.rst index 7b154915..c20b2ad1 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -23,6 +23,8 @@ Another great release. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained and the Libspotify backend is working much better. - :mod:`mopidy.backends.libspotify` is now the default backend. +- A Spotify application key is now bundled with the source. The + ``SPOTIFY_LIB_APPKEY`` setting is thus removed. - MPD frontend: - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. diff --git a/mopidy/backends/libspotify/session_manager.py b/mopidy/backends/libspotify/session_manager.py index 2de6ae63..707423aa 100644 --- a/mopidy/backends/libspotify/session_manager.py +++ b/mopidy/backends/libspotify/session_manager.py @@ -13,7 +13,7 @@ logger = logging.getLogger('mopidy.backends.libspotify.session_manager') class LibspotifySessionManager(SpotifySessionManager, threading.Thread): cache_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) settings_location = os.path.expanduser(settings.SPOTIFY_LIB_CACHE) - appkey_file = os.path.expanduser(settings.SPOTIFY_LIB_APPKEY) + appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() def __init__(self, username, password, core_queue, output_queue): diff --git a/mopidy/backends/libspotify/spotify_appkey.key b/mopidy/backends/libspotify/spotify_appkey.key new file mode 100644 index 0000000000000000000000000000000000000000..1f840b962d9245820e73803ae5995650b4f84f62 GIT binary patch literal 321 zcmV-H0lxkL&xsG-pVlEz7LL?2e{+JtQpZk(M<9(;xguUY#VZNv&txxTh0nuFe(N{} zC?#&u)&58KeoT-KpSTN{8Wb)hzuj?jZNaN?^McImAMP|w&4GR8DyOK-#=V!cSw`&V5lyby`QwVzk}bWQ#Ui#m2fN)=wRSqK33~=D8OATMF|fdmT#G0B?yVov-+)u7w0gkTjyb{I{VGW`-;#R z$iCRsr@I8@9i#w7y@Y$>dnR3OOhWI%a!F~QeP*7Os+7-($V~m!LFZ(l=H!@+PtT&9 literal 0 HcmV?d00001 diff --git a/mopidy/settings.py b/mopidy/settings.py index 949b2e06..b17af913 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -160,11 +160,6 @@ MPD_SERVER_HOSTNAME = u'127.0.0.1' #: Default: 6600 MPD_SERVER_PORT = 6600 -#: Path to your libspotify application key. -#: -#: Used by :mod:`mopidy.backends.libspotify`. -SPOTIFY_LIB_APPKEY = u'~/.mopidy/spotify_appkey.key' - #: Path to the libspotify cache. #: #: Used by :mod:`mopidy.backends.libspotify`. diff --git a/setup.py b/setup.py index 76c38e4b..fabc8353 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup( author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', packages=packages, + package_data={'mopidy': ['backends/libspotify/spotify_appkey.key']}, cmdclass=cmdclasses, data_files=data_files, scripts=['bin/mopidy'],