From ff6a484add5b42b3f819a7a8f8a886c9089c5d10 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:00:34 +0100 Subject: [PATCH 001/127] docs: Add dummy output --- docs/api/outputs.rst | 1 + docs/modules/outputs/dummy.rst | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 docs/modules/outputs/dummy.rst diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index 5ef1606d..0650563e 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -17,4 +17,5 @@ Outputs are responsible for playing audio. Output implementations ====================== +* :mod:`mopidy.outputs.dummy` * :mod:`mopidy.outputs.gstreamer` diff --git a/docs/modules/outputs/dummy.rst b/docs/modules/outputs/dummy.rst new file mode 100644 index 00000000..56436c94 --- /dev/null +++ b/docs/modules/outputs/dummy.rst @@ -0,0 +1,10 @@ +******************************************************* +:mod:`mopidy.outputs.dummy` -- Dummy output for testing +******************************************************* + +.. inheritance-diagram:: mopidy.outputs.dummy + +.. automodule:: mopidy.outputs.dummy + :synopsis: Dummy output for testing + :members: + :undoc-members: From 7accf783ca5725d8e6bf2326cd863db6af56f311 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:23:30 +0100 Subject: [PATCH 002/127] Remove call to stop() on LocalPlaybackController init --- mopidy/backends/local/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 532c3976..6a1d3c51 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -50,12 +50,6 @@ class LocalBackend(Backend): class LocalPlaybackController(PlaybackController): - def __init__(self, *args, **kwargs): - super(LocalPlaybackController, self).__init__(*args, **kwargs) - - # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'? - self.stop() - @property def time_position(self): return self.backend.output.get_position() From be225c23f1a794b10124fb21c4ef61b1ee1f7eb0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:31:11 +0100 Subject: [PATCH 003/127] Simplify DummyBackend instantiation --- mopidy/backends/dummy/__init__.py | 7 +++++-- tests/frontends/mpd/audio_output_test.py | 3 +-- tests/frontends/mpd/command_list_test.py | 3 +-- tests/frontends/mpd/connection_test.py | 3 +-- tests/frontends/mpd/current_playlist_test.py | 3 +-- tests/frontends/mpd/dispatcher_test.py | 3 +-- tests/frontends/mpd/music_db_test.py | 9 ++++----- tests/frontends/mpd/playback_test.py | 5 ++--- tests/frontends/mpd/reflection_test.py | 3 +-- tests/frontends/mpd/regression_test.py | 7 +++---- tests/frontends/mpd/status_test.py | 3 +-- tests/frontends/mpd/stickers_test.py | 3 +-- tests/frontends/mpd/stored_playlists_test.py | 3 +-- 13 files changed, 23 insertions(+), 32 deletions(-) diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 9c6885bc..2d72ec8a 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -2,7 +2,9 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, PlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) +from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist +from mopidy.outputs.dummy import DummyOutput class DummyQueue(object): @@ -22,10 +24,11 @@ class DummyBackend(Backend): """ def __init__(self, *args, **kwargs): + kwargs['core_queue'] = DummyQueue() + kwargs['output'] = DummyOutput(core_queue=DummyQueue()) + kwargs['mixer_class'] = DummyMixer super(DummyBackend, self).__init__(*args, **kwargs) - self.core_queue = DummyQueue() - self.current_playlist = CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index b81e727e..77ed05c4 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_enableoutput(self): diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py index 6c801c3f..effc9862 100644 --- a/tests/frontends/mpd/command_list_test.py +++ b/tests/frontends/mpd/command_list_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class CommandListsTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_command_list_begin(self): diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 21753054..a4abbd27 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_close(self): diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index a4179637..06ff30ac 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -2,12 +2,11 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_add(self): diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 2a2ee4db..183f01d8 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -4,11 +4,10 @@ from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern -from mopidy.mixers.dummy import DummyMixer class MpdDispatcherTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_register_same_pattern_twice_fails(self): diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 05b8ebd0..36d92adf 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_count(self): @@ -65,7 +64,7 @@ class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseFindTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_find_album(self): @@ -104,7 +103,7 @@ class MusicDatabaseFindTest(unittest.TestCase): class MusicDatabaseListTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_list_foo_returns_ack(self): @@ -295,7 +294,7 @@ class MusicDatabaseListTest(unittest.TestCase): class MusicDatabaseSearchTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_search_album(self): diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 4e60546d..45aaaf83 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -2,12 +2,11 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_consume_off(self): @@ -166,7 +165,7 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_next(self): diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index a4491d75..0f096930 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class ReflectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_commands_returns_list_of_all_commands(self): diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py index 3cfdb855..63dc5ae4 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/regression_test.py @@ -3,7 +3,6 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class IssueGH17RegressionTest(unittest.TestCase): @@ -18,7 +17,7 @@ class IssueGH17RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), None, Track(uri='d'), Track(uri='e'), Track(uri='f')]) @@ -52,7 +51,7 @@ class IssueGH18RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) @@ -91,7 +90,7 @@ class IssueGH22RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 1afe6ccd..14fef262 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -2,12 +2,11 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_clearerror(self): diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py index 5b66d723..e5aed398 100644 --- a/tests/frontends/mpd/stickers_test.py +++ b/tests/frontends/mpd/stickers_test.py @@ -2,11 +2,10 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class StickersHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_sticker_get(self): diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index a24cbb88..f0b37b1a 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -3,12 +3,11 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track, Playlist class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) + self.b = DummyBackend() self.h = dispatcher.MpdDispatcher(backend=self.b) def test_listplaylist(self): From 52cdaa9d4f5fed89d75294a9ce9a669b2a0fe452 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:33:10 +0100 Subject: [PATCH 004/127] Output.get_position() should return None if position is unknown --- mopidy/outputs/base.py | 2 +- mopidy/outputs/dummy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index 372d7d70..ae1af8cf 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -67,7 +67,7 @@ class BaseOutput(object): *MUST be implemented by subclass.* - :rtype: int + :rtype: int or :class:`None` if unknown """ raise NotImplementedError diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index 060ee02f..e78d269c 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -32,7 +32,7 @@ class DummyOutput(BaseOutput): end_of_data_stream_called = False #: For testing. Contains the current position. - position = 0 + position = None #: For testing. Contains the current state. state = 'NULL' From 4515f0764e9082632893df9c042d95f54bd11223 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:33:53 +0100 Subject: [PATCH 005/127] Use time position from output if provided, else internally calculated position --- mopidy/backends/base/playback.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 8a3eeee5..8ab60470 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -282,6 +282,9 @@ class PlaybackController(object): @property def time_position(self): """Time position in milliseconds.""" + output_position = self.backend.output.get_position() + if output_position is not None: + return output_position if self.state == self.PLAYING: time_since_started = (self._current_wall_time - self._play_time_started) From 4b13dd9046b46d9fd04bfdc072c68dcf463d81c5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:34:08 +0100 Subject: [PATCH 006/127] Remove LocalPlaybackController which is now redundant --- mopidy/backends/local/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 6a1d3c51..578e0b5e 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -39,7 +39,7 @@ class LocalBackend(Backend): provider=library_provider) playback_provider = LocalPlaybackProvider(backend=self) - self.playback = LocalPlaybackController(backend=self, + self.playback = PlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) @@ -49,12 +49,6 @@ class LocalBackend(Backend): self.uri_handlers = [u'file://'] -class LocalPlaybackController(PlaybackController): - @property - def time_position(self): - return self.backend.output.get_position() - - class LocalPlaybackProvider(BasePlaybackProvider): def pause(self): return self.backend.output.set_state('PAUSED') From 1f9414d2a77c98a2f23d18d60b87f3ca14892958 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Nov 2010 23:38:27 +0100 Subject: [PATCH 007/127] Return None from GStreamerOutput.get_position() upon failure --- mopidy/outputs/gstreamer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 3b037f62..3c2f5ea7 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -265,5 +265,5 @@ class GStreamerPlayerThread(BaseThread): position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] return position // gst.MSECOND except gst.QueryError, e: - logger.error('time_position failed: %s', e) - return 0 + logger.debug(u'GStreamer time position: %s', e) + return None From 2bef4f2e498b2eda7ad5018f032e7303e8225d37 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 22 Jan 2011 14:03:13 +0100 Subject: [PATCH 008/127] Ready for v0.4.0 development --- docs/changes.rst | 11 +++++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index cecf3ffa..8b17cb25 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,17 @@ Changes This change log is used to track all major changes to Mopidy. +0.4.0 (in development) +====================== + +No description yet. + + +**Changes** + +- No changes yet. + + 0.3.0 (2010-01-22) ================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index fffa25c7..fc56efac 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -3,7 +3,7 @@ if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') def get_version(): - return u'0.3.0' + return u'0.4.0' class MopidyException(Exception): def __init__(self, message, *args, **kwargs): diff --git a/tests/version_test.py b/tests/version_test.py index a8bc2955..9574aaf5 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -14,5 +14,6 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.1.0a3') < SV('0.1.0')) self.assert_(SV('0.1.0') < SV('0.2.0')) self.assert_(SV('0.1.0') < SV('1.0.0')) - self.assert_(SV('0.2.0') < SV(get_version())) - self.assert_(SV(get_version()) < SV('0.3.1')) + self.assert_(SV('0.2.0') < SV('0.3.0')) + self.assert_(SV('0.3.0') < SV(get_version())) + self.assert_(SV(get_version()) < SV('0.4.1')) From c097af1a8360635f59ccb24903cb1c72bcf0ab46 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 23 Jan 2011 18:39:05 +0100 Subject: [PATCH 009/127] Fix a bunch of pylint errors/warnings in test suite --- tests/__init__.py | 2 ++ tests/backends/base/current_playlist.py | 2 -- tests/backends/base/playback.py | 35 ++++++++++++------------- tests/frontends/mpd/playback_test.py | 2 ++ tests/frontends/mpd/serializer_test.py | 6 ++--- tests/mixers/base_test.py | 2 ++ tests/models_test.py | 6 +++-- tests/outputs/gstreamer_test.py | 1 - tests/utils/path_test.py | 2 +- tests/utils/settings_test.py | 4 +-- 10 files changed, 32 insertions(+), 30 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index c8618f3f..82073e97 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,7 +1,9 @@ import os try: # 2.7 + # pylint: disable = E0611,F0401 from unittest.case import SkipTest + # pylint: enable = E0611,F0401 except ImportError: try: # Nose from nose.plugins.skip import SkipTest diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index 2b6cb84e..c2a9df6f 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,11 +1,9 @@ import multiprocessing import random -from mopidy import settings from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track from mopidy.outputs.dummy import DummyOutput -from mopidy.utils import get_class from tests.backends.base import populate_playlist diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 26662f96..77eb012d 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -2,11 +2,9 @@ import multiprocessing import random import time -from mopidy import settings from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track from mopidy.outputs.dummy import DummyOutput -from mopidy.utils import get_class from tests import SkipTest from tests.backends.base import populate_playlist @@ -212,7 +210,7 @@ class PlaybackControllerTest(object): def test_next_until_end_of_playlist_and_play_from_start(self): self.playback.play() - for track in self.tracks: + for _ in self.tracks: self.playback.next() self.assertEqual(self.playback.current_track, None) @@ -258,7 +256,7 @@ class PlaybackControllerTest(object): @populate_playlist def test_next_track_at_end_of_playlist(self): self.playback.play() - for track in self.current_playlist.cp_tracks[1:]: + for _ in self.current_playlist.cp_tracks[1:]: self.playback.next() self.assertEqual(self.playback.track_at_next, None) @@ -266,7 +264,7 @@ class PlaybackControllerTest(object): def test_next_track_at_end_of_playlist_with_repeat(self): self.playback.repeat = True self.playback.play() - for track in self.tracks[1:]: + for _ in self.tracks[1:]: self.playback.next() self.assertEqual(self.playback.track_at_next, self.tracks[0]) @@ -348,7 +346,7 @@ class PlaybackControllerTest(object): def test_end_of_track_until_end_of_playlist_and_play_from_start(self): self.playback.play() - for track in self.tracks: + for _ in self.tracks: self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, None) @@ -394,7 +392,7 @@ class PlaybackControllerTest(object): @populate_playlist def test_end_of_track_track_at_end_of_playlist(self): self.playback.play() - for track in self.current_playlist.cp_tracks[1:]: + for _ in self.current_playlist.cp_tracks[1:]: self.playback.on_end_of_track() self.assertEqual(self.playback.track_at_next, None) @@ -402,7 +400,7 @@ class PlaybackControllerTest(object): def test_end_of_track_track_at_end_of_playlist_with_repeat(self): self.playback.repeat = True self.playback.play() - for track in self.tracks[1:]: + for _ in self.tracks[1:]: self.playback.on_end_of_track() self.assertEqual(self.playback.track_at_next, self.tracks[0]) @@ -466,7 +464,7 @@ class PlaybackControllerTest(object): @populate_playlist def test_previous_track_with_consume(self): self.playback.consume = True - for track in self.tracks: + for _ in self.tracks: self.playback.next() self.assertEqual(self.playback.track_at_previous, self.playback.current_track) @@ -474,7 +472,7 @@ class PlaybackControllerTest(object): @populate_playlist def test_previous_track_with_random(self): self.playback.random = True - for track in self.tracks: + for _ in self.tracks: self.playback.next() self.assertEqual(self.playback.track_at_previous, self.playback.current_track) @@ -547,7 +545,6 @@ class PlaybackControllerTest(object): @populate_playlist def test_on_current_playlist_change_when_stopped(self): - current_track = self.playback.current_track self.backend.current_playlist.append([self.tracks[2]]) self.assertEqual(self.playback.state, self.playback.STOPPED) self.assertEqual(self.playback.current_track, None) @@ -677,9 +674,10 @@ class PlaybackControllerTest(object): self.playback.seek(0) self.assertEqual(self.playback.state, self.playback.PLAYING) + @SkipTest @populate_playlist def test_seek_beyond_end_of_song(self): - raise SkipTest # FIXME need to decide return value + # FIXME need to decide return value self.playback.play() result = self.playback.seek(self.tracks[0].length*100) self.assert_(not result, 'Seek return value was %s' % result) @@ -696,9 +694,10 @@ class PlaybackControllerTest(object): self.playback.seek(self.current_playlist.tracks[-1].length * 100) self.assertEqual(self.playback.state, self.playback.STOPPED) + @SkipTest @populate_playlist def test_seek_beyond_start_of_song(self): - raise SkipTest # FIXME need to decide return value + # FIXME need to decide return value self.playback.play() result = self.playback.seek(-1000) self.assert_(not result, 'Seek return value was %s' % result) @@ -770,7 +769,7 @@ class PlaybackControllerTest(object): def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.playback.consume = True self.playback.play() - for i in range(len(self.backend.current_playlist.tracks)): + for _ in range(len(self.backend.current_playlist.tracks)): self.playback.on_end_of_track() self.assertEqual(len(self.backend.current_playlist.tracks), 0) @@ -824,14 +823,14 @@ class PlaybackControllerTest(object): def test_random_until_end_of_playlist(self): self.playback.random = True self.playback.play() - for track in self.tracks[1:]: + for _ in self.tracks[1:]: self.playback.next() self.assertEqual(self.playback.track_at_next, None) @populate_playlist def test_random_until_end_of_playlist_and_play_from_start(self): self.playback.repeat = True - for track in self.tracks: + for _ in self.tracks: self.playback.next() self.assertNotEqual(self.playback.track_at_next, None) self.assertEqual(self.playback.state, self.playback.STOPPED) @@ -843,7 +842,7 @@ class PlaybackControllerTest(object): self.playback.repeat = True self.playback.random = True self.playback.play() - for track in self.tracks: + for _ in self.tracks: self.playback.next() self.assertNotEqual(self.playback.track_at_next, None) @@ -852,7 +851,7 @@ class PlaybackControllerTest(object): self.playback.random = True self.playback.play() played = [] - for track in self.tracks: + for _ in self.tracks: self.assert_(self.playback.current_track not in played) played.append(self.playback.current_track) self.playback.next() diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 43614173..282ce303 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -323,6 +323,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.b.current_playlist.append( [Track(uri='1', length=40000), seek_track]) result = self.h.handle_request(u'seek "1" "30"') + self.assert_(u'OK' in result) self.assertEqual(self.b.playback.current_track, seek_track) def test_seek_without_quotes(self): @@ -343,6 +344,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.b.current_playlist.append( [Track(length=40000), seek_track]) result = self.h.handle_request(u'seekid "1" "30"') + self.assert_(u'OK' in result) self.assertEqual(self.b.playback.current_cpid, 1) self.assertEqual(self.b.playback.current_track, seek_track) diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index 7e4500ea..cfed498a 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -7,8 +7,6 @@ from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, Playlist, Track -from tests import data_folder, SkipTest - class TrackMpdFormatTest(unittest.TestCase): track = Track( uri=u'a uri', @@ -54,7 +52,8 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Id', 2) in result) def test_track_to_mpd_format_for_nonempty_track(self): - result = translator.track_to_mpd_format(self.track, position=9, cpid=122) + result = translator.track_to_mpd_format( + self.track, position=9, cpid=122) self.assert_(('file', 'a uri') in result) self.assert_(('Time', 137) in result) self.assert_(('Artist', 'an artist') in result) @@ -219,7 +218,6 @@ class TracksToTagCacheFormatTest(unittest.TestCase): def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') - formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track]) result = self.consume_headers(result) diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py index d6129ad5..395d8f7b 100644 --- a/tests/mixers/base_test.py +++ b/tests/mixers/base_test.py @@ -10,7 +10,9 @@ class BaseMixerTest(object): def setUp(self): assert self.mixer_class is not None, \ "mixer_class must be set in subclass" + # pylint: disable = E1102 self.mixer = self.mixer_class(None) + # pylint: enable = E1102 def test_initial_volume(self): self.assertEqual(self.mixer.volume, self.INITIAL) diff --git a/tests/models_test.py b/tests/models_test.py index 0b44f337..4d99a413 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -205,8 +205,10 @@ class AlbumTest(unittest.TestCase): def test_eq(self): artists = [Artist()] - album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') - album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, musicbrainz_id='id') + album1 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, + musicbrainz_id='id') + album2 = Album(name=u'name', uri=u'uri', artists=artists, num_tracks=2, + musicbrainz_id='id') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 3a578280..b4cb5ef9 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -11,7 +11,6 @@ if sys.platform == 'win32': from mopidy import settings from mopidy.outputs.gstreamer import GStreamerOutput from mopidy.utils.path import path_to_uri -from mopidy.utils.process import pickle_connection from tests import data_folder diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 4366305c..3743d59e 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -9,7 +9,7 @@ import unittest from mopidy.utils.path import (get_or_create_folder, mtime, path_to_uri, uri_to_path, split_path, find_files) -from tests import SkipTest, data_folder +from tests import data_folder class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 8e2575b9..77500190 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -57,7 +57,7 @@ class SettingsProxyTest(unittest.TestCase): def test_getattr_raises_error_on_missing_setting(self): try: - test = self.settings.TEST + _ = self.settings.TEST self.fail(u'Should raise exception') except SettingsError as e: self.assertEqual(u'Setting "TEST" is not set.', e.message) @@ -65,7 +65,7 @@ class SettingsProxyTest(unittest.TestCase): def test_getattr_raises_error_on_empty_setting(self): self.settings.TEST = u'' try: - test = self.settings.TEST + _ = self.settings.TEST self.fail(u'Should raise exception') except SettingsError as e: self.assertEqual(u'Setting "TEST" is empty.', e.message) From e1668efff6a04a8b3ae6525b30660a23b2a11e2b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 25 Jan 2011 20:12:09 +0100 Subject: [PATCH 010/127] Show version number in log output --- mopidy/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/core.py b/mopidy/core.py index 1a4ed7cc..56febe5b 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -56,7 +56,7 @@ class CoreProcess(BaseThread): def setup_logging(self): setup_logging(self.options.verbosity_level, self.options.save_debug_log) - logger.info(u'-- Starting Mopidy --') + logger.info(u'-- Starting Mopidy %s --', get_version()) def setup_settings(self): get_or_create_folder('~/.mopidy/') From 11edbc160b0eb7d2ca43b22cfaab9f9eb3b810cd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 25 Jan 2011 22:03:28 +0100 Subject: [PATCH 011/127] Special case get_version() to include git revision id if we're running from a git repo --- mopidy/__init__.py | 24 +++++++++++++++++++++++- tests/version_test.py | 8 ++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index fc56efac..4bb4609b 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,9 +1,31 @@ +import os import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') +VERSION = (0, 4, 0) + +def is_in_git_repo(): + git_dir = os.path.abspath(os.path.join( + os.path.dirname(__file__), '../.git')) + return os.path.exists(git_dir) + +def get_git_version(): + if not is_in_git_repo(): + return None + git_version = os.popen('git describe').read().strip() + if git_version.startswith('v'): + git_version = git_version[1:] + return git_version + +def get_plain_version(): + return '.'.join(map(str, VERSION)) + def get_version(): - return u'0.4.0' + if is_in_git_repo(): + return get_git_version() + else: + return get_plain_version() class MopidyException(Exception): def __init__(self, message, *args, **kwargs): diff --git a/tests/version_test.py b/tests/version_test.py index a035f520..f1f86b59 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,11 +1,11 @@ from distutils.version import StrictVersion as SV import unittest -from mopidy import get_version +from mopidy import get_plain_version class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): - SV(get_version()) + SV(get_plain_version()) def test_versions_can_be_strictly_ordered(self): self.assert_(SV('0.1.0a0') < SV('0.1.0a1')) @@ -16,5 +16,5 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.1.0') < SV('1.0.0')) self.assert_(SV('0.2.0') < SV('0.3.0')) self.assert_(SV('0.3.0') < SV('0.3.1')) - self.assert_(SV('0.3.1') < SV(get_version())) - self.assert_(SV(get_version()) < SV('0.4.1')) + self.assert_(SV('0.3.1') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.4.1')) From 242f27f943a107bf7dd2a472f08a71a8382f6467 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 26 Jan 2011 00:04:23 +0100 Subject: [PATCH 012/127] Use subprocess instead of os.popen --- mopidy/__init__.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 4bb4609b..cbf1c757 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,30 +1,27 @@ -import os import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') +from subprocess import PIPE, Popen + VERSION = (0, 4, 0) -def is_in_git_repo(): - git_dir = os.path.abspath(os.path.join( - os.path.dirname(__file__), '../.git')) - return os.path.exists(git_dir) - def get_git_version(): - if not is_in_git_repo(): - return None - git_version = os.popen('git describe').read().strip() - if git_version.startswith('v'): - git_version = git_version[1:] - return git_version + process = Popen(['git', 'describe'], stdout=PIPE) + if process.wait() != 0: + raise Exception|('Execution of "git describe" failed') + version = process.stdout.read().strip() + if version.startswith('v'): + version = version[1:] + return version def get_plain_version(): return '.'.join(map(str, VERSION)) def get_version(): - if is_in_git_repo(): + try: return get_git_version() - else: + except Exception: return get_plain_version() class MopidyException(Exception): From 7f860265886a14f7e7e4665bf4badc5eb9f07142 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 26 Jan 2011 00:07:17 +0100 Subject: [PATCH 013/127] Fix typo, redirect stderr --- mopidy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index cbf1c757..873ee182 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -7,9 +7,9 @@ from subprocess import PIPE, Popen VERSION = (0, 4, 0) def get_git_version(): - process = Popen(['git', 'describe'], stdout=PIPE) + process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) if process.wait() != 0: - raise Exception|('Execution of "git describe" failed') + raise Exception('Execution of "git describe" failed') version = process.stdout.read().strip() if version.startswith('v'): version = version[1:] From 5e87dcbfffde607d4c0d551ffd22a1b9f8f97d4f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 28 Jan 2011 11:52:18 +0100 Subject: [PATCH 014/127] Add 'date' query support to 'find' and 'search' to improve ncmpcpp media library browsing --- docs/changes.rst | 6 +++++- mopidy/frontends/mpd/protocol/music_db.py | 16 +++++++++++---- tests/frontends/mpd/music_db_test.py | 25 +++++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index c7c65621..7a5d0664 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,7 +13,11 @@ No description yet. **Changes** -- No changes yet. +- MPD frontend: + + - Add support for "date" queries to both the ``find`` and ``search`` + commands. This makes media library browsing in ncmpcpp work, though very + slow due to all the meta data requests to Spotify. 0.3.1 (2010-01-22) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index fb3a3a09..d50388f5 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -41,8 +41,8 @@ def count(frontend, tag, needle): return [('songs', 0), ('playtime', 0)] # TODO @handle_pattern(r'^find ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?' - ' "[^"]+"\s?)+)$') + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|' + r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') def find(frontend, mpd_query): """ *musicpd.org, music database section:* @@ -62,6 +62,10 @@ def find(frontend, mpd_query): - does not add quotes around the field argument. - capitalizes the type argument. + + *ncmpcpp:* + + - also uses the search type "date". """ query = _build_query(mpd_query) return frontend.backend.library.find_exact(**query).mpd_format() @@ -290,8 +294,8 @@ def rescan(frontend, uri=None): return update(frontend, uri, rescan_unmodified_files=True) @handle_pattern(r'^search ' - r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"?' - ' "[^"]+"\s?)+)$') + r'(?P("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|' + r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$') def search(frontend, mpd_query): """ *musicpd.org, music database section:* @@ -314,6 +318,10 @@ def search(frontend, mpd_query): - does not add quotes around the field argument. - capitalizes the field argument. + + *ncmpcpp:* + + - also uses the search type "date". """ query = _build_query(mpd_query) return frontend.backend.library.search(**query).mpd_format() diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 05b8ebd0..486eac4f 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -92,7 +92,20 @@ class MusicDatabaseFindTest(unittest.TestCase): result = self.h.handle_request(u'find title "what"') self.assert_(u'OK' in result) + def test_find_date(self): + result = self.h.handle_request(u'find "date" "2002-01-01"') + self.assert_(u'OK' in result) + + def test_find_date_without_quotes(self): + result = self.h.handle_request(u'find date "2002-01-01"') + self.assert_(u'OK' in result) + + def test_find_date_with_capital_d_and_incomplete_date(self): + result = self.h.handle_request(u'find Date "2005"') + self.assert_(u'OK' in result) + def test_find_else_should_fail(self): + result = self.h.handle_request(u'find "somethingelse" "what"') self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments') @@ -338,6 +351,18 @@ class MusicDatabaseSearchTest(unittest.TestCase): result = self.h.handle_request(u'search any "anything"') self.assert_(u'OK' in result) + def test_search_date(self): + result = self.h.handle_request(u'search "date" "2002-01-01"') + self.assert_(u'OK' in result) + + def test_search_date_without_quotes(self): + result = self.h.handle_request(u'search date "2002-01-01"') + self.assert_(u'OK' in result) + + def test_search_date_with_capital_d_and_incomplete_date(self): + result = self.h.handle_request(u'search Date "2005"') + self.assert_(u'OK' in result) + def test_search_else_should_fail(self): result = self.h.handle_request(u'search "sometype" "something"') self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments') From 8d419bb255ba1510aee8cf234f19ccda943b8890 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 5 Feb 2011 23:18:31 +0100 Subject: [PATCH 015/127] Update section on the CI server --- docs/development/contributing.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index a9cd8dc3..fbc7baee 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -107,14 +107,14 @@ For more documentation on testing, check out the `nose documentation Continuous integration server ============================= -We run a continuous integration server called Hudson at -http://hudson.mopidy.com/ that runs all test on multiple platforms (Ubuntu, OS -X, etc.) for every commit we push to GitHub. +We run a continuous integration (CI) server at http://ci.mopidy.com/ that runs +all test on multiple platforms (Ubuntu, OS X, etc.) for every commit we push to +GitHub. -In addition to running tests, Hudson also does coverage statistics and uses -pylint to check for errors and possible improvements in our code. So, if you're -out of work, the code coverage and pylint data in Hudson should give you a -place to start. +In addition to running tests, the CI server also gathers coverage statistics +and uses pylint to check for errors and possible improvements in our code. So, +if you're out of work, the code coverage and pylint data at the CI server +should give you a place to start. Writing documentation From f33c65ddfee414258bfeb473fd7be73f553e7878 Mon Sep 17 00:00:00 2001 From: Martins Grunskis Date: Wed, 9 Feb 2011 17:05:29 +0200 Subject: [PATCH 016/127] mopidy --list-settings fails on fresh install --- mopidy/utils/settings.py | 11 +++++++++-- tests/utils/settings_test.py | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 7715721e..dace1495 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -129,6 +129,14 @@ def validate_settings(defaults, settings): return errors +def mask_value_if_secret(key, value): + masked_value = value + + if key.endswith('PASSWORD') and value: + masked_value = u'********' + + return masked_value + def list_settings_optparse_callback(*args): """ Prints a list of all settings. @@ -141,8 +149,7 @@ def list_settings_optparse_callback(*args): lines = [] for (key, value) in sorted(settings.current.iteritems()): default_value = settings.default.get(key) - if key.endswith('PASSWORD') and len(value): - value = u'********' + value = mask_value_if_secret(key, value) lines.append(u'%s:' % key) lines.append(u' Value: %s' % repr(value)) if value != default_value and default_value is not None: diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 77500190..46efe9e3 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -3,6 +3,7 @@ import unittest from mopidy import settings as default_settings_module, SettingsError from mopidy.utils.settings import validate_settings, SettingsProxy +from mopidy.utils.settings import mask_value_if_secret class ValidateSettingsTest(unittest.TestCase): def setUp(self): @@ -46,6 +47,13 @@ class ValidateSettingsTest(unittest.TestCase): {'FOO': '', 'BAR': ''}) self.assertEquals(len(result), 2) + def test_mask_if_secret(self): + not_secret = mask_value_if_secret('SPOTIFY_USERNAME', 'foo') + self.assertEquals('foo', not_secret) + + secret = mask_value_if_secret('SPOTIFY_PASSWORD', 'bar') + self.assertEquals(u'********', secret) + class SettingsProxyTest(unittest.TestCase): def setUp(self): From e150a24dfead8e2d7f31c17a5500256d7a383c3d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Feb 2011 17:08:40 +0100 Subject: [PATCH 017/127] Review patch for GH-63 - Moved mask_value_if_secret til after the method using it. Most important/interesting method first, lower level/more detailed later. - Made the logic in mask_value_if_secret simpler to understand, I think, by making the two different cases more obvious. - Split the test in two, as it actually tested two different cases. - Changed the test names to explain what should happen, and not just what is tested. - Add a new test that tests the case which caused the crash: a setting being None. --- mopidy/utils/settings.py | 14 ++++++-------- tests/utils/settings_test.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index dace1495..529c6fb1 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -129,14 +129,6 @@ def validate_settings(defaults, settings): return errors -def mask_value_if_secret(key, value): - masked_value = value - - if key.endswith('PASSWORD') and value: - masked_value = u'********' - - return masked_value - def list_settings_optparse_callback(*args): """ Prints a list of all settings. @@ -158,3 +150,9 @@ def list_settings_optparse_callback(*args): lines.append(u' Error: %s' % errors[key]) print u'Settings: %s' % indent('\n'.join(lines), places=2) sys.exit(0) + +def mask_value_if_secret(key, value): + if key.endswith('PASSWORD') and value: + return u'********' + else: + return value diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index 46efe9e3..11914f61 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -45,14 +45,19 @@ class ValidateSettingsTest(unittest.TestCase): def test_two_errors_are_both_reported(self): result = validate_settings(self.defaults, {'FOO': '', 'BAR': ''}) - self.assertEquals(len(result), 2) - - def test_mask_if_secret(self): - not_secret = mask_value_if_secret('SPOTIFY_USERNAME', 'foo') - self.assertEquals('foo', not_secret) + self.assertEqual(len(result), 2) + def test_masks_value_if_secret(self): secret = mask_value_if_secret('SPOTIFY_PASSWORD', 'bar') - self.assertEquals(u'********', secret) + self.assertEqual(u'********', secret) + + def test_does_not_mask_value_if_not_secret(self): + not_secret = mask_value_if_secret('SPOTIFY_USERNAME', 'foo') + self.assertEqual('foo', not_secret) + + def test_does_not_mask_value_if_none(self): + not_secret = mask_value_if_secret('SPOTIFY_USERNAME', None) + self.assertEqual(None, not_secret) class SettingsProxyTest(unittest.TestCase): From 8593a55dc54d5e8127a111e3b1aa7bfb2d052465 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Feb 2011 17:11:39 +0100 Subject: [PATCH 018/127] Update changelog with GH-63 patch --- docs/changes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 7a5d0664..64c7b7f4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -19,6 +19,11 @@ No description yet. commands. This makes media library browsing in ncmpcpp work, though very slow due to all the meta data requests to Spotify. +- Settings: + + - Fix crash on ``--list-settings`` on clean installation. (Fixes: + :issue:`63`) + 0.3.1 (2010-01-22) ================== From 159831c6e718716d2e413669d3dc8871b490f99a Mon Sep 17 00:00:00 2001 From: Martins Grunskis Date: Wed, 9 Feb 2011 18:48:15 +0200 Subject: [PATCH 019/127] mopidy-scan fails if track has no artist name stack trace: Traceback (most recent call last): File ./scan, line 27, in for a in tracks_to_tag_cache_format(tracks): File /home/martins/source/mopidy/mopidy/frontends/mpd/translator.py, line 138, in tracks_to_tag_cache_format _add_to_tag_cache(result, *tracks_to_directory_tree(tracks)) File /home/martins/source/mopidy/mopidy/frontends/mpd/translator.py, line 151, in _add_to_tag_cache _add_to_tag_cache(result, *entry) File /home/martins/source/mopidy/mopidy/frontends/mpd/translator.py, line 156, in _add_to_tag_cache track_result = dict(track_to_mpd_format(track)) File /home/martins/source/mopidy/mopidy/frontends/mpd/translator.py, line 28, in track_to_mpd_format ('Artist', artists_to_mpd_format(track.artists)), File /home/martins/source/mopidy/mopidy/frontends/mpd/translator.py, line 88, in artists_to_mpd_format return u', '.join([a.name for a in artists if a]) TypeError: sequence item 0: expected string or Unicode, NoneType found --- mopidy/frontends/mpd/translator.py | 2 +- tests/frontends/mpd/serializer_test.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 3ead23c7..c8fe6be4 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -85,7 +85,7 @@ def artists_to_mpd_format(artists): :rtype: string """ artists.sort(key=lambda a: a.name) - return u', '.join([a.name for a in artists]) + return u', '.join([a.name for a in artists if a.name]) def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None): """ diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index cfed498a..b0c57588 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -95,6 +95,11 @@ class TrackMpdFormatTest(unittest.TestCase): translated = translator.artists_to_mpd_format(artists) self.assertEqual(translated, u'ABBA, Beatles') + def test_artists_to_mpd_format_artist_with_no_name(self): + artists = [Artist(name=None)] + translated = translator.artists_to_mpd_format(artists) + self.assertEqual(translated, u'') + class PlaylistMpdFormatTest(unittest.TestCase): def test_mpd_format(self): From e2c0096f5f6da01746ec4398482dd6be79827b42 Mon Sep 17 00:00:00 2001 From: Martins Grunskis Date: Wed, 9 Feb 2011 18:57:39 +0200 Subject: [PATCH 020/127] there is no num_tracks in some of my mp3s --- mopidy/backends/local/translator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index 51522ead..be7ab8a8 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -100,8 +100,11 @@ def _convert_mpd_data(data, tracks, music_dir): albumartist_kwargs = {} if 'track' in data: - album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) - track_kwargs['track_no'] = int(data['track'].split('/')[0]) + if '/' in data['track']: + album_kwargs['num_tracks'] = int(data['track'].split('/')[1]) + track_kwargs['track_no'] = int(data['track'].split('/')[0]) + else: + track_kwargs['track_no'] = int(data['track']) if 'artist' in data: artist_kwargs['name'] = data['artist'] From 91853eca2ca759e28adba261650b3d5834b09234 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Feb 2011 18:14:33 +0100 Subject: [PATCH 021/127] Give Martins credit for the patch --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 64c7b7f4..b1e8e608 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,8 +21,8 @@ No description yet. - Settings: - - Fix crash on ``--list-settings`` on clean installation. (Fixes: - :issue:`63`) + - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins + Grunskis for the bug report and patch. (Fixes: :issue:`63`) 0.3.1 (2010-01-22) From 2463fdd945141e0e44415647e5067862ccb2c730 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Feb 2011 01:36:36 +0100 Subject: [PATCH 022/127] Fix test that was declared twice --- tests/frontends/mpd/playback_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 282ce303..64e9f882 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -271,7 +271,7 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) self.assertEqual(self.b.playback.current_track.uri, 'a') - def test_play_minus_one_plays_current_track_if_current_track_is_set(self): + def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.assertEqual(self.b.playback.current_track, None) self.b.playback.play() From 04f8276ffbb215d74a8aefcee6342a59b6b2987f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Feb 2011 01:37:00 +0100 Subject: [PATCH 023/127] Ignore rotated logs --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e0026170..17a6200d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ cover/ coverage.xml dist/ docs/_build/ -mopidy.log +mopidy.log* nosetests.xml From 15c992e06a605078c9fe245551af19c1f6ac429e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Feb 2011 01:45:36 +0100 Subject: [PATCH 024/127] Add 'play -1' support, which fixes resume in MPoD --- docs/changes.rst | 2 ++ mopidy/frontends/mpd/protocol/playback.py | 23 ++++++++++++++++------- tests/frontends/mpd/playback_test.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b1e8e608..63c0c22b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -19,6 +19,8 @@ No description yet. commands. This makes media library browsing in ncmpcpp work, though very slow due to all the meta data requests to Spotify. + - Add support for ``play "-1"``, which fixes resume in MPoD. + - Settings: - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 19922bc3..3f0421dc 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -132,10 +132,13 @@ def playid(frontend, cpid): Begins playing the playlist at song ``SONGID``. - *GMPC:* + *Clarifications:* - - issues ``playid "-1"`` after playlist replacement to start playback - at the first track. + - ``playid "-1"`` when paused resumes playback. + - ``playid "-1"`` when stopped with a current track starts playback at the + current track. + - ``playid "-1"`` when stopped without a current track, e.g. after playlist + replacement, starts playback at the first track. """ cpid = int(cpid) paused = (frontend.backend.playback.state == @@ -161,17 +164,23 @@ def playpos(frontend, songpos): Begins playing the playlist at song number ``SONGPOS``. - *Many clients:* + *Clarifications:* - - issue ``play "-1"`` after playlist replacement to start the current - track. If the current track is not set, start playback at the first - track. + - ``playid "-1"`` when paused resumes playback. + - ``playid "-1"`` when stopped with a current track starts playback at the + current track. + - ``playid "-1"`` when stopped without a current track, e.g. after playlist + replacement, starts playback at the first track. *BitMPC:* - issues ``play 6`` without quotes around the argument. """ songpos = int(songpos) + paused = (frontend.backend.playback.state == + frontend.backend.playback.PAUSED) + if songpos == -1 and paused: + return frontend.backend.playback.resume() try: if songpos == -1: cp_track = _get_cp_track_for_play_minus_one(frontend) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 64e9f882..face0318 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -257,6 +257,18 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) self.assertEqual(self.b.playback.current_track, None) + def test_play_minus_one_resumes_if_paused(self): + self.b.current_playlist.append([Track(length=40000)]) + self.b.playback.seek(30000) + self.assert_(self.b.playback.time_position >= 30000) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.b.playback.pause() + self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) + result = self.h.handle_request(u'play "-1"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position >= 30000) + def test_playid(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'playid "0"') From 163ead53dac4f3f6e7d12bcc46433e489456f3f6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Feb 2011 02:03:18 +0100 Subject: [PATCH 025/127] Add 'play -1' support when playing --- docs/changes.rst | 4 ++- mopidy/frontends/mpd/protocol/playback.py | 42 +++++++++++------------ tests/frontends/mpd/playback_test.py | 20 +++++++++++ 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 63c0c22b..bd102ef7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -19,7 +19,9 @@ No description yet. commands. This makes media library browsing in ncmpcpp work, though very slow due to all the meta data requests to Spotify. - - Add support for ``play "-1"``, which fixes resume in MPoD. + - Add support for ``play "-1"`` when in playing or paused state, which fixes + resume and addition of tracks to the current playlist while playing for the + MPoD client. - Settings: diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index 3f0421dc..d009969d 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -134,6 +134,7 @@ def playid(frontend, cpid): *Clarifications:* + - ``playid "-1"`` when playing is ignored. - ``playid "-1"`` when paused resumes playback. - ``playid "-1"`` when stopped with a current track starts playback at the current track. @@ -141,15 +142,10 @@ def playid(frontend, cpid): replacement, starts playback at the first track. """ cpid = int(cpid) - paused = (frontend.backend.playback.state == - frontend.backend.playback.PAUSED) - if cpid == -1 and paused: - return frontend.backend.playback.resume() + if cpid == -1: + return _play_minus_one(frontend) try: - if cpid == -1: - cp_track = _get_cp_track_for_play_minus_one(frontend) - else: - cp_track = frontend.backend.current_playlist.get(cpid=cpid) + cp_track = frontend.backend.current_playlist.get(cpid=cpid) return frontend.backend.playback.play(cp_track) except LookupError: raise MpdNoExistError(u'No such song', command=u'playid') @@ -166,6 +162,7 @@ def playpos(frontend, songpos): *Clarifications:* + - ``playid "-1"`` when playing is ignored. - ``playid "-1"`` when paused resumes playback. - ``playid "-1"`` when stopped with a current track starts playback at the current track. @@ -177,26 +174,27 @@ def playpos(frontend, songpos): - issues ``play 6`` without quotes around the argument. """ songpos = int(songpos) - paused = (frontend.backend.playback.state == - frontend.backend.playback.PAUSED) - if songpos == -1 and paused: - return frontend.backend.playback.resume() + if songpos == -1: + return _play_minus_one(frontend) try: - if songpos == -1: - cp_track = _get_cp_track_for_play_minus_one(frontend) - else: - cp_track = frontend.backend.current_playlist.cp_tracks[songpos] + cp_track = frontend.backend.current_playlist.cp_tracks[songpos] return frontend.backend.playback.play(cp_track) except IndexError: raise MpdArgError(u'Bad song index', command=u'play') -def _get_cp_track_for_play_minus_one(frontend): - if not frontend.backend.current_playlist.cp_tracks: - return # Fail silently - cp_track = frontend.backend.playback.current_cp_track - if cp_track is None: +def _play_minus_one(frontend): + if (frontend.backend.playback.state == frontend.backend.playback.PLAYING): + return # Nothing to do + elif (frontend.backend.playback.state == frontend.backend.playback.PAUSED): + return frontend.backend.playback.resume() + elif frontend.backend.playback.current_cp_track is not None: + cp_track = frontend.backend.playback.current_cp_track + return frontend.backend.playback.play(cp_track) + elif frontend.backend.current_playlist.cp_tracks: cp_track = frontend.backend.current_playlist.cp_tracks[0] - return cp_track + return frontend.backend.playback.play(cp_track) + else: + return # Fail silently @handle_pattern(r'^previous$') def previous(frontend): diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index face0318..83355aea 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -257,6 +257,16 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) self.assertEqual(self.b.playback.current_track, None) + def test_play_minus_is_ignored_if_playing(self): + self.b.current_playlist.append([Track(length=40000)]) + self.b.playback.seek(30000) + self.assert_(self.b.playback.time_position >= 30000) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + result = self.h.handle_request(u'play "-1"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position >= 30000) + def test_play_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) @@ -302,6 +312,16 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) self.assertEqual(self.b.playback.current_track, None) + def test_playid_minus_is_ignored_if_playing(self): + self.b.current_playlist.append([Track(length=40000)]) + self.b.playback.seek(30000) + self.assert_(self.b.playback.time_position >= 30000) + self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + result = self.h.handle_request(u'playid "-1"') + self.assert_(u'OK' in result) + self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position >= 30000) + def test_playid_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) From b89150f5d25ef0c307cf9f02a42895937a4936f5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 12 Feb 2011 02:21:38 +0100 Subject: [PATCH 026/127] Skip tests instead of commenting them out --- tests/frontends/mpd/playback_test.py | 41 ++++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 83355aea..8f0560c7 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -5,6 +5,8 @@ from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track +from tests import SkipTest + class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend(mixer_class=DummyMixer) @@ -147,26 +149,29 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): self.assert_(u'OK' in result) self.assert_(expected in result) - #def test_replay_gain_status_off(self): - # expected = u'off' - # self.h._replay_gain_mode(expected) - # result = self.h.handle_request(u'replay_gain_status') - # self.assert_(u'OK' in result) - # self.assert_(expected in result) + def test_replay_gain_status_off(self): + raise SkipTest + expected = u'off' + self.h._replay_gain_mode(expected) + result = self.h.handle_request(u'replay_gain_status') + self.assert_(u'OK' in result) + self.assert_(expected in result) - #def test_replay_gain_status_track(self): - # expected = u'track' - # self.h._replay_gain_mode(expected) - # result = self.h.handle_request(u'replay_gain_status') - # self.assert_(u'OK' in result) - # self.assert_(expected in result) + def test_replay_gain_status_track(self): + raise SkipTest + expected = u'track' + self.h._replay_gain_mode(expected) + result = self.h.handle_request(u'replay_gain_status') + self.assert_(u'OK' in result) + self.assert_(expected in result) - #def test_replay_gain_status_album(self): - # expected = u'album' - # self.h._replay_gain_mode(expected) - # result = self.h.handle_request(u'replay_gain_status') - # self.assert_(u'OK' in result) - # self.assert_(expected in result) + def test_replay_gain_status_album(self): + raise SkipTest + expected = u'album' + self.h._replay_gain_mode(expected) + result = self.h.handle_request(u'replay_gain_status') + self.assert_(u'OK' in result) + self.assert_(expected in result) class PlaybackControlHandlerTest(unittest.TestCase): From 5dc6a17d204ec0e500033ee72571d55cf5c7ae47 Mon Sep 17 00:00:00 2001 From: Henrik Olsson Date: Wed, 16 Feb 2011 19:42:56 +0100 Subject: [PATCH 027/127] Fixed exception when there's no artist name in the metadata when scanning --- mopidy/frontends/mpd/translator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index 3ead23c7..a3aaaf93 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -85,7 +85,7 @@ def artists_to_mpd_format(artists): :rtype: string """ artists.sort(key=lambda a: a.name) - return u', '.join([a.name for a in artists]) + return u', '.join([a.name or '' for a in artists]) def tracks_to_mpd_format(tracks, start=0, end=None, cpids=None): """ From 57ad32a40f823f6c88aa0b2154bbd7522cd6531c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Feb 2011 23:36:30 +0100 Subject: [PATCH 028/127] Mopidy can be used with libspotify 0.0.7 and pyspotify 1.1+mopidy20110223 --- docs/installation/libspotify.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 5d278fe2..ca0ad87d 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -30,7 +30,7 @@ If you run a Debian based Linux distribution, like Ubuntu, see http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source on your installation. Then, simply run:: - sudo apt-get install libspotify6 + sudo apt-get install libspotify7 When libspotify has been installed, continue with :ref:`pyspotify_installation`. @@ -39,14 +39,14 @@ When libspotify has been installed, continue with On Linux from source -------------------- -Download and install libspotify 0.0.6 for your OS and CPU architecture from +Download and install libspotify 0.0.7 for your OS and CPU architecture from https://developer.spotify.com/en/libspotify/. For 64-bit Linux the process is as follows:: - wget http://developer.spotify.com/download/libspotify/libspotify-0.0.6-linux6-x86_64.tar.gz - tar zxfv libspotify-0.0.6-linux6-x86_64.tar.gz - cd libspotify-0.0.6-linux6-x86_64/ + wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz + tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz + cd libspotify-0.0.7-linux6-x86_64/ sudo make install prefix=/usr/local sudo ldconfig @@ -113,4 +113,4 @@ Get the pyspotify code, and install it:: It is important that you install pyspotify from the ``mopidy`` branch of the ``mopidy/pyspotify`` repository, as the upstream repository at ``winjer/pyspotify`` is not updated with changes needed to support e.g. -libspotify 0.0.6 and high bitrate audio. +libspotify 0.0.7 and high bitrate audio. From 0c845dd246ec270aaf38e7166319631599e13c36 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 26 Feb 2011 17:23:15 +0100 Subject: [PATCH 029/127] Add Tox for testing on both Python 2.6 and 2.7 --- .gitignore | 1 + requirements/tests.txt | 1 + tox.ini | 13 +++++++++++++ 3 files changed, 15 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 17a6200d..3fed7452 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.swp .coverage .noseids +.tox MANIFEST build/ cover/ diff --git a/requirements/tests.txt b/requirements/tests.txt index 33f49451..71dab096 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,2 +1,3 @@ coverage nose +tox diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..8b91c6b7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py26,py27,docs + +[testenv] +deps = nose +commands = nosetests [] + +[testenv:docs] +basepython = python +changedir = docs +deps = sphinx +commands = + sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html From 18eec035b5a76f6dbf6a945ddbe389478fc3d877 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 18:12:01 +0100 Subject: [PATCH 030/127] Add Pykka as dependency --- docs/installation/index.rst | 2 ++ requirements/core.txt | 1 + 2 files changed, 3 insertions(+) create mode 100644 requirements/core.txt diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 56f0015b..19016ce9 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,6 +25,8 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 +- `Pykka `_ >= 0.9.1 + - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. - Mixer dependencies: The default mixer does not require any additional diff --git a/requirements/core.txt b/requirements/core.txt new file mode 100644 index 00000000..54781a3b --- /dev/null +++ b/requirements/core.txt @@ -0,0 +1 @@ +Pykka >= 0.9.1 From 9fc7e013ff25e971aaa964d8aeaf55ed06936c1a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 20:11:34 +0100 Subject: [PATCH 031/127] Move main() into mopidy.core --- bin/mopidy | 2 +- mopidy/__main__.py | 13 +++---------- mopidy/core.py | 5 +++++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/bin/mopidy b/bin/mopidy index 0472518e..aabf21d3 100755 --- a/bin/mopidy +++ b/bin/mopidy @@ -1,5 +1,5 @@ #! /usr/bin/env python if __name__ == '__main__': - from mopidy.__main__ import main + from mopidy.core import main main() diff --git a/mopidy/__main__.py b/mopidy/__main__.py index 20e78f5a..169c2754 100644 --- a/mopidy/__main__.py +++ b/mopidy/__main__.py @@ -1,17 +1,10 @@ -import os -import sys - # Add ../ to the path so we can run Mopidy from a Git checkout without # installing it on the system. +import os +import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../'))) -from mopidy.core import CoreProcess - -def main(): - # Explictly call run() instead of start(), since we don't need to start - # another process. - CoreProcess().run() - if __name__ == '__main__': + from mopidy.core import main main() diff --git a/mopidy/core.py b/mopidy/core.py index 56febe5b..f56bb334 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -12,6 +12,11 @@ from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') +def main(): + # Explictly call run() instead of start(), since we don't need to start + # another process. + CoreProcess().run() + class CoreProcess(BaseThread): def __init__(self): self.core_queue = multiprocessing.Queue() From 0acaa8f2853bda3f2d3ebe3b161978c84c3537ca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 21:33:53 +0100 Subject: [PATCH 032/127] Log startup message directly from setup_logging --- mopidy/utils/log.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index cc1c19c1..c74ff5ea 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -1,13 +1,15 @@ import logging import logging.handlers -from mopidy import settings +from mopidy import get_version, settings def setup_logging(verbosity_level, save_debug_log): setup_root_logger() setup_console_logging(verbosity_level) if save_debug_log: setup_debug_logging_to_file() + logger = logging.getLogger('mopidy.utils.log') + logger.info(u'-- Starting Mopidy %s --', get_version()) def setup_root_logger(): root = logging.getLogger('') From 4749e77f1dd6dd51a8b03ff80de2100fc9fa569a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 21:34:38 +0100 Subject: [PATCH 033/127] Replace CoreProcess with simple setup functions --- mopidy/core.py | 148 ++++++++++++++++--------------------------------- 1 file changed, 49 insertions(+), 99 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index f56bb334..ab457a00 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -3,117 +3,67 @@ import multiprocessing import optparse import sys +from pykka.actor import ThreadingActor + from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class from mopidy.utils.log import setup_logging from mopidy.utils.path import get_or_create_folder, get_or_create_file -from mopidy.utils.process import BaseThread, GObjectEventThread +from mopidy.utils.process import GObjectEventThread from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') def main(): - # Explictly call run() instead of start(), since we don't need to start - # another process. - CoreProcess().run() + options = parse_options() + setup_logging(options.verbosity_level, options.save_debug_log) + setup_settings() + setup_gobject_loop() + setup_output() + setup_backend() + setup_frontends() -class CoreProcess(BaseThread): - def __init__(self): - self.core_queue = multiprocessing.Queue() - super(CoreProcess, self).__init__(self.core_queue) - self.name = 'CoreProcess' - self.options = self.parse_options() - self.gobject_loop = None - self.output = None - self.backend = None - self.frontends = [] +def parse_options(): + parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser.add_option('-q', '--quiet', + action='store_const', const=0, dest='verbosity_level', + help='less output (warning level)') + parser.add_option('-v', '--verbose', + action='store_const', const=2, dest='verbosity_level', + help='more output (debug level)') + parser.add_option('--save-debug-log', + action='store_true', dest='save_debug_log', + help='save debug log to "./mopidy.log"') + parser.add_option('--list-settings', + action='callback', callback=list_settings_optparse_callback, + help='list current settings') + return parser.parse_args()[0] - def parse_options(self): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) - parser.add_option('-q', '--quiet', - action='store_const', const=0, dest='verbosity_level', - help='less output (warning level)') - parser.add_option('-v', '--verbose', - action='store_const', const=2, dest='verbosity_level', - help='more output (debug level)') - parser.add_option('--save-debug-log', - action='store_true', dest='save_debug_log', - help='save debug log to "./mopidy.log"') - parser.add_option('--list-settings', - action='callback', callback=list_settings_optparse_callback, - help='list current settings') - return parser.parse_args()[0] +def setup_settings(): + get_or_create_folder('~/.mopidy/') + get_or_create_file('~/.mopidy/settings.py') + settings.validate() - def run_inside_try(self): - self.setup() - while True: - message = self.core_queue.get() - self.process_message(message) +def setup_gobject_loop(): + gobject_loop = GObjectEventThread() + gobject_loop.start() + return gobject_loop - def setup(self): - self.setup_logging() - self.setup_settings() - self.gobject_loop = self.setup_gobject_loop(self.core_queue) - self.output = self.setup_output(self.core_queue) - self.backend = self.setup_backend(self.core_queue, self.output) - self.frontends = self.setup_frontends(self.core_queue, self.backend) +def setup_output(): + output = get_class(settings.OUTPUT)() + output.start() + return output - def setup_logging(self): - setup_logging(self.options.verbosity_level, - self.options.save_debug_log) - logger.info(u'-- Starting Mopidy %s --', get_version()) +def setup_backend(): + return get_class(settings.BACKENDS[0])() - def setup_settings(self): - get_or_create_folder('~/.mopidy/') - get_or_create_file('~/.mopidy/settings.py') - settings.validate() - - def setup_gobject_loop(self, core_queue): - gobject_loop = GObjectEventThread(core_queue) - gobject_loop.start() - return gobject_loop - - def setup_output(self, core_queue): - output = get_class(settings.OUTPUT)(core_queue) - output.start() - return output - - def setup_backend(self, core_queue, output): - return get_class(settings.BACKENDS[0])(core_queue, output) - - def setup_frontends(self, core_queue, backend): - frontends = [] - for frontend_class_name in settings.FRONTENDS: - try: - frontend = get_class(frontend_class_name)(core_queue, backend) - frontend.start() - frontends.append(frontend) - except OptionalDependencyError as e: - logger.info(u'Disabled: %s (%s)', frontend_class_name, e) - return frontends - - def process_message(self, message): - if message.get('to') == 'core': - self.process_message_to_core(message) - elif message.get('to') == 'output': - self.output.process_message(message) - elif message.get('to') == 'frontend': - for frontend in self.frontends: - frontend.process_message(message) - elif message['command'] == 'end_of_track': - self.backend.playback.on_end_of_track() - elif message['command'] == 'stop_playback': - self.backend.playback.stop() - elif message['command'] == 'set_stored_playlists': - self.backend.stored_playlists.playlists = message['playlists'] - else: - logger.warning(u'Cannot handle message: %s', message) - - def process_message_to_core(self, message): - assert message['to'] == 'core', u'Message recipient must be "core".' - if message['command'] == 'exit': - if message['reason'] is not None: - logger.info(u'Exiting (%s)', message['reason']) - sys.exit(message['status']) - else: - logger.warning(u'Cannot handle message: %s', message) +def setup_frontends(): + frontends = [] + for frontend_class_name in settings.FRONTENDS: + try: + frontend = get_class(frontend_class_name)() + frontend.start() + frontends.append(frontend) + except OptionalDependencyError as e: + logger.info(u'Disabled: %s (%s)', frontend_class_name, e) + return frontends From 9dafc6ebe982eea05a45f2f451b80269e860fc6a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 21:35:14 +0100 Subject: [PATCH 034/127] Remove lifetime methods from output API --- mopidy/outputs/base.py | 27 --------------------------- mopidy/outputs/dummy.py | 18 ------------------ 2 files changed, 45 deletions(-) diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index 372d7d70..fbc86688 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -3,33 +3,6 @@ class BaseOutput(object): Base class for audio outputs. """ - def __init__(self, core_queue): - self.core_queue = core_queue - - def start(self): - """ - Start the output. - - *MAY be implemented by subclasses.* - """ - pass - - def destroy(self): - """ - Destroy the output. - - *MAY be implemented by subclasses.* - """ - pass - - def process_message(self, message): - """ - Process messages with the output as destination. - - *MUST be implemented by subclass.* - """ - raise NotImplementedError - def play_uri(self, uri): """ Play URI. diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index 060ee02f..738575f3 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -8,15 +8,6 @@ class DummyOutput(BaseOutput): # pylint: disable = R0902 # Too many instance attributes (9/7) - #: For testing. :class:`True` if :meth:`start` has been called. - start_called = False - - #: For testing. :class:`True` if :meth:`destroy` has been called. - destroy_called = False - - #: For testing. Contains all messages :meth:`process_message` has received. - messages = [] - #: For testing. Contains the last URI passed to :meth:`play_uri`. uri = None @@ -40,15 +31,6 @@ class DummyOutput(BaseOutput): #: For testing. Contains the current volume. volume = 100 - def start(self): - self.start_called = True - - def destroy(self): - self.destroy_called = True - - def process_message(self, message): - self.messages.append(message) - def play_uri(self, uri): self.uri = uri return True From 3d56188619b3b3d0549b58f4202cba8d400d9788 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 21:36:26 +0100 Subject: [PATCH 035/127] Convert GstreamerOutput to an actor --- mopidy/outputs/gstreamer.py | 181 +++++++----------------------------- 1 file changed, 36 insertions(+), 145 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 3b037f62..3fb58a2e 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -3,113 +3,37 @@ pygst.require('0.10') import gst import logging -import multiprocessing + +from pykka.actor import ThreadingActor from mopidy import settings from mopidy.outputs.base import BaseOutput -from mopidy.utils.process import (BaseThread, pickle_connection, - unpickle_connection) logger = logging.getLogger('mopidy.outputs.gstreamer') -class GStreamerOutput(BaseOutput): +class GStreamerOutput(ThreadingActor, BaseOutput): """ - Audio output through GStreamer. - - Starts :class:`GStreamerMessagesThread` and :class:`GStreamerPlayerThread`. + Audio output through `GStreamer `_. **Settings:** - :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK` + """ - def __init__(self, *args, **kwargs): - super(GStreamerOutput, self).__init__(*args, **kwargs) - self.output_queue = multiprocessing.Queue() - self.player_thread = GStreamerPlayerThread(self.core_queue, - self.output_queue) - - def start(self): - self.player_thread.start() - - def destroy(self): - self.player_thread.destroy() - - def process_message(self, message): - assert message['to'] == 'output', \ - u'Message recipient must be "output".' - self.output_queue.put(message) - - def _send_recv(self, message): - (my_end, other_end) = multiprocessing.Pipe() - message['to'] = 'output' - message['reply_to'] = pickle_connection(other_end) - self.process_message(message) - my_end.poll(None) - return my_end.recv() - - def _send(self, message): - message['to'] = 'output' - self.process_message(message) - - def play_uri(self, uri): - return self._send_recv({'command': 'play_uri', 'uri': uri}) - - def deliver_data(self, capabilities, data): - return self._send({ - 'command': 'deliver_data', - 'caps': capabilities, - 'data': data, - }) - - def end_of_data_stream(self): - return self._send({'command': 'end_of_data_stream'}) - - def get_position(self): - return self._send_recv({'command': 'get_position'}) - - def set_position(self, position): - return self._send_recv({'command': 'set_position', - 'position': position}) - - def set_state(self, state): - return self._send_recv({'command': 'set_state', 'state': state}) - - def get_volume(self): - return self._send_recv({'command': 'get_volume'}) - - def set_volume(self, volume): - return self._send_recv({'command': 'set_volume', 'volume': volume}) - - -class GStreamerPlayerThread(BaseThread): - """ - A process for all work related to GStreamer. - - The main loop processes events from both Mopidy and GStreamer. - - This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be - running too. This is not enforced in any way by the code. - - Make sure this subprocess is started by the MainThread in the top-most - parent process, and not some other thread. If not, we can get into the - problems described at - http://jameswestby.net/weblog/tech/14-caution-python-multiprocessing-and-glib-dont-mix.html. - """ - - def __init__(self, core_queue, output_queue): - super(GStreamerPlayerThread, self).__init__(core_queue) - self.name = u'GStreamerPlayerThread' - self.output_queue = output_queue + def __init__(self): self.gst_pipeline = None - def run_inside_try(self): - self.setup() - while True: - message = self.output_queue.get() - self.process_mopidy_message(message) + # XXX Run setup after actor starts? + self._setup_gstreamer() + + def _setup_gstreamer(self): + """ + **Warning:** :class:`GStreamerOutput` requires + :class:`mopidy.utils.process.GObjectEventThread` to be running. This is + not enforced by :class:`GStreamerOutput` itself. + """ - def setup(self): logger.debug(u'Setting up GStreamer pipeline') self.gst_pipeline = gst.parse_launch(' ! '.join([ @@ -122,7 +46,7 @@ class GStreamerPlayerThread(BaseThread): if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend': uri_bin = gst.element_factory_make('uridecodebin', 'uri') - uri_bin.connect('pad-added', self.process_new_pad, pad) + uri_bin.connect('pad-added', self._process_new_pad, pad) self.gst_pipeline.add(uri_bin) else: app_src = gst.element_factory_make('appsrc', 'appsrc') @@ -141,45 +65,12 @@ class GStreamerPlayerThread(BaseThread): # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() gst_bus.add_signal_watch() - gst_bus.connect('message', self.process_gst_message) + gst_bus.connect('message', self._process_gstreamer_message) - def process_new_pad(self, source, pad, target_pad): + def _process_new_pad(self, source, pad, target_pad): pad.link(target_pad) - def process_mopidy_message(self, message): - """Process messages from the rest of Mopidy.""" - if message['command'] == 'play_uri': - response = self.play_uri(message['uri']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'deliver_data': - self.deliver_data(message['caps'], message['data']) - elif message['command'] == 'end_of_data_stream': - self.end_of_data_stream() - elif message['command'] == 'set_state': - response = self.set_state(message['state']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'get_volume': - volume = self.get_volume() - connection = unpickle_connection(message['reply_to']) - connection.send(volume) - elif message['command'] == 'set_volume': - response = self.set_volume(message['volume']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'set_position': - response = self.set_position(message['position']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - elif message['command'] == 'get_position': - response = self.get_position() - connection = unpickle_connection(message['reply_to']) - connection.send(response) - else: - logger.warning(u'Cannot handle message: %s', message) - - def process_gst_message(self, bus, message): + def _process_gstreamer_message(self, bus, message): """Process messages from GStreamer.""" if message.type == gst.MESSAGE_EOS: logger.debug(u'GStreamer signalled end-of-stream. ' @@ -189,7 +80,7 @@ class GStreamerPlayerThread(BaseThread): self.set_state('NULL') error, debug = message.parse_error() logger.error(u'%s %s', error, debug) - # FIXME Should we send 'stop_playback' to core here? Can we + # FIXME Should we send 'stop_playback' to the backend here? Can we # differentiate on how serious the error is? def play_uri(self, uri): @@ -198,7 +89,7 @@ class GStreamerPlayerThread(BaseThread): self.gst_pipeline.get_by_name('uri').set_property('uri', uri) return self.set_state('PLAYING') - def deliver_data(self, caps_string, data): + def deliver_data(self, capabilities, data): """Deliver audio data to be played""" app_src = self.gst_pipeline.get_by_name('appsrc') caps = gst.caps_from_string(caps_string) @@ -216,6 +107,21 @@ class GStreamerPlayerThread(BaseThread): """ self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream') + def get_position(self): + try: + position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] + return position // gst.MSECOND + except gst.QueryError, e: + logger.error('time_position failed: %s', e) + return 0 + + def set_position(self, position): + self.gst_pipeline.get_state() # block until state changes are done + handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME), + gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) + self.gst_pipeline.get_state() # block until seek is done + return handeled + def set_state(self, state_name): """ Set the GStreamer state. Returns :class:`True` if successful. @@ -252,18 +158,3 @@ class GStreamerPlayerThread(BaseThread): gst_volume = self.gst_pipeline.get_by_name('volume') gst_volume.set_property('volume', volume / 100.0) return True - - def set_position(self, position): - self.gst_pipeline.get_state() # block until state changes are done - handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME), - gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) - self.gst_pipeline.get_state() # block until seek is done - return handeled - - def get_position(self): - try: - position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] - return position // gst.MSECOND - except gst.QueryError, e: - logger.error('time_position failed: %s', e) - return 0 From 3a1de6578df2c93222abdf0bc4ded3af8a724946 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 21:37:55 +0100 Subject: [PATCH 036/127] Remove unused BaseProcess and strip queue stuff from BaseThread --- mopidy/utils/process.py | 53 +++-------------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 11dafa8a..7a5ba62f 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -1,8 +1,5 @@ import logging -import multiprocessing import multiprocessing.dummy -from multiprocessing.reduction import reduce_connection -import pickle import gobject gobject.threads_init() @@ -11,52 +8,10 @@ from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') -def pickle_connection(connection): - return pickle.dumps(reduce_connection(connection)) - -def unpickle_connection(pickled_connection): - # From http://stackoverflow.com/questions/1446004 - (func, args) = pickle.loads(pickled_connection) - return func(*args) - -class BaseProcess(multiprocessing.Process): - def __init__(self, core_queue): - super(BaseProcess, self).__init__() - self.core_queue = core_queue - - def run(self): - logger.debug(u'%s: Starting process', self.name) - try: - self.run_inside_try() - except KeyboardInterrupt: - logger.info(u'Interrupted by user') - self.exit(0, u'Interrupted by user') - except SettingsError as e: - logger.error(e.message) - self.exit(1, u'Settings error') - except ImportError as e: - logger.error(e) - self.exit(2, u'Import error') - except Exception as e: - logger.exception(e) - self.exit(3, u'Unknown error') - - def run_inside_try(self): - raise NotImplementedError - - def destroy(self): - self.terminate() - - def exit(self, status=0, reason=None): - self.core_queue.put({'to': 'core', 'command': 'exit', - 'status': status, 'reason': reason}) - self.destroy() - class BaseThread(multiprocessing.dummy.Process): - def __init__(self, core_queue): + def __init__(self): super(BaseThread, self).__init__() - self.core_queue = core_queue # No thread should block process from exiting self.daemon = True @@ -84,8 +39,6 @@ class BaseThread(multiprocessing.dummy.Process): pass def exit(self, status=0, reason=None): - self.core_queue.put({'to': 'core', 'command': 'exit', - 'status': status, 'reason': reason}) self.destroy() @@ -98,8 +51,8 @@ class GObjectEventThread(BaseThread): :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc. """ - def __init__(self, core_queue): - super(GObjectEventThread, self).__init__(core_queue) + def __init__(self): + super(GObjectEventThread, self).__init__() self.name = u'GObjectEventThread' self.loop = None From b88d8d5d8aa92bbb989387f95e6dca19107045f9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:03:11 +0100 Subject: [PATCH 037/127] Actorify mixers --- mopidy/core.py | 9 ++++--- mopidy/mixers/alsa.py | 8 +++--- mopidy/mixers/base.py | 10 +------- mopidy/mixers/denon.py | 20 ++++++--------- mopidy/mixers/dummy.py | 7 +++--- mopidy/mixers/gstreamer_software.py | 16 ++++++++---- mopidy/mixers/nad.py | 38 ++++++++++++----------------- mopidy/mixers/osa.py | 5 ++-- 8 files changed, 54 insertions(+), 59 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index ab457a00..51a0b737 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -20,6 +20,7 @@ def main(): setup_settings() setup_gobject_loop() setup_output() + setup_mixer() setup_backend() setup_frontends() @@ -50,11 +51,13 @@ def setup_gobject_loop(): return gobject_loop def setup_output(): - output = get_class(settings.OUTPUT)() - output.start() - return output + return get_class(settings.OUTPUT).start_proxy() + +def setup_mixer(): + return get_class(settings.MIXER).start_proxy() def setup_backend(): + # XXX Convert backend to one or more actors? return get_class(settings.BACKENDS[0])() def setup_frontends(): diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index 4aa5952f..658677e5 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -1,12 +1,14 @@ import alsaaudio import logging +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.mixers.base import BaseMixer logger = logging.getLogger('mopidy.mixers.alsa') -class AlsaMixer(BaseMixer): +class AlsaMixer(ThreadingActor, BaseMixer): """ Mixer which uses the Advanced Linux Sound Architecture (ALSA) to control volume. @@ -20,8 +22,8 @@ class AlsaMixer(BaseMixer): - :attr:`mopidy.settings.MIXER_ALSA_CONTROL` """ - def __init__(self, *args, **kwargs): - super(AlsaMixer, self).__init__(*args, **kwargs) + def __init__(self): + # XXX Do mixer detection after actor starts? self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index f7f9525c..74996cb6 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -2,17 +2,12 @@ from mopidy import settings class BaseMixer(object): """ - :param backend: a backend instance - :type backend: :class:`mopidy.backends.base.Backend` - **Settings:** - :attr:`mopidy.settings.MIXER_MAX_VOLUME` """ - def __init__(self, backend, *args, **kwargs): - self.backend = backend - self.amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 + amplification_factor = settings.MIXER_MAX_VOLUME / 100.0 @property def volume(self): @@ -35,9 +30,6 @@ class BaseMixer(object): volume = 100 self._set_volume(volume) - def destroy(self): - pass - def _get_volume(self): """ Return volume as integer in range [0, 100]. :class:`None` if unknown. diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index f0712f95..50c321b6 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -1,12 +1,13 @@ import logging -from threading import Lock + +from pykka.actor import ThreadingActor from mopidy import settings from mopidy.mixers.base import BaseMixer logger = logging.getLogger(u'mopidy.mixers.denon') -class DenonMixer(BaseMixer): +class DenonMixer(ThreadingActor, BaseMixer): """ Mixer for controlling Denon amplifiers and receivers using the RS-232 protocol. @@ -24,12 +25,12 @@ class DenonMixer(BaseMixer): - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` """ - def __init__(self, *args, **kwargs): + def __init__(self): """ Connects using the serial specifications from Denon's RS-232 Protocol specification: 9600bps 8N1. """ - super(DenonMixer, self).__init__(*args, **kwargs) + # XXX Do setup after actor starts? device = kwargs.get('device', None) if device: self._device = device @@ -38,14 +39,11 @@ class DenonMixer(BaseMixer): self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 - self._lock = Lock() def _get_volume(self): - self._lock.acquire() - self.ensure_open_device() + self._ensure_open_device() self._device.write('MV?\r') vol = str(self._device.readline()[2:4]) - self._lock.release() logger.debug(u'_get_volume() = %s' % vol) return self._levels.index(vol) @@ -53,14 +51,12 @@ class DenonMixer(BaseMixer): # Clamp according to Denon-spec if volume > 99: volume = 99 - self._lock.acquire() - self.ensure_open_device() + self._ensure_open_device() self._device.write('MV%s\r'% self._levels[volume]) vol = self._device.readline()[2:4] - self._lock.release() self._volume = self._levels.index(vol) - def ensure_open_device(self): + def _ensure_open_device(self): if not self._device.isOpen(): logger.debug(u'(re)connecting to Denon device') self._device.open() diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py index 12a8137e..186bc7aa 100644 --- a/mopidy/mixers/dummy.py +++ b/mopidy/mixers/dummy.py @@ -1,10 +1,11 @@ +from pykka.actor import ThreadingActor + from mopidy.mixers.base import BaseMixer -class DummyMixer(BaseMixer): +class DummyMixer(ThreadingActor, BaseMixer): """Mixer which just stores and reports the chosen volume.""" - def __init__(self, *args, **kwargs): - super(DummyMixer, self).__init__(*args, **kwargs) + def __init__(self): self._volume = None def _get_volume(self): diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 9dca3690..95635794 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,13 +1,19 @@ +from pykka.actor import ThreadingActor +from pykka.proxy import ActorProxy +from pykka.registry import ActorRegistry + from mopidy.mixers.base import BaseMixer -class GStreamerSoftwareMixer(BaseMixer): +class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" - def __init__(self, *args, **kwargs): - super(GStreamerSoftwareMixer, self).__init__(*args, **kwargs) + def __init__(self): + # XXX Get reference to output without hardcoding GStreamerOutput + output_refs = ActorRegistry.get_by_class_name('GStreamerOutput') + self.output = ActorProxy(output_refs[0]) def _get_volume(self): - return self.backend.output.get_volume() + return self.output.get_volume().get() def _set_volume(self, volume): - self.backend.output.set_volume(volume) + self.output.set_volume(volume).get() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 5cf92826..0765882d 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -2,13 +2,15 @@ import logging from serial import Serial from multiprocessing import Pipe +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.mixers.base import BaseMixer from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.mixers.nad') -class NadMixer(BaseMixer): +class NadMixer(ThreadingActor, BaseMixer): """ Mixer for controlling NAD amplifiers and receivers using the NAD RS-232 protocol. @@ -36,21 +38,19 @@ class NadMixer(BaseMixer): """ - def __init__(self, *args, **kwargs): - super(NadMixer, self).__init__(*args, **kwargs) - self._volume = None - self._pipe, other_end = Pipe() - NadTalker(self.backend.core_queue, pipe=other_end).start() + def __init__(self): + self._volume_cache = None + self._nad_talker = NadTalker.start_proxy() def _get_volume(self): - return self._volume + return self._volume_cache def _set_volume(self, volume): - self._volume = volume - self._pipe.send({'command': 'set_volume', 'volume': volume}) + self._volume_cache = volume + self._nad_talker.set_volume(volume) -class NadTalker(BaseThread): +class NadTalker(ThreadingActor): """ Independent process which does the communication with the NAD device. @@ -72,21 +72,15 @@ class NadTalker(BaseThread): # Volume in range 0..VOLUME_LEVELS. :class:`None` before calibration. _nad_volume = None - def __init__(self, core_queue, pipe=None): - super(NadTalker, self).__init__(core_queue) - self.name = u'NadTalker' - self.pipe = pipe + def __init__(self): self._device = None - def run_inside_try(self): + # XXX Do after actor starts? + self._setup() + + def _setup(self): self._open_connection() self._set_device_to_known_state() - while self.pipe.poll(None): - message = self.pipe.recv() - if message['command'] == 'set_volume': - self._set_volume(message['volume']) - elif message['command'] == 'reset_device': - self._set_device_to_known_state() def _open_connection(self): # Opens serial connection to the device. @@ -164,7 +158,7 @@ class NadTalker(BaseThread): self._nad_volume = 0 logger.info(u'Done calibrating NAD amplifier') - def _set_volume(self, volume): + def set_volume(self, volume): # Increase or decrease the amplifier volume until it matches the given # target volume. logger.debug(u'Setting volume to %d' % volume) diff --git a/mopidy/mixers/osa.py b/mopidy/mixers/osa.py index 2ea04cf2..53983095 100644 --- a/mopidy/mixers/osa.py +++ b/mopidy/mixers/osa.py @@ -1,9 +1,11 @@ from subprocess import Popen, PIPE import time +from pykka.actor import ThreadingActor + from mopidy.mixers.base import BaseMixer -class OsaMixer(BaseMixer): +class OsaMixer(ThreadingActor, BaseMixer): """ Mixer which uses ``osascript`` on OS X to control volume. @@ -14,7 +16,6 @@ class OsaMixer(BaseMixer): **Settings:** - None - """ CACHE_TTL = 30 From 6a78f11383486fec34fee265e8fa5549c62bad2e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:15:45 +0100 Subject: [PATCH 038/127] Remove mpd_format() from CurrentPlaylistController --- mopidy/backends/base/current_playlist.py | 7 ------- .../mpd/protocol/current_playlist.py | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index fe7d1de9..bd424a2d 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -2,8 +2,6 @@ from copy import copy import logging import random -from mopidy.frontends.mpd import translator - logger = logging.getLogger('mopidy.backends.base') class CurrentPlaylistController(object): @@ -197,8 +195,3 @@ class CurrentPlaylistController(object): random.shuffle(shuffled) self._cp_tracks = before + shuffled + after self.version += 1 - - def mpd_format(self, *args, **kwargs): - """Not a part of the generic backend API.""" - kwargs['cpids'] = [ct[0] for ct in self._cp_tracks] - return translator.tracks_to_mpd_format(self.tracks, *args, **kwargs) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 2f0a9f8f..13635a33 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -1,6 +1,7 @@ -from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) +from mopidy.frontends.mpd.protocol import handle_pattern +from mopidy.frontends.mpd.translator import tracks_to_mpd_format @handle_pattern(r'^add "(?P[^"]*)"$') def add(frontend, uri): @@ -218,7 +219,9 @@ def playlistid(frontend, cpid=None): except LookupError: raise MpdNoExistError(u'No such song', command=u'playlistid') else: - return frontend.backend.current_playlist.mpd_format() + cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] + return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + cpids=cpids) @handle_pattern(r'^playlistinfo$') @handle_pattern(r'^playlistinfo "(?P-?\d+)"$') @@ -248,7 +251,9 @@ def playlistinfo(frontend, songpos=None, end = songpos + 1 if start == -1: end = None - return frontend.backend.current_playlist.mpd_format(start, end) + cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] + return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + start, end, cpids=cpids) else: if start is None: start = 0 @@ -259,7 +264,9 @@ def playlistinfo(frontend, songpos=None, end = int(end) if end > len(frontend.backend.current_playlist.tracks): end = None - return frontend.backend.current_playlist.mpd_format(start, end) + cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] + return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + start, end, cpids=cpids) @handle_pattern(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @handle_pattern(r'^playlistsearch (?P\S+) "(?P[^"]+)"$') @@ -298,7 +305,9 @@ def plchanges(frontend, version): """ # XXX Naive implementation that returns all tracks as changed if int(version) < frontend.backend.current_playlist.version: - return frontend.backend.current_playlist.mpd_format() + cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] + return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + cpids=cpids) @handle_pattern(r'^plchangesposid "(?P\d+)"$') def plchangesposid(frontend, version): From ab01828c054a91a54a5edab3394c48629664045a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:19:23 +0100 Subject: [PATCH 039/127] Strip Backend to a minimum --- mopidy/backends/base/__init__.py | 53 -------------------------------- 1 file changed, 53 deletions(-) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 096a433f..963d9eb4 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -1,12 +1,4 @@ -from copy import copy import logging -import random -import time - -from mopidy import settings -from mopidy.frontends.mpd import translator -from mopidy.models import Playlist -from mopidy.utils import get_class from .current_playlist import CurrentPlaylistController from .library import LibraryController, BaseLibraryProvider @@ -17,30 +9,6 @@ from .stored_playlists import (StoredPlaylistsController, logger = logging.getLogger('mopidy.backends.base') class Backend(object): - """ - :param core_queue: a queue for sending messages to - :class:`mopidy.process.CoreProcess` - :type core_queue: :class:`multiprocessing.Queue` - :param output: the audio output - :type output: :class:`mopidy.outputs.gstreamer.GStreamerOutput` or similar - :param mixer_class: either a mixer class, or :class:`None` to use the mixer - defined in settings - :type mixer_class: a subclass of :class:`mopidy.mixers.BaseMixer` or - :class:`None` - """ - - def __init__(self, core_queue=None, output=None, mixer_class=None): - self.core_queue = core_queue - self.output = output - if mixer_class is None: - mixer_class = get_class(settings.MIXER) - self.mixer = mixer_class(self) - - #: A :class:`multiprocessing.Queue` which can be used by e.g. library - #: callbacks executing in other threads to send messages to the core - #: thread, so that action may be taken in the correct thread. - core_queue = None - #: The current playlist controller. An instance of #: :class:`mopidy.backends.base.CurrentPlaylistController`. current_playlist = None @@ -62,24 +30,3 @@ class Backend(object): #: List of URI prefixes this backend can handle. uri_handlers = [] - - def destroy(self): - """ - Call destroy on all sub-components in backend so that they can cleanup - after themselves. - """ - - if self.current_playlist: - self.current_playlist.destroy() - - if self.library: - self.library.destroy() - - if self.mixer: - self.mixer.destroy() - - if self.playback: - self.playback.destroy() - - if self.stored_playlists: - self.stored_playlists.destroy() From 38e5cec0f03cfac11ad55416dcb0d41c09525533 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:35:22 +0100 Subject: [PATCH 040/127] Partly actorify MpdFrontend --- mopidy/core.py | 3 +- mopidy/frontends/mpd/__init__.py | 49 ++++++++++++++------------------ mopidy/frontends/mpd/session.py | 11 +------ mopidy/frontends/mpd/thread.py | 18 ------------ 4 files changed, 23 insertions(+), 58 deletions(-) delete mode 100644 mopidy/frontends/mpd/thread.py diff --git a/mopidy/core.py b/mopidy/core.py index 51a0b737..c687aed1 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -64,8 +64,7 @@ def setup_frontends(): frontends = [] for frontend_class_name in settings.FRONTENDS: try: - frontend = get_class(frontend_class_name)() - frontend.start() + frontend = get_class(frontend_class_name).start_proxy() frontends.append(frontend) except OptionalDependencyError as e: logger.info(u'Disabled: %s (%s)', frontend_class_name, e) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 2f87088c..b3d5b5e8 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,13 +1,15 @@ +import asyncore import logging +from pykka.actor import ThreadingActor + from mopidy.frontends.base import BaseFrontend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.thread import MpdThread -from mopidy.utils.process import unpickle_connection +from mopidy.frontends.mpd.server import MpdServer +from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.mpd') -class MpdFrontend(BaseFrontend): +class MpdFrontend(ThreadingActor, BaseFrontend): """ The MPD frontend. @@ -18,32 +20,23 @@ class MpdFrontend(BaseFrontend): - :attr:`mopidy.settings.MPD_SERVER_PORT` """ - def __init__(self, *args, **kwargs): - super(MpdFrontend, self).__init__(*args, **kwargs) - self.thread = None - self.dispatcher = MpdDispatcher(self.backend) - - def start(self): - """Starts the MPD server.""" - self.thread = MpdThread(self.core_queue) - self.thread.start() + def __init__(self): + # XXX Do setup after actor starts? + self._thread = MpdThread() + self._thread.start() def destroy(self): """Destroys the MPD server.""" - self.thread.destroy() + self._thread.destroy() - def process_message(self, message): - """ - Processes messages with the MPD frontend as destination. - :param message: the message - :type message: dict - """ - assert message['to'] == 'frontend', \ - u'Message recipient must be "frontend".' - if message['command'] == 'mpd_request': - response = self.dispatcher.handle_request(message['request']) - connection = unpickle_connection(message['reply_to']) - connection.send(response) - else: - pass # Ignore messages for other frontends +class MpdThread(BaseThread): + def __init__(self): + super(BaseThread, self).__init__() + self.name = u'MpdThread' + + def run_inside_try(self): + logger.debug(u'Starting MPD server thread') + server = MpdServer(self.core_queue) + server.start() + asyncore.loop() diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index e8e3291d..349fceb9 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -5,7 +5,6 @@ import multiprocessing from mopidy import settings from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION from mopidy.utils.log import indent -from mopidy.utils.process import pickle_connection logger = logging.getLogger('mopidy.frontends.mpd.session') @@ -53,15 +52,7 @@ class MpdSession(asynchat.async_chat): if response is not None: self.send_response(response) return - my_end, other_end = multiprocessing.Pipe() - self.core_queue.put({ - 'to': 'frontend', - 'command': 'mpd_request', - 'request': request, - 'reply_to': pickle_connection(other_end), - }) - my_end.poll(None) - response = my_end.recv() + # TODO-PYKKA: Process request using MpdDispatcher/backend if response is not None: self.handle_response(response) diff --git a/mopidy/frontends/mpd/thread.py b/mopidy/frontends/mpd/thread.py deleted file mode 100644 index 0ad5ee68..00000000 --- a/mopidy/frontends/mpd/thread.py +++ /dev/null @@ -1,18 +0,0 @@ -import asyncore -import logging - -from mopidy.frontends.mpd.server import MpdServer -from mopidy.utils.process import BaseThread - -logger = logging.getLogger('mopidy.frontends.mpd.thread') - -class MpdThread(BaseThread): - def __init__(self, core_queue): - super(MpdThread, self).__init__(core_queue) - self.name = u'MpdThread' - - def run_inside_try(self): - logger.debug(u'Starting MPD server thread') - server = MpdServer(self.core_queue) - server.start() - asyncore.loop() From 18e252a9d39283a63ef55d024fd192882a0917f5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:36:23 +0100 Subject: [PATCH 041/127] Strip FrontendBase to a minimum --- mopidy/frontends/base.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/mopidy/frontends/base.py b/mopidy/frontends/base.py index bf1c9bda..811644b1 100644 --- a/mopidy/frontends/base.py +++ b/mopidy/frontends/base.py @@ -1,40 +1,5 @@ class BaseFrontend(object): """ Base class for frontends. - - :param core_queue: queue for messaging the core - :type core_queue: :class:`multiprocessing.Queue` - :param backend: the backend - :type backend: :class:`mopidy.backends.base.Backend` """ - - def __init__(self, core_queue, backend): - self.core_queue = core_queue - self.backend = backend - - def start(self): - """ - Start the frontend. - - *MAY be implemented by subclass.* - """ - pass - - def destroy(self): - """ - Destroy the frontend. - - *MAY be implemented by subclass.* - """ - pass - - def process_message(self, message): - """ - Process messages for the frontend. - - *MUST be implemented by subclass.* - - :param message: the message - :type message: dict - """ - raise NotImplementedError + pass From 43cb8d077c1bf59be3b040b0fbf19cfe1f630e5b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:41:07 +0100 Subject: [PATCH 042/127] Actorify LastfmFrontend --- mopidy/frontends/lastfm.py | 47 ++++++++------------------------------ 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index d2c9af88..f8a46c0f 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -1,5 +1,4 @@ import logging -import multiprocessing import time try: @@ -8,6 +7,8 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) +from pykka.actor import ThreadingActor + from mopidy import settings, SettingsError from mopidy.frontends.base import BaseFrontend from mopidy.utils.process import BaseThread @@ -17,7 +18,7 @@ logger = logging.getLogger('mopidy.frontends.lastfm') API_KEY = '2236babefa8ebb3d93ea467560d00d04' API_SECRET = '94d9a09c0cd5be955c4afaeaffcaefcd' -class LastfmFrontend(BaseFrontend): +class LastfmFrontend(ThreadingActor, BaseFrontend): """ Frontend which scrobbles the music you play to your `Last.fm `_ profile. @@ -36,38 +37,14 @@ class LastfmFrontend(BaseFrontend): - :attr:`mopidy.settings.LASTFM_PASSWORD` """ - def __init__(self, *args, **kwargs): - super(LastfmFrontend, self).__init__(*args, **kwargs) - (self.connection, other_end) = multiprocessing.Pipe() - self.thread = LastfmFrontendThread(self.core_queue, other_end) - - def start(self): - self.thread.start() - - def destroy(self): - self.thread.destroy() - - def process_message(self, message): - if self.thread.is_alive(): - self.connection.send(message) - - -class LastfmFrontendThread(BaseThread): - def __init__(self, core_queue, connection): - super(LastfmFrontendThread, self).__init__(core_queue) - self.name = u'LastfmFrontendThread' - self.connection = connection + def __init__(self): self.lastfm = None self.last_start_time = None - def run_inside_try(self): - self.setup() - while self.lastfm is not None: - self.connection.poll(None) - message = self.connection.recv() - self.process_message(message) + # TODO-PYKKA: Do setup after actor starts + self._setup() - def setup(self): + def _setup(self): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) @@ -78,17 +55,11 @@ class LastfmFrontendThread(BaseThread): except SettingsError as e: logger.info(u'Last.fm scrobbler not started') logger.debug(u'Last.fm settings error: %s', e) + self.stop() except (pylast.NetworkError, pylast.MalformedResponseError, pylast.WSError) as e: logger.error(u'Error during Last.fm setup: %s', e) - - def process_message(self, message): - if message['command'] == 'started_playing': - self.started_playing(message['track']) - elif message['command'] == 'stopped_playing': - self.stopped_playing(message['track'], message['stop_position']) - else: - pass # Ignore commands for other frontends + self.stop() def started_playing(self, track): artists = ', '.join([a.name for a in track.artists]) From d75ca5b4e8dce13ece6238bcf5b082a29ca65581 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:42:55 +0100 Subject: [PATCH 043/127] Make MpdFrontend start --- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/frontends/mpd/server.py | 6 ++---- mopidy/frontends/mpd/session.py | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index b3d5b5e8..f4684b9d 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -37,6 +37,6 @@ class MpdThread(BaseThread): def run_inside_try(self): logger.debug(u'Starting MPD server thread') - server = MpdServer(self.core_queue) + server = MpdServer() server.start() asyncore.loop() diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 7caf21f9..231bdf40 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -15,9 +15,8 @@ class MpdServer(asyncore.dispatcher): for each client connection. """ - def __init__(self, core_queue): + def __init__(self): asyncore.dispatcher.__init__(self) - self.core_queue = core_queue def start(self): """Start MPD server.""" @@ -47,8 +46,7 @@ class MpdServer(asyncore.dispatcher): (client_socket, client_socket_address) = self.accept() logger.info(u'MPD client connection from [%s]:%s', client_socket_address[0], client_socket_address[1]) - MpdSession(self, client_socket, client_socket_address, - self.core_queue).start() + MpdSession(self, client_socket, client_socket_address).start() def handle_close(self): """Handle end of client connection.""" diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 349fceb9..399c2adb 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -14,13 +14,11 @@ class MpdSession(asynchat.async_chat): MPD requests to the dispatcher. """ - def __init__(self, server, client_socket, client_socket_address, - core_queue): + def __init__(self, server, client_socket, client_socket_address): asynchat.async_chat.__init__(self, sock=client_socket) self.server = server self.client_address = client_socket_address[0] self.client_port = client_socket_address[1] - self.core_queue = core_queue self.input_buffer = [] self.authenticated = False self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) From f0fd2c9df279830cb18f304f51c2e8af34d5aa9e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:52:04 +0100 Subject: [PATCH 044/127] Change XXXs to TODO-PYKKA to easier find them again --- mopidy/core.py | 2 +- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/mixers/alsa.py | 2 +- mopidy/mixers/denon.py | 2 +- mopidy/mixers/gstreamer_software.py | 2 +- mopidy/mixers/nad.py | 2 +- mopidy/outputs/gstreamer.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index c687aed1..747ef5fb 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -57,7 +57,7 @@ def setup_mixer(): return get_class(settings.MIXER).start_proxy() def setup_backend(): - # XXX Convert backend to one or more actors? + # TODO-PYKKA: Convert backend to one or more actors? return get_class(settings.BACKENDS[0])() def setup_frontends(): diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index f4684b9d..3a1211ae 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -21,7 +21,7 @@ class MpdFrontend(ThreadingActor, BaseFrontend): """ def __init__(self): - # XXX Do setup after actor starts? + # TODO-PYKKA: Do setup after actor starts? self._thread = MpdThread() self._thread.start() diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index 658677e5..f4f40ca1 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -23,7 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer): """ def __init__(self): - # XXX Do mixer detection after actor starts? + # PYKKA-TODO: Do mixer detection after actor starts? self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 50c321b6..36f0f3bd 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -30,7 +30,7 @@ class DenonMixer(ThreadingActor, BaseMixer): Connects using the serial specifications from Denon's RS-232 Protocol specification: 9600bps 8N1. """ - # XXX Do setup after actor starts? + # TODO-PYKKA: Do setup after actor starts? device = kwargs.get('device', None) if device: self._device = device diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 95635794..f9d171ca 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -8,7 +8,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" def __init__(self): - # XXX Get reference to output without hardcoding GStreamerOutput + # TODO-PYKKA Get reference to output without hardcoding GStreamerOutput output_refs = ActorRegistry.get_by_class_name('GStreamerOutput') self.output = ActorProxy(output_refs[0]) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 0765882d..90d5d5b2 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -75,7 +75,7 @@ class NadTalker(ThreadingActor): def __init__(self): self._device = None - # XXX Do after actor starts? + # TODO-PYKKA: Do after actor starts? self._setup() def _setup(self): diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 3fb58a2e..b0b98d99 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -24,7 +24,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def __init__(self): self.gst_pipeline = None - # XXX Run setup after actor starts? + # TODO-PYKKA: Run setup after actor starts? self._setup_gstreamer() def _setup_gstreamer(self): From 3a3777e93e8e10cb1b55d9a2665128368aee6c4f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 Mar 2011 22:59:25 +0100 Subject: [PATCH 045/127] Update Spotify backend enough to make it start without errors --- mopidy/backends/spotify/__init__.py | 4 +--- mopidy/backends/spotify/session_manager.py | 27 ++++++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index d36f6250..ad3dd7ea 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -67,8 +67,6 @@ class SpotifyBackend(Backend): logger.info(u'Mopidy uses SPOTIFY(R) CORE') logger.debug(u'Connecting to Spotify') spotify = SpotifySessionManager( - settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD, - core_queue=self.core_queue, - output=self.output) + settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD) spotify.start() return spotify diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 9736f2eb..d08a7c97 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -2,7 +2,10 @@ import logging import os import threading -import spotify.manager +from spotify.manager import SpotifySessionManager as PyspotifySessionManager + +from pykka.registry import ActorRegistry +from pykka.proxy import ActorProxy from mopidy import get_version, settings from mopidy.backends.spotify.translator import SpotifyTranslator @@ -14,18 +17,21 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager') # pylint: disable = R0901 # SpotifySessionManager: Too many ancestors (9/7) -class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): +class SpotifySessionManager(BaseThread, PyspotifySessionManager): cache_location = settings.SPOTIFY_CACHE_PATH settings_location = settings.SPOTIFY_CACHE_PATH 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): - spotify.manager.SpotifySessionManager.__init__( - self, username, password) - BaseThread.__init__(self, core_queue) + def __init__(self, username, password): + PyspotifySessionManager.__init__(self, username, password) + BaseThread.__init__(self) self.name = 'SpotifySMThread' - self.output = output + + # TODO-PYKKA Get reference to output without hardcoding GStreamerOutput + output_refs = ActorRegistry.get_by_class_name('GStreamerOutput') + self.output = ActorProxy(output_refs[0]) + self.connected = threading.Event() self.session = None @@ -88,7 +94,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): def play_token_lost(self, session): """Callback used by pyspotify""" logger.debug(u'Play token lost') - self.core_queue.put({'command': 'stop_playback'}) + # TODO-PYKKA: Send 'stop_playback' to backend def log_message(self, session, data): """Callback used by pyspotify""" @@ -107,10 +113,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): playlists.append( SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) playlists = filter(None, playlists) - self.core_queue.put({ - 'command': 'set_stored_playlists', - 'playlists': playlists, - }) + # TODO-PYKKA: Set stored_playlists on backend logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) def search(self, query, connection): From 65c64b4a8abc2cb74419c277f50d54cdc472e4e7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Mar 2011 20:39:50 +0100 Subject: [PATCH 046/127] Actorify backends --- mopidy/backends/base/current_playlist.py | 2 ++ mopidy/backends/base/library.py | 2 ++ mopidy/backends/base/playback.py | 4 ++++ mopidy/backends/dummy/__init__.py | 14 +++----------- mopidy/backends/local/__init__.py | 4 +++- mopidy/backends/spotify/__init__.py | 5 ++++- mopidy/core.py | 3 +-- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index bd424a2d..ffdce176 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -10,6 +10,8 @@ class CurrentPlaylistController(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend self._cp_tracks = [] diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index fd018b5f..9ca103c0 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -10,6 +10,8 @@ class LibraryController(object): :type provider: instance of :class:`BaseLibraryProvider` """ + pykka_traversable = True + def __init__(self, backend, provider): self.backend = backend self.provider = provider diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 8a3eeee5..e4ffa075 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -15,6 +15,8 @@ class PlaybackController(object): # pylint: disable = R0902 # Too many instance attributes + pykka_traversable = True + #: Constant representing the paused state. PAUSED = u'paused' @@ -461,6 +463,7 @@ class PlaybackController(object): For internal use only. Should be called by the backend directly after a track has started playing. """ + return # TODO-PYKKA Send started_playing event to interested parties if self.current_track is not None: self.backend.core_queue.put({ 'to': 'frontend', @@ -476,6 +479,7 @@ class PlaybackController(object): is stopped playing, e.g. at the next, previous, and stop actions and at end-of-track. """ + return # TODO-PYKKA Send stopped_playing event to interested parties if self.current_track is not None: self.backend.core_queue.put({ 'to': 'frontend', diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 9c6885bc..82051188 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -1,3 +1,5 @@ +from pykka.actor import ThreadingActor + from mopidy.backends.base import (Backend, CurrentPlaylistController, PlaybackController, BasePlaybackProvider, LibraryController, BaseLibraryProvider, StoredPlaylistsController, @@ -5,15 +7,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, from mopidy.models import Playlist -class DummyQueue(object): - def __init__(self): - self.received_messages = [] - - def put(self, message): - self.received_messages.append(message) - - -class DummyBackend(Backend): +class DummyBackend(ThreadingActor, Backend): """ A backend which implements the backend API in the simplest way possible. Used in tests of the frontends. @@ -24,8 +18,6 @@ class DummyBackend(Backend): def __init__(self, *args, **kwargs): super(DummyBackend, self).__init__(*args, **kwargs) - self.core_queue = DummyQueue() - self.current_playlist = CurrentPlaylistController(backend=self) library_provider = DummyLibraryProvider(backend=self) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e3e1d5dc..c5b42b74 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -4,6 +4,8 @@ import multiprocessing import os import shutil +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, PlaybackController, @@ -16,7 +18,7 @@ from .translator import parse_m3u, parse_mpd_tag_cache logger = logging.getLogger(u'mopidy.backends.local') -class LocalBackend(Backend): +class LocalBackend(ThreadingActor, Backend): """ A backend for playing music from a local music archive. diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index ad3dd7ea..d85e91b2 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -1,5 +1,7 @@ import logging +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, PlaybackController, StoredPlaylistsController) @@ -8,7 +10,7 @@ logger = logging.getLogger('mopidy.backends.spotify') ENCODING = 'utf-8' -class SpotifyBackend(Backend): +class SpotifyBackend(ThreadingActor, Backend): """ A backend for playing music from the `Spotify `_ music streaming service. The backend uses the official `libspotify @@ -59,6 +61,7 @@ class SpotifyBackend(Backend): self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + # TODO-PYKKA: Do setup after actor starts? self.spotify = self._connect() def _connect(self): diff --git a/mopidy/core.py b/mopidy/core.py index 747ef5fb..a82ec2d1 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -57,8 +57,7 @@ def setup_mixer(): return get_class(settings.MIXER).start_proxy() def setup_backend(): - # TODO-PYKKA: Convert backend to one or more actors? - return get_class(settings.BACKENDS[0])() + return get_class(settings.BACKENDS[0]).start_proxy() def setup_frontends(): frontends = [] From a88cf1e2cada6cc010c36579e38822ee1d84f4fd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Mar 2011 21:08:45 +0100 Subject: [PATCH 047/127] Hook an MpdDispatcher directly onto each MpdSession --- mopidy/frontends/mpd/dispatcher.py | 18 +++++++++++++++--- mopidy/frontends/mpd/session.py | 8 +++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index ab5f2e8c..932eb3e3 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -1,5 +1,8 @@ import re +from pykka.proxy import ActorProxy +from pykka.registry import ActorRegistry + from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, MpdUnknownCommand) from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers @@ -14,11 +17,20 @@ from mopidy.utils import flatten class MpdDispatcher(object): """ - Dispatches MPD requests to the correct handler. + The MPD session feeds the MPD dispatcher with requests. The dispatcher + finds the correct handler, processes the request and sends the response + back to the MPD session. """ - def __init__(self, backend=None): - self.backend = backend + # XXX Consider merging MpdDispatcher into MpdSession + + def __init__(self, session): + self.session = session + + # TODO-PYKKA: Get reference to backend in a more generic way + backend_refs = ActorRegistry.get_by_class_name('SpotifyBackend') + self.backend = ActorProxy(backend_refs[0]) + self.command_list = False self.command_list_ok = False diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 399c2adb..eb8c4f11 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -3,6 +3,7 @@ import logging import multiprocessing from mopidy import settings +from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION from mopidy.utils.log import indent @@ -10,8 +11,8 @@ logger = logging.getLogger('mopidy.frontends.mpd.session') class MpdSession(asynchat.async_chat): """ - The MPD client session. Keeps track of a single client and passes its - MPD requests to the dispatcher. + The MPD client session. Keeps track of a single client session. Any + requests from the client is passed on to the MPD request dispatcher. """ def __init__(self, server, client_socket, client_socket_address): @@ -22,6 +23,7 @@ class MpdSession(asynchat.async_chat): self.input_buffer = [] self.authenticated = False self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) + self.dispatcher = MpdDispatcher(self) def start(self): """Start a new client session.""" @@ -50,7 +52,7 @@ class MpdSession(asynchat.async_chat): if response is not None: self.send_response(response) return - # TODO-PYKKA: Process request using MpdDispatcher/backend + response = self.dispatcher.handle_request(request) if response is not None: self.handle_response(response) From 7f725e8c783dfd3226fbe3e2f954efada12f8a45 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Mar 2011 23:20:42 +0100 Subject: [PATCH 048/127] Upgrade to Pykka 0.10 --- docs/installation/index.rst | 2 +- mopidy/core.py | 8 ++++---- mopidy/mixers/nad.py | 2 +- requirements/core.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 19016ce9..ed6107d8 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- `Pykka `_ >= 0.9.1 +- `Pykka `_ >= 0.10 - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. diff --git a/mopidy/core.py b/mopidy/core.py index a82ec2d1..e21efd00 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -51,19 +51,19 @@ def setup_gobject_loop(): return gobject_loop def setup_output(): - return get_class(settings.OUTPUT).start_proxy() + return get_class(settings.OUTPUT).start().proxy() def setup_mixer(): - return get_class(settings.MIXER).start_proxy() + return get_class(settings.MIXER).start().proxy() def setup_backend(): - return get_class(settings.BACKENDS[0]).start_proxy() + return get_class(settings.BACKENDS[0]).start().proxy() def setup_frontends(): frontends = [] for frontend_class_name in settings.FRONTENDS: try: - frontend = get_class(frontend_class_name).start_proxy() + frontend = get_class(frontend_class_name).start().proxy() frontends.append(frontend) except OptionalDependencyError as e: logger.info(u'Disabled: %s (%s)', frontend_class_name, e) diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 90d5d5b2..2844f579 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -40,7 +40,7 @@ class NadMixer(ThreadingActor, BaseMixer): def __init__(self): self._volume_cache = None - self._nad_talker = NadTalker.start_proxy() + self._nad_talker = NadTalker.start().proxy() def _get_volume(self): return self._volume_cache diff --git a/requirements/core.txt b/requirements/core.txt index 54781a3b..1b5cfc4b 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -1 +1 @@ -Pykka >= 0.9.1 +Pykka >= 0.10 From 0575d5d3be96dacb64fa8aa9274387268c77227a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 8 Mar 2011 23:28:24 +0100 Subject: [PATCH 049/127] Use Actor.post_start() to do actor setup in the actor's own thread --- mopidy/backends/spotify/__init__.py | 4 +++- mopidy/frontends/lastfm.py | 5 +---- mopidy/frontends/mpd/__init__.py | 8 +++----- mopidy/mixers/denon.py | 19 +++++++------------ mopidy/mixers/nad.py | 5 +---- mopidy/outputs/gstreamer.py | 2 +- 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index d85e91b2..021277f2 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -61,7 +61,9 @@ class SpotifyBackend(ThreadingActor, Backend): self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] - # TODO-PYKKA: Do setup after actor starts? + self.spotify = None + + def post_start(self): self.spotify = self._connect() def _connect(self): diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index f8a46c0f..4a703c22 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -41,10 +41,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): self.lastfm = None self.last_start_time = None - # TODO-PYKKA: Do setup after actor starts - self._setup() - - def _setup(self): + def post_start(self): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 3a1211ae..96500d9b 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -21,14 +21,12 @@ class MpdFrontend(ThreadingActor, BaseFrontend): """ def __init__(self): - # TODO-PYKKA: Do setup after actor starts? + self._thread = None + + def post_start(self): self._thread = MpdThread() self._thread.start() - def destroy(self): - """Destroys the MPD server.""" - self._thread.destroy() - class MpdThread(BaseThread): def __init__(self): diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 36f0f3bd..832e7d1a 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -25,21 +25,16 @@ class DenonMixer(ThreadingActor, BaseMixer): - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` """ - def __init__(self): - """ - Connects using the serial specifications from Denon's RS-232 Protocol - specification: 9600bps 8N1. - """ - # TODO-PYKKA: Do setup after actor starts? - device = kwargs.get('device', None) - if device: - self._device = device - else: - from serial import Serial - self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) + def __init__(self, *args, **kwargs): + self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 + def post_start(self): + if self._device is None: + from serial import Serial + self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) + def _get_volume(self): self._ensure_open_device() self._device.write('MV?\r') diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 2844f579..e12b8b92 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -75,10 +75,7 @@ class NadTalker(ThreadingActor): def __init__(self): self._device = None - # TODO-PYKKA: Do after actor starts? - self._setup() - - def _setup(self): + def post_start(self): self._open_connection() self._set_device_to_known_state() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index b0b98d99..2a3d9adc 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -24,7 +24,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def __init__(self): self.gst_pipeline = None - # TODO-PYKKA: Run setup after actor starts? + def post_start(self): self._setup_gstreamer() def _setup_gstreamer(self): From ab1f9d0cc6ca6ae6cbdf7ebf570c26166af889af Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 09:45:48 +0100 Subject: [PATCH 050/127] Add Spotify URI to loading tracks so they can be played --- mopidy/backends/spotify/translator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 50ee07d1..d81622f9 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -28,9 +28,9 @@ class SpotifyTranslator(object): @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 not spotify_track.is_loaded(): + return Track(uri=uri, name=u'[loading...]') if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR: date = dt.date(spotify_track.album().year(), 1, 1) else: From d8ea7ea884ad4dc6f4a06e88c08cf76bec7dc066 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 19:30:12 +0100 Subject: [PATCH 051/127] Lookup other actors by their known superclasses --- mopidy/backends/spotify/session_manager.py | 8 ++++---- mopidy/frontends/mpd/dispatcher.py | 8 ++++---- mopidy/mixers/gstreamer_software.py | 11 +++++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index d08a7c97..b79fc48f 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -5,11 +5,11 @@ import threading from spotify.manager import SpotifySessionManager as PyspotifySessionManager from pykka.registry import ActorRegistry -from pykka.proxy import ActorProxy from mopidy import get_version, settings from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist +from mopidy.outputs.base import BaseOutput from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') @@ -28,9 +28,9 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): BaseThread.__init__(self) self.name = 'SpotifySMThread' - # TODO-PYKKA Get reference to output without hardcoding GStreamerOutput - output_refs = ActorRegistry.get_by_class_name('GStreamerOutput') - self.output = ActorProxy(output_refs[0]) + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() self.connected = threading.Event() self.session = None diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 932eb3e3..b934070e 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -1,8 +1,8 @@ import re -from pykka.proxy import ActorProxy from pykka.registry import ActorRegistry +from mopidy.backends.base import Backend from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError, MpdUnknownCommand) from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers @@ -27,9 +27,9 @@ class MpdDispatcher(object): def __init__(self, session): self.session = session - # TODO-PYKKA: Get reference to backend in a more generic way - backend_refs = ActorRegistry.get_by_class_name('SpotifyBackend') - self.backend = ActorProxy(backend_refs[0]) + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self.backend = backend_refs[0].proxy() self.command_list = False self.command_list_ok = False diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index f9d171ca..2125aab3 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -1,16 +1,19 @@ from pykka.actor import ThreadingActor -from pykka.proxy import ActorProxy from pykka.registry import ActorRegistry from mopidy.mixers.base import BaseMixer +from mopidy.outputs.base import BaseOutput class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" def __init__(self): - # TODO-PYKKA Get reference to output without hardcoding GStreamerOutput - output_refs = ActorRegistry.get_by_class_name('GStreamerOutput') - self.output = ActorProxy(output_refs[0]) + self.output = None + + def post_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() def _get_volume(self): return self.output.get_volume().get() From 650f7773064a9d854c10528cc41c303cd19f35a9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 20:37:23 +0100 Subject: [PATCH 052/127] Reimplement pause on lost play token and setting of stored playlists --- mopidy/backends/spotify/session_manager.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index b79fc48f..ed4d665c 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -7,6 +7,7 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager from pykka.registry import ActorRegistry from mopidy import get_version, settings +from mopidy.backends.base import Backend from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist from mopidy.outputs.base import BaseOutput @@ -28,16 +29,25 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): BaseThread.__init__(self) self.name = 'SpotifySMThread' - output_refs = ActorRegistry.get_by_class(BaseOutput) - assert len(output_refs) == 1, 'Expected exactly one running output.' - self.output = output_refs[0].proxy() + self.output = None + self.backend = None self.connected = threading.Event() self.session = None def run_inside_try(self): + self.setup() self.connect() + def setup(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self.backend = backend_refs[0].proxy() + def logged_in(self, session, error): """Callback used by pyspotify""" logger.info(u'Connected to Spotify') @@ -94,7 +104,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def play_token_lost(self, session): """Callback used by pyspotify""" logger.debug(u'Play token lost') - # TODO-PYKKA: Send 'stop_playback' to backend + self.backend.playback.pause() def log_message(self, session, data): """Callback used by pyspotify""" @@ -113,7 +123,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): playlists.append( SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) playlists = filter(None, playlists) - # TODO-PYKKA: Set stored_playlists on backend + self.backend.stored_playlists = playlists logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) def search(self, query, connection): From 8c163104d116bbd479a5cf14d83c1c439479dd9a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 21:37:47 +0100 Subject: [PATCH 053/127] Reimplement sending of started/stopped_playing events from the backend to interested frontends --- mopidy/backends/base/playback.py | 22 ++++++++++++++-------- mopidy/frontends/lastfm.py | 8 ++++++++ mopidy/frontends/mpd/__init__.py | 3 +++ 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index e4ffa075..80133c33 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -2,6 +2,10 @@ import logging import random import time +from pykka.registry import ActorRegistry + +from mopidy.frontends.base import BaseFrontend + logger = logging.getLogger('mopidy.backends.base') class PlaybackController(object): @@ -463,10 +467,11 @@ class PlaybackController(object): For internal use only. Should be called by the backend directly after a track has started playing. """ - return # TODO-PYKKA Send started_playing event to interested parties - if self.current_track is not None: - self.backend.core_queue.put({ - 'to': 'frontend', + if self.current_track is None: + return + frontend_refs = ActorRegistry.get_by_class(BaseFrontend) + for frontend_ref in frontend_refs: + frontend_ref.send_one_way({ 'command': 'started_playing', 'track': self.current_track, }) @@ -479,10 +484,11 @@ class PlaybackController(object): is stopped playing, e.g. at the next, previous, and stop actions and at end-of-track. """ - return # TODO-PYKKA Send stopped_playing event to interested parties - if self.current_track is not None: - self.backend.core_queue.put({ - 'to': 'frontend', + if self.current_track is None: + return + frontend_refs = ActorRegistry.get_by_class(BaseFrontend) + for frontend_ref in frontend_refs: + frontend_ref.send_one_way({ 'command': 'stopped_playing', 'track': self.current_track, 'stop_position': self.time_position, diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 4a703c22..d723052e 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -58,6 +58,14 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): logger.error(u'Error during Last.fm setup: %s', e) self.stop() + def react(self, message): + if message.get('command') == 'started_playing': + self.started_playing(message['track']) + elif message.get('command') == 'stopped_playing': + self.stopped_playing(message['track'], message['stop_position']) + else: + pass # Ignore any other messages + def started_playing(self, track): artists = ', '.join([a.name for a in track.artists]) duration = track.length and track.length // 1000 or 0 diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 96500d9b..c7f469dd 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -27,6 +27,9 @@ class MpdFrontend(ThreadingActor, BaseFrontend): self._thread = MpdThread() self._thread.start() + def react(self, message): + pass # Ignore any messages + class MpdThread(BaseThread): def __init__(self): From 9b77d34e24ea8619cf159d4c0109f7fbed5567b0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 21:41:32 +0100 Subject: [PATCH 054/127] Move AlsaMixer initialization to post_start() --- mopidy/mixers/alsa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index f4f40ca1..f19819e8 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -23,7 +23,9 @@ class AlsaMixer(ThreadingActor, BaseMixer): """ def __init__(self): - # PYKKA-TODO: Do mixer detection after actor starts? + self._mixer = None + + def post_start(self): self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None From 985213ffa2bd463bb185e3407fe08b868b97244f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 9 Mar 2011 21:46:18 +0100 Subject: [PATCH 055/127] Remove unused import --- mopidy/backends/local/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index c5b42b74..db2eab37 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -12,7 +12,6 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album -from mopidy.utils.process import pickle_connection from .translator import parse_m3u, parse_mpd_tag_cache From 46c6ee68df97629a7d411aabefe560ee0eaca0ef Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Mar 2011 23:19:44 +0100 Subject: [PATCH 056/127] Fix ~200 backend tests --- docs/development/contributing.rst | 4 ++-- mopidy/backends/local/__init__.py | 9 +++++++++ mopidy/outputs/dummy.py | 4 +++- requirements/tests.txt | 1 + tests/backends/base/current_playlist.py | 14 ++++---------- tests/backends/base/library.py | 6 +----- tests/backends/base/playback.py | 16 ++++++---------- tests/backends/base/stored_playlists.py | 5 +---- tests/backends/local/stored_playlists_test.py | 3 +-- 9 files changed, 28 insertions(+), 34 deletions(-) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index fbc7baee..9ea3533f 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -74,11 +74,11 @@ Running tests To run tests, you need a couple of dependencies. They can be installed through Debian/Ubuntu package management:: - sudo aptitude install python-coverage python-nose + sudo aptitude install python-coverage python-mock python-nose Or, they can be installed using ``pip``:: - sudo pip install -r requirements-tests.txt + sudo pip install -r requirements/tests.txt Then, to run all tests, go to the project directory and run:: diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index db2eab37..80762c89 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -5,6 +5,7 @@ import os import shutil from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, @@ -12,6 +13,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController, BasePlaybackProvider, StoredPlaylistsController, BaseStoredPlaylistsProvider) from mopidy.models import Playlist, Track, Album +from mopidy.outputs.base import BaseOutput from .translator import parse_m3u, parse_mpd_tag_cache @@ -49,6 +51,13 @@ class LocalBackend(ThreadingActor, Backend): self.uri_handlers = [u'file://'] + self.output = None + + def post_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + class LocalPlaybackController(PlaybackController): def __init__(self, *args, **kwargs): diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index 738575f3..f09965f7 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -1,6 +1,8 @@ +from pykka.actor import ThreadingActor + from mopidy.outputs.base import BaseOutput -class DummyOutput(BaseOutput): +class DummyOutput(ThreadingActor, BaseOutput): """ Audio output used for testing. """ diff --git a/requirements/tests.txt b/requirements/tests.txt index 33f49451..fb54268f 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,2 +1,3 @@ coverage +mock nose diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index c2a9df6f..ee5e1111 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,9 +1,9 @@ +import mock import multiprocessing import random -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track -from mopidy.outputs.dummy import DummyOutput +from mopidy.outputs.base import BaseOutput from tests.backends.base import populate_playlist @@ -11,19 +11,13 @@ class CurrentPlaylistControllerTest(object): tracks = [] def setUp(self): - self.core_queue = multiprocessing.Queue() - self.output = DummyOutput(self.core_queue) - self.backend = self.backend_class( - self.core_queue, self.output, DummyMixer) + self.backend = self.backend_class() + self.backend.output = mock.Mock(spec=BaseOutput) self.controller = self.backend.current_playlist self.playback = self.backend.playback assert len(self.tracks) == 3, 'Need three tracks to run tests.' - def tearDown(self): - self.backend.destroy() - self.output.destroy() - def test_add(self): for track in self.tracks: cp_track = self.controller.add(track) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 71f62147..bff26c4c 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -1,4 +1,3 @@ -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track, Album, Artist from tests import SkipTest, data_folder @@ -15,12 +14,9 @@ class LibraryControllerTest(object): Track()] def setUp(self): - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.library = self.backend.library - def tearDown(self): - self.backend.destroy() - def test_refresh(self): self.library.refresh() diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 77eb012d..aa200047 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -1,10 +1,10 @@ +import mock import multiprocessing import random import time -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track -from mopidy.outputs.dummy import DummyOutput +from mopidy.outputs.base import BaseOutput from tests import SkipTest from tests.backends.base import populate_playlist @@ -15,10 +15,8 @@ class PlaybackControllerTest(object): tracks = [] def setUp(self): - self.core_queue = multiprocessing.Queue() - self.output = DummyOutput(self.core_queue) - self.backend = self.backend_class( - self.core_queue, self.output, DummyMixer) + self.backend = self.backend_class() + self.backend.output = mock.Mock(spec=BaseOutput) self.playback = self.backend.playback self.current_playlist = self.backend.current_playlist @@ -27,10 +25,6 @@ class PlaybackControllerTest(object): assert self.tracks[0].length >= 2000, \ 'First song needs to be at least 2000 miliseconds' - def tearDown(self): - self.backend.destroy() - self.output.destroy() - def test_initial_state_is_stopped(self): self.assertEqual(self.playback.state, self.playback.STOPPED) @@ -733,10 +727,12 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.stop(), None) def test_time_position_when_stopped(self): + self.backend.output.get_position = mock.Mock(return_value=0) self.assertEqual(self.playback.time_position, 0) @populate_playlist def test_time_position_when_stopped_with_playlist(self): + self.backend.output.get_position = mock.Mock(return_value=0) self.assertEqual(self.playback.time_position, 0) @SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 0ac0b167..12e48256 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -3,7 +3,6 @@ import shutil import tempfile from mopidy import settings -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist from tests import SkipTest, data_folder @@ -14,12 +13,10 @@ class StoredPlaylistsControllerTest(object): settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') settings.LOCAL_MUSIC_PATH = data_folder('') - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.stored = self.backend.stored_playlists def tearDown(self): - self.backend.destroy() - if os.path.exists(settings.LOCAL_PLAYLIST_PATH): shutil.rmtree(settings.LOCAL_PLAYLIST_PATH) diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index a7d9043f..5bc16d1c 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -70,8 +70,7 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, self.stored.save(playlist) - self.backend.destroy() - self.backend = self.backend_class(mixer_class=DummyMixer) + self.backend = self.backend_class() self.stored = self.backend.stored_playlists self.assert_(self.stored.playlists) From 0c6ad8e8f08f1e55a2b6a97c875fdb0e0cb86643 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Mar 2011 23:23:06 +0100 Subject: [PATCH 057/127] Fix output tests --- tests/outputs/gstreamer_test.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index b4cb5ef9..801f476e 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -18,12 +18,10 @@ class GStreamerOutputTest(unittest.TestCase): def setUp(self): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) self.song_uri = path_to_uri(data_folder('song1.wav')) - self.core_queue = multiprocessing.Queue() - self.output = GStreamerOutput(self.core_queue) - self.output.start() + self.output = GStreamerOutput() + self.output.post_start() def tearDown(self): - self.output.destroy() settings.runtime.clear() def test_play_uri_existing_file(self): From 122e13be855d2077c9ddb0ad71806ac42b22cf97 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 10 Mar 2011 23:23:46 +0100 Subject: [PATCH 058/127] Fix mixer tests --- tests/mixers/base_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mixers/base_test.py b/tests/mixers/base_test.py index 395d8f7b..54cd8773 100644 --- a/tests/mixers/base_test.py +++ b/tests/mixers/base_test.py @@ -11,7 +11,7 @@ class BaseMixerTest(object): assert self.mixer_class is not None, \ "mixer_class must be set in subclass" # pylint: disable = E1102 - self.mixer = self.mixer_class(None) + self.mixer = self.mixer_class() # pylint: enable = E1102 def test_initial_volume(self): From 49d62aecab3d860dfad048b3683d3cbe5795681f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 15:55:25 +0100 Subject: [PATCH 059/127] Do not give MpdDispatcher an MpdSession object yet, as it is not needed to get current tests passing --- mopidy/frontends/mpd/dispatcher.py | 4 +--- mopidy/frontends/mpd/session.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index b934070e..c1249028 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -24,9 +24,7 @@ class MpdDispatcher(object): # XXX Consider merging MpdDispatcher into MpdSession - def __init__(self, session): - self.session = session - + def __init__(self): backend_refs = ActorRegistry.get_by_class(Backend) assert len(backend_refs) == 1, 'Expected exactly one running backend.' self.backend = backend_refs[0].proxy() diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index eb8c4f11..78e6f624 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -23,7 +23,7 @@ class MpdSession(asynchat.async_chat): self.input_buffer = [] self.authenticated = False self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) - self.dispatcher = MpdDispatcher(self) + self.dispatcher = MpdDispatcher() def start(self): """Start a new client session.""" From a4a015af1d6ed099cf68efe85235a57d452442e5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 16:05:22 +0100 Subject: [PATCH 060/127] Update MPD/audio_output --- mopidy/frontends/mpd/protocol/audio_output.py | 2 +- tests/frontends/mpd/audio_output_test.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/audio_output.py b/mopidy/frontends/mpd/protocol/audio_output.py index d25fc118..98c1d645 100644 --- a/mopidy/frontends/mpd/protocol/audio_output.py +++ b/mopidy/frontends/mpd/protocol/audio_output.py @@ -34,6 +34,6 @@ def outputs(frontend): """ return [ ('outputid', 0), - ('outputname', frontend.backend.__class__.__name__), + ('outputname', None), ('outputenabled', 1), ] diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index b81e727e..d4ddbc92 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -2,12 +2,14 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') @@ -20,6 +22,6 @@ class AudioOutputHandlerTest(unittest.TestCase): def test_outputs(self): result = self.h.handle_request(u'outputs') self.assert_(u'outputid: 0' in result) - self.assert_(u'outputname: DummyBackend' in result) + self.assert_(u'outputname: None' in result) self.assert_(u'outputenabled: 1' in result) self.assert_(u'OK' in result) From 88780c717b4d1af05c03c0ca5c4a4ce59cde6c81 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 16:05:36 +0100 Subject: [PATCH 061/127] Update MPD/connection --- tests/frontends/mpd/connection_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 44ce78ca..4d2a309d 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -3,14 +3,14 @@ import unittest from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() def tearDown(self): + self.b.stop() settings.runtime.clear() def test_close(self): From c67eb4b973f3431e8716cadd1f05cc597e0b654e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 16:41:48 +0100 Subject: [PATCH 062/127] Make backend providers traversable so they can be set up for tests --- mopidy/backends/base/library.py | 2 ++ mopidy/backends/base/playback.py | 2 ++ mopidy/backends/base/stored_playlists.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index 9ca103c0..a30ed412 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -84,6 +84,8 @@ class BaseLibraryProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 80133c33..ce63abf7 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -501,6 +501,8 @@ class BasePlaybackProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index 6578c046..aca78a8c 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -11,6 +11,8 @@ class StoredPlaylistsController(object): :type provider: instance of :class:`BaseStoredPlaylistsProvider` """ + pykka_traversable = True + def __init__(self, backend, provider): self.backend = backend self.provider = provider @@ -125,6 +127,8 @@ class BaseStoredPlaylistsProvider(object): :type backend: :class:`mopidy.backends.base.Backend` """ + pykka_traversable = True + def __init__(self, backend): self.backend = backend self._playlists = [] From 462f9d3afb52757c51eead2ce4b14c530641adf3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 17:08:05 +0100 Subject: [PATCH 063/127] Make dummy library available through actor proxies --- mopidy/backends/dummy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 82051188..90c87dac 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -38,13 +38,13 @@ class DummyBackend(ThreadingActor, Backend): class DummyLibraryProvider(BaseLibraryProvider): def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) - self._library = [] + self.dummy_library = [] def find_exact(self, **query): return Playlist() def lookup(self, uri): - matches = filter(lambda t: uri == t.uri, self._library) + matches = filter(lambda t: uri == t.uri, self.dummy_library) if matches: return matches[0] From 0d57a74cb3bdcc552d3bd18bb00d01b18d1ddf74 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 17:46:22 +0100 Subject: [PATCH 064/127] Update MPD/current_playlist --- .../mpd/protocol/current_playlist.py | 82 ++++--- tests/frontends/mpd/current_playlist_test.py | 206 +++++++++--------- 2 files changed, 153 insertions(+), 135 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 13635a33..8ef5e026 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -19,9 +19,9 @@ def add(frontend, uri): """ if not uri: return - for handler_prefix in frontend.backend.uri_handlers: + for handler_prefix in frontend.backend.uri_handlers.get(): if uri.startswith(handler_prefix): - track = frontend.backend.library.lookup(uri) + track = frontend.backend.library.lookup(uri).get() if track is not None: frontend.backend.current_playlist.add(track) return @@ -51,13 +51,14 @@ def addid(frontend, uri, songpos=None): raise MpdNoExistError(u'No such song', command=u'addid') if songpos is not None: songpos = int(songpos) - track = frontend.backend.library.lookup(uri) + track = frontend.backend.library.lookup(uri).get() if track is None: raise MpdNoExistError(u'No such song', command=u'addid') - if songpos and songpos > len(frontend.backend.current_playlist.tracks): + if songpos and songpos > len( + frontend.backend.current_playlist.tracks.get()): raise MpdArgError(u'Bad song index', command=u'addid') cp_track = frontend.backend.current_playlist.add(track, - at_position=songpos) + at_position=songpos).get() return ('Id', cp_track[0]) @handle_pattern(r'^delete "(?P\d+):(?P\d+)*"$') @@ -73,8 +74,8 @@ def delete_range(frontend, start, end=None): if end is not None: end = int(end) else: - end = len(frontend.backend.current_playlist.tracks) - cp_tracks = frontend.backend.current_playlist.cp_tracks[start:end] + end = len(frontend.backend.current_playlist.tracks.get()) + cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end] if not cp_tracks: raise MpdArgError(u'Bad song index', command=u'delete') for (cpid, _) in cp_tracks: @@ -85,7 +86,7 @@ def delete_songpos(frontend, songpos): """See :meth:`delete_range`""" try: songpos = int(songpos) - (cpid, _) = frontend.backend.current_playlist.cp_tracks[songpos] + (cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos] frontend.backend.current_playlist.remove(cpid=cpid) except IndexError: raise MpdArgError(u'Bad song index', command=u'delete') @@ -101,9 +102,9 @@ def deleteid(frontend, cpid): """ try: cpid = int(cpid) - if frontend.backend.playback.current_cpid == cpid: + if frontend.backend.playback.current_cpid.get() == cpid: frontend.backend.playback.next() - return frontend.backend.current_playlist.remove(cpid=cpid) + return frontend.backend.current_playlist.remove(cpid=cpid).get() except LookupError: raise MpdNoExistError(u'No such song', command=u'deleteid') @@ -129,7 +130,7 @@ def move_range(frontend, start, to, end=None): ``TO`` in the playlist. """ if end is None: - end = len(frontend.backend.current_playlist.tracks) + end = len(frontend.backend.current_playlist.tracks.get()) start = int(start) end = int(end) to = int(to) @@ -155,8 +156,9 @@ def moveid(frontend, cpid, to): """ cpid = int(cpid) to = int(to) - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - position = frontend.backend.current_playlist.cp_tracks.index(cp_track) + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + position = frontend.backend.current_playlist.cp_tracks.get().index( + cp_track) frontend.backend.current_playlist.move(position, position + 1, to) @handle_pattern(r'^playlist$') @@ -190,9 +192,9 @@ def playlistfind(frontend, tag, needle): """ if tag == 'filename': try: - cp_track = frontend.backend.current_playlist.get(uri=needle) + cp_track = frontend.backend.current_playlist.get(uri=needle).get() (cpid, track) = cp_track - position = frontend.backend.current_playlist.cp_tracks.index( + position = frontend.backend.current_playlist.cp_tracks.get().index( cp_track) return track.mpd_format(cpid=cpid, position=position) except LookupError: @@ -212,16 +214,17 @@ def playlistid(frontend, cpid=None): if cpid is not None: try: cpid = int(cpid) - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - position = frontend.backend.current_playlist.cp_tracks.index( + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + position = frontend.backend.current_playlist.cp_tracks.get().index( cp_track) return cp_track[1].mpd_format(position=position, cpid=cpid) except LookupError: raise MpdNoExistError(u'No such song', command=u'playlistid') else: - cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] - return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, - cpids=cpids) + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), cpids=cpids) @handle_pattern(r'^playlistinfo$') @handle_pattern(r'^playlistinfo "(?P-?\d+)"$') @@ -251,21 +254,26 @@ def playlistinfo(frontend, songpos=None, end = songpos + 1 if start == -1: end = None - cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] - return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), start, end, cpids=cpids) else: if start is None: start = 0 start = int(start) - if not (0 <= start <= len(frontend.backend.current_playlist.tracks)): + if not (0 <= start <= len( + frontend.backend.current_playlist.tracks.get())): raise MpdArgError(u'Bad song index', command=u'playlistinfo') if end is not None: end = int(end) - if end > len(frontend.backend.current_playlist.tracks): + if end > len(frontend.backend.current_playlist.tracks.get()): end = None - cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] - return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), start, end, cpids=cpids) @handle_pattern(r'^playlistsearch "(?P[^"]+)" "(?P[^"]+)"$') @@ -305,9 +313,10 @@ def plchanges(frontend, version): """ # XXX Naive implementation that returns all tracks as changed if int(version) < frontend.backend.current_playlist.version: - cpids = [ct[0] for ct in frontend.backend.current_playlist.cp_tracks] - return tracks_to_mpd_format(frontend.backend.current_playlist.tracks, - cpids=cpids) + cpids = [ct[0] for ct in + frontend.backend.current_playlist.cp_tracks.get()] + return tracks_to_mpd_format( + frontend.backend.current_playlist.tracks.get(), cpids=cpids) @handle_pattern(r'^plchangesposid "(?P\d+)"$') def plchangesposid(frontend, version): @@ -324,10 +333,10 @@ def plchangesposid(frontend, version): ``playlistlength`` returned by status command. """ # XXX Naive implementation that returns all tracks as changed - if int(version) != frontend.backend.current_playlist.version: + if int(version) != frontend.backend.current_playlist.version.get(): result = [] for (position, (cpid, _)) in enumerate( - frontend.backend.current_playlist.cp_tracks): + frontend.backend.current_playlist.cp_tracks.get()): result.append((u'cpos', position)) result.append((u'Id', cpid)) return result @@ -360,7 +369,7 @@ def swap(frontend, songpos1, songpos2): """ songpos1 = int(songpos1) songpos2 = int(songpos2) - tracks = frontend.backend.current_playlist.tracks + tracks = frontend.backend.current_playlist.tracks.get() song1 = tracks[songpos1] song2 = tracks[songpos2] del tracks[songpos1] @@ -381,8 +390,9 @@ def swapid(frontend, cpid1, cpid2): """ cpid1 = int(cpid1) cpid2 = int(cpid2) - cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1) - cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2) - position1 = frontend.backend.current_playlist.cp_tracks.index(cp_track1) - position2 = frontend.backend.current_playlist.cp_tracks.index(cp_track2) + cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get() + cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get() + cp_tracks = frontend.backend.current_playlist.cp_tracks.get() + position1 = cp_tracks.index(cp_track1) + position2 = cp_tracks.index(cp_track2) swap(frontend, position1, position2) diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index a4179637..8e189dee 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -2,31 +2,28 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_add(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'add "dummy://foo"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[5], needle) self.assertEqual(len(result), 1) - self.assert_(u'OK' in result) - - def test_add_with_uri_not_found_in_library_should_not_call_lookup(self): - self.b.library.lookup = lambda uri: self.fail("Shouldn't run") - result = self.h.handle_request(u'add "foo"') - self.assertEqual(result[0], - u'ACK [50@0] {add} directory or file not found') + self.assertEqual(result[0], u'OK') + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) def test_add_with_uri_not_found_in_library_should_ack(self): result = self.h.handle_request(u'add "dummy://foo"') @@ -40,41 +37,43 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[5], needle) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[5][0] + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[5], needle) + self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0] in result) self.assert_(u'OK' in result) - def test_addid_with_empty_uri_does_not_lookup_and_acks(self): - self.b.library.lookup = lambda uri: self.fail("Shouldn't run") + def test_addid_with_empty_uri_acks(self): result = self.h.handle_request(u'addid ""') self.assertEqual(result[0], u'ACK [50@0] {addid} No such song') def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo" "3"') - self.assertEqual(len(self.b.current_playlist.tracks), 6) - self.assertEqual(self.b.current_playlist.tracks[3], needle) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[3][0] + self.assertEqual(len(self.b.current_playlist.tracks.get()), 6) + self.assertEqual(self.b.current_playlist.tracks.get()[3], needle) + self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0] in result) self.assert_(u'OK' in result) def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') - self.b.library.provider._library = [Track(), Track(), needle, Track()] + self.b.library.provider.dummy_library = [ + Track(), Track(), needle, Track()] self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'addid "dummy://foo" "6"') self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index') @@ -85,65 +84,65 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_clear(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'clear') - self.assertEqual(len(self.b.current_playlist.tracks), 0) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 0) + self.assertEqual(self.b.playback.current_track.get(), None) self.assert_(u'OK' in result) def test_delete_songpos(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "%d"' % - self.b.current_playlist.cp_tracks[2][0]) - self.assertEqual(len(self.b.current_playlist.tracks), 4) + self.b.current_playlist.cp_tracks.get()[2][0]) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 4) self.assert_(u'OK' in result) def test_delete_songpos_out_of_bounds(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "5"') - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "1:"') - self.assertEqual(len(self.b.current_playlist.tracks), 1) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assert_(u'OK' in result) def test_delete_closed_range(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "1:3"') - self.assertEqual(len(self.b.current_playlist.tracks), 3) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 3) self.assert_(u'OK' in result) def test_delete_range_out_of_bounds(self): self.b.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) result = self.h.handle_request(u'delete "5:7"') - self.assertEqual(len(self.b.current_playlist.tracks), 5) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 5) self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') def test_deleteid(self): self.b.current_playlist.append([Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) result = self.h.handle_request(u'deleteid "1"') - self.assertEqual(len(self.b.current_playlist.tracks), 1) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 1) self.assert_(u'OK' in result) def test_deleteid_does_not_exist(self): self.b.current_playlist.append([Track(), Track()]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) result = self.h.handle_request(u'deleteid "12345"') - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song') def test_move_songpos(self): @@ -152,12 +151,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "1" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'b') + self.assertEqual(tracks[1].name, 'a') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_move_open_range(self): @@ -166,12 +166,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "2:" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'f') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'b') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'c') + self.assertEqual(tracks[1].name, 'd') + self.assertEqual(tracks[2].name, 'e') + self.assertEqual(tracks[3].name, 'f') + self.assertEqual(tracks[4].name, 'a') + self.assertEqual(tracks[5].name, 'b') self.assert_(u'OK' in result) def test_move_closed_range(self): @@ -180,12 +181,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'move "1:3" "0"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'b') + self.assertEqual(tracks[1].name, 'c') + self.assertEqual(tracks[2].name, 'a') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_moveid(self): @@ -194,12 +196,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'moveid "4" "2"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'b') + self.assertEqual(tracks[2].name, 'e') + self.assertEqual(tracks[3].name, 'c') + self.assertEqual(tracks[4].name, 'd') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_playlist_returns_same_as_playlistinfo(self): @@ -361,14 +364,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): def test_plchangesposid(self): self.b.current_playlist.append([Track(), Track(), Track()]) result = self.h.handle_request(u'plchangesposid "0"') + cp_tracks = self.b.current_playlist.cp_tracks.get() self.assert_(u'cpos: 0' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[0][0] + self.assert_(u'Id: %d' % cp_tracks[0][0] in result) self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[1][0] + self.assert_(u'Id: %d' % cp_tracks[1][0] in result) self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks[2][0] + self.assert_(u'Id: %d' % cp_tracks[2][0] in result) self.assert_(u'OK' in result) @@ -377,9 +381,9 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle') - self.assert_(version < self.b.current_playlist.version) + self.assert_(version < self.b.current_playlist.version.get()) self.assert_(u'OK' in result) def test_shuffle_with_open_range(self): @@ -387,13 +391,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle "4:"') - self.assert_(version < self.b.current_playlist.version) - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') + self.assert_(version < self.b.current_playlist.version.get()) + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'b') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') self.assert_(u'OK' in result) def test_shuffle_with_closed_range(self): @@ -401,13 +406,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - version = self.b.current_playlist.version + version = self.b.current_playlist.version.get() result = self.h.handle_request(u'shuffle "1:3"') - self.assert_(version < self.b.current_playlist.version) - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + self.assert_(version < self.b.current_playlist.version.get()) + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'e') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_swap(self): @@ -416,12 +422,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'swap "1" "4"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'e') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'b') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) def test_swapid(self): @@ -430,10 +437,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) result = self.h.handle_request(u'swapid "1" "4"') - self.assertEqual(self.b.current_playlist.tracks[0].name, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].name, 'e') - self.assertEqual(self.b.current_playlist.tracks[2].name, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].name, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].name, 'b') - self.assertEqual(self.b.current_playlist.tracks[5].name, 'f') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(tracks[0].name, 'a') + self.assertEqual(tracks[1].name, 'e') + self.assertEqual(tracks[2].name, 'c') + self.assertEqual(tracks[3].name, 'd') + self.assertEqual(tracks[4].name, 'b') + self.assertEqual(tracks[5].name, 'f') self.assert_(u'OK' in result) From 4e82730732e466ca00b1f7a1201cf227ab708826 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 17:57:06 +0100 Subject: [PATCH 065/127] Update MPD/dispatcher --- tests/frontends/mpd/dispatcher_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 2a2ee4db..8c17c9ee 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -4,12 +4,14 @@ from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern -from mopidy.mixers.dummy import DummyMixer class MpdDispatcherTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_register_same_pattern_twice_fails(self): func = lambda: None From 517a2a935619180ebf534310de14ac5c1eb76c2b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 19 Mar 2011 18:05:09 +0100 Subject: [PATCH 066/127] Update MPD/music_db --- mopidy/frontends/mpd/protocol/music_db.py | 10 +++---- .../mpd/protocol/stored_playlists.py | 2 +- tests/frontends/mpd/music_db_test.py | 29 +++++++++++++------ 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index d50388f5..a6836533 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -68,7 +68,7 @@ def find(frontend, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return frontend.backend.library.find_exact(**query).mpd_format() + return frontend.backend.library.find_exact(**query).get().mpd_format() @handle_pattern(r'^findadd ' r'(?P("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? ' @@ -215,7 +215,7 @@ def _list_build_query(field, mpd_query): def _list_artist(frontend, query): artists = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: for artist in track.artists: artists.add((u'Artist', artist.name)) @@ -223,7 +223,7 @@ def _list_artist(frontend, query): def _list_album(frontend, query): albums = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: if track.album is not None: albums.add((u'Album', track.album.name)) @@ -231,7 +231,7 @@ def _list_album(frontend, query): def _list_date(frontend, query): dates = set() - playlist = frontend.backend.library.find_exact(**query) + playlist = frontend.backend.library.find_exact(**query).get() for track in playlist.tracks: if track.date is not None: dates.add((u'Date', track.date.strftime('%Y-%m-%d'))) @@ -324,7 +324,7 @@ def search(frontend, mpd_query): - also uses the search type "date". """ query = _build_query(mpd_query) - return frontend.backend.library.search(**query).mpd_format() + return frontend.backend.library.search(**query).get().mpd_format() @handle_pattern(r'^update( "(?P[^"]+)")*$') def update(frontend, uri=None, rescan_unmodified_files=False): diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index c34b1676..f0a63544 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -66,7 +66,7 @@ def listplaylists(frontend): Last-Modified: 2010-02-06T02:11:08Z """ result = [] - for playlist in frontend.backend.stored_playlists.playlists: + for playlist in frontend.backend.stored_playlists.playlists.get(): result.append((u'playlist', playlist.name)) last_modified = (playlist.last_modified or dt.datetime.now()).isoformat() diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 486eac4f..595ef02c 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -2,12 +2,14 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher -from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_count(self): result = self.h.handle_request(u'count "tag" "needle"') @@ -65,8 +67,11 @@ class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseFindTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_find_album(self): result = self.h.handle_request(u'find "album" "what"') @@ -117,8 +122,11 @@ class MusicDatabaseFindTest(unittest.TestCase): class MusicDatabaseListTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_list_foo_returns_ack(self): result = self.h.handle_request(u'list "foo"') @@ -308,8 +316,11 @@ class MusicDatabaseListTest(unittest.TestCase): class MusicDatabaseSearchTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() def test_search_album(self): result = self.h.handle_request(u'search "album" "analbum"') From 2700698ec7dbe08d5d57f7c4caf7fb8729fa05a5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:00:20 +0100 Subject: [PATCH 067/127] Require Pykka 0.11 --- docs/installation/index.rst | 2 +- requirements/core.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index ed6107d8..3ef5af99 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- `Pykka `_ >= 0.10 +- `Pykka `_ >= 0.11 - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. diff --git a/requirements/core.txt b/requirements/core.txt index 1b5cfc4b..f0f0bd39 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -1 +1 @@ -Pykka >= 0.10 +Pykka >= 0.11 From 7206d68648c91790ac4fa14a3074c77c97c01636 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:02:25 +0100 Subject: [PATCH 068/127] Remove mixer from the Backend API as it is independent --- mopidy/backends/base/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 963d9eb4..038e2d7b 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -17,9 +17,6 @@ class Backend(object): # :class:`mopidy.backends.base.LibraryController`. library = None - #: The sound mixer. An instance of :class:`mopidy.mixers.BaseMixer`. - mixer = None - #: The playback controller. An instance of #: :class:`mopidy.backends.base.PlaybackController`. playback = None From dad2422a96df7d79a7a4aeac8aea224be4be94f4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:10:14 +0100 Subject: [PATCH 069/127] MpdDispatcher requires a mixer to be running --- mopidy/frontends/mpd/dispatcher.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index c1249028..f5c30b23 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -13,6 +13,7 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) # pylint: enable = W0611 +from mopidy.mixers.base import BaseMixer from mopidy.utils import flatten class MpdDispatcher(object): @@ -29,6 +30,10 @@ class MpdDispatcher(object): assert len(backend_refs) == 1, 'Expected exactly one running backend.' self.backend = backend_refs[0].proxy() + mixer_refs = ActorRegistry.get_by_class(BaseMixer) + assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' + self.mixer = mixer_refs[0].proxy() + self.command_list = False self.command_list_ok = False From 1048795f2bd34abbe3c99984a4fe98c04f075ee7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:14:59 +0100 Subject: [PATCH 070/127] Update working MPD tests to start mixer actor --- tests/frontends/mpd/audio_output_test.py | 3 +++ tests/frontends/mpd/connection_test.py | 5 ++++- tests/frontends/mpd/current_playlist_test.py | 3 +++ tests/frontends/mpd/dispatcher_test.py | 3 +++ tests/frontends/mpd/music_db_test.py | 9 +++++++++ 5 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py index d4ddbc92..afa99d26 100644 --- a/tests/frontends/mpd/audio_output_test.py +++ b/tests/frontends/mpd/audio_output_test.py @@ -2,14 +2,17 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher +from mopidy.mixers.dummy import DummyMixer class AudioOutputHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_enableoutput(self): result = self.h.handle_request(u'enableoutput "0"') diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 4d2a309d..cf161a5a 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -3,14 +3,17 @@ import unittest from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher +from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): - self.b.stop() + self.b.stop().get() + self.mixer.stop().get() settings.runtime.clear() def test_close(self): diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/current_playlist_test.py index 8e189dee..eb113ed7 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/current_playlist_test.py @@ -2,15 +2,18 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher +from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track class CurrentPlaylistHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_add(self): needle = Track(uri='dummy://foo') diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 8c17c9ee..77e0ddf0 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -4,14 +4,17 @@ from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern +from mopidy.mixers.dummy import DummyMixer class MpdDispatcherTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_register_same_pattern_twice_fails(self): func = lambda: None diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 595ef02c..fa5634be 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -2,14 +2,17 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher +from mopidy.mixers.dummy import DummyMixer class MusicDatabaseHandlerTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_count(self): result = self.h.handle_request(u'count "tag" "needle"') @@ -68,10 +71,12 @@ class MusicDatabaseHandlerTest(unittest.TestCase): class MusicDatabaseFindTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_find_album(self): result = self.h.handle_request(u'find "album" "what"') @@ -123,10 +128,12 @@ class MusicDatabaseFindTest(unittest.TestCase): class MusicDatabaseListTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_list_foo_returns_ack(self): result = self.h.handle_request(u'list "foo"') @@ -317,10 +324,12 @@ class MusicDatabaseListTest(unittest.TestCase): class MusicDatabaseSearchTest(unittest.TestCase): def setUp(self): self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() self.h = dispatcher.MpdDispatcher() def tearDown(self): self.b.stop().get() + self.mixer.stop().get() def test_search_album(self): result = self.h.handle_request(u'search "album" "analbum"') From cbe727d2bd202a4d000d2acb518a2b9b8d399f9b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:19:30 +0100 Subject: [PATCH 071/127] Update MPD/playback --- mopidy/frontends/mpd/protocol/playback.py | 43 +++--- tests/frontends/mpd/playback_test.py | 175 ++++++++++++---------- 2 files changed, 117 insertions(+), 101 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/playback.py b/mopidy/frontends/mpd/protocol/playback.py index d009969d..65282f42 100644 --- a/mopidy/frontends/mpd/protocol/playback.py +++ b/mopidy/frontends/mpd/protocol/playback.py @@ -1,3 +1,4 @@ +from mopidy.backends.base import PlaybackController from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError, MpdNotImplemented) @@ -86,7 +87,7 @@ def next_(frontend): order as the first time. """ - return frontend.backend.playback.next() + return frontend.backend.playback.next().get() @handle_pattern(r'^pause$') @handle_pattern(r'^pause "(?P[01])"$') @@ -103,11 +104,11 @@ def pause(frontend, state=None): - Calls ``pause`` without any arguments to toogle pause. """ if state is None: - if (frontend.backend.playback.state == - frontend.backend.playback.PLAYING): + if (frontend.backend.playback.state.get() == + PlaybackController.PLAYING): frontend.backend.playback.pause() - elif (frontend.backend.playback.state == - frontend.backend.playback.PAUSED): + elif (frontend.backend.playback.state.get() == + PlaybackController.PAUSED): frontend.backend.playback.resume() elif int(state): frontend.backend.playback.pause() @@ -120,7 +121,7 @@ def play(frontend): The original MPD server resumes from the paused state on ``play`` without arguments. """ - return frontend.backend.playback.play() + return frontend.backend.playback.play().get() @handle_pattern(r'^playid "(?P\d+)"$') @handle_pattern(r'^playid "(?P-1)"$') @@ -145,8 +146,8 @@ def playid(frontend, cpid): if cpid == -1: return _play_minus_one(frontend) try: - cp_track = frontend.backend.current_playlist.get(cpid=cpid) - return frontend.backend.playback.play(cp_track) + cp_track = frontend.backend.current_playlist.get(cpid=cpid).get() + return frontend.backend.playback.play(cp_track).get() except LookupError: raise MpdNoExistError(u'No such song', command=u'playid') @@ -177,22 +178,22 @@ def playpos(frontend, songpos): if songpos == -1: return _play_minus_one(frontend) try: - cp_track = frontend.backend.current_playlist.cp_tracks[songpos] - return frontend.backend.playback.play(cp_track) + cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos] + return frontend.backend.playback.play(cp_track).get() except IndexError: raise MpdArgError(u'Bad song index', command=u'play') def _play_minus_one(frontend): - if (frontend.backend.playback.state == frontend.backend.playback.PLAYING): + if (frontend.backend.playback.state.get() == PlaybackController.PLAYING): return # Nothing to do - elif (frontend.backend.playback.state == frontend.backend.playback.PAUSED): - return frontend.backend.playback.resume() - elif frontend.backend.playback.current_cp_track is not None: - cp_track = frontend.backend.playback.current_cp_track - return frontend.backend.playback.play(cp_track) - elif frontend.backend.current_playlist.cp_tracks: - cp_track = frontend.backend.current_playlist.cp_tracks[0] - return frontend.backend.playback.play(cp_track) + elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED): + return frontend.backend.playback.resume().get() + elif frontend.backend.playback.current_cp_track.get() is not None: + cp_track = frontend.backend.playback.current_cp_track.get() + return frontend.backend.playback.play(cp_track).get() + elif frontend.backend.current_playlist.cp_tracks.get(): + cp_track = frontend.backend.current_playlist.cp_tracks.get()[0] + return frontend.backend.playback.play(cp_track).get() else: return # Fail silently @@ -240,7 +241,7 @@ def previous(frontend): ``previous`` should do a seek to time position 0. """ - return frontend.backend.playback.previous() + return frontend.backend.playback.previous().get() @handle_pattern(r'^random (?P[01])$') @handle_pattern(r'^random "(?P[01])"$') @@ -351,7 +352,7 @@ def setvol(frontend, volume): volume = 0 if volume > 100: volume = 100 - frontend.backend.mixer.volume = volume + frontend.mixer.volume = volume @handle_pattern(r'^single (?P[01])$') @handle_pattern(r'^single "(?P[01])"$') diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/playback_test.py index 8f0560c7..8601aa9c 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/playback_test.py @@ -1,5 +1,6 @@ import unittest +from mopidy.backends.base import PlaybackController from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer @@ -7,29 +8,38 @@ from mopidy.models import Track from tests import SkipTest +PAUSED = PlaybackController.PAUSED +PLAYING = PlaybackController.PLAYING +STOPPED = PlaybackController.STOPPED + class PlaybackOptionsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_consume_off(self): result = self.h.handle_request(u'consume "0"') - self.assertFalse(self.b.playback.consume) + self.assertFalse(self.b.playback.consume.get()) 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.assertFalse(self.b.playback.consume.get()) 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.assertTrue(self.b.playback.consume.get()) 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.assertTrue(self.b.playback.consume.get()) self.assert_(u'OK' in result) def test_crossfade(self): @@ -38,97 +48,97 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): def test_random_off(self): result = self.h.handle_request(u'random "0"') - self.assertFalse(self.b.playback.random) + self.assertFalse(self.b.playback.random.get()) 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.assertFalse(self.b.playback.random.get()) 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.assertTrue(self.b.playback.random.get()) 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.assertTrue(self.b.playback.random.get()) 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.assertFalse(self.b.playback.repeat.get()) 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.assertFalse(self.b.playback.repeat.get()) 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.assertTrue(self.b.playback.repeat.get()) 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.assertTrue(self.b.playback.repeat.get()) 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) - self.assertEqual(0, self.b.mixer.volume) + self.assertEqual(0, self.mixer.volume.get()) def test_setvol_min(self): result = self.h.handle_request(u'setvol "0"') self.assert_(u'OK' in result) - self.assertEqual(0, self.b.mixer.volume) + self.assertEqual(0, self.mixer.volume.get()) def test_setvol_middle(self): result = self.h.handle_request(u'setvol "50"') self.assert_(u'OK' in result) - self.assertEqual(50, self.b.mixer.volume) + self.assertEqual(50, self.mixer.volume.get()) def test_setvol_max(self): result = self.h.handle_request(u'setvol "100"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.mixer.volume) + self.assertEqual(100, self.mixer.volume.get()) def test_setvol_above_max(self): result = self.h.handle_request(u'setvol "110"') self.assert_(u'OK' in result) - self.assertEqual(100, self.b.mixer.volume) + self.assertEqual(100, self.mixer.volume.get()) def test_setvol_plus_is_ignored(self): result = self.h.handle_request(u'setvol "+10"') self.assert_(u'OK' in result) - self.assertEqual(10, self.b.mixer.volume) + self.assertEqual(10, self.mixer.volume.get()) def test_setvol_without_quotes(self): result = self.h.handle_request(u'setvol 50') self.assert_(u'OK' in result) - self.assertEqual(50, self.b.mixer.volume) + self.assertEqual(50, self.mixer.volume.get()) def test_single_off(self): result = self.h.handle_request(u'single "0"') - self.assertFalse(self.b.playback.single) + self.assertFalse(self.b.playback.single.get()) 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.assertFalse(self.b.playback.single.get()) 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.assertTrue(self.b.playback.single.get()) 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.assertTrue(self.b.playback.single.get()) self.assert_(u'OK' in result) def test_replay_gain_mode_off(self): @@ -176,8 +186,13 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): class PlaybackControlHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_next(self): result = self.h.handle_request(u'next') @@ -189,155 +204,155 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.h.handle_request(u'pause "1"') result = self.h.handle_request(u'pause "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_pause_on(self): self.b.current_playlist.append([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) + self.assertEqual(PAUSED, self.b.playback.state.get()) def test_pause_toggle(self): self.b.current_playlist.append([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) + self.assertEqual(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'pause') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PAUSED, self.b.playback.state) + self.assertEqual(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'pause') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_without_pos(self): self.b.current_playlist.append([Track()]) - self.b.playback.state = self.b.playback.PAUSED + self.b.playback.state = PAUSED result = self.h.handle_request(u'play') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos(self): self.b.current_playlist.append([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) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos_without_quotes(self): self.b.current_playlist.append([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) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_play_with_pos_out_of_bounds(self): self.b.current_playlist.append([]) result = self.h.handle_request(u'play "0"') self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) + self.assertEqual(STOPPED, self.b.playback.state.get()) def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'a') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'a') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.playback.play() self.b.playback.next() self.b.playback.stop() - self.assertNotEqual(self.b.playback.current_track, None) + self.assertNotEqual(self.b.playback.current_track.get(), None) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'b') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'b') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(STOPPED, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get(), None) def test_play_minus_is_ignored_if_playing(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_play_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) self.b.playback.pause() - self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) + self.assertEquals(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'play "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid(self): self.b.current_playlist.append([Track()]) result = self.h.handle_request(u'playid "0"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) + self.assertEqual(PLAYING, self.b.playback.state.get()) def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'a') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'a') def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(self.b.playback.current_track.get(), None) self.b.playback.play() self.b.playback.next() self.b.playback.stop() - self.assertNotEqual(self.b.playback.current_track, None) + self.assertNotEqual(self.b.playback.current_track.get(), None) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assertEqual(self.b.playback.current_track.uri, 'b') + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get().uri, 'b') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.b.current_playlist.clear() result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) - self.assertEqual(self.b.playback.current_track, None) + self.assertEqual(STOPPED, self.b.playback.state.get()) + self.assertEqual(self.b.playback.current_track.get(), None) def test_playid_minus_is_ignored_if_playing(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid_minus_one_resumes_if_paused(self): self.b.current_playlist.append([Track(length=40000)]) self.b.playback.seek(30000) - self.assert_(self.b.playback.time_position >= 30000) - self.assertEquals(self.b.playback.PLAYING, self.b.playback.state) + self.assert_(self.b.playback.time_position.get() >= 30000) + self.assertEquals(PLAYING, self.b.playback.state.get()) self.b.playback.pause() - self.assertEquals(self.b.playback.PAUSED, self.b.playback.state) + self.assertEquals(PAUSED, self.b.playback.state.get()) result = self.h.handle_request(u'playid "-1"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.PLAYING, self.b.playback.state) - self.assert_(self.b.playback.time_position >= 30000) + self.assertEqual(PLAYING, self.b.playback.state.get()) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_playid_which_does_not_exist(self): self.b.current_playlist.append([Track()]) @@ -361,20 +376,20 @@ class PlaybackControlHandlerTest(unittest.TestCase): [Track(uri='1', length=40000), seek_track]) result = self.h.handle_request(u'seek "1" "30"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.current_track, seek_track) + self.assertEqual(self.b.playback.current_track.get(), seek_track) def test_seek_without_quotes(self): self.b.current_playlist.append([Track(length=40000)]) self.h.handle_request(u'seek 0') result = self.h.handle_request(u'seek 0 30') self.assert_(u'OK' in result) - self.assert_(self.b.playback.time_position >= 30000) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_seekid(self): self.b.current_playlist.append([Track(length=40000)]) result = self.h.handle_request(u'seekid "0" "30"') self.assert_(u'OK' in result) - self.assert_(self.b.playback.time_position >= 30000) + self.assert_(self.b.playback.time_position.get() >= 30000) def test_seekid_with_cpid(self): seek_track = Track(uri='2', length=40000) @@ -382,10 +397,10 @@ class PlaybackControlHandlerTest(unittest.TestCase): [Track(length=40000), seek_track]) result = self.h.handle_request(u'seekid "1" "30"') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.current_cpid, 1) - self.assertEqual(self.b.playback.current_track, seek_track) + self.assertEqual(self.b.playback.current_cpid.get(), 1) + self.assertEqual(self.b.playback.current_track.get(), seek_track) def test_stop(self): result = self.h.handle_request(u'stop') self.assert_(u'OK' in result) - self.assertEqual(self.b.playback.STOPPED, self.b.playback.state) + self.assertEqual(STOPPED, self.b.playback.state.get()) From 2e1ea8080ead98fae9b83862f34fa73047907cd1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:21:43 +0100 Subject: [PATCH 072/127] Update MPD/reflection --- mopidy/frontends/mpd/protocol/reflection.py | 2 +- tests/frontends/mpd/reflection_test.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 83efdd94..ab782440 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -81,4 +81,4 @@ def urlhandlers(frontend): Gets a list of available URL handlers. """ - return [(u'handler', uri) for uri in frontend.backend.uri_handlers] + return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()] diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index a4491d75..be95c49b 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class ReflectionHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_commands_returns_list_of_all_commands(self): result = self.h.handle_request(u'commands') From 21153dd52b9f026b708fb9ad01d137f2696df128 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:29:43 +0100 Subject: [PATCH 073/127] Update MPD/regression tests --- mopidy/frontends/mpd/protocol/status.py | 38 ++++++++++++---------- tests/frontends/mpd/regression_test.py | 43 +++++++++++++++++-------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index e18f1ea4..9de82756 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,5 +1,6 @@ from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import MpdNotImplemented +from mopidy.backends.base import PlaybackController @handle_pattern(r'^clearerror$') def clearerror(frontend): @@ -152,28 +153,28 @@ def status(frontend): def _status_bitrate(frontend): if frontend.backend.playback.current_track is not None: - return frontend.backend.playback.current_track.bitrate + return frontend.backend.playback.current_track.get().bitrate def _status_consume(frontend): - if frontend.backend.playback.consume: + if frontend.backend.playback.consume.get(): return 1 else: return 0 def _status_playlist_length(frontend): - return len(frontend.backend.current_playlist.tracks) + return len(frontend.backend.current_playlist.tracks.get()) def _status_playlist_version(frontend): - return frontend.backend.current_playlist.version + return frontend.backend.current_playlist.version.get() def _status_random(frontend): - return int(frontend.backend.playback.random) + return int(frontend.backend.playback.random.get()) def _status_repeat(frontend): - return int(frontend.backend.playback.repeat) + return int(frontend.backend.playback.repeat.get()) def _status_single(frontend): - return int(frontend.backend.playback.single) + return int(frontend.backend.playback.single.get()) def _status_songid(frontend): if frontend.backend.playback.current_cpid is not None: @@ -182,14 +183,15 @@ def _status_songid(frontend): return _status_songpos(frontend) def _status_songpos(frontend): - return frontend.backend.playback.current_playlist_position + return frontend.backend.playback.current_playlist_position.get() def _status_state(frontend): - if frontend.backend.playback.state == frontend.backend.playback.PLAYING: + state = frontend.backend.playback.state.get() + if state == PlaybackController.PLAYING: return u'play' - elif frontend.backend.playback.state == frontend.backend.playback.STOPPED: + elif state == PlaybackController.STOPPED: return u'stop' - elif frontend.backend.playback.state == frontend.backend.playback.PAUSED: + elif state == PlaybackController.PAUSED: return u'pause' def _status_time(frontend): @@ -197,19 +199,21 @@ def _status_time(frontend): _status_time_total(frontend) // 1000) def _status_time_elapsed(frontend): - return frontend.backend.playback.time_position + return frontend.backend.playback.time_position.get() def _status_time_total(frontend): - if frontend.backend.playback.current_track is None: + current_track = frontend.backend.playback.current_track.get() + if current_track is None: return 0 - elif frontend.backend.playback.current_track.length is None: + elif current_track.length is None: return 0 else: - return frontend.backend.playback.current_track.length + return current_track.length def _status_volume(frontend): - if frontend.backend.mixer.volume is not None: - return frontend.backend.mixer.volume + volume = frontend.mixer.volume.get() + if volume is not None: + return volume else: return 0 diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py index 7e7163d8..1d661b5a 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/regression_test.py @@ -18,26 +18,31 @@ class IssueGH17RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), None, Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) # Playlist order: abcfde self.mpd.handle_request(u'play') - self.assertEquals('a', self.backend.playback.current_track.uri) + self.assertEquals('a', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'random "1"') self.mpd.handle_request(u'next') - self.assertEquals('b', self.backend.playback.current_track.uri) + self.assertEquals('b', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') # Should now be at track 'c', but playback fails and it skips ahead - self.assertEquals('f', self.backend.playback.current_track.uri) + self.assertEquals('f', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') - self.assertEquals('d', self.backend.playback.current_track.uri) + self.assertEquals('d', self.backend.playback.current_track.get().uri) self.mpd.handle_request(u'next') - self.assertEquals('e', self.backend.playback.current_track.uri) + self.assertEquals('e', self.backend.playback.current_track.get().uri) class IssueGH18RegressionTest(unittest.TestCase): @@ -52,11 +57,16 @@ class IssueGH18RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) @@ -67,11 +77,11 @@ class IssueGH18RegressionTest(unittest.TestCase): self.mpd.handle_request(u'next') self.mpd.handle_request(u'next') - cp_track_1 = self.backend.playback.current_cp_track + cp_track_1 = self.backend.playback.current_cp_track.get() self.mpd.handle_request(u'next') - cp_track_2 = self.backend.playback.current_cp_track + cp_track_2 = self.backend.playback.current_cp_track.get() self.mpd.handle_request(u'next') - cp_track_3 = self.backend.playback.current_cp_track + cp_track_3 = self.backend.playback.current_cp_track.get() self.assertNotEqual(cp_track_1, cp_track_2) self.assertNotEqual(cp_track_2, cp_track_3) @@ -91,11 +101,16 @@ class IssueGH22RegressionTest(unittest.TestCase): """ def setUp(self): - self.backend = DummyBackend(mixer_class=DummyMixer) + self.backend = DummyBackend.start().proxy() self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - self.mpd = dispatcher.MpdDispatcher(backend=self.backend) + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test(self): random.seed(1) From f73268c332038b9e440679d7b9323da036a2324c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:38:09 +0100 Subject: [PATCH 074/127] Update MPD/server --- tests/frontends/mpd/server_test.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index 48c7e790..ef963347 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -1,11 +1,19 @@ import unittest from mopidy import settings +from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import server +from mopidy.mixers.dummy import DummyMixer class MpdServerTest(unittest.TestCase): def setUp(self): - self.server = server.MpdServer(None) + self.backend = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.server = server.MpdServer() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): server.socket.has_ipv6 = True @@ -20,9 +28,13 @@ class MpdServerTest(unittest.TestCase): class MpdSessionTest(unittest.TestCase): def setUp(self): - self.session = server.MpdSession(None, None, (None, None), None) + self.backend = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.session = server.MpdSession(None, None, (None, None)) def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() settings.runtime.clear() def test_found_terminator_catches_decode_error(self): From 0de8d1b4b8f2238972e38bc91bf3d39771ce1bd5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:56:43 +0100 Subject: [PATCH 075/127] Update MPD/status --- mopidy/backends/base/playback.py | 26 +++++++++++------------ mopidy/frontends/mpd/protocol/status.py | 27 +++++++++++++----------- tests/frontends/mpd/status_test.py | 28 +++++++++++++++++-------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index ce63abf7..2e690b4a 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -68,8 +68,8 @@ class PlaybackController(object): self._state = self.STOPPED self._shuffled = [] self._first_shuffle = True - self._play_time_accumulated = 0 - self._play_time_started = None + self.play_time_accumulated = 0 + self.play_time_started = None def destroy(self): """ @@ -275,7 +275,7 @@ class PlaybackController(object): def state(self, new_state): (old_state, self._state) = (self.state, new_state) logger.debug(u'Changing state: %s -> %s', old_state, new_state) - # FIXME _play_time stuff assumes backend does not have a better way of + # FIXME play_time stuff assumes backend does not have a better way of # handeling this stuff :/ if (old_state in (self.PLAYING, self.STOPPED) and new_state == self.PLAYING): @@ -290,23 +290,23 @@ class PlaybackController(object): """Time position in milliseconds.""" if self.state == self.PLAYING: time_since_started = (self._current_wall_time - - self._play_time_started) - return self._play_time_accumulated + time_since_started + self.play_time_started) + return self.play_time_accumulated + time_since_started elif self.state == self.PAUSED: - return self._play_time_accumulated + return self.play_time_accumulated elif self.state == self.STOPPED: return 0 def _play_time_start(self): - self._play_time_accumulated = 0 - self._play_time_started = self._current_wall_time + self.play_time_accumulated = 0 + self.play_time_started = self._current_wall_time def _play_time_pause(self): - time_since_started = self._current_wall_time - self._play_time_started - self._play_time_accumulated += time_since_started + time_since_started = self._current_wall_time - self.play_time_started + self.play_time_accumulated += time_since_started def _play_time_resume(self): - self._play_time_started = self._current_wall_time + self.play_time_started = self._current_wall_time @property def _current_wall_time(self): @@ -439,8 +439,8 @@ class PlaybackController(object): self.next() return True - self._play_time_started = self._current_wall_time - self._play_time_accumulated = time_position + self.play_time_started = self._current_wall_time + self.play_time_accumulated = time_position return self.provider.seek(time_position) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 9de82756..87d58f89 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -1,6 +1,6 @@ +from mopidy.backends.base import PlaybackController from mopidy.frontends.mpd.protocol import handle_pattern from mopidy.frontends.mpd.exceptions import MpdNotImplemented -from mopidy.backends.base import PlaybackController @handle_pattern(r'^clearerror$') def clearerror(frontend): @@ -24,10 +24,11 @@ def currentsong(frontend): Displays the song info of the current song (same song that is identified in status). """ - if frontend.backend.playback.current_track is not None: - return frontend.backend.playback.current_track.mpd_format( - position=frontend.backend.playback.current_playlist_position, - cpid=frontend.backend.playback.current_cpid) + current_cp_track = frontend.backend.playback.current_cp_track.get() + if current_cp_track is not None: + return current_cp_track[1].mpd_format( + position=frontend.backend.playback.current_playlist_position.get(), + cpid=current_cp_track[0]) @handle_pattern(r'^idle$') @handle_pattern(r'^idle (?P.+)$') @@ -141,19 +142,20 @@ def status(frontend): ('xfade', _status_xfade(frontend)), ('state', _status_state(frontend)), ] - if frontend.backend.playback.current_track is not None: + if frontend.backend.playback.current_track.get() is not None: result.append(('song', _status_songpos(frontend))) result.append(('songid', _status_songid(frontend))) - if frontend.backend.playback.state in (frontend.backend.playback.PLAYING, - frontend.backend.playback.PAUSED): + if frontend.backend.playback.state.get() in (PlaybackController.PLAYING, + PlaybackController.PAUSED): result.append(('time', _status_time(frontend))) result.append(('elapsed', _status_time_elapsed(frontend))) result.append(('bitrate', _status_bitrate(frontend))) return result def _status_bitrate(frontend): - if frontend.backend.playback.current_track is not None: - return frontend.backend.playback.current_track.get().bitrate + current_track = frontend.backend.playback.current_track.get() + if current_track is not None: + return current_track.bitrate def _status_consume(frontend): if frontend.backend.playback.consume.get(): @@ -177,8 +179,9 @@ def _status_single(frontend): return int(frontend.backend.playback.single.get()) def _status_songid(frontend): - if frontend.backend.playback.current_cpid is not None: - return frontend.backend.playback.current_cpid + current_cpid = frontend.backend.playback.current_cpid.get() + if current_cpid is not None: + return current_cpid else: return _status_songpos(frontend) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 1afe6ccd..791d734f 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,14 +1,24 @@ import unittest +from mopidy.backends.base import PlaybackController from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd import dispatcher from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track +PAUSED = PlaybackController.PAUSED +PLAYING = PlaybackController.PLAYING +STOPPED = PlaybackController.STOPPED + class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_clearerror(self): result = self.h.handle_request(u'clearerror') @@ -77,7 +87,7 @@ class StatusHandlerTest(unittest.TestCase): self.assertEqual(int(result['volume']), 0) def test_status_method_contains_volume(self): - self.b.mixer.volume = 17 + self.mixer.volume = 17 result = dict(dispatcher.status.status(self.h)) self.assert_('volume' in result) self.assertEqual(int(result['volume']), 17) @@ -136,20 +146,20 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(int(result['xfade']) >= 0) def test_status_method_contains_state_is_play(self): - self.b.playback.state = self.b.playback.PLAYING + self.b.playback.state = PLAYING result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): - self.b.playback.state = self.b.playback.STOPPED + self.b.playback.state = STOPPED result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): - self.b.playback.state = self.b.playback.PLAYING - self.b.playback.state = self.b.playback.PAUSED + self.b.playback.state = PLAYING + self.b.playback.state = PAUSED result = dict(dispatcher.status.status(self.h)) self.assert_('state' in result) self.assertEqual(result['state'], 'pause') @@ -189,8 +199,8 @@ class StatusHandlerTest(unittest.TestCase): self.assert_(position <= total) def test_status_method_when_playing_contains_elapsed(self): - self.b.playback.state = self.b.playback.PAUSED - self.b.playback._play_time_accumulated = 59123 + self.b.playback.state = PAUSED + self.b.playback.play_time_accumulated = 59123 result = dict(dispatcher.status.status(self.h)) self.assert_('elapsed' in result) self.assertEqual(int(result['elapsed']), 59123) From 289cefc53a746254bf04d82bcc5871928f8ae47f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 00:58:13 +0100 Subject: [PATCH 076/127] Update MPD/stickers --- tests/frontends/mpd/stickers_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py index 5b66d723..83d43792 100644 --- a/tests/frontends/mpd/stickers_test.py +++ b/tests/frontends/mpd/stickers_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class StickersHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_sticker_get(self): result = self.h.handle_request( From f0ebc242344ee03847c7da601fee8de49539d399 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:04:18 +0100 Subject: [PATCH 077/127] Update MPD/stored playlists --- .../mpd/protocol/stored_playlists.py | 9 ++++--- tests/frontends/mpd/stored_playlists_test.py | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/stored_playlists.py b/mopidy/frontends/mpd/protocol/stored_playlists.py index f0a63544..6eccffac 100644 --- a/mopidy/frontends/mpd/protocol/stored_playlists.py +++ b/mopidy/frontends/mpd/protocol/stored_playlists.py @@ -19,8 +19,8 @@ def listplaylist(frontend, name): file: relative/path/to/file3.mp3 """ try: - return ['file: %s' % t.uri - for t in frontend.backend.stored_playlists.get(name=name).tracks] + playlist = frontend.backend.stored_playlists.get(name=name).get() + return ['file: %s' % t.uri for t in playlist.tracks] except LookupError: raise MpdNoExistError(u'No such playlist', command=u'listplaylist') @@ -39,7 +39,8 @@ def listplaylistinfo(frontend, name): Album, Artist, Track """ try: - return frontend.backend.stored_playlists.get(name=name).mpd_format() + playlist = frontend.backend.stored_playlists.get(name=name).get() + return playlist.mpd_format() except LookupError: raise MpdNoExistError( u'No such playlist', command=u'listplaylistinfo') @@ -92,7 +93,7 @@ def load(frontend, name): - ``load`` appends the given playlist to the current playlist. """ try: - playlist = frontend.backend.stored_playlists.get(name=name) + playlist = frontend.backend.stored_playlists.get(name=name).get() frontend.backend.current_playlist.append(playlist.tracks) except LookupError: raise MpdNoExistError(u'No such playlist', command=u'load') diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py index a24cbb88..e981c9ed 100644 --- a/tests/frontends/mpd/stored_playlists_test.py +++ b/tests/frontends/mpd/stored_playlists_test.py @@ -8,8 +8,13 @@ from mopidy.models import Track, Playlist class StoredPlaylistsHandlerTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_listplaylist(self): self.b.stored_playlists.playlists = [ @@ -49,22 +54,23 @@ class StoredPlaylistsHandlerTest(unittest.TestCase): def test_load_known_playlist_appends_to_current_playlist(self): self.b.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(len(self.b.current_playlist.tracks), 2) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 2) self.b.stored_playlists.playlists = [Playlist(name='A-list', tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] result = self.h.handle_request(u'load "A-list"') self.assert_(u'OK' in result) - self.assertEqual(len(self.b.current_playlist.tracks), 5) - self.assertEqual(self.b.current_playlist.tracks[0].uri, 'a') - self.assertEqual(self.b.current_playlist.tracks[1].uri, 'b') - self.assertEqual(self.b.current_playlist.tracks[2].uri, 'c') - self.assertEqual(self.b.current_playlist.tracks[3].uri, 'd') - self.assertEqual(self.b.current_playlist.tracks[4].uri, 'e') + tracks = self.b.current_playlist.tracks.get() + self.assertEqual(len(tracks), 5) + self.assertEqual(tracks[0].uri, 'a') + self.assertEqual(tracks[1].uri, 'b') + self.assertEqual(tracks[2].uri, 'c') + self.assertEqual(tracks[3].uri, 'd') + self.assertEqual(tracks[4].uri, 'e') def test_load_unknown_playlist_acks(self): result = self.h.handle_request(u'load "unknown playlist"') self.assert_(u'ACK [50@0] {load} No such playlist' in result) - self.assertEqual(len(self.b.current_playlist.tracks), 0) + self.assertEqual(len(self.b.current_playlist.tracks.get()), 0) def test_playlistadd(self): result = self.h.handle_request( From 20e55873e9dd46e4d9c339f01c4ef35952b463dd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:10:32 +0100 Subject: [PATCH 078/127] Update MPD/command list --- tests/frontends/mpd/command_list_test.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py index 6c801c3f..7ff96bac 100644 --- a/tests/frontends/mpd/command_list_test.py +++ b/tests/frontends/mpd/command_list_test.py @@ -6,8 +6,13 @@ from mopidy.mixers.dummy import DummyMixer class CommandListsTest(unittest.TestCase): def setUp(self): - self.b = DummyBackend(mixer_class=DummyMixer) - self.h = dispatcher.MpdDispatcher(backend=self.b) + self.b = DummyBackend.start().proxy() + self.mixer = DummyMixer.start().proxy() + self.h = dispatcher.MpdDispatcher() + + def tearDown(self): + self.b.stop().get() + self.mixer.stop().get() def test_command_list_begin(self): result = self.h.handle_request(u'command_list_begin') From bcff80c714b783cc9eced45ba49633a56c7e1055 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:14:33 +0100 Subject: [PATCH 079/127] Switch from multiprocessing.dummy to threading to fix startup issue --- mopidy/utils/process.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index 7a5ba62f..dbc6cada 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -1,5 +1,5 @@ import logging -import multiprocessing.dummy +import threading import gobject gobject.threads_init() @@ -9,7 +9,7 @@ from mopidy import SettingsError logger = logging.getLogger('mopidy.utils.process') -class BaseThread(multiprocessing.dummy.Process): +class BaseThread(threading.Thread): def __init__(self): super(BaseThread, self).__init__() # No thread should block process from exiting From 4087675334fdc833a4b828aac039cccfa342f646 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:14:51 +0100 Subject: [PATCH 080/127] Remove unused imports --- mopidy/backends/local/__init__.py | 1 - mopidy/core.py | 1 - mopidy/frontends/mpd/protocol/status.py | 3 +-- mopidy/frontends/mpd/session.py | 1 - mopidy/mixers/nad.py | 7 +++---- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 80762c89..2a06ca61 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -1,6 +1,5 @@ import glob import logging -import multiprocessing import os import shutil diff --git a/mopidy/core.py b/mopidy/core.py index e21efd00..78f0dd6c 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,5 +1,4 @@ import logging -import multiprocessing import optparse import sys diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 87d58f89..a78efc0a 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -92,8 +92,7 @@ def stats(frontend): 'artists': 0, # TODO 'albums': 0, # TODO 'songs': 0, # TODO - # TODO Does not work after multiprocessing branch merge - 'uptime': 0, # frontend.session.stats_uptime(), + 'uptime': 0, # TODO 'db_playtime': 0, # TODO 'db_update': 0, # TODO 'playtime': 0, # TODO diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py index 78e6f624..5a473eca 100644 --- a/mopidy/frontends/mpd/session.py +++ b/mopidy/frontends/mpd/session.py @@ -1,6 +1,5 @@ import asynchat import logging -import multiprocessing from mopidy import settings from mopidy.frontends.mpd.dispatcher import MpdDispatcher diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index e12b8b92..3d279736 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -1,6 +1,5 @@ import logging -from serial import Serial -from multiprocessing import Pipe +import serial from pykka.actor import ThreadingActor @@ -84,8 +83,8 @@ class NadTalker(ThreadingActor): # Communication settings: 115200 bps 8N1 logger.info(u'Connecting to serial device "%s"', settings.MIXER_EXT_PORT) - self._device = Serial(port=settings.MIXER_EXT_PORT, baudrate=115200, - timeout=self.TIMEOUT) + self._device = serial.Serial(port=settings.MIXER_EXT_PORT, + baudrate=115200, timeout=self.TIMEOUT) self._get_device_model() def _set_device_to_known_state(self): From b67b57955606ec768ebdf8413e0fb625406a85e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:20:42 +0100 Subject: [PATCH 081/127] Spotify backend needs a reference to the output --- mopidy/backends/spotify/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 021277f2..ff8e542c 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -1,10 +1,12 @@ import logging from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry from mopidy import settings from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, PlaybackController, StoredPlaylistsController) +from mopidy.outputs.base import BaseOutput logger = logging.getLogger('mopidy.backends.spotify') @@ -61,9 +63,14 @@ class SpotifyBackend(ThreadingActor, Backend): self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.output = None self.spotify = None def post_start(self): + output_refs = ActorRegistry.get_by_class(BaseOutput) + assert len(output_refs) == 1, 'Expected exactly one running output.' + self.output = output_refs[0].proxy() + self.spotify = self._connect() def _connect(self): From b5da3c3404ab0d774a5d69b5ff4b25b95ce25c71 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 20 Mar 2011 01:37:06 +0100 Subject: [PATCH 082/127] GStreamerOutput need a reference to the backend for notifying it on end of track --- mopidy/outputs/gstreamer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 2a3d9adc..29ffbecc 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -5,8 +5,10 @@ import gst import logging from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry from mopidy import settings +from mopidy.backends.base import Backend from mopidy.outputs.base import BaseOutput logger = logging.getLogger('mopidy.outputs.gstreamer') @@ -74,8 +76,8 @@ class GStreamerOutput(ThreadingActor, BaseOutput): """Process messages from GStreamer.""" if message.type == gst.MESSAGE_EOS: logger.debug(u'GStreamer signalled end-of-stream. ' - 'Sending end_of_track to core_queue ...') - self.core_queue.put({'command': 'end_of_track'}) + 'Telling backend ...') + self._get_backend().playback.on_end_of_track() elif message.type == gst.MESSAGE_ERROR: self.set_state('NULL') error, debug = message.parse_error() @@ -83,6 +85,11 @@ class GStreamerOutput(ThreadingActor, BaseOutput): # FIXME Should we send 'stop_playback' to the backend here? Can we # differentiate on how serious the error is? + def _get_backend(self): + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + return backend_refs[0].proxy() + def play_uri(self, uri): """Play audio at URI""" self.set_state('READY') From af118a3f256797c0976f049059df675750683aa6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 21 Mar 2011 00:18:12 +0100 Subject: [PATCH 083/127] Fix future usage in LocalBackend --- mopidy/backends/local/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 2a06ca61..b4a407f9 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -67,24 +67,24 @@ class LocalPlaybackController(PlaybackController): @property def time_position(self): - return self.backend.output.get_position() + return self.backend.output.get_position().get() class LocalPlaybackProvider(BasePlaybackProvider): def pause(self): - return self.backend.output.set_state('PAUSED') + return self.backend.output.set_state('PAUSED').get() def play(self, track): - return self.backend.output.play_uri(track.uri) + return self.backend.output.play_uri(track.uri).get() def resume(self): - return self.backend.output.set_state('PLAYING') + return self.backend.output.set_state('PLAYING').get() def seek(self, time_position): - return self.backend.output.set_position(time_position) + return self.backend.output.set_position(time_position).get() def stop(self): - return self.backend.output.set_state('READY') + return self.backend.output.set_state('READY').get() class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): From b83dad78457af09e012c79e334cbfb2066952a0b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 21 Mar 2011 00:34:54 +0100 Subject: [PATCH 084/127] Update tests to match change in previous commit --- tests/backends/base/playback.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index aa200047..8ea48a3a 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -727,12 +727,18 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.stop(), None) def test_time_position_when_stopped(self): - self.backend.output.get_position = mock.Mock(return_value=0) + future = mock.Mock() + future.get = mock.Mock(return_value=0) + self.backend.output.get_position = mock.Mock(return_value=future) + self.assertEqual(self.playback.time_position, 0) @populate_playlist def test_time_position_when_stopped_with_playlist(self): - self.backend.output.get_position = mock.Mock(return_value=0) + future = mock.Mock() + future.get = mock.Mock(return_value=0) + self.backend.output.get_position = mock.Mock(return_value=future) + self.assertEqual(self.playback.time_position, 0) @SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput From e79346b5c3aa9185ca21d0026c94675c456e3e09 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 22:19:36 +0100 Subject: [PATCH 085/127] Remove unused import --- mopidy/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 78f0dd6c..b2361e59 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -2,8 +2,6 @@ import logging import optparse import sys -from pykka.actor import ThreadingActor - from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class from mopidy.utils.log import setup_logging From 4f546246f835b6dc6f1a731939948af22c28fba6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 22:24:04 +0100 Subject: [PATCH 086/127] Fix sound with Spotify backend (YAY :-) ) --- mopidy/outputs/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 29ffbecc..0eb716ec 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -96,7 +96,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): self.gst_pipeline.get_by_name('uri').set_property('uri', uri) return self.set_state('PLAYING') - def deliver_data(self, capabilities, data): + def deliver_data(self, caps_string, data): """Deliver audio data to be played""" app_src = self.gst_pipeline.get_by_name('appsrc') caps = gst.caps_from_string(caps_string) From d7149d8ea09c897fb954652beeef3bf008448d9e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 22:37:20 +0100 Subject: [PATCH 087/127] Raise EnvironmentError instead of Exception to make pylint happy --- mopidy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 873ee182..e9ced3ae 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -9,7 +9,7 @@ VERSION = (0, 4, 0) def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) if process.wait() != 0: - raise Exception('Execution of "git describe" failed') + raise EnvironmentError('Execution of "git describe" failed') version = process.stdout.read().strip() if version.startswith('v'): version = version[1:] @@ -21,7 +21,7 @@ def get_plain_version(): def get_version(): try: return get_git_version() - except Exception: + except EnvironmentError: return get_plain_version() class MopidyException(Exception): From a0805ba80fa3d24ec59bd9e58c76fd0d043a179c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 22:37:36 +0100 Subject: [PATCH 088/127] Remove unused imports --- mopidy/core.py | 1 - mopidy/frontends/lastfm.py | 1 - mopidy/mixers/nad.py | 1 - 3 files changed, 3 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index b2361e59..9c4d63d4 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,6 +1,5 @@ import logging import optparse -import sys from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index d723052e..84e612fa 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -11,7 +11,6 @@ from pykka.actor import ThreadingActor from mopidy import settings, SettingsError from mopidy.frontends.base import BaseFrontend -from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.lastfm') diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 3d279736..65eff58c 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -5,7 +5,6 @@ from pykka.actor import ThreadingActor from mopidy import settings from mopidy.mixers.base import BaseMixer -from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.mixers.nad') From b07f37117f1edbbb3b2cb02a027b7d6d9e41cd9c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 22:43:32 +0100 Subject: [PATCH 089/127] Fix Pylint error --- mopidy/frontends/mpd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index c7f469dd..608d9586 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -33,7 +33,7 @@ class MpdFrontend(ThreadingActor, BaseFrontend): class MpdThread(BaseThread): def __init__(self): - super(BaseThread, self).__init__() + super(MpdThread, self).__init__() self.name = u'MpdThread' def run_inside_try(self): From 168d6ea434852b1214c4c6b26985601955a6fc3f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 23 Mar 2011 23:05:20 +0100 Subject: [PATCH 090/127] Replace last multiprocessing.Connection with Queue.Queue --- mopidy/backends/spotify/library.py | 10 ++++------ mopidy/backends/spotify/session_manager.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 5e2f66ae..2e7dbab1 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -1,5 +1,5 @@ import logging -import multiprocessing +import Queue from spotify import Link, SpotifyError @@ -54,8 +54,6 @@ class SpotifyLibraryProvider(BaseLibraryProvider): 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 + queue = Queue.Queue() + self.backend.spotify.search(spotify_query.encode(ENCODING), queue) + return queue.get() diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index ed4d665c..64d2b338 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -126,13 +126,13 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.backend.stored_playlists = playlists logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) - def search(self, query, connection): + def search(self, query, queue): """Search method used by Mopidy backend""" def callback(results, userdata=None): # TODO Include results from results.albums(), etc. too playlist = Playlist(tracks=[ SpotifyTranslator.to_mopidy_track(t) for t in results.tracks()]) - connection.send(playlist) + queue.put(playlist) self.connected.wait() self.session.search(query, callback) From be42217d3bcd9e08c4c87c6f9978c0699996e4b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 24 Mar 2011 06:58:16 +0100 Subject: [PATCH 091/127] Fix 'stored_playlists does not have "playlists" attribute' error --- mopidy/backends/spotify/session_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 64d2b338..398e24fe 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -123,7 +123,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): playlists.append( SpotifyTranslator.to_mopidy_playlist(spotify_playlist)) playlists = filter(None, playlists) - self.backend.stored_playlists = playlists + self.backend.stored_playlists.playlists = playlists logger.debug(u'Refreshed %d stored playlist(s)', len(playlists)) def search(self, query, queue): From 5d6fc7b092aba7bf95f98a80407b01dd0469fe1a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 24 Mar 2011 13:52:41 +0100 Subject: [PATCH 092/127] Avoid blocking forever waiting on search query responses from Spotify --- mopidy/backends/spotify/library.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 2e7dbab1..40d4a099 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -56,4 +56,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider): logger.debug(u'Spotify search query: %s' % spotify_query) queue = Queue.Queue() self.backend.spotify.search(spotify_query.encode(ENCODING), queue) - return queue.get() + try: + return queue.get(timeout=3) # XXX What is an reasonable timeout? + except Queue.Empty: + return Playlist(tracks=[]) From 4452957c10d0a485b417cc06b4a83a96b549203d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 00:39:19 +0200 Subject: [PATCH 093/127] Require Pykka 0.12 (not released yet) --- docs/installation/index.rst | 2 +- mopidy/backends/local/__init__.py | 2 +- mopidy/backends/spotify/__init__.py | 2 +- mopidy/frontends/lastfm.py | 2 +- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/mixers/alsa.py | 2 +- mopidy/mixers/denon.py | 2 +- mopidy/mixers/gstreamer_software.py | 2 +- mopidy/mixers/nad.py | 2 +- mopidy/outputs/gstreamer.py | 2 +- requirements/core.txt | 2 +- tests/outputs/gstreamer_test.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 3ef5af99..d1fbd0f6 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -25,7 +25,7 @@ Otherwise, make sure you got the required dependencies installed. - Python >= 2.6, < 3 -- `Pykka `_ >= 0.11 +- `Pykka `_ >= 0.12 - GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index b4a407f9..58cb405d 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -52,7 +52,7 @@ class LocalBackend(ThreadingActor, Backend): self.output = None - def post_start(self): + def pre_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index ff8e542c..9f2cd99f 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -66,7 +66,7 @@ class SpotifyBackend(ThreadingActor, Backend): self.output = None self.spotify = None - def post_start(self): + def pre_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 84e612fa..8d6cf2a8 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -40,7 +40,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): self.lastfm = None self.last_start_time = None - def post_start(self): + def pre_start(self): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 608d9586..6c39a09b 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -23,7 +23,7 @@ class MpdFrontend(ThreadingActor, BaseFrontend): def __init__(self): self._thread = None - def post_start(self): + def pre_start(self): self._thread = MpdThread() self._thread.start() diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index f19819e8..a7f5431e 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -25,7 +25,7 @@ class AlsaMixer(ThreadingActor, BaseMixer): def __init__(self): self._mixer = None - def post_start(self): + def pre_start(self): self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 832e7d1a..966f804a 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -30,7 +30,7 @@ class DenonMixer(ThreadingActor, BaseMixer): self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 - def post_start(self): + def pre_start(self): if self._device is None: from serial import Serial self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 2125aab3..b93e1b52 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -10,7 +10,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): def __init__(self): self.output = None - def post_start(self): + def pre_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 65eff58c..db4b40c1 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -73,7 +73,7 @@ class NadTalker(ThreadingActor): def __init__(self): self._device = None - def post_start(self): + def pre_start(self): self._open_connection() self._set_device_to_known_state() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 0eb716ec..37bfb35c 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -26,7 +26,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def __init__(self): self.gst_pipeline = None - def post_start(self): + def pre_start(self): self._setup_gstreamer() def _setup_gstreamer(self): diff --git a/requirements/core.txt b/requirements/core.txt index f0f0bd39..aaae84f8 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -1 +1 @@ -Pykka >= 0.11 +Pykka >= 0.12 diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 801f476e..4e101319 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -19,7 +19,7 @@ class GStreamerOutputTest(unittest.TestCase): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) self.song_uri = path_to_uri(data_folder('song1.wav')) self.output = GStreamerOutput() - self.output.post_start() + self.output.pre_start() def tearDown(self): settings.runtime.clear() From e0b212a275edbbf9552049aa32d08d988ba262bf Mon Sep 17 00:00:00 2001 From: Antoine Pierlot-Garcin Date: Wed, 30 Mar 2011 00:49:51 -0400 Subject: [PATCH 094/127] Spotify backend: better error handling. --- mopidy/backends/spotify/session_manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 9736f2eb..7ed12ada 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -34,6 +34,9 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): def logged_in(self, session, error): """Callback used by pyspotify""" + if error: + logger.error(u'Spotify login error: %s', error) + return logger.info(u'Connected to Spotify') self.session = session if settings.SPOTIFY_HIGH_BITRATE: @@ -55,7 +58,7 @@ class SpotifySessionManager(spotify.manager.SpotifySessionManager, BaseThread): def connection_error(self, session, error): """Callback used by pyspotify""" - logger.error(u'Connection error: %s', error) + logger.error(u'Spotify connection error: %s', error) def message_to_user(self, session, message): """Callback used by pyspotify""" From 4c5db1e25e429bf8e4836b7b87109b813604ed75 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 19:35:33 +0200 Subject: [PATCH 095/127] docs: How to turn off 'idle' use ncmpcpp --- docs/clients/mpd.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index e27aa446..f5066210 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -31,6 +31,9 @@ ncmpcpp A console client that generally works well with Mopidy, and is regularly used by Mopidy developers. +Search +^^^^^^ + Search only works for ncmpcpp versions 0.5.1 and higher, and in two of the three search modes: @@ -42,6 +45,19 @@ three search modes: If you run Ubuntu 10.04 or older, you can fetch an updated version of ncmpcpp from `Launchpad `_. +Communication mode +^^^^^^^^^^^^^^^^^^ + +In newer versions of ncmpcpp, like 0.5.5 shipped with Ubuntu 11.04, ncmcpp +defaults to "notifications" mode for MPD communications, which Mopidy currently +does not support. To workaround this limitation in Mopidy, edit the ncmpcpp +configuration file at ``~/.ncmpcpp/config`` and add the following setting:: + + mpd_communication_mode = "polling" + +You can track the development of "notifications" mode support in Mopidy in +:issue:`32`. + Graphical clients ================= From 36b9c1af193b83a76f5ebdc0c628cb3cc22ab19a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 20:14:58 +0200 Subject: [PATCH 096/127] Update changelog with fixes by bok --- docs/changes.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index bd102ef7..b33f3432 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -11,8 +11,23 @@ This change log is used to track all major changes to Mopidy. No description yet. +**Important changes** + +- If you use the Spotify backend, you *should* upgrade to libspotify 0.0.7 and + the latest pyspotify from the Mopidy developers. Follow the instructions at + :doc:`/installation/libspotify/`. + + **Changes** +- Spotify backend: + + - Fixed a segmentation fault due to a bug in Pyspotify. Thanks to Antoine + Pierlot-Garcin for patches to Pyspotify. + + - Better error messages on wrong login or network problems. Thanks to Antoine + Pierlot-Garcin for patches to Mopidy and Pyspotify. + - MPD frontend: - Add support for "date" queries to both the ``find`` and ``search`` From dff599372703150b1d6e840a04601d95716c19b3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 20:18:04 +0200 Subject: [PATCH 097/127] Add Github issue reference --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index b33f3432..2bec7719 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,7 +26,7 @@ No description yet. Pierlot-Garcin for patches to Pyspotify. - Better error messages on wrong login or network problems. Thanks to Antoine - Pierlot-Garcin for patches to Mopidy and Pyspotify. + Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :GH:`77`) - MPD frontend: From 3aded359c5e6017beb0b8772daecb11624821753 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 20:21:56 +0200 Subject: [PATCH 098/127] Fix error in issue reference --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 2bec7719..4d6c6524 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,7 +26,7 @@ No description yet. Pierlot-Garcin for patches to Pyspotify. - Better error messages on wrong login or network problems. Thanks to Antoine - Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :GH:`77`) + Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`) - MPD frontend: From 62f07cdd7ff08f7aa201504deeda12234be4d904 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 23:08:36 +0200 Subject: [PATCH 099/127] Sleep forever when main() is done to be able to catch KeyboardInterrupt --- mopidy/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mopidy/core.py b/mopidy/core.py index 9c4d63d4..a1c6b361 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,5 +1,8 @@ import logging import optparse +import time + +from pykka.registry import ActorRegistry from mopidy import get_version, settings, OptionalDependencyError from mopidy.utils import get_class @@ -19,6 +22,11 @@ def main(): setup_mixer() setup_backend() setup_frontends() + try: + time.sleep(10000*24*60*60) + except KeyboardInterrupt: + logger.info(u'Exiting...') + ActorRegistry.stop_all() def parse_options(): parser = optparse.OptionParser(version='Mopidy %s' % get_version()) From 4703783eed3c20f39654a05496b310e2d34ef49f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 30 Mar 2011 23:28:54 +0200 Subject: [PATCH 100/127] Upgrade to work with final release of Pykka 0.12 --- mopidy/backends/local/__init__.py | 2 +- mopidy/backends/spotify/__init__.py | 2 +- mopidy/frontends/lastfm.py | 4 ++-- mopidy/frontends/mpd/__init__.py | 4 ++-- mopidy/mixers/alsa.py | 2 +- mopidy/mixers/denon.py | 2 +- mopidy/mixers/gstreamer_software.py | 2 +- mopidy/mixers/nad.py | 2 +- mopidy/outputs/gstreamer.py | 2 +- tests/outputs/gstreamer_test.py | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 58cb405d..2fa96dab 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -52,7 +52,7 @@ class LocalBackend(ThreadingActor, Backend): self.output = None - def pre_start(self): + def on_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 9f2cd99f..1ac5f0be 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -66,7 +66,7 @@ class SpotifyBackend(ThreadingActor, Backend): self.output = None self.spotify = None - def pre_start(self): + def on_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 8d6cf2a8..04716c61 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -40,7 +40,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): self.lastfm = None self.last_start_time = None - def pre_start(self): + def on_start(self): try: username = settings.LASTFM_USERNAME password_hash = pylast.md5(settings.LASTFM_PASSWORD) @@ -57,7 +57,7 @@ class LastfmFrontend(ThreadingActor, BaseFrontend): logger.error(u'Error during Last.fm setup: %s', e) self.stop() - def react(self, message): + def on_receive(self, message): if message.get('command') == 'started_playing': self.started_playing(message['track']) elif message.get('command') == 'stopped_playing': diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 6c39a09b..24c21c38 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -23,11 +23,11 @@ class MpdFrontend(ThreadingActor, BaseFrontend): def __init__(self): self._thread = None - def pre_start(self): + def on_start(self): self._thread = MpdThread() self._thread.start() - def react(self, message): + def on_receive(self, message): pass # Ignore any messages diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index a7f5431e..6329bbbb 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -25,7 +25,7 @@ class AlsaMixer(ThreadingActor, BaseMixer): def __init__(self): self._mixer = None - def pre_start(self): + def on_start(self): self._mixer = alsaaudio.Mixer(self._get_mixer_control()) assert self._mixer is not None diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 966f804a..3922d20a 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -30,7 +30,7 @@ class DenonMixer(ThreadingActor, BaseMixer): self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 - def pre_start(self): + def on_start(self): if self._device is None: from serial import Serial self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2) diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index b93e1b52..d6365b4b 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -10,7 +10,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): def __init__(self): self.output = None - def pre_start(self): + def on_start(self): output_refs = ActorRegistry.get_by_class(BaseOutput) assert len(output_refs) == 1, 'Expected exactly one running output.' self.output = output_refs[0].proxy() diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index db4b40c1..bd53376e 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -73,7 +73,7 @@ class NadTalker(ThreadingActor): def __init__(self): self._device = None - def pre_start(self): + def on_start(self): self._open_connection() self._set_device_to_known_state() diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 37bfb35c..0596addb 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -26,7 +26,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def __init__(self): self.gst_pipeline = None - def pre_start(self): + def on_start(self): self._setup_gstreamer() def _setup_gstreamer(self): diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index 4e101319..d2e3c263 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -19,7 +19,7 @@ class GStreamerOutputTest(unittest.TestCase): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) self.song_uri = path_to_uri(data_folder('song1.wav')) self.output = GStreamerOutput() - self.output.pre_start() + self.output.on_start() def tearDown(self): settings.runtime.clear() From f25f3d95c71c2129523dbd1309aa328e1e51355d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Apr 2011 12:44:30 +0200 Subject: [PATCH 101/127] Update changelog after merge of feature/pykka-actors --- docs/changes.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4d6c6524..b667016f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,13 +13,25 @@ No description yet. **Important changes** +- Mopidy now depends on `Pykka ` >=0.12. If you + install from APT, Pykka will automatically be installed. If you are not + installing from APT, you may install Pykka from PyPI:: + + sudo pip install -U Pykka + - If you use the Spotify backend, you *should* upgrade to libspotify 0.0.7 and - the latest pyspotify from the Mopidy developers. Follow the instructions at + the latest pyspotify from the Mopidy developers. If you install from APT, + libspotify and pyspotify will automatically be upgraded. If you are not + installing from APT, follow the instructions at :doc:`/installation/libspotify/`. **Changes** +- Mopidy now use Pykka actors for thread management and inter-thread + communication. The immediate advantage of this is that Mopidy now works on + Python 2.7. (Fixes: :issue:`66`) + - Spotify backend: - Fixed a segmentation fault due to a bug in Pyspotify. Thanks to Antoine From 2bacf5fe0b6a0d14a89c02e9e49a3c14233ddb2f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 3 Apr 2011 12:56:35 +0200 Subject: [PATCH 102/127] Fix link in changelog --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index b667016f..e2d9b6e7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -13,7 +13,7 @@ No description yet. **Important changes** -- Mopidy now depends on `Pykka ` >=0.12. If you +- Mopidy now depends on `Pykka `_ >=0.12. If you install from APT, Pykka will automatically be installed. If you are not installing from APT, you may install Pykka from PyPI:: From 9e3cfc10724ef3f900e6d9a01d512cac03f2d6a8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 5 Apr 2011 21:33:12 +0200 Subject: [PATCH 103/127] Give jkp credits for fixing bugs in Pyspotify --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index e2d9b6e7..e72455ec 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -34,8 +34,8 @@ No description yet. - Spotify backend: - - Fixed a segmentation fault due to a bug in Pyspotify. Thanks to Antoine - Pierlot-Garcin for patches to Pyspotify. + - Fixed multiple segmentation faults due to bus in Pyspotify. Thanks to + Antoine Pierlot-Garcin and Jamie Kirkpatrick for patches to Pyspotify. - Better error messages on wrong login or network problems. Thanks to Antoine Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`) From f3480ef6808496179089f19af0c93e4771466507 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 5 Apr 2011 21:36:05 +0200 Subject: [PATCH 104/127] Ironic: Misspelling 'bugs' --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index e72455ec..f8f9f6af 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -34,7 +34,7 @@ No description yet. - Spotify backend: - - Fixed multiple segmentation faults due to bus in Pyspotify. Thanks to + - Fixed multiple segmentation faults due to bugs in Pyspotify. Thanks to Antoine Pierlot-Garcin and Jamie Kirkpatrick for patches to Pyspotify. - Better error messages on wrong login or network problems. Thanks to Antoine From 176409f0e8f0186ce4e98e6a8329f4517f75fbac Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 5 Apr 2011 21:46:11 +0200 Subject: [PATCH 105/127] Update changelog with patches from grunskis and octe --- docs/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index f8f9f6af..af10970d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -40,6 +40,14 @@ No description yet. - Better error messages on wrong login or network problems. Thanks to Antoine Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`) +- Local backend: + + - Fix crash in :cmd:`mopidy-scan` if a track has no artist name. Thanks to + Martins Grunskis for test and patch and "octe" for patch. + + - Fix crash in `tag_cache` parsing if a track has no total number of tracks + in the album. Thanks to Martins Grunskis for the patch. + - MPD frontend: - Add support for "date" queries to both the ``find`` and ``search`` From c5dd33a343ea66396329523503903a90c7d911bc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 5 Apr 2011 21:50:07 +0200 Subject: [PATCH 106/127] Replace invalid :cmd: with valid :command: --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index af10970d..b875784b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -42,8 +42,8 @@ No description yet. - Local backend: - - Fix crash in :cmd:`mopidy-scan` if a track has no artist name. Thanks to - Martins Grunskis for test and patch and "octe" for patch. + - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks + to Martins Grunskis for test and patch and "octe" for patch. - Fix crash in `tag_cache` parsing if a track has no total number of tracks in the album. Thanks to Martins Grunskis for the patch. From 9499250a7f82c441b79202b05865441b2cc2e39c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 17:51:36 +0200 Subject: [PATCH 107/127] Remove Album._artists workaround --- mopidy/frontends/mpd/translator.py | 1 + mopidy/models.py | 11 +++++------ tests/models_test.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpd/translator.py b/mopidy/frontends/mpd/translator.py index c8fe6be4..562b2d2d 100644 --- a/mopidy/frontends/mpd/translator.py +++ b/mopidy/frontends/mpd/translator.py @@ -84,6 +84,7 @@ def artists_to_mpd_format(artists): :type track: array of :class:`mopidy.models.Artist` :rtype: string """ + artists = list(artists) artists.sort(key=lambda a: a.name) return u', '.join([a.name for a in artists if a.name]) diff --git a/mopidy/models.py b/mopidy/models.py index 8e7585f1..fc4f1423 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -65,6 +65,7 @@ class ImmutableObject(object): % key) return self.__class__(**data) + class Artist(ImmutableObject): """ :param uri: artist URI @@ -105,6 +106,9 @@ class Album(ImmutableObject): #: The album name. Read-only. name = None + #: A set of album artists. Read-only. + artists = frozenset() + #: The number of tracks in the album. Read-only. num_tracks = 0 @@ -112,14 +116,9 @@ class Album(ImmutableObject): musicbrainz_id = None def __init__(self, *args, **kwargs): - self._artists = frozenset(kwargs.pop('artists', [])) + self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) super(Album, self).__init__(*args, **kwargs) - @property - def artists(self): - """List of :class:`Artist` elements. Read-only.""" - return list(self._artists) - class Track(ImmutableObject): """ diff --git a/tests/models_test.py b/tests/models_test.py index 4d99a413..771d712b 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -142,9 +142,9 @@ class AlbumTest(unittest.TestCase): self.assertRaises(AttributeError, setattr, album, 'name', None) def test_artists(self): - artists = [Artist()] - album = Album(artists=artists) - self.assertEqual(album.artists, artists) + artist = Artist() + album = Album(artists=[artist]) + self.assert_(artist in album.artists) self.assertRaises(AttributeError, setattr, album, 'artists', None) def test_num_tracks(self): From fce596439568c224996b4f58b492192326c356f5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 17:54:12 +0200 Subject: [PATCH 108/127] Remove Track._artists workaround --- mopidy/models.py | 10 ++++------ tests/models_test.py | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index fc4f1423..b8e172f6 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -148,6 +148,9 @@ class Track(ImmutableObject): #: The track name. Read-only. name = None + #: A set of track artists. Read-only. + artists = frozenset() + #: The track :class:`Album`. Read-only. album = None @@ -167,14 +170,9 @@ class Track(ImmutableObject): musicbrainz_id = None def __init__(self, *args, **kwargs): - self._artists = frozenset(kwargs.pop('artists', [])) + self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) super(Track, self).__init__(*args, **kwargs) - @property - def artists(self): - """List of :class:`Artist`. Read-only.""" - return list(self._artists) - def mpd_format(self, *args, **kwargs): return translator.track_to_mpd_format(self, *args, **kwargs) diff --git a/tests/models_test.py b/tests/models_test.py index 771d712b..a8c34112 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -39,11 +39,11 @@ class GenericCopyTets(unittest.TestCase): self.assertEqual('bar', copy.uri) def test_copying_track_with_private_internal_value(self): - artists1 = [Artist(name='foo')] - artists2 = [Artist(name='bar')] - track = Track(artists=artists1) - copy = track.copy(artists=artists2) - self.assertEqual(copy.artists, artists2) + artist1 = Artist(name='foo') + artist2 = Artist(name='bar') + track = Track(artists=[artist1]) + copy = track.copy(artists=[artist2]) + self.assert_(artist2 in copy.artists) def test_copying_track_with_invalid_key(self): test = lambda: Track().copy(invalid_key=True) From 037da6f4602c9121b809d4ed450bc7493c6fe967 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 18:01:33 +0200 Subject: [PATCH 109/127] Remove Playlist._tracks workaround --- mopidy/models.py | 14 +++++--------- tests/models_test.py | 10 +++++----- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index b8e172f6..129c9ba8 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,5 +1,3 @@ -from copy import copy - from mopidy.frontends.mpd import translator class ImmutableObject(object): @@ -195,24 +193,22 @@ class Playlist(ImmutableObject): #: The playlist name. Read-only. name = None + #: The playlist's tracks. Read-only. + tracks = tuple() + #: The playlist modification time. Read-only. #: #: :class:`datetime.datetime`, or :class:`None` if unknown. last_modified = None def __init__(self, *args, **kwargs): - self._tracks = kwargs.pop('tracks', []) + self.__dict__['tracks'] = tuple(kwargs.pop('tracks', [])) super(Playlist, self).__init__(*args, **kwargs) - @property - def tracks(self): - """List of :class:`Track` elements. Read-only.""" - return copy(self._tracks) - @property def length(self): """The number of tracks in the playlist. Read-only.""" - return len(self._tracks) + return len(self.tracks) def mpd_format(self, *args, **kwargs): return translator.playlist_to_mpd_format(self, *args, **kwargs) diff --git a/tests/models_test.py b/tests/models_test.py index a8c34112..101ba877 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -486,7 +486,7 @@ class PlaylistTest(unittest.TestCase): def test_tracks(self): tracks = [Track(), Track(), Track()] playlist = Playlist(tracks=tracks) - self.assertEqual(playlist.tracks, tracks) + self.assertEqual(list(playlist.tracks), tracks) self.assertRaises(AttributeError, setattr, playlist, 'tracks', None) def test_length(self): @@ -509,7 +509,7 @@ class PlaylistTest(unittest.TestCase): new_playlist = playlist.copy(uri=u'another uri') self.assertEqual(new_playlist.uri, u'another uri') self.assertEqual(new_playlist.name, u'a name') - self.assertEqual(new_playlist.tracks, tracks) + self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_name(self): @@ -520,7 +520,7 @@ class PlaylistTest(unittest.TestCase): new_playlist = playlist.copy(name=u'another name') self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'another name') - self.assertEqual(new_playlist.tracks, tracks) + self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_tracks(self): @@ -532,7 +532,7 @@ class PlaylistTest(unittest.TestCase): new_playlist = playlist.copy(tracks=new_tracks) self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'a name') - self.assertEqual(new_playlist.tracks, new_tracks) + self.assertEqual(list(new_playlist.tracks), new_tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_last_modified(self): @@ -544,7 +544,7 @@ class PlaylistTest(unittest.TestCase): new_playlist = playlist.copy(last_modified=new_last_modified) self.assertEqual(new_playlist.uri, u'an uri') self.assertEqual(new_playlist.name, u'a name') - self.assertEqual(new_playlist.tracks, tracks) + self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, new_last_modified) def test_invalid_kwarg(self): From 005260eb9c518dd6eb62a6b1d3c5d4de4dfffaee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 18:18:00 +0200 Subject: [PATCH 110/127] Add __repr__ to ImmutableObject to get better output from failed model comparisions in tests --- mopidy/models.py | 11 +++++++++++ tests/models_test.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/mopidy/models.py b/mopidy/models.py index 129c9ba8..ef60ebbe 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -21,6 +21,17 @@ class ImmutableObject(object): return super(ImmutableObject, self).__setattr__(name, value) raise AttributeError('Object is immutable.') + def __repr__(self): + kwarg_pairs = [] + for (key, value) in sorted(self.__dict__.items()): + if isinstance(value, (frozenset, tuple)): + value = list(value) + kwarg_pairs.append('%s=%s' % (key, repr(value))) + return '%(classname)s(%(kwargs)s)' % { + 'classname': self.__class__.__name__, + 'kwargs': ', '.join(kwarg_pairs), + } + def __hash__(self): hash_sum = 0 for key, value in self.__dict__.items(): diff --git a/tests/models_test.py b/tests/models_test.py index 101ba877..9657c562 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -73,6 +73,11 @@ class ArtistTest(unittest.TestCase): test = lambda: Artist(foo='baz') self.assertRaises(TypeError, test) + def test_repr(self): + self.assertEquals( + "Artist(name='name', uri='uri')", + repr(Artist(uri='uri', name='name'))) + def test_eq_name(self): artist1 = Artist(name=u'name') artist2 = Artist(name=u'name') @@ -164,6 +169,16 @@ class AlbumTest(unittest.TestCase): test = lambda: Album(foo='baz') self.assertRaises(TypeError, test) + def test_repr_without_artists(self): + self.assertEquals( + "Album(artists=[], name='name', uri='uri')", + repr(Album(uri='uri', name='name'))) + + def test_repr_with_artists(self): + self.assertEquals( + "Album(artists=[Artist(name='foo')], name='name', uri='uri')", + repr(Album(uri='uri', name='name', artists=[Artist(name='foo')]))) + def test_eq_name(self): album1 = Album(name=u'name') album2 = Album(name=u'name') @@ -319,6 +334,16 @@ class TrackTest(unittest.TestCase): test = lambda: Track(foo='baz') self.assertRaises(TypeError, test) + def test_repr_without_artists(self): + self.assertEquals( + "Track(artists=[], name='name', uri='uri')", + repr(Track(uri='uri', name='name'))) + + def test_repr_with_artists(self): + self.assertEquals( + "Track(artists=[Artist(name='foo')], name='name', uri='uri')", + repr(Track(uri='uri', name='name', artists=[Artist(name='foo')]))) + def test_eq_uri(self): track1 = Track(uri=u'uri1') track2 = Track(uri=u'uri1') @@ -551,6 +576,17 @@ class PlaylistTest(unittest.TestCase): test = lambda: Playlist(foo='baz') self.assertRaises(TypeError, test) + def test_repr_without_tracks(self): + self.assertEquals( + "Playlist(name='name', tracks=[], uri='uri')", + repr(Playlist(uri='uri', name='name'))) + + def test_repr_with_tracks(self): + self.assertEquals( + "Playlist(name='name', tracks=[Track(artists=[], name='foo')], " + "uri='uri')", + repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')]))) + def test_eq(self): # FIXME missing all equal and hash tests raise SkipTest From ba738b5c1ea1399800a7169c4d586efddab2b58b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 18:36:14 +0200 Subject: [PATCH 111/127] Add missing tests for eq/ne of Playlist --- tests/models_test.py | 73 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/tests/models_test.py b/tests/models_test.py index 9657c562..afbf9d50 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -587,6 +587,75 @@ class PlaylistTest(unittest.TestCase): "uri='uri')", repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')]))) + def test_eq_name(self): + playlist1 = Playlist(name=u'name') + playlist2 = Playlist(name=u'name') + self.assertEqual(playlist1, playlist2) + self.assertEqual(hash(playlist1), hash(playlist2)) + + def test_eq_uri(self): + playlist1 = Playlist(uri=u'uri') + playlist2 = Playlist(uri=u'uri') + self.assertEqual(playlist1, playlist2) + self.assertEqual(hash(playlist1), hash(playlist2)) + + def test_eq_tracks(self): + tracks = [Track()] + playlist1 = Playlist(tracks=tracks) + playlist2 = Playlist(tracks=tracks) + self.assertEqual(playlist1, playlist2) + self.assertEqual(hash(playlist1), hash(playlist2)) + + def test_eq_uri(self): + playlist1 = Playlist(last_modified=1) + playlist2 = Playlist(last_modified=1) + self.assertEqual(playlist1, playlist2) + self.assertEqual(hash(playlist1), hash(playlist2)) + def test_eq(self): - # FIXME missing all equal and hash tests - raise SkipTest + tracks = [Track()] + playlist1 = Playlist(uri=u'uri', name=u'name', tracks=tracks, + last_modified=1) + playlist2 = Playlist(uri=u'uri', name=u'name', tracks=tracks, + last_modified=1) + self.assertEqual(playlist1, playlist2) + self.assertEqual(hash(playlist1), hash(playlist2)) + + def test_eq_none(self): + self.assertNotEqual(Playlist(), None) + + def test_eq_other(self): + self.assertNotEqual(Playlist(), 'other') + + def test_ne_name(self): + playlist1 = Playlist(name=u'name1') + playlist2 = Playlist(name=u'name2') + self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(hash(playlist1), hash(playlist2)) + + def test_ne_uri(self): + playlist1 = Playlist(uri=u'uri1') + playlist2 = Playlist(uri=u'uri2') + self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(hash(playlist1), hash(playlist2)) + + def test_ne_tracks(self): + playlist1 = Playlist(tracks=[Track(uri=u'uri1')]) + playlist2 = Playlist(tracks=[Track(uri=u'uri2')]) + self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(hash(playlist1), hash(playlist2)) + + def test_ne_uri(self): + playlist1 = Playlist(last_modified=1) + playlist2 = Playlist(last_modified=2) + self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(hash(playlist1), hash(playlist2)) + + def test_ne(self): + playlist1 = Playlist(uri=u'uri1', name=u'name2', + tracks=[Track(uri=u'uri1')], last_modified=1) + playlist2 = Playlist(uri=u'uri2', name=u'name2', + tracks=[Track(uri=u'uri2')], last_modified=2) + self.assertNotEqual(playlist1, playlist2) + self.assertNotEqual(hash(playlist1), hash(playlist2)) + From 342c13f81102dfacf7416f91bd735acef89c6d39 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 22:40:50 +0200 Subject: [PATCH 112/127] Fix 'song: None' in 'status' response --- docs/changes.rst | 3 +++ mopidy/backends/base/playback.py | 11 ++++----- tests/frontends/mpd/regression_test.py | 33 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b875784b..18627de7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,6 +58,9 @@ No description yet. resume and addition of tracks to the current playlist while playing for the MPoD client. + - Fix bug where ``status`` returned ``song: None``, which caused MPDroid to + crash. (Fixes: :issue:`69`) + - Settings: - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 2e690b4a..88ae141d 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -246,7 +246,7 @@ class PlaybackController(object): if self.repeat or self.consume or self.random: return self.current_cp_track - if self.current_cp_track is None or self.current_playlist_position == 0: + if self.current_playlist_position in (None, 0): return None return self.backend.current_playlist.cp_tracks[ @@ -452,11 +452,10 @@ class PlaybackController(object): stopping :type clear_current_track: boolean """ - if self.state == self.STOPPED: - return - self._trigger_stopped_playing_event() - if self.provider.stop(): - self.state = self.STOPPED + if self.state != self.STOPPED: + self._trigger_stopped_playing_event() + if self.provider.stop(): + self.state = self.STOPPED if clear_current_track: self.current_cp_track = None diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/regression_test.py index 1d661b5a..f786cf0a 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/regression_test.py @@ -123,3 +123,36 @@ class IssueGH22RegressionTest(unittest.TestCase): self.mpd.handle_request(u'deleteid "5"') self.mpd.handle_request(u'deleteid "6"') self.mpd.handle_request(u'status') + + +class IssueGH69RegressionTest(unittest.TestCase): + """ + The issue: https://github.com/mopidy/mopidy/issues#issue/69 + + How to reproduce: + + Play track, stop, clear current playlist, load a new playlist, status. + + The status response now contains "song: None". + """ + + def setUp(self): + self.backend = DummyBackend.start().proxy() + self.backend.current_playlist.append([ + Track(uri='a'), Track(uri='b'), Track(uri='c'), + Track(uri='d'), Track(uri='e'), Track(uri='f')]) + self.backend.stored_playlists.create('foo') + self.mixer = DummyMixer.start().proxy() + self.mpd = dispatcher.MpdDispatcher() + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() + + def test(self): + self.mpd.handle_request(u'play') + self.mpd.handle_request(u'stop') + self.mpd.handle_request(u'clear') + self.mpd.handle_request(u'load "foo"') + response = self.mpd.handle_request(u'status') + self.assert_('song: None' not in response) From e188f19595871add5ad09e0e98743db64ea92fab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 6 Apr 2011 23:05:55 +0200 Subject: [PATCH 113/127] Fix typo in 0.3.0 changelog --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 18627de7..dbdc9d3b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -115,7 +115,7 @@ to this problem. :doc:`/installation/libspotify/`. - If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run - ``sudp pip install --upgrade pylast`` or install Mopidy from APT. + ``sudo pip install --upgrade pylast`` or install Mopidy from APT. **Changes** From 79771d6e2ce09c9fcca254c46d862e4db9362fb3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 7 Apr 2011 15:55:03 +0200 Subject: [PATCH 114/127] Rename data_folder() to path_to_data_dir() --- tests/__init__.py | 10 ++-- tests/backends/base/library.py | 6 +- tests/backends/base/stored_playlists.py | 6 +- tests/backends/local/__init__.py | 4 +- tests/backends/local/library_test.py | 6 +- tests/backends/local/playback_test.py | 4 +- tests/backends/local/stored_playlists_test.py | 4 +- tests/backends/local/translator_test.py | 56 +++++++++---------- tests/outputs/gstreamer_test.py | 4 +- tests/scanner_test.py | 8 +-- tests/utils/path_test.py | 6 +- 11 files changed, 57 insertions(+), 57 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 82073e97..1d4d2e3d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -16,9 +16,9 @@ from mopidy import settings # Nuke any local settings to ensure same test env all over settings.local.clear() -def data_folder(name): - folder = os.path.dirname(__file__) - folder = os.path.join(folder, 'data') - folder = os.path.abspath(folder) - return os.path.join(folder, name) +def path_to_data_dir(name): + path = os.path.dirname(__file__) + path = os.path.join(path, 'data') + path = os.path.abspath(path) + return os.path.join(path, name) diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index bff26c4c..2a3de730 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -1,6 +1,6 @@ from mopidy.models import Playlist, Track, Album, Artist -from tests import SkipTest, data_folder +from tests import SkipTest, path_to_data_dir class LibraryControllerTest(object): artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()] @@ -8,9 +8,9 @@ class LibraryControllerTest(object): Album(name='album2', artists=artists[1:2]), Album()] tracks = [Track(name='track1', length=4000, artists=artists[:1], - album=albums[0], uri='file://' + data_folder('uri1')), + album=albums[0], uri='file://' + path_to_data_dir('uri1')), Track(name='track2', length=4000, artists=artists[1:2], - album=albums[1], uri='file://' + data_folder('uri2')), + album=albums[1], uri='file://' + path_to_data_dir('uri2')), Track()] def setUp(self): diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 12e48256..839d5bed 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -5,13 +5,13 @@ import tempfile from mopidy import settings from mopidy.models import Playlist -from tests import SkipTest, data_folder +from tests import SkipTest, path_to_data_dir class StoredPlaylistsControllerTest(object): def setUp(self): settings.LOCAL_PLAYLIST_PATH = tempfile.mkdtemp() - settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') - settings.LOCAL_MUSIC_PATH = data_folder('') + settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') + settings.LOCAL_MUSIC_PATH = path_to_data_dir('') self.backend = self.backend_class() self.stored = self.backend.stored_playlists diff --git a/tests/backends/local/__init__.py b/tests/backends/local/__init__.py index 60a1bd4d..d2213297 100644 --- a/tests/backends/local/__init__.py +++ b/tests/backends/local/__init__.py @@ -1,6 +1,6 @@ from mopidy.utils.path import path_to_uri -from tests import data_folder +from tests import path_to_data_dir -song = data_folder('song%s.wav') +song = path_to_data_dir('song%s.wav') generate_song = lambda i: path_to_uri(song % i) diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 0c44924a..68ab22e9 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -9,7 +9,7 @@ if sys.platform == 'win32': from mopidy import settings from mopidy.backends.local import LocalBackend -from tests import data_folder +from tests import path_to_data_dir from tests.backends.base.library import LibraryControllerTest class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): @@ -17,8 +17,8 @@ class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): backend_class = LocalBackend def setUp(self): - settings.LOCAL_TAG_CACHE_FILE = data_folder('library_tag_cache') - settings.LOCAL_MUSIC_PATH = data_folder('') + settings.LOCAL_TAG_CACHE_FILE = path_to_data_dir('library_tag_cache') + settings.LOCAL_MUSIC_PATH = path_to_data_dir('') super(LocalLibraryControllerTest, self).setUp() diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 2007cff8..2cdeadb9 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -11,7 +11,7 @@ from mopidy.backends.local import LocalBackend from mopidy.models import Track from mopidy.utils.path import path_to_uri -from tests import data_folder +from tests import path_to_data_dir from tests.backends.base.playback import PlaybackControllerTest from tests.backends.local import generate_song @@ -32,7 +32,7 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): settings.runtime.clear() def add_track(self, path): - uri = path_to_uri(data_folder(path)) + uri = path_to_uri(path_to_data_dir(path)) track = Track(uri=uri, length=4464) self.backend.current_playlist.add(track) diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index 5bc16d1c..b426e9ce 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -14,7 +14,7 @@ from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track from mopidy.utils.path import path_to_uri -from tests import data_folder +from tests import path_to_data_dir from tests.backends.base.stored_playlists import \ StoredPlaylistsControllerTest from tests.backends.local import generate_song @@ -65,7 +65,7 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, self.assertEqual(uri, contents.strip()) def test_playlists_are_loaded_at_startup(self): - track = Track(uri=path_to_uri(data_folder('uri2'))) + track = Track(uri=path_to_uri(path_to_data_dir('uri2'))) playlist = Playlist(tracks=[track], name='test') self.stored.save(playlist) diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index b7fd212c..a4e9f317 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -8,26 +8,26 @@ from mopidy.utils.path import path_to_uri from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache from mopidy.models import Track, Artist, Album -from tests import SkipTest, data_folder +from tests import SkipTest, path_to_data_dir -song1_path = data_folder('song1.mp3') -song2_path = data_folder('song2.mp3') -encoded_path = data_folder(u'æøå.mp3') +song1_path = path_to_data_dir('song1.mp3') +song2_path = path_to_data_dir('song2.mp3') +encoded_path = path_to_data_dir(u'æøå.mp3') song1_uri = path_to_uri(song1_path) song2_uri = path_to_uri(song2_path) encoded_uri = path_to_uri(encoded_path) class M3UToUriTest(unittest.TestCase): def test_empty_file(self): - uris = parse_m3u(data_folder('empty.m3u')) + uris = parse_m3u(path_to_data_dir('empty.m3u')) self.assertEqual([], uris) def test_basic_file(self): - uris = parse_m3u(data_folder('one.m3u')) + uris = parse_m3u(path_to_data_dir('one.m3u')) self.assertEqual([song1_uri], uris) def test_file_with_comment(self): - uris = parse_m3u(data_folder('comment.m3u')) + uris = parse_m3u(path_to_data_dir('comment.m3u')) self.assertEqual([song1_uri], uris) def test_file_with_absolute_files(self): @@ -64,11 +64,11 @@ class M3UToUriTest(unittest.TestCase): os.remove(tmp.name) def test_encoding_is_latin1(self): - uris = parse_m3u(data_folder('encoding.m3u')) + uris = parse_m3u(path_to_data_dir('encoding.m3u')) self.assertEqual([encoded_uri], uris) def test_open_missing_file(self): - uris = parse_m3u(data_folder('non-existant.m3u')) + uris = parse_m3u(path_to_data_dir('non-existant.m3u')) self.assertEqual([], uris) @@ -81,7 +81,7 @@ expected_albums = [Album(name='albumname', artists=expected_artists, expected_tracks = [] def generate_track(path, ident): - uri = path_to_uri(data_folder(path)) + uri = path_to_uri(path_to_data_dir(path)) track = Track(name='trackname', artists=expected_artists, track_no=1, album=expected_albums[0], length=4000, uri=uri) expected_tracks.append(track) @@ -98,28 +98,28 @@ generate_track('subdir1/subsubdir/song9.mp3', 1) class MPDTagCacheToTracksTest(unittest.TestCase): def test_emtpy_cache(self): - tracks = parse_mpd_tag_cache(data_folder('empty_tag_cache'), - data_folder('')) + tracks = parse_mpd_tag_cache(path_to_data_dir('empty_tag_cache'), + path_to_data_dir('')) self.assertEqual(set(), tracks) def test_simple_cache(self): - tracks = parse_mpd_tag_cache(data_folder('simple_tag_cache'), - data_folder('')) - uri = path_to_uri(data_folder('song1.mp3')) + tracks = parse_mpd_tag_cache(path_to_data_dir('simple_tag_cache'), + path_to_data_dir('')) + uri = path_to_uri(path_to_data_dir('song1.mp3')) track = Track(name='trackname', artists=expected_artists, track_no=1, album=expected_albums[0], length=4000, uri=uri) self.assertEqual(set([track]), tracks) def test_advanced_cache(self): - tracks = parse_mpd_tag_cache(data_folder('advanced_tag_cache'), - data_folder('')) + tracks = parse_mpd_tag_cache(path_to_data_dir('advanced_tag_cache'), + path_to_data_dir('')) self.assertEqual(set(expected_tracks), tracks) def test_unicode_cache(self): - tracks = parse_mpd_tag_cache(data_folder('utf8_tag_cache'), - data_folder('')) + tracks = parse_mpd_tag_cache(path_to_data_dir('utf8_tag_cache'), + path_to_data_dir('')) - uri = path_to_uri(data_folder('song1.mp3')) + uri = path_to_uri(path_to_data_dir('song1.mp3')) artists = [Artist(name=u'æøå')] album = Album(name=u'æøå', artists=artists) track = Track(uri=uri, name=u'æøå', artists=artists, @@ -132,14 +132,14 @@ class MPDTagCacheToTracksTest(unittest.TestCase): raise SkipTest def test_cache_with_blank_track_info(self): - tracks = parse_mpd_tag_cache(data_folder('blank_tag_cache'), - data_folder('')) - uri = path_to_uri(data_folder('song1.mp3')) + tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'), + path_to_data_dir('')) + uri = path_to_uri(path_to_data_dir('song1.mp3')) self.assertEqual(set([Track(uri=uri, length=4000)]), tracks) def test_musicbrainz_tagcache(self): - tracks = parse_mpd_tag_cache(data_folder('musicbrainz_tag_cache'), - data_folder('')) + tracks = parse_mpd_tag_cache(path_to_data_dir('musicbrainz_tag_cache'), + path_to_data_dir('')) artist = list(expected_tracks[0].artists)[0].copy( musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') albumartist = list(expected_tracks[0].artists)[0].copy( @@ -153,9 +153,9 @@ class MPDTagCacheToTracksTest(unittest.TestCase): self.assertEqual(track, list(tracks)[0]) def test_albumartist_tag_cache(self): - tracks = parse_mpd_tag_cache(data_folder('albumartist_tag_cache'), - data_folder('')) - uri = path_to_uri(data_folder('song1.mp3')) + tracks = parse_mpd_tag_cache(path_to_data_dir('albumartist_tag_cache'), + path_to_data_dir('')) + uri = path_to_uri(path_to_data_dir('song1.mp3')) artist = Artist(name='albumartistname') album = expected_albums[0].copy(artists=[artist]) track = Track(name='trackname', artists=expected_artists, track_no=1, diff --git a/tests/outputs/gstreamer_test.py b/tests/outputs/gstreamer_test.py index d2e3c263..31a16756 100644 --- a/tests/outputs/gstreamer_test.py +++ b/tests/outputs/gstreamer_test.py @@ -12,12 +12,12 @@ from mopidy import settings from mopidy.outputs.gstreamer import GStreamerOutput from mopidy.utils.path import path_to_uri -from tests import data_folder +from tests import path_to_data_dir class GStreamerOutputTest(unittest.TestCase): def setUp(self): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) - self.song_uri = path_to_uri(data_folder('song1.wav')) + self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) self.output = GStreamerOutput() self.output.on_start() diff --git a/tests/scanner_test.py b/tests/scanner_test.py index a1b53bcf..b98c5aa9 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -4,7 +4,7 @@ from datetime import date from mopidy.scanner import Scanner, translator from mopidy.models import Track, Artist, Album -from tests import data_folder +from tests import path_to_data_dir class FakeGstDate(object): def __init__(self, year, month, day): @@ -132,12 +132,12 @@ class ScannerTest(unittest.TestCase): self.data = {} def scan(self, path): - scanner = Scanner(data_folder(path), + scanner = Scanner(path_to_data_dir(path), self.data_callback, self.error_callback) scanner.start() def check(self, name, key, value): - name = data_folder(name) + name = path_to_data_dir(name) self.assertEqual(self.data[name][key], value) def data_callback(self, data): @@ -159,7 +159,7 @@ class ScannerTest(unittest.TestCase): def test_uri_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'uri', 'file://' - + data_folder('scanner/simple/song1.mp3')) + + path_to_data_dir('scanner/simple/song1.mp3')) def test_duration_is_set(self): self.scan('scanner/simple') diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 3743d59e..088a7049 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -9,7 +9,7 @@ import unittest from mopidy.utils.path import (get_or_create_folder, mtime, path_to_uri, uri_to_path, split_path, find_files) -from tests import data_folder +from tests import path_to_data_dir class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): @@ -117,7 +117,7 @@ class SplitPathTest(unittest.TestCase): class FindFilesTest(unittest.TestCase): def find(self, path): - return list(find_files(data_folder(path))) + return list(find_files(path_to_data_dir(path))) def test_basic_folder(self): self.assert_(self.find('')) @@ -128,7 +128,7 @@ class FindFilesTest(unittest.TestCase): def test_file(self): files = self.find('blank.mp3') self.assertEqual(len(files), 1) - self.assert_(files[0], data_folder('blank.mp3')) + self.assert_(files[0], path_to_data_dir('blank.mp3')) def test_names_are_unicode(self): is_unicode = lambda f: isinstance(f, unicode) From 6708ab79deea311c2b5c93c3fe778e63d0200ea7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Apr 2011 21:16:31 +0200 Subject: [PATCH 115/127] Reduce log level from warning to info (fix #71) --- docs/changes.rst | 3 +++ mopidy/backends/spotify/playback.py | 2 +- mopidy/backends/spotify/translator.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index dbdc9d3b..b2cff585 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -40,6 +40,9 @@ No description yet. - Better error messages on wrong login or network problems. Thanks to Antoine Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`) + - Reduce log level for trivial log messages from warning to info. (Fixes: + :issue:`71`) + - Local backend: - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index a066d90e..69050eb8 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -23,7 +23,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.output.set_state('PLAYING') return True except SpotifyError as e: - logger.warning('Play %s failed: %s', track.uri, e) + logger.info('Playback of %s failed: %s', track.uri, e) return False def resume(self): diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index d81622f9..651154f8 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -60,5 +60,5 @@ class SpotifyTranslator(object): tracks=[cls.to_mopidy_track(t) for t in spotify_playlist], ) except SpotifyError, e: - logger.warning(u'Failed translating Spotify playlist ' + logger.info(u'Failed translating Spotify playlist ' '(probably a playlist folder boundary): %s', e) From 2a3b6f21330c37ebecffa8ecc746c537f2b1c3dc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Apr 2011 22:47:49 +0200 Subject: [PATCH 116/127] Replace symlinks with real files (fixes #68) --- tests/data/scanner/advanced/song1.mp3 | Bin 13 -> 9360 bytes tests/data/scanner/advanced/song2.mp3 | Bin 13 -> 9360 bytes tests/data/scanner/advanced/song3.mp3 | Bin 13 -> 9360 bytes tests/data/scanner/advanced/subdir1/song4.mp3 | Bin 16 -> 9360 bytes tests/data/scanner/advanced/subdir1/song5.mp3 | Bin 16 -> 9360 bytes .../advanced/subdir1/subsubdir/song8.mp3 | Bin 19 -> 9360 bytes .../advanced/subdir1/subsubdir/song9.mp3 | Bin 19 -> 9360 bytes tests/data/scanner/advanced/subdir2/song6.mp3 | Bin 16 -> 9360 bytes tests/data/scanner/advanced/subdir2/song7.mp3 | Bin 16 -> 9360 bytes tests/data/scanner/simple/song1.mp3 | Bin 13 -> 9360 bytes tests/data/song1.flac | Bin 10 -> 14691 bytes tests/data/song1.mp3 | Bin 9 -> 9360 bytes tests/data/song1.ogg | Bin 9 -> 8671 bytes tests/data/song1.wav | Bin 9 -> 35292 bytes tests/data/song2.flac | Bin 10 -> 14691 bytes tests/data/song2.mp3 | Bin 9 -> 9360 bytes tests/data/song2.ogg | Bin 9 -> 8671 bytes tests/data/song2.wav | Bin 9 -> 35292 bytes tests/data/song3.flac | Bin 10 -> 14691 bytes tests/data/song3.mp3 | Bin 9 -> 9360 bytes tests/data/song3.ogg | Bin 9 -> 8671 bytes tests/data/song3.wav | Bin 9 -> 35292 bytes 22 files changed, 0 insertions(+), 0 deletions(-) mode change 120000 => 100644 tests/data/scanner/advanced/song1.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/song2.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/song3.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir1/song4.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir1/song5.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir2/song6.mp3 mode change 120000 => 100644 tests/data/scanner/advanced/subdir2/song7.mp3 mode change 120000 => 100644 tests/data/scanner/simple/song1.mp3 mode change 120000 => 100644 tests/data/song1.flac mode change 120000 => 100644 tests/data/song1.mp3 mode change 120000 => 100644 tests/data/song1.ogg mode change 120000 => 100644 tests/data/song1.wav mode change 120000 => 100644 tests/data/song2.flac mode change 120000 => 100644 tests/data/song2.mp3 mode change 120000 => 100644 tests/data/song2.ogg mode change 120000 => 100644 tests/data/song2.wav mode change 120000 => 100644 tests/data/song3.flac mode change 120000 => 100644 tests/data/song3.mp3 mode change 120000 => 100644 tests/data/song3.ogg mode change 120000 => 100644 tests/data/song3.wav diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3 deleted file mode 120000 index 6896a7a2..00000000 --- a/tests/data/scanner/advanced/song1.mp3 +++ /dev/null @@ -1 +0,0 @@ -../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song1.mp3 b/tests/data/scanner/advanced/song1.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3 deleted file mode 120000 index 6896a7a2..00000000 --- a/tests/data/scanner/advanced/song2.mp3 +++ /dev/null @@ -1 +0,0 @@ -../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song2.mp3 b/tests/data/scanner/advanced/song2.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3 deleted file mode 120000 index 6896a7a2..00000000 --- a/tests/data/scanner/advanced/song3.mp3 +++ /dev/null @@ -1 +0,0 @@ -../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/song3.mp3 b/tests/data/scanner/advanced/song3.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3 deleted file mode 120000 index 45812ac5..00000000 --- a/tests/data/scanner/advanced/subdir1/song4.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song4.mp3 b/tests/data/scanner/advanced/subdir1/song4.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3 deleted file mode 120000 index 45812ac5..00000000 --- a/tests/data/scanner/advanced/subdir1/song5.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/song5.mp3 b/tests/data/scanner/advanced/subdir1/song5.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 deleted file mode 120000 index e84bdc24..00000000 --- a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 deleted file mode 120000 index e84bdc24..00000000 --- a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 b/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3 deleted file mode 120000 index 45812ac5..00000000 --- a/tests/data/scanner/advanced/subdir2/song6.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song6.mp3 b/tests/data/scanner/advanced/subdir2/song6.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3 deleted file mode 120000 index 45812ac5..00000000 --- a/tests/data/scanner/advanced/subdir2/song7.mp3 +++ /dev/null @@ -1 +0,0 @@ -../../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/advanced/subdir2/song7.mp3 b/tests/data/scanner/advanced/subdir2/song7.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3 deleted file mode 120000 index 6896a7a2..00000000 --- a/tests/data/scanner/simple/song1.mp3 +++ /dev/null @@ -1 +0,0 @@ -../sample.mp3 \ No newline at end of file diff --git a/tests/data/scanner/simple/song1.mp3 b/tests/data/scanner/simple/song1.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad5aa37a97b42325de1381c519052cf17693e7dc GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/tests/data/song1.flac b/tests/data/song1.flac deleted file mode 120000 index e5e7c129..00000000 --- a/tests/data/song1.flac +++ /dev/null @@ -1 +0,0 @@ -blank.flac \ No newline at end of file diff --git a/tests/data/song1.flac b/tests/data/song1.flac new file mode 100644 index 0000000000000000000000000000000000000000..ae18d36f2ba07805ab46b7aae5b6727491870b47 GIT binary patch literal 14691 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 literal 0 HcmV?d00001 diff --git a/tests/data/song1.mp3 b/tests/data/song1.mp3 deleted file mode 120000 index 03cdf66f..00000000 --- a/tests/data/song1.mp3 +++ /dev/null @@ -1 +0,0 @@ -blank.mp3 \ No newline at end of file diff --git a/tests/data/song1.mp3 b/tests/data/song1.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef159a700449f6a2bf4c03fc206be8f2ff1c7469 GIT binary patch literal 9360 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 i`}Ggczgz$M7U3;$Kn@+<3 literal 0 HcmV?d00001 diff --git a/tests/data/song1.ogg b/tests/data/song1.ogg deleted file mode 120000 index 33e773e1..00000000 --- a/tests/data/song1.ogg +++ /dev/null @@ -1 +0,0 @@ -blank.ogg \ No newline at end of file diff --git a/tests/data/song1.ogg b/tests/data/song1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e67e428b1f3e6f67839e34379d9ea6ff70c386a3 GIT binary patch literal 8671 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 literal 0 HcmV?d00001 diff --git a/tests/data/song1.wav b/tests/data/song1.wav deleted file mode 120000 index 72a38fad..00000000 --- a/tests/data/song1.wav +++ /dev/null @@ -1 +0,0 @@ -blank.wav \ No newline at end of file diff --git a/tests/data/song1.wav b/tests/data/song1.wav new file mode 100644 index 0000000000000000000000000000000000000000..0041c7ba42b9ae5499f8a73092f66c2a1e024fe1 GIT binary patch literal 35292 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 literal 0 HcmV?d00001 diff --git a/tests/data/song2.flac b/tests/data/song2.flac deleted file mode 120000 index e5e7c129..00000000 --- a/tests/data/song2.flac +++ /dev/null @@ -1 +0,0 @@ -blank.flac \ No newline at end of file diff --git a/tests/data/song2.flac b/tests/data/song2.flac new file mode 100644 index 0000000000000000000000000000000000000000..ae18d36f2ba07805ab46b7aae5b6727491870b47 GIT binary patch literal 14691 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 literal 0 HcmV?d00001 diff --git a/tests/data/song2.mp3 b/tests/data/song2.mp3 deleted file mode 120000 index 03cdf66f..00000000 --- a/tests/data/song2.mp3 +++ /dev/null @@ -1 +0,0 @@ -blank.mp3 \ No newline at end of file diff --git a/tests/data/song2.mp3 b/tests/data/song2.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef159a700449f6a2bf4c03fc206be8f2ff1c7469 GIT binary patch literal 9360 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 i`}Ggczgz$M7U3;$Kn@+<3 literal 0 HcmV?d00001 diff --git a/tests/data/song2.ogg b/tests/data/song2.ogg deleted file mode 120000 index 33e773e1..00000000 --- a/tests/data/song2.ogg +++ /dev/null @@ -1 +0,0 @@ -blank.ogg \ No newline at end of file diff --git a/tests/data/song2.ogg b/tests/data/song2.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e67e428b1f3e6f67839e34379d9ea6ff70c386a3 GIT binary patch literal 8671 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 literal 0 HcmV?d00001 diff --git a/tests/data/song2.wav b/tests/data/song2.wav deleted file mode 120000 index 72a38fad..00000000 --- a/tests/data/song2.wav +++ /dev/null @@ -1 +0,0 @@ -blank.wav \ No newline at end of file diff --git a/tests/data/song2.wav b/tests/data/song2.wav new file mode 100644 index 0000000000000000000000000000000000000000..0041c7ba42b9ae5499f8a73092f66c2a1e024fe1 GIT binary patch literal 35292 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 literal 0 HcmV?d00001 diff --git a/tests/data/song3.flac b/tests/data/song3.flac deleted file mode 120000 index e5e7c129..00000000 --- a/tests/data/song3.flac +++ /dev/null @@ -1 +0,0 @@ -blank.flac \ No newline at end of file diff --git a/tests/data/song3.flac b/tests/data/song3.flac new file mode 100644 index 0000000000000000000000000000000000000000..ae18d36f2ba07805ab46b7aae5b6727491870b47 GIT binary patch literal 14691 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 literal 0 HcmV?d00001 diff --git a/tests/data/song3.mp3 b/tests/data/song3.mp3 deleted file mode 120000 index 03cdf66f..00000000 --- a/tests/data/song3.mp3 +++ /dev/null @@ -1 +0,0 @@ -blank.mp3 \ No newline at end of file diff --git a/tests/data/song3.mp3 b/tests/data/song3.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ef159a700449f6a2bf4c03fc206be8f2ff1c7469 GIT binary patch literal 9360 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 i`}Ggczgz$M7U3;$Kn@+<3 literal 0 HcmV?d00001 diff --git a/tests/data/song3.ogg b/tests/data/song3.ogg deleted file mode 120000 index 33e773e1..00000000 --- a/tests/data/song3.ogg +++ /dev/null @@ -1 +0,0 @@ -blank.ogg \ No newline at end of file diff --git a/tests/data/song3.ogg b/tests/data/song3.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e67e428b1f3e6f67839e34379d9ea6ff70c386a3 GIT binary patch literal 8671 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 literal 0 HcmV?d00001 diff --git a/tests/data/song3.wav b/tests/data/song3.wav deleted file mode 120000 index 72a38fad..00000000 --- a/tests/data/song3.wav +++ /dev/null @@ -1 +0,0 @@ -blank.wav \ No newline at end of file diff --git a/tests/data/song3.wav b/tests/data/song3.wav new file mode 100644 index 0000000000000000000000000000000000000000..0041c7ba42b9ae5499f8a73092f66c2a1e024fe1 GIT binary patch literal 35292 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 literal 0 HcmV?d00001 From 833f311cf6d7cb94cd98bed84bada82090782876 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Apr 2011 22:53:27 +0200 Subject: [PATCH 117/127] Update changelog with symlink fix --- docs/changes.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index b2cff585..fe7b9927 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -69,6 +69,11 @@ No description yet. - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins Grunskis for the bug report and patch. (Fixes: :issue:`63`) +- Packaging: + + - Replace test data symlinks with real files to avoid symlink issues when + installing with pip. (Fixes: :issue:`68`) + 0.3.1 (2010-01-22) ================== From 7f5aa6ed199df2ee57ab4d88b3a801430f9736ee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 11 Apr 2011 23:01:20 +0200 Subject: [PATCH 118/127] Update GitHub issues links to match new URL scheme --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4587c16d..7ae3c126 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -202,4 +202,4 @@ latex_documents = [ needs_sphinx = '1.0' -extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues#issue/%s', 'GH-')} +extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')} From f20b3b26f6e0631b3ef799a8170da7bf6147dcec Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Apr 2011 15:02:40 +0200 Subject: [PATCH 119/127] Add platform and python information to startup logging (fixes #73) --- mopidy/__init__.py | 19 ++++++++++++++----- mopidy/core.py | 2 +- mopidy/utils/log.py | 6 ++++-- tests/version_test.py | 12 +++++++++++- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index e9ced3ae..1fbf99c8 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,3 +1,4 @@ +import platform import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') @@ -6,6 +7,12 @@ from subprocess import PIPE, Popen VERSION = (0, 4, 0) +def get_version(): + try: + return get_git_version() + except EnvironmentError: + return get_plain_version() + def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) if process.wait() != 0: @@ -18,11 +25,13 @@ def get_git_version(): def get_plain_version(): return '.'.join(map(str, VERSION)) -def get_version(): - try: - return get_git_version() - except EnvironmentError: - return get_plain_version() +def get_platform(): + return platform.platform() + +def get_python(): + implementation = platform.python_implementation() + version = platform.python_version() + return u' '.join([implementation, version]) class MopidyException(Exception): def __init__(self, message, *args, **kwargs): diff --git a/mopidy/core.py b/mopidy/core.py index a1c6b361..093f783d 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -29,7 +29,7 @@ def main(): ActorRegistry.stop_all() def parse_options(): - parser = optparse.OptionParser(version='Mopidy %s' % get_version()) + parser = optparse.OptionParser(version=u'Mopidy %s' % get_version()) parser.add_option('-q', '--quiet', action='store_const', const=0, dest='verbosity_level', help='less output (warning level)') diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index c74ff5ea..531b68b6 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -1,7 +1,8 @@ import logging import logging.handlers +import platform -from mopidy import get_version, settings +from mopidy import get_version, get_platform, get_python, settings def setup_logging(verbosity_level, save_debug_log): setup_root_logger() @@ -9,7 +10,8 @@ def setup_logging(verbosity_level, save_debug_log): if save_debug_log: setup_debug_logging_to_file() logger = logging.getLogger('mopidy.utils.log') - logger.info(u'-- Starting Mopidy %s --', get_version()) + logger.info(u'Starting Mopidy %s on %s %s', + get_version(), get_platform(), get_python()) def setup_root_logger(): root = logging.getLogger('') diff --git a/tests/version_test.py b/tests/version_test.py index f1f86b59..7f204283 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,7 +1,8 @@ from distutils.version import StrictVersion as SV import unittest +import platform -from mopidy import get_plain_version +from mopidy import get_version, get_plain_version, get_platform, get_python class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): @@ -18,3 +19,12 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.3.0') < SV('0.3.1')) self.assert_(SV('0.3.1') < SV(get_plain_version())) self.assert_(SV(get_plain_version()) < SV('0.4.1')) + + def test_get_platform_contains_platform(self): + self.assert_(platform.platform() in get_platform()) + + def test_get_python_contains_python_implementation(self): + self.assert_(platform.python_implementation() in get_python()) + + def test_get_python_contains_python_version(self): + self.assert_(platform.python_version() in get_python()) From 0b91b26910d2404ab9282bd2c6387aff7d77e1b7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Apr 2011 17:55:53 +0200 Subject: [PATCH 120/127] Check if creation of IPv6 sockets works before using it (fixes #75) --- mopidy/frontends/mpd/server.py | 18 ++++++++++++++++-- tests/frontends/mpd/server_test.py | 6 ++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 231bdf40..8507e266 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -9,6 +9,20 @@ from .session import MpdSession logger = logging.getLogger('mopidy.frontends.mpd.server') +def _try_ipv6_socket(): + """Determine if system really supports IPv6""" + if not socket.has_ipv6: + return False + try: + socket.socket(socket.AF_INET6).close() + return True + except IOError, e: + logger.debug(u'Platform supports IPv6, but socket ' + 'creation failed, disabling: %s', e) + return False + +has_ipv6 = _try_ipv6_socket() + class MpdServer(asyncore.dispatcher): """ The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession` @@ -21,7 +35,7 @@ class MpdServer(asyncore.dispatcher): def start(self): """Start MPD server.""" try: - if socket.has_ipv6: + if has_ipv6: self.create_socket(socket.AF_INET6, socket.SOCK_STREAM) # Explicitly configure socket to work for both IPv4 and IPv6 self.socket.setsockopt( @@ -53,7 +67,7 @@ class MpdServer(asyncore.dispatcher): self.close() def _format_hostname(self, hostname): - if (socket.has_ipv6 + if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % hostname return hostname diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py index ef963347..32e90450 100644 --- a/tests/frontends/mpd/server_test.py +++ b/tests/frontends/mpd/server_test.py @@ -10,20 +10,22 @@ class MpdServerTest(unittest.TestCase): self.backend = DummyBackend.start().proxy() self.mixer = DummyMixer.start().proxy() self.server = server.MpdServer() + self.has_ipv6 = server.has_ipv6 def tearDown(self): self.backend.stop().get() self.mixer.stop().get() + server.has_ipv6 = self.has_ipv6 def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): - server.socket.has_ipv6 = True + server.has_ipv6 = True self.assertEqual(self.server._format_hostname('0.0.0.0'), '::ffff:0.0.0.0') self.assertEqual(self.server._format_hostname('127.0.0.1'), '::ffff:127.0.0.1') def test_format_hostname_does_nothing_when_only_ipv4_available(self): - server.socket.has_ipv6 = False + server.has_ipv6 = False self.assertEquals(self.server._format_hostname('0.0.0.0'), '0.0.0.0') class MpdSessionTest(unittest.TestCase): From 3e0a04ab9138bc948ae8750325c9022a2e501ab1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 24 Apr 2011 01:25:10 +0200 Subject: [PATCH 121/127] Exit main thread if no actors are running --- mopidy/core.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index a1c6b361..5a83b1e6 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -23,9 +23,11 @@ def main(): setup_backend() setup_frontends() try: - time.sleep(10000*24*60*60) + while ActorRegistry.get_all(): + time.sleep(1) + logger.info(u'No actors left. Exiting...') except KeyboardInterrupt: - logger.info(u'Exiting...') + logger.info(u'User interrupt. Exiting...') ActorRegistry.stop_all() def parse_options(): From b5b2319ac1d83ebc5aad3a42baafb3560cdd2d11 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Apr 2011 17:57:04 +0200 Subject: [PATCH 122/127] Use uridecodebin for all playblack --- mopidy/backends/spotify/playback.py | 2 +- mopidy/outputs/gstreamer.py | 28 +++++++--------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/mopidy/backends/spotify/playback.py b/mopidy/backends/spotify/playback.py index a066d90e..b02c2d9f 100644 --- a/mopidy/backends/spotify/playback.py +++ b/mopidy/backends/spotify/playback.py @@ -20,7 +20,7 @@ class SpotifyPlaybackProvider(BasePlaybackProvider): self.backend.spotify.session.load( Link.from_string(track.uri).as_track()) self.backend.spotify.session.play(1) - self.backend.output.set_state('PLAYING') + self.backend.output.play_uri('appsrc://') return True except SpotifyError as e: logger.warning('Play %s failed: %s', track.uri, e) diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index 0596addb..a6d1e9dd 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -46,23 +46,9 @@ class GStreamerOutput(ThreadingActor, BaseOutput): pad = self.gst_pipeline.get_by_name('convert').get_pad('sink') - if settings.BACKENDS[0] == 'mopidy.backends.local.LocalBackend': - uri_bin = gst.element_factory_make('uridecodebin', 'uri') - uri_bin.connect('pad-added', self._process_new_pad, pad) - self.gst_pipeline.add(uri_bin) - else: - app_src = gst.element_factory_make('appsrc', 'appsrc') - app_src_caps = gst.Caps(""" - audio/x-raw-int, - endianness=(int)1234, - channels=(int)2, - width=(int)16, - depth=(int)16, - signed=(boolean)true, - rate=(int)44100""") - app_src.set_property('caps', app_src_caps) - self.gst_pipeline.add(app_src) - app_src.get_pad('src').link(pad) + uridecodebin = gst.element_factory_make('uridecodebin', 'uri') + uridecodebin.connect('pad-added', self._process_new_pad, pad) + self.gst_pipeline.add(uridecodebin) # Setup bus and message processor gst_bus = self.gst_pipeline.get_bus() @@ -98,12 +84,12 @@ class GStreamerOutput(ThreadingActor, BaseOutput): def deliver_data(self, caps_string, data): """Deliver audio data to be played""" - app_src = self.gst_pipeline.get_by_name('appsrc') + source = self.gst_pipeline.get_by_name('source') caps = gst.caps_from_string(caps_string) buffer_ = gst.Buffer(buffer(data)) buffer_.set_caps(caps) - app_src.set_property('caps', caps) - app_src.emit('push-buffer', buffer_) + source.set_property('caps', caps) + source.emit('push-buffer', buffer_) def end_of_data_stream(self): """ @@ -112,7 +98,7 @@ class GStreamerOutput(ThreadingActor, BaseOutput): We will get a GStreamer message when the stream playback reaches the token, and can then do any end-of-stream related tasks. """ - self.gst_pipeline.get_by_name('appsrc').emit('end-of-stream') + self.gst_pipeline.get_by_name('source').emit('end-of-stream') def get_position(self): try: From aa0c309d233334b33740131a67b392845e211cc8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Apr 2011 00:47:25 +0200 Subject: [PATCH 123/127] Ignore tracks without uri as they are probably local files --- mopidy/backends/spotify/translator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index d81622f9..64422485 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -57,7 +57,9 @@ class SpotifyTranslator(object): 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], + # FIXME if check on link is a hackish workaround for is_local + tracks=[cls.to_mopidy_track(t) for t in spotify_playlist + if str(Link.from_track(t, 0))], ) except SpotifyError, e: logger.warning(u'Failed translating Spotify playlist ' From 043338d3af4b532e649174148214e0d225407374 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 25 Apr 2011 15:14:00 +0200 Subject: [PATCH 124/127] Revert "Merge branch 'feature/multi-backend' into develop" This reverts commit c8639f48dac5c057b87c10ab8a271c129f8668ec, reversing changes made to 3e0a04ab9138bc948ae8750325c9022a2e501ab1. --- docs/api/outputs.rst | 1 - docs/modules/outputs/dummy.rst | 10 ---------- mopidy/backends/base/playback.py | 11 ----------- mopidy/backends/local/__init__.py | 14 +++++++++++++- mopidy/outputs/base.py | 2 +- mopidy/outputs/dummy.py | 2 +- mopidy/outputs/gstreamer.py | 15 --------------- tests/frontends/mpd/music_db_test.py | 2 ++ 8 files changed, 17 insertions(+), 40 deletions(-) delete mode 100644 docs/modules/outputs/dummy.rst diff --git a/docs/api/outputs.rst b/docs/api/outputs.rst index 0650563e..5ef1606d 100644 --- a/docs/api/outputs.rst +++ b/docs/api/outputs.rst @@ -17,5 +17,4 @@ Outputs are responsible for playing audio. Output implementations ====================== -* :mod:`mopidy.outputs.dummy` * :mod:`mopidy.outputs.gstreamer` diff --git a/docs/modules/outputs/dummy.rst b/docs/modules/outputs/dummy.rst deleted file mode 100644 index 56436c94..00000000 --- a/docs/modules/outputs/dummy.rst +++ /dev/null @@ -1,10 +0,0 @@ -******************************************************* -:mod:`mopidy.outputs.dummy` -- Dummy output for testing -******************************************************* - -.. inheritance-diagram:: mopidy.outputs.dummy - -.. automodule:: mopidy.outputs.dummy - :synopsis: Dummy output for testing - :members: - :undoc-members: diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 88aa0877..88ae141d 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -5,7 +5,6 @@ import time from pykka.registry import ActorRegistry from mopidy.frontends.base import BaseFrontend -from mopidy.outputs.base import BaseOutput logger = logging.getLogger('mopidy.backends.base') @@ -289,9 +288,6 @@ class PlaybackController(object): @property def time_position(self): """Time position in milliseconds.""" - output_position = self._time_position_from_output() - if output_position is not None: - return output_position if self.state == self.PLAYING: time_since_started = (self._current_wall_time - self.play_time_started) @@ -301,13 +297,6 @@ class PlaybackController(object): elif self.state == self.STOPPED: return 0 - def _time_position_from_output(self): - output_refs = ActorRegistry.get_by_class(BaseOutput) - if not output_refs: - return None - output = output_refs[0].proxy() - return output.get_position() - def _play_time_start(self): self.play_time_accumulated = 0 self.play_time_started = self._current_wall_time diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index fc7f170c..2fa96dab 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -41,7 +41,7 @@ class LocalBackend(ThreadingActor, Backend): provider=library_provider) playback_provider = LocalPlaybackProvider(backend=self) - self.playback = PlaybackController(backend=self, + self.playback = LocalPlaybackController(backend=self, provider=playback_provider) stored_playlists_provider = LocalStoredPlaylistsProvider(backend=self) @@ -58,6 +58,18 @@ class LocalBackend(ThreadingActor, Backend): self.output = output_refs[0].proxy() +class LocalPlaybackController(PlaybackController): + def __init__(self, *args, **kwargs): + super(LocalPlaybackController, self).__init__(*args, **kwargs) + + # XXX Why do we call stop()? Is it to set GStreamer state to 'READY'? + self.stop() + + @property + def time_position(self): + return self.backend.output.get_position().get() + + class LocalPlaybackProvider(BasePlaybackProvider): def pause(self): return self.backend.output.set_state('PAUSED').get() diff --git a/mopidy/outputs/base.py b/mopidy/outputs/base.py index 11c2f86e..fbc86688 100644 --- a/mopidy/outputs/base.py +++ b/mopidy/outputs/base.py @@ -40,7 +40,7 @@ class BaseOutput(object): *MUST be implemented by subclass.* - :rtype: int or :class:`None` if unknown + :rtype: int """ raise NotImplementedError diff --git a/mopidy/outputs/dummy.py b/mopidy/outputs/dummy.py index 02f1bfca..f09965f7 100644 --- a/mopidy/outputs/dummy.py +++ b/mopidy/outputs/dummy.py @@ -25,7 +25,7 @@ class DummyOutput(ThreadingActor, BaseOutput): end_of_data_stream_called = False #: For testing. Contains the current position. - position = None + position = 0 #: For testing. Contains the current state. state = 'NULL' diff --git a/mopidy/outputs/gstreamer.py b/mopidy/outputs/gstreamer.py index d7ff6e6d..0596addb 100644 --- a/mopidy/outputs/gstreamer.py +++ b/mopidy/outputs/gstreamer.py @@ -165,18 +165,3 @@ class GStreamerOutput(ThreadingActor, BaseOutput): gst_volume = self.gst_pipeline.get_by_name('volume') gst_volume.set_property('volume', volume / 100.0) return True - - def set_position(self, position): - self.gst_pipeline.get_state() # block until state changes are done - handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME), - gst.SEEK_FLAG_FLUSH, position * gst.MSECOND) - self.gst_pipeline.get_state() # block until seek is done - return handeled - - def get_position(self): - try: - position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0] - return position // gst.MSECOND - except gst.QueryError, e: - logger.debug(u'GStreamer time position: %s', e) - return None diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py index 28469136..fa5634be 100644 --- a/tests/frontends/mpd/music_db_test.py +++ b/tests/frontends/mpd/music_db_test.py @@ -386,3 +386,5 @@ class MusicDatabaseSearchTest(unittest.TestCase): def test_search_else_should_fail(self): result = self.h.handle_request(u'search "sometype" "something"') self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments') + + From a7e6596578dfaaff3705c5cade28653debd35f7f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Apr 2011 23:07:36 +0200 Subject: [PATCH 125/127] Pause playback on Spoticy connection errors (fixes #65) --- docs/changes.rst | 2 ++ mopidy/backends/spotify/session_manager.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index fe7b9927..d00bde97 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -43,6 +43,8 @@ No description yet. - Reduce log level for trivial log messages from warning to info. (Fixes: :issue:`71`) + - Pause playback on network connection errors. (Fixes: :issue:`65`) + - Local backend: - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index e92fe89e..395f3f28 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -74,7 +74,11 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def connection_error(self, session, error): """Callback used by pyspotify""" - logger.error(u'Spotify connection error: %s', error) + if error is None: + logger.info(u'Spotify connection error resolved') + else: + logger.error(u'Spotify connection error: %s', error) + self.backend.playback.pause() def message_to_user(self, session, message): """Callback used by pyspotify""" From f35e1348bc71cbe9ed94a67960ff6222afc81742 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Apr 2011 23:24:23 +0200 Subject: [PATCH 126/127] Update changelog with fixes from @adamcik --- docs/changes.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index d00bde97..ecc474c3 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -30,7 +30,7 @@ No description yet. - Mopidy now use Pykka actors for thread management and inter-thread communication. The immediate advantage of this is that Mopidy now works on - Python 2.7. (Fixes: :issue:`66`) + Python 2.7, which is the default on e.g. Ubuntu 11.04. (Fixes: :issue:`66`) - Spotify backend: @@ -66,6 +66,14 @@ No description yet. - Fix bug where ``status`` returned ``song: None``, which caused MPDroid to crash. (Fixes: :issue:`69`) + - Gracefully fallback to IPv4 sockets on systems that supports IPv6, but has + turned it off. (Fixes: :issue:`75`) + +- GStreamer output: + + - Use ``uridecodebin`` for playing audio from both Spotify and the local + backend. This contributes to support for multiple backends simultaneously. + - Settings: - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins @@ -76,6 +84,11 @@ No description yet. - Replace test data symlinks with real files to avoid symlink issues when installing with pip. (Fixes: :issue:`68`) +- Debugging: + + - Include platform, architecture, Linux distribution, and Python version in + the debug log, to ease debugging of issues with attached debug logs. + 0.3.1 (2010-01-22) ================== From b8a8da1cdc7e854cd2c4e6ba4f922156675e38ec Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Apr 2011 23:47:38 +0200 Subject: [PATCH 127/127] Add description of 0.4.0 release --- docs/changes.rst | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index ecc474c3..5a7db6ad 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,10 +5,24 @@ Changes This change log is used to track all major changes to Mopidy. -0.4.0 (in development) -====================== +0.4.0 (2011-04-27) +================== -No description yet. +Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've +fixed a bunch of issues and bugs, with the help of several new contributors +who are credited in the changelog below. The major change of 0.4.0 is an +internal refactoring which clears way for future features, and which also make +Mopidy work on Python 2.7. In other words, Mopidy 0.4.0 works on Ubuntu 11.04 +and Arch Linux. + +Please note that 0.4.0 requires some updated dependencies, as listed under +*Important changes* below. Also, the known bug in the Spotify playlist +loading from Mopidy 0.3.0 is still present. + +.. warning:: Known bug in Spotify playlist loading + + There is a known bug in the loading of Spotify playlists. To avoid the bug, + follow the simple workaround described at :issue:`59`. **Important changes**