From e91b39c38b3a19dbe7c8b4ef442397c6e00606dc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Oct 2010 14:06:44 +0200 Subject: [PATCH 001/350] Add MprisFrontend that connects to D-Bus' SystemBus --- mopidy/frontends/mpris.py | 67 +++++++++++++++++++++++++++++++++++++++ mopidy/settings.py | 1 + 2 files changed, 68 insertions(+) create mode 100644 mopidy/frontends/mpris.py diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py new file mode 100644 index 00000000..95521d10 --- /dev/null +++ b/mopidy/frontends/mpris.py @@ -0,0 +1,67 @@ +import logging +import multiprocessing +import socket +import time + +try: + import dbus +except ImportError as import_error: + from mopidy import OptionalDependencyError + raise OptionalDependencyError(import_error) + +from mopidy import get_version, settings, SettingsError +from mopidy.frontends.base import BaseFrontend +from mopidy.utils.process import BaseThread + +logger = logging.getLogger('mopidy.frontends.mpris') + +BUS_NAME = u'org.mpris.MediaPlayer2.mopidy' + +class MprisFrontend(BaseFrontend): + """ + Frontend which lets you control Mopidy through the Media Player Remote + Interfacing Specification (MPRIS) D-Bus interface. + + An example of an MPRIS client is Ubuntu's audio indicator applet. + + **Dependencies:** + + - ``dbus`` Python bindings. The package is named ``python-dbus`` in + Ubuntu/Debian. + """ + + def __init__(self, *args, **kwargs): + super(MprisFrontend, self).__init__(*args, **kwargs) + (self.connection, other_end) = multiprocessing.Pipe() + self.thread = MprisFrontendThread(self.core_queue, other_end) + + def start(self): + self.thread.start() + + def destroy(self): + self.thread.destroy() + + def process_message(self, message): + self.connection.send(message) + + +class MprisFrontendThread(BaseThread): + def __init__(self, core_queue, connection): + super(MprisFrontendThread, self).__init__(core_queue) + self.name = u'MprisFrontendThread' + self.connection = connection + self.bus = None + + def run_inside_try(self): + self.setup() + while True: + self.connection.poll(None) + message = self.connection.recv() + self.process_message(message) + + def setup(self): + self.bus = dbus.SystemBus() + logger.info(u'Connected to D-Bus/MPRIS') + + def process_message(self, message): + pass # Ignore commands for other frontends diff --git a/mopidy/settings.py b/mopidy/settings.py index c9d7b9fc..eb3a3874 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -52,6 +52,7 @@ DEBUG_LOG_FILENAME = u'mopidy.log' FRONTENDS = ( u'mopidy.frontends.mpd.MpdFrontend', u'mopidy.frontends.lastfm.LastfmFrontend', + u'mopidy.frontends.mpris.MprisFrontend', ) #: Which GStreamer audio sink to use in :mod:`mopidy.outputs.gstreamer`. From 54f294879d9113961ab0781f71105682513e07b6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Oct 2010 23:05:46 +0200 Subject: [PATCH 002/350] Do not require 'readon' and 'status' fields on 'exit' messages --- mopidy/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 69760094..701c8144 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -100,8 +100,8 @@ class CoreProcess(BaseThread): 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: + if message.get('reason') is not None: logger.info(u'Exiting (%s)', message['reason']) - sys.exit(message['status']) + sys.exit(message.get('status', 0)) else: logger.warning(u'Cannot handle message: %s', message) From a8b872540352954d39beb87dc1c08cfa17737523 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Oct 2010 23:06:09 +0200 Subject: [PATCH 003/350] Add full support for org.mpris.MediaPlayer2 interface (not including TrackList and Player) --- mopidy/frontends/mpris.py | 123 +++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 95521d10..3e31ca9d 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -1,21 +1,22 @@ +import gobject +gobject.threads_init() + import logging import multiprocessing -import socket -import time try: import dbus + import dbus.service + from dbus.mainloop.glib import DBusGMainLoop except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) -from mopidy import get_version, settings, SettingsError from mopidy.frontends.base import BaseFrontend from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.mpris') -BUS_NAME = u'org.mpris.MediaPlayer2.mopidy' class MprisFrontend(BaseFrontend): """ @@ -50,7 +51,12 @@ class MprisFrontendThread(BaseThread): super(MprisFrontendThread, self).__init__(core_queue) self.name = u'MprisFrontendThread' self.connection = connection - self.bus = None + self.dbus_objects = [] + + def destroy(self): + for dbus_object in self.dbus_objects: + dbus_object.remove_from_connection() + self.dbus_objects = [] def run_inside_try(self): self.setup() @@ -60,8 +66,111 @@ class MprisFrontendThread(BaseThread): self.process_message(message) def setup(self): - self.bus = dbus.SystemBus() - logger.info(u'Connected to D-Bus/MPRIS') + self.dbus_objects.append(MprisObject(self.core_queue)) + + # TODO Move to another thread if we need to process messages + logger.debug(u'Starting GLib main loop') + loop = gobject.MainLoop() + loop.run() def process_message(self, message): pass # Ignore commands for other frontends + + +class MprisObject(dbus.service.Object): + """Implements http://www.mpris.org/2.0/spec/""" + + bus_name = 'org.mpris.MediaPlayer2.mopidy' + object_path = '/org/mpris/MediaPlayer2' + property_interface = 'org.freedesktop.DBus.Properties' + root_interface = 'org.mpris.MediaPlayer2' + player_interface = 'org.mpris.MediaPlayer2.Player' + properties = { + root_interface: { + 'CanQuit': (True, None), + 'CanRaise': (False, None), + # TODO Add track list support + 'HasTrackList': (False, None), + 'Identity': ('Mopidy', None), + # TODO Return URI schemes supported by backend configuration + 'SupportedUriSchemes': (dbus.Array(signature='s'), None), + # TODO Return MIME types supported by local backend if active + 'SupportedMimeTypes': (dbus.Array(signature='s'), None), + }, + player_interface: { + # TODO + }, + } + + def __init__(self, core_queue): + self.core_queue = core_queue + logger.debug(u'Prepare the D-Bus main loop before connecting') + DBusGMainLoop(set_as_default=True) + logger.info(u'Connecting to D-Bus') + bus = dbus.SessionBus() + logger.debug(u'Connecting to D-Bus: claiming service name') + # FIXME We segfault at the next line 80% of the time + bus_name = dbus.service.BusName(self.bus_name, bus) + logger.debug(u'Connecting to D-Bus: registering service object') + super(MprisObject, self).__init__(object_path=self.object_path, + bus_name=bus_name) + logger.debug(u'Connecting to D-Bus: done') + + + ### Property interface + + @dbus.service.method(dbus_interface=property_interface, + in_signature='ss', out_signature='v') + def Get(self, interface, prop): + getter, setter = self.properties[interface][prop] + return getter() if callable(getter) else getter + + @dbus.service.method(dbus_interface=property_interface, + in_signature='s', out_signature='a{sv}') + def GetAll(self, interface): + """ + To test, start Mopidy and then run the following in a Python shell:: + + import dbus + bus = dbus.SessionBus() + player = bus.get_object('org.mpris.MediaPlayer2.mopidy', + '/org/mpris/MediaPlayer2') + props = player.GetAll('org.mpris.MediaPlayer2', + dbus_interface='org.freedesktop.DBus.Properties') + """ + getters = {} + for key, (getter, setter) in self.properties[interface].iteritems(): + getters[key] = getter() if callable(getter) else getter + return getters + + @dbus.service.method(dbus_interface=property_interface, + in_signature='ssv', out_signature='') + def Set(self, interface, prop, value): + getter, setter = self.properties[interface][prop] + if setter is not None: + setter(value) + + + ### Root interface + + @dbus.service.method(dbus_interface=root_interface) + def Raise(self): + pass # We do not have a GUI + + @dbus.service.method(dbus_interface=root_interface) + def Quit(self): + """ + To test, start Mopidy and then run the following in a Python shell:: + + import dbus + bus = dbus.SessionBus() + player = bus.get_object('org.mpris.MediaPlayer2.mopidy', + '/org/mpris/MediaPlayer2') + player.Quit(dbus_interface='org.mpris.MediaPlayer2') + """ + self.core_queue.put({'to': 'core', 'command': 'exit'}) + + + ### Player interface + + # TODO From 46088b5ae4c130fe2290e7aed1dc7ba3fcfa97c4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 15:02:18 +0200 Subject: [PATCH 004/350] Run glib.threads_init() in addition to gobject.threads_init() --- mopidy/frontends/mpris.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 3e31ca9d..6c1e4c16 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -7,7 +7,8 @@ import multiprocessing try: import dbus import dbus.service - from dbus.mainloop.glib import DBusGMainLoop + from dbus.mainloop.glib import DBusGMainLoop, threads_init + threads_init() except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) From 467785b3c3ce309c963c93fbdfc8c2542fac6aeb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 16:38:28 +0200 Subject: [PATCH 005/350] Add shell for player interface's properties --- mopidy/frontends/mpris.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 6c1e4c16..4278d70f 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -94,12 +94,40 @@ class MprisObject(dbus.service.Object): 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), # TODO Return URI schemes supported by backend configuration - 'SupportedUriSchemes': (dbus.Array(signature='s'), None), + 'SupportedUriSchemes': (dbus.Array([], signature='s'), None), # TODO Return MIME types supported by local backend if active - 'SupportedMimeTypes': (dbus.Array(signature='s'), None), + 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), }, player_interface: { - # TODO + # TODO Get backend.playback.state + 'PlaybackStatus': ('Stopped', None), + # TODO Get/set loop status + 'LoopStatus': ('None', None), + 'Rate': (1.0, None), + # TODO Get/set backend.playback.random + 'Shuffle': (False, None), + # TODO Get meta data + 'Metadata': ({ + 'mpris:trackid': '', # TODO Use (cpid, track.uri) + }, None), + # TODO Get/set volume + 'Volume': (1.0, None), + # TODO Get backend.playback.time_position + 'Position': (0, None), + 'MinimumRate': (1.0, None), + 'MaximumRate': (1.0, None), + # TODO True if CanControl and backend.playback.track_at_next + 'CanGoNext': (False, None), + # TODO True if CanControl and backend.playback.track_at_previous + 'CanGoPrevious': (False, None), + # TODO True if CanControl and backend.playback.current_track + 'CanPlay': (False, None), + # TODO True if CanControl and backend.playback.current_track + 'CanPause': (False, None), + # TODO Set to True when the rest is implemented + 'CanSeek': (False, None), + # TODO Set to True when the rest is implemented + 'CanControl': (False, None), }, } From d1397baa2cfecf5a8bb4698531149a39f8c77e57 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 18:07:26 +0200 Subject: [PATCH 006/350] Add link to Ubuntu's sound menu docs --- mopidy/frontends/mpris.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 4278d70f..07473f20 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -24,7 +24,8 @@ class MprisFrontend(BaseFrontend): Frontend which lets you control Mopidy through the Media Player Remote Interfacing Specification (MPRIS) D-Bus interface. - An example of an MPRIS client is Ubuntu's audio indicator applet. + An example of an MPRIS client is `Ubuntu's sound menu + `_. **Dependencies:** From a86b4d3313c4bc4245dddc66123fec047ebbbcdb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 18:09:18 +0200 Subject: [PATCH 007/350] Trigger PropertiesChanged signal --- mopidy/frontends/mpris.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 07473f20..e00d3e0e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -84,7 +84,7 @@ class MprisObject(dbus.service.Object): bus_name = 'org.mpris.MediaPlayer2.mopidy' object_path = '/org/mpris/MediaPlayer2' - property_interface = 'org.freedesktop.DBus.Properties' + properties_interface = 'org.freedesktop.DBus.Properties' root_interface = 'org.mpris.MediaPlayer2' player_interface = 'org.mpris.MediaPlayer2.Player' properties = { @@ -149,13 +149,13 @@ class MprisObject(dbus.service.Object): ### Property interface - @dbus.service.method(dbus_interface=property_interface, + @dbus.service.method(dbus_interface=properties_interface, in_signature='ss', out_signature='v') def Get(self, interface, prop): getter, setter = self.properties[interface][prop] return getter() if callable(getter) else getter - @dbus.service.method(dbus_interface=property_interface, + @dbus.service.method(dbus_interface=properties_interface, in_signature='s', out_signature='a{sv}') def GetAll(self, interface): """ @@ -173,12 +173,20 @@ class MprisObject(dbus.service.Object): getters[key] = getter() if callable(getter) else getter return getters - @dbus.service.method(dbus_interface=property_interface, + @dbus.service.method(dbus_interface=properties_interface, in_signature='ssv', out_signature='') def Set(self, interface, prop, value): getter, setter = self.properties[interface][prop] if setter is not None: setter(value) + self.PropertiesChanged(interface, + {prop: self.Get(interface, prop)}, []) + + @dbus.service.signal(dbus_interface=properties_interface, + signature='sa{sv}as') + def PropertiesChanged(self, interface, changed_properties, + invalidated_properties): + pass ### Root interface From 9f00d467c3478db7671ee4fd486ad9d1ced3a862 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 30 Oct 2010 18:09:52 +0200 Subject: [PATCH 008/350] Add the shell of the player interface with pseudo code --- mopidy/frontends/mpris.py | 68 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index e00d3e0e..a0709a87 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -211,4 +211,70 @@ class MprisObject(dbus.service.Object): ### Player interface - # TODO + @dbus.service.method(dbus_interface=player_interface) + def Next(self): + # TODO call playback.next(), keep playback.state unchanged + pass + + @dbus.service.method(dbus_interface=player_interface) + def OpenUri(self, uri): + # TODO Pseudo code: + # if uri.scheme not in SupportedUriSchemes: return + # if uri.mime_type not in SupportedMimeTypes: return + # track = library.lookup(uri) + # cp_track = current_playlist.add(track) + # playback.play(cp_track) + pass + + @dbus.service.method(dbus_interface=player_interface) + def Pause(self): + # TODO call playback.pause() + pass + + @dbus.service.method(dbus_interface=player_interface) + def Play(self): + # TODO Pseudo code: + # if playback.state == playback.PAUSED: playback.resume() + # elif playback.state == playback.STOPPED: playback.play() + pass + + @dbus.service.method(dbus_interface=player_interface) + def PlayPause(self): + # TODO Pseudo code: + # if playback.state == playback.PLAYING: playback.pause() + # elif playback.state == playback.PAUSED: playback.resume() + # elif playback.state == playback.STOPPED: playback.play() + pass + + @dbus.service.method(dbus_interface=player_interface) + def Previous(self): + # TODO call playback.previous(), keep playback.state unchanged + pass + + @dbus.service.method(dbus_interface=player_interface) + def Seek(self, offset): + # TODO Pseudo code: + # new_position = playback.time_position + offset + # if new_position > playback.current_track.length: + # playback.next() + # return + # if new_position < 0: new_position = 0 + # playback.seek(new_position) + pass + + @dbus.service.method(dbus_interface=player_interface) + def SetPosition(self, track_id, position): + # TODO Pseudo code: + # if track_id != playback.current_track.track_id: return + # if not 0 <= position <= playback.current_track.length: return + # playback.seek(position) + pass + + @dbus.service.method(dbus_interface=player_interface) + def Stop(self): + # TODO call playback.stop() + pass + + @dbus.service.signal(dbus_interface=player_interface, signature='x') + def Seeked(self, position): + pass From 3d6ce8e878413298d02cd236f046771b5b197962 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:07:36 +0200 Subject: [PATCH 009/350] Send libindicate notifications when Mopidy starts and quits --- data/mopidy.desktop | 1 + mopidy/frontends/mpris.py | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/data/mopidy.desktop b/data/mopidy.desktop index f5ca43bb..f1383cdb 100644 --- a/data/mopidy.desktop +++ b/data/mopidy.desktop @@ -8,3 +8,4 @@ TryExec=mopidy Exec=mopidy Terminal=true Categories=AudioVideo;Audio;Player;ConsoleOnly +StartupNotify=true diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index a0709a87..cb18e6e4 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -53,6 +53,7 @@ class MprisFrontendThread(BaseThread): super(MprisFrontendThread, self).__init__(core_queue) self.name = u'MprisFrontendThread' self.connection = connection + self.indicate_server = None self.dbus_objects = [] def destroy(self): @@ -62,6 +63,10 @@ class MprisFrontendThread(BaseThread): def run_inside_try(self): self.setup() + # TODO Move to another thread if we need to process messages + logger.debug(u'Starting GLib main loop') + loop = gobject.MainLoop() + loop.run() while True: self.connection.poll(None) message = self.connection.recv() @@ -69,11 +74,27 @@ class MprisFrontendThread(BaseThread): def setup(self): self.dbus_objects.append(MprisObject(self.core_queue)) + self.send_startup_notification() - # TODO Move to another thread if we need to process messages - logger.debug(u'Starting GLib main loop') - loop = gobject.MainLoop() - loop.run() + def send_startup_notification(self): + """ + Send startup notification using libindicate to make Mopidy appear in + e.g. `Ubuntu's sound menu `_. + + A reference to the libindicate server is kept for as long as Mopidy is + running. When Mopidy exits, the server will be unreferenced and Mopidy + will automatically be unregistered from e.g. the sound menu. + """ + try: + import indicate + self.indicate_server = indicate.Server() + self.indicate_server.set_type('music.mopidy') + # FIXME Location of .desktop file shouldn't be hardcoded + self.indicate_server.set_desktop_file( + '/usr/local/share/applications/mopidy.desktop') + self.indicate_server.show() + except ImportError: + pass def process_message(self, message): pass # Ignore commands for other frontends From bca750a5e87b3e7bc01307fca9bd3de0663b1c79 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:09:29 +0200 Subject: [PATCH 010/350] Cleanup log messages --- mopidy/frontends/mpris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index cb18e6e4..5832b665 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -157,7 +157,7 @@ class MprisObject(dbus.service.Object): self.core_queue = core_queue logger.debug(u'Prepare the D-Bus main loop before connecting') DBusGMainLoop(set_as_default=True) - logger.info(u'Connecting to D-Bus') + logger.debug(u'Connecting to D-Bus: getting session bus') bus = dbus.SessionBus() logger.debug(u'Connecting to D-Bus: claiming service name') # FIXME We segfault at the next line 80% of the time @@ -165,7 +165,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'Connecting to D-Bus: registering service object') super(MprisObject, self).__init__(object_path=self.object_path, bus_name=bus_name) - logger.debug(u'Connecting to D-Bus: done') + logger.info(u'Connected to D-Bus') ### Property interface From b0b129b78261663a7e9f8ff10c82200e005cda2c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:27:17 +0200 Subject: [PATCH 011/350] Log on all calls to dbus service methods --- mopidy/frontends/mpris.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 5832b665..93a2adee 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -173,6 +173,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=properties_interface, in_signature='ss', out_signature='v') def Get(self, interface, prop): + logger.debug(u'%s.Get called', self.properties_interface) getter, setter = self.properties[interface][prop] return getter() if callable(getter) else getter @@ -189,6 +190,7 @@ class MprisObject(dbus.service.Object): props = player.GetAll('org.mpris.MediaPlayer2', dbus_interface='org.freedesktop.DBus.Properties') """ + logger.debug(u'%s.GetAll called', self.properties_interface) getters = {} for key, (getter, setter) in self.properties[interface].iteritems(): getters[key] = getter() if callable(getter) else getter @@ -197,6 +199,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=properties_interface, in_signature='ssv', out_signature='') def Set(self, interface, prop, value): + logger.debug(u'%s.Set called', self.properties_interface) getter, setter = self.properties[interface][prop] if setter is not None: setter(value) @@ -207,6 +210,8 @@ class MprisObject(dbus.service.Object): signature='sa{sv}as') def PropertiesChanged(self, interface, changed_properties, invalidated_properties): + logger.debug(u'%s.PropertiesChanged signaled', + self.properties_interface) pass @@ -214,6 +219,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=root_interface) def Raise(self): + logger.debug(u'%s.Raise called', self.root_interface) pass # We do not have a GUI @dbus.service.method(dbus_interface=root_interface) @@ -227,6 +233,7 @@ class MprisObject(dbus.service.Object): '/org/mpris/MediaPlayer2') player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ + logger.debug(u'%s.Quit called', self.root_interface) self.core_queue.put({'to': 'core', 'command': 'exit'}) @@ -234,11 +241,13 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def Next(self): + logger.debug(u'%s.Next called', self.player_interface) # TODO call playback.next(), keep playback.state unchanged pass @dbus.service.method(dbus_interface=player_interface) def OpenUri(self, uri): + logger.debug(u'%s.OpenUri called', self.player_interface) # TODO Pseudo code: # if uri.scheme not in SupportedUriSchemes: return # if uri.mime_type not in SupportedMimeTypes: return @@ -249,11 +258,13 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def Pause(self): + logger.debug(u'%s.Pause called', self.player_interface) # TODO call playback.pause() pass @dbus.service.method(dbus_interface=player_interface) def Play(self): + logger.debug(u'%s.Play called', self.player_interface) # TODO Pseudo code: # if playback.state == playback.PAUSED: playback.resume() # elif playback.state == playback.STOPPED: playback.play() @@ -261,6 +272,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def PlayPause(self): + logger.debug(u'%s.PlayPause called', self.player_interface) # TODO Pseudo code: # if playback.state == playback.PLAYING: playback.pause() # elif playback.state == playback.PAUSED: playback.resume() @@ -269,11 +281,13 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def Previous(self): + logger.debug(u'%s.Previous called', self.player_interface) # TODO call playback.previous(), keep playback.state unchanged pass @dbus.service.method(dbus_interface=player_interface) def Seek(self, offset): + logger.debug(u'%s.Seek called', self.player_interface) # TODO Pseudo code: # new_position = playback.time_position + offset # if new_position > playback.current_track.length: @@ -285,6 +299,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def SetPosition(self, track_id, position): + logger.debug(u'%s.SetPosition called', self.player_interface) # TODO Pseudo code: # if track_id != playback.current_track.track_id: return # if not 0 <= position <= playback.current_track.length: return @@ -293,9 +308,11 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def Stop(self): + logger.debug(u'%s.Stop called', self.player_interface) # TODO call playback.stop() pass @dbus.service.signal(dbus_interface=player_interface, signature='x') def Seeked(self, position): + logger.debug(u'%s.Seeked signaled', self.player_interface) pass From ac85936a9b0050839bbb848c7857134e3144a521 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 00:49:13 +0200 Subject: [PATCH 012/350] Reuse the common GObjectEventThread in the MPRIS frontend --- mopidy/frontends/mpris.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 93a2adee..41c0118f 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -1,6 +1,3 @@ -import gobject -gobject.threads_init() - import logging import multiprocessing @@ -49,6 +46,13 @@ class MprisFrontend(BaseFrontend): class MprisFrontendThread(BaseThread): + """ + A process for communicating with MPRIS clients. + + This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be + running too. This is not enforced in any way by the code. + """ + def __init__(self, core_queue, connection): super(MprisFrontendThread, self).__init__(core_queue) self.name = u'MprisFrontendThread' @@ -63,10 +67,6 @@ class MprisFrontendThread(BaseThread): def run_inside_try(self): self.setup() - # TODO Move to another thread if we need to process messages - logger.debug(u'Starting GLib main loop') - loop = gobject.MainLoop() - loop.run() while True: self.connection.poll(None) message = self.connection.recv() From 66cc9d8c6f91378fadbbc3e40fe4397e43b7b757 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:04:37 +0200 Subject: [PATCH 013/350] Allow reply_to to not be set in messages to the MPD frontend --- mopidy/frontends/mpd/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index ce9abc6d..8cf3d756 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -42,7 +42,8 @@ class MpdFrontend(BaseFrontend): 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) + if 'reply_to' in message: + connection = unpickle_connection(message['reply_to']) + connection.send(response) else: pass # Ignore messages for other frontends From 208435cd79817fe436d7680cd1cbc5ce05219ae3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Oct 2010 01:06:00 +0200 Subject: [PATCH 014/350] Made play/next/prev in Ubuntu's Sound Menu work by wiring the MPRIS methods to their MPD frontend equivalents #thisisatemporaryhacktoprovethatitworks #ipromise --- mopidy/frontends/mpris.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 41c0118f..55d96b87 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -245,6 +245,13 @@ class MprisObject(dbus.service.Object): # TODO call playback.next(), keep playback.state unchanged pass + # XXX Proof of concept only. Throw away, write tests, reimplement: + self.core_queue.put({ + 'to': 'frontend', + 'command': 'mpd_request', + 'request': 'next', + }) + @dbus.service.method(dbus_interface=player_interface) def OpenUri(self, uri): logger.debug(u'%s.OpenUri called', self.player_interface) @@ -273,17 +280,31 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def PlayPause(self): logger.debug(u'%s.PlayPause called', self.player_interface) + # TODO Pseudo code: # if playback.state == playback.PLAYING: playback.pause() # elif playback.state == playback.PAUSED: playback.resume() # elif playback.state == playback.STOPPED: playback.play() - pass + + # XXX Proof of concept only. Throw away, write tests, reimplement: + self.core_queue.put({ + 'to': 'frontend', + 'command': 'mpd_request', + 'request': 'play', + }) @dbus.service.method(dbus_interface=player_interface) def Previous(self): logger.debug(u'%s.Previous called', self.player_interface) + # TODO call playback.previous(), keep playback.state unchanged - pass + + # XXX Proof of concept only. Throw away, write tests, reimplement: + self.core_queue.put({ + 'to': 'frontend', + 'command': 'mpd_request', + 'request': 'previous', + }) @dbus.service.method(dbus_interface=player_interface) def Seek(self, offset): From 25d33f29b3d83f934fb0c74bd10849561473f62e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 13 Apr 2011 13:30:53 +0200 Subject: [PATCH 015/350] Initial shot at updating the MPRIS backend for Pykka --- mopidy/frontends/mpris.py | 95 +++++++++++---------------------------- 1 file changed, 26 insertions(+), 69 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 55d96b87..2bf2e08e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -1,5 +1,4 @@ import logging -import multiprocessing try: import dbus @@ -10,13 +9,16 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) +from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry + +from mopidy.backends.base import Backend from mopidy.frontends.base import BaseFrontend -from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.frontends.mpris') -class MprisFrontend(BaseFrontend): +class MprisFrontend(ThreadingActor, BaseFrontend): """ Frontend which lets you control Mopidy through the Media Player Remote Interfacing Specification (MPRIS) D-Bus interface. @@ -30,52 +32,22 @@ class MprisFrontend(BaseFrontend): Ubuntu/Debian. """ - def __init__(self, *args, **kwargs): - super(MprisFrontend, self).__init__(*args, **kwargs) - (self.connection, other_end) = multiprocessing.Pipe() - self.thread = MprisFrontendThread(self.core_queue, other_end) + # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be + # running too. This is not enforced in any way by the code. - def start(self): - self.thread.start() - - def destroy(self): - self.thread.destroy() - - def process_message(self, message): - self.connection.send(message) - - -class MprisFrontendThread(BaseThread): - """ - A process for communicating with MPRIS clients. - - This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be - running too. This is not enforced in any way by the code. - """ - - def __init__(self, core_queue, connection): - super(MprisFrontendThread, self).__init__(core_queue) - self.name = u'MprisFrontendThread' - self.connection = connection + def __init__(self): self.indicate_server = None self.dbus_objects = [] - def destroy(self): + def on_start(self): + self.dbus_objects.append(MprisObject()) + self.send_startup_notification() + + def on_stop(self): for dbus_object in self.dbus_objects: dbus_object.remove_from_connection() self.dbus_objects = [] - def run_inside_try(self): - self.setup() - while True: - self.connection.poll(None) - message = self.connection.recv() - self.process_message(message) - - def setup(self): - self.dbus_objects.append(MprisObject(self.core_queue)) - self.send_startup_notification() - def send_startup_notification(self): """ Send startup notification using libindicate to make Mopidy appear in @@ -91,14 +63,11 @@ class MprisFrontendThread(BaseThread): self.indicate_server.set_type('music.mopidy') # FIXME Location of .desktop file shouldn't be hardcoded self.indicate_server.set_desktop_file( - '/usr/local/share/applications/mopidy.desktop') + '/usr/share/applications/mopidy.desktop') self.indicate_server.show() except ImportError: pass - def process_message(self, message): - pass # Ignore commands for other frontends - class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.0/spec/""" @@ -153,8 +122,11 @@ class MprisObject(dbus.service.Object): }, } - def __init__(self, core_queue): - self.core_queue = core_queue + 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() + logger.debug(u'Prepare the D-Bus main loop before connecting') DBusGMainLoop(set_as_default=True) logger.debug(u'Connecting to D-Bus: getting session bus') @@ -234,7 +206,7 @@ class MprisObject(dbus.service.Object): player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ logger.debug(u'%s.Quit called', self.root_interface) - self.core_queue.put({'to': 'core', 'command': 'exit'}) + ActorRegistry.stop_all() ### Player interface @@ -243,14 +215,8 @@ class MprisObject(dbus.service.Object): def Next(self): logger.debug(u'%s.Next called', self.player_interface) # TODO call playback.next(), keep playback.state unchanged - pass - - # XXX Proof of concept only. Throw away, write tests, reimplement: - self.core_queue.put({ - 'to': 'frontend', - 'command': 'mpd_request', - 'request': 'next', - }) + # XXX Proof of concept only. Throw away, write tests. + self.backend.playback.next().get() @dbus.service.method(dbus_interface=player_interface) def OpenUri(self, uri): @@ -266,8 +232,8 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=player_interface) def Pause(self): logger.debug(u'%s.Pause called', self.player_interface) - # TODO call playback.pause() - pass + # XXX Proof of concept only. Throw away, write tests. + self.backend.playback.pause().get() @dbus.service.method(dbus_interface=player_interface) def Play(self): @@ -287,24 +253,15 @@ class MprisObject(dbus.service.Object): # elif playback.state == playback.STOPPED: playback.play() # XXX Proof of concept only. Throw away, write tests, reimplement: - self.core_queue.put({ - 'to': 'frontend', - 'command': 'mpd_request', - 'request': 'play', - }) + self.backend.playback.pause().get() @dbus.service.method(dbus_interface=player_interface) def Previous(self): logger.debug(u'%s.Previous called', self.player_interface) # TODO call playback.previous(), keep playback.state unchanged - # XXX Proof of concept only. Throw away, write tests, reimplement: - self.core_queue.put({ - 'to': 'frontend', - 'command': 'mpd_request', - 'request': 'previous', - }) + self.backend.playback.previous().get() @dbus.service.method(dbus_interface=player_interface) def Seek(self, offset): From cd2e683154376d20d2718963ce6cbd299e08e7c9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 11 May 2011 18:46:25 +0200 Subject: [PATCH 016/350] Add libindicate dependency to docstring --- mopidy/frontends/mpris.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 2bf2e08e..3ef2f4b4 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -30,6 +30,9 @@ class MprisFrontend(ThreadingActor, BaseFrontend): - ``dbus`` Python bindings. The package is named ``python-dbus`` in Ubuntu/Debian. + - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the + Ubuntu Sound Menu. The package is named ``python-indicate`` in + Ubuntu/Debian. """ # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be From d3c195fecfb8ec26f11020e0cc2f3b3a5d58f7e4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 11 May 2011 18:47:15 +0200 Subject: [PATCH 017/350] Log indicate import failure at debug level instead of ignoring it completely --- mopidy/frontends/mpris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 3ef2f4b4..89ea5bd2 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -68,8 +68,8 @@ class MprisFrontend(ThreadingActor, BaseFrontend): self.indicate_server.set_desktop_file( '/usr/share/applications/mopidy.desktop') self.indicate_server.show() - except ImportError: - pass + except ImportError as e: + logger.debug(u'Startup notification was not sent. (%s)', e) class MprisObject(dbus.service.Object): From a70650f80feba38c669bc8ce4c1bee3321c33e15 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 11 May 2011 20:35:43 +0200 Subject: [PATCH 018/350] Use dbus.PROPERTIES_IFACE constant instead of defining it ourselves --- mopidy/frontends/mpris.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 89ea5bd2..ef22f91e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -77,7 +77,6 @@ class MprisObject(dbus.service.Object): bus_name = 'org.mpris.MediaPlayer2.mopidy' object_path = '/org/mpris/MediaPlayer2' - properties_interface = 'org.freedesktop.DBus.Properties' root_interface = 'org.mpris.MediaPlayer2' player_interface = 'org.mpris.MediaPlayer2.Player' properties = { @@ -145,14 +144,14 @@ class MprisObject(dbus.service.Object): ### Property interface - @dbus.service.method(dbus_interface=properties_interface, + @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface, prop): - logger.debug(u'%s.Get called', self.properties_interface) + logger.debug(u'%s.Get called', dbus.dbus.PROPERTIES_IFACE) getter, setter = self.properties[interface][prop] return getter() if callable(getter) else getter - @dbus.service.method(dbus_interface=properties_interface, + @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface): """ @@ -165,28 +164,27 @@ class MprisObject(dbus.service.Object): props = player.GetAll('org.mpris.MediaPlayer2', dbus_interface='org.freedesktop.DBus.Properties') """ - logger.debug(u'%s.GetAll called', self.properties_interface) + logger.debug(u'%s.GetAll called', dbus.PROPERTIES_IFACE) getters = {} for key, (getter, setter) in self.properties[interface].iteritems(): getters[key] = getter() if callable(getter) else getter return getters - @dbus.service.method(dbus_interface=properties_interface, + @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ssv', out_signature='') def Set(self, interface, prop, value): - logger.debug(u'%s.Set called', self.properties_interface) + logger.debug(u'%s.Set called', dbus.PROPERTIES_IFACE) getter, setter = self.properties[interface][prop] if setter is not None: setter(value) self.PropertiesChanged(interface, {prop: self.Get(interface, prop)}, []) - @dbus.service.signal(dbus_interface=properties_interface, + @dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE, signature='sa{sv}as') def PropertiesChanged(self, interface, changed_properties, invalidated_properties): - logger.debug(u'%s.PropertiesChanged signaled', - self.properties_interface) + logger.debug(u'%s.PropertiesChanged signaled', dbus.PROPERTIES_IFACE) pass From 8fdeec121022a8228c9511ce827179064a4d5e0c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 May 2011 00:16:03 +0200 Subject: [PATCH 019/350] Simplify D-Bus service setup --- mopidy/frontends/mpris.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index ef22f91e..6b43b48d 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -2,9 +2,9 @@ import logging try: import dbus + import dbus.mainloop.glib import dbus.service - from dbus.mainloop.glib import DBusGMainLoop, threads_init - threads_init() + import gobject except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) @@ -17,6 +17,11 @@ from mopidy.frontends.base import BaseFrontend logger = logging.getLogger('mopidy.frontends.mpris') +# Must be done before dbus.SessionBus() is called +gobject.threads_init() +dbus.mainloop.glib.threads_init() +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + class MprisFrontend(ThreadingActor, BaseFrontend): """ @@ -129,16 +134,9 @@ class MprisObject(dbus.service.Object): assert len(backend_refs) == 1, 'Expected exactly one running backend.' self.backend = backend_refs[0].proxy() - logger.debug(u'Prepare the D-Bus main loop before connecting') - DBusGMainLoop(set_as_default=True) - logger.debug(u'Connecting to D-Bus: getting session bus') - bus = dbus.SessionBus() - logger.debug(u'Connecting to D-Bus: claiming service name') - # FIXME We segfault at the next line 80% of the time - bus_name = dbus.service.BusName(self.bus_name, bus) - logger.debug(u'Connecting to D-Bus: registering service object') - super(MprisObject, self).__init__(object_path=self.object_path, - bus_name=bus_name) + logger.debug(u'Connecting to D-Bus...') + bus_name = dbus.service.BusName(self.bus_name, dbus.SessionBus()) + super(MprisObject, self).__init__(bus_name, self.object_path) logger.info(u'Connected to D-Bus') From 0f1336d0ce358ab59a09aad1e364085c52f12876 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 12 May 2011 00:21:40 +0200 Subject: [PATCH 020/350] Move constants out of class --- mopidy/frontends/mpris.py | 65 ++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 6b43b48d..4f02267a 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -22,6 +22,11 @@ gobject.threads_init() dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) +BUS_NAME = 'org.mpris.MediaPlayer2.mopidy' +OBJECT_PATH = '/org/mpris/MediaPlayer2' +ROOT_IFACE = 'org.mpris.MediaPlayer2' +PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' + class MprisFrontend(ThreadingActor, BaseFrontend): """ @@ -80,12 +85,8 @@ class MprisFrontend(ThreadingActor, BaseFrontend): class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.0/spec/""" - bus_name = 'org.mpris.MediaPlayer2.mopidy' - object_path = '/org/mpris/MediaPlayer2' - root_interface = 'org.mpris.MediaPlayer2' - player_interface = 'org.mpris.MediaPlayer2.Player' properties = { - root_interface: { + ROOT_IFACE: { 'CanQuit': (True, None), 'CanRaise': (False, None), # TODO Add track list support @@ -96,7 +97,7 @@ class MprisObject(dbus.service.Object): # TODO Return MIME types supported by local backend if active 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), }, - player_interface: { + PLAYER_IFACE: { # TODO Get backend.playback.state 'PlaybackStatus': ('Stopped', None), # TODO Get/set loop status @@ -135,8 +136,8 @@ class MprisObject(dbus.service.Object): self.backend = backend_refs[0].proxy() logger.debug(u'Connecting to D-Bus...') - bus_name = dbus.service.BusName(self.bus_name, dbus.SessionBus()) - super(MprisObject, self).__init__(bus_name, self.object_path) + bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus()) + super(MprisObject, self).__init__(bus_name, OBJECT_PATH) logger.info(u'Connected to D-Bus') @@ -188,12 +189,12 @@ class MprisObject(dbus.service.Object): ### Root interface - @dbus.service.method(dbus_interface=root_interface) + @dbus.service.method(dbus_interface=ROOT_IFACE) def Raise(self): - logger.debug(u'%s.Raise called', self.root_interface) + logger.debug(u'%s.Raise called', ROOT_IFACE) pass # We do not have a GUI - @dbus.service.method(dbus_interface=root_interface) + @dbus.service.method(dbus_interface=ROOT_IFACE) def Quit(self): """ To test, start Mopidy and then run the following in a Python shell:: @@ -204,22 +205,22 @@ class MprisObject(dbus.service.Object): '/org/mpris/MediaPlayer2') player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ - logger.debug(u'%s.Quit called', self.root_interface) + logger.debug(u'%s.Quit called', ROOT_IFACE) ActorRegistry.stop_all() ### Player interface - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): - logger.debug(u'%s.Next called', self.player_interface) + logger.debug(u'%s.Next called', PLAYER_IFACE) # TODO call playback.next(), keep playback.state unchanged # XXX Proof of concept only. Throw away, write tests. self.backend.playback.next().get() - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): - logger.debug(u'%s.OpenUri called', self.player_interface) + logger.debug(u'%s.OpenUri called', PLAYER_IFACE) # TODO Pseudo code: # if uri.scheme not in SupportedUriSchemes: return # if uri.mime_type not in SupportedMimeTypes: return @@ -228,23 +229,23 @@ class MprisObject(dbus.service.Object): # playback.play(cp_track) pass - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Pause(self): - logger.debug(u'%s.Pause called', self.player_interface) + logger.debug(u'%s.Pause called', PLAYER_IFACE) # XXX Proof of concept only. Throw away, write tests. self.backend.playback.pause().get() - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Play(self): - logger.debug(u'%s.Play called', self.player_interface) + logger.debug(u'%s.Play called', PLAYER_IFACE) # TODO Pseudo code: # if playback.state == playback.PAUSED: playback.resume() # elif playback.state == playback.STOPPED: playback.play() pass - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def PlayPause(self): - logger.debug(u'%s.PlayPause called', self.player_interface) + logger.debug(u'%s.PlayPause called', PLAYER_IFACE) # TODO Pseudo code: # if playback.state == playback.PLAYING: playback.pause() @@ -254,17 +255,17 @@ class MprisObject(dbus.service.Object): # XXX Proof of concept only. Throw away, write tests, reimplement: self.backend.playback.pause().get() - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Previous(self): - logger.debug(u'%s.Previous called', self.player_interface) + logger.debug(u'%s.Previous called', PLAYER_IFACE) # TODO call playback.previous(), keep playback.state unchanged # XXX Proof of concept only. Throw away, write tests, reimplement: self.backend.playback.previous().get() - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): - logger.debug(u'%s.Seek called', self.player_interface) + logger.debug(u'%s.Seek called', PLAYER_IFACE) # TODO Pseudo code: # new_position = playback.time_position + offset # if new_position > playback.current_track.length: @@ -274,22 +275,22 @@ class MprisObject(dbus.service.Object): # playback.seek(new_position) pass - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def SetPosition(self, track_id, position): - logger.debug(u'%s.SetPosition called', self.player_interface) + logger.debug(u'%s.SetPosition called', PLAYER_IFACE) # TODO Pseudo code: # if track_id != playback.current_track.track_id: return # if not 0 <= position <= playback.current_track.length: return # playback.seek(position) pass - @dbus.service.method(dbus_interface=player_interface) + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Stop(self): - logger.debug(u'%s.Stop called', self.player_interface) + logger.debug(u'%s.Stop called', PLAYER_IFACE) # TODO call playback.stop() pass - @dbus.service.signal(dbus_interface=player_interface, signature='x') + @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') def Seeked(self, position): - logger.debug(u'%s.Seeked signaled', self.player_interface) + logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) pass From fbf3d23fd86ac0fbbdd269a00b4e8bfc0ca3910b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 01:11:30 +0200 Subject: [PATCH 021/350] Some initial simple unit testing of the MPRIS frontend, without real D-Bus or real backend --- mopidy/frontends/mpris.py | 31 +++++++++++-------- .../frontends/mpris/player_interface_test.py | 31 +++++++++++++++++++ tests/frontends/mpris/root_interface_test.py | 22 +++++++++++++ 3 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 tests/frontends/mpris/player_interface_test.py create mode 100644 tests/frontends/mpris/root_interface_test.py diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 4f02267a..d2f982e3 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -131,17 +131,26 @@ class MprisObject(dbus.service.Object): } 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() + self._backend = None + bus_name = self._connect_to_dbus() + super(MprisObject, self).__init__(bus_name, OBJECT_PATH) + def _connect_to_dbus(self): logger.debug(u'Connecting to D-Bus...') bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus()) - super(MprisObject, self).__init__(bus_name, OBJECT_PATH) logger.info(u'Connected to D-Bus') + return bus_name + + @property + def backend(self): + if self._backend is None: + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self._backend = backend_refs[0].proxy() + return self._backend - ### Property interface + ### Properties interface @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') @@ -214,8 +223,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) - # TODO call playback.next(), keep playback.state unchanged - # XXX Proof of concept only. Throw away, write tests. + # TODO keep playback.state unchanged self.backend.playback.next().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -232,7 +240,6 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Pause(self): logger.debug(u'%s.Pause called', PLAYER_IFACE) - # XXX Proof of concept only. Throw away, write tests. self.backend.playback.pause().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -258,9 +265,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Previous(self): logger.debug(u'%s.Previous called', PLAYER_IFACE) - - # TODO call playback.previous(), keep playback.state unchanged - # XXX Proof of concept only. Throw away, write tests, reimplement: + # TODO keep playback.state unchanged self.backend.playback.previous().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -287,10 +292,10 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Stop(self): logger.debug(u'%s.Stop called', PLAYER_IFACE) - # TODO call playback.stop() - pass + self.backend.playback.stop().get() @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') def Seeked(self, position): logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) + # TODO What should we do here? pass diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py new file mode 100644 index 00000000..d44196a0 --- /dev/null +++ b/tests/frontends/mpris/player_interface_test.py @@ -0,0 +1,31 @@ +import mock +import unittest + +from pykka.registry import ActorRegistry + +from mopidy.backends.base import Backend +from mopidy.frontends import mpris + +class PlayerInterfaceTest(unittest.TestCase): + def setUp(self): + mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) + mpris.MprisObject._connect_to_dbus = mock.Mock() + self.backend = mock.Mock(spec=Backend) + self.mpris_object = mpris.MprisObject() + self.mpris_object._backend = self.backend + + def test_next_should_call_next_on_backend(self): + self.mpris_object.Next() + self.assert_(self.backend.playback.next.called) + + def test_pause_should_call_pause_on_backend(self): + self.mpris_object.Pause() + self.assert_(self.backend.playback.pause.called) + + def test_previous_should_call_previous_on_backend(self): + self.mpris_object.Previous() + self.assert_(self.backend.playback.previous.called) + + def test_stop_should_call_stop_on_backend(self): + self.mpris_object.Stop() + self.assert_(self.backend.playback.stop.called) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py new file mode 100644 index 00000000..864fdf4e --- /dev/null +++ b/tests/frontends/mpris/root_interface_test.py @@ -0,0 +1,22 @@ +import mock +import unittest + +from pykka.registry import ActorRegistry + +from mopidy.frontends import mpris + +class RootInterfaceTest(unittest.TestCase): + def setUp(self): + mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) + mpris.MprisObject._connect_to_dbus = mock.Mock() + self.mpris_object = mpris.MprisObject() + + def test_constructor_connects_to_dbus(self): + self.assert_(self.mpris_object._connect_to_dbus.called) + + def test_raise_does_nothing(self): + self.mpris_object.Raise() + + def test_quit_should_stop_all_actors(self): + self.mpris_object.Quit() + self.assert_(mpris.ActorRegistry.stop_all.called) From b21f1caa2b261afcc72d5311cf1cd61a21a97617 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 01:38:41 +0200 Subject: [PATCH 022/350] Test properties of the root interface --- mopidy/frontends/mpris.py | 2 +- tests/frontends/mpris/root_interface_test.py | 24 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index d2f982e3..565d6359 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -155,7 +155,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface, prop): - logger.debug(u'%s.Get called', dbus.dbus.PROPERTIES_IFACE) + logger.debug(u'%s.Get called', dbus.PROPERTIES_IFACE) getter, setter = self.properties[interface][prop] return getter() if callable(getter) else getter diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 864fdf4e..a37918a8 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -14,9 +14,33 @@ class RootInterfaceTest(unittest.TestCase): def test_constructor_connects_to_dbus(self): self.assert_(self.mpris_object._connect_to_dbus.called) + def test_can_raise_returns_false(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'CanRaise') + self.assertFalse(result) + def test_raise_does_nothing(self): self.mpris_object.Raise() + def test_can_quit_returns_true(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'CanQuit') + self.assertTrue(result) + def test_quit_should_stop_all_actors(self): self.mpris_object.Quit() self.assert_(mpris.ActorRegistry.stop_all.called) + + def test_has_track_list_returns_false(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'HasTrackList') + self.assertFalse(result) + + def test_identify_is_mopidy(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'Identity') + self.assertEquals('Mopidy', result) + + def test_supported_uri_schemes_is_empty(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') + self.assertEquals(0, len(result)) + + def test_supported_mime_types_is_empty(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'SupportedMimeTypes') + self.assertEquals(0, len(result)) From f038c338b1a7d2b149bfc0f2e1f39c68d4228e52 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 01:42:34 +0200 Subject: [PATCH 023/350] Add missing DesktopEntry property to root interface --- mopidy/frontends/mpris.py | 1 + tests/frontends/mpris/root_interface_test.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 565d6359..d3ce8f44 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -92,6 +92,7 @@ class MprisObject(dbus.service.Object): # TODO Add track list support 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), + 'DesktopEntry': ('mopidy', None), # TODO Return URI schemes supported by backend configuration 'SupportedUriSchemes': (dbus.Array([], signature='s'), None), # TODO Return MIME types supported by local backend if active diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index a37918a8..74ac2cf2 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -37,6 +37,10 @@ class RootInterfaceTest(unittest.TestCase): result = self.mpris_object.Get(mpris.ROOT_IFACE, 'Identity') self.assertEquals('Mopidy', result) + def test_desktop_entry_is_mopidy(self): + result = self.mpris_object.Get(mpris.ROOT_IFACE, 'DesktopEntry') + self.assertEquals('mopidy', result) + def test_supported_uri_schemes_is_empty(self): result = self.mpris_object.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') self.assertEquals(0, len(result)) From f3cfa22c75484d79a2772cc45c94295d1dd471dc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 02:15:16 +0200 Subject: [PATCH 024/350] Implement PlaybackStatus property --- mopidy/frontends/mpris.py | 53 +++++++++++++------ .../frontends/mpris/player_interface_test.py | 16 ++++++ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index d3ce8f44..9a1194c7 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -13,6 +13,7 @@ from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry from mopidy.backends.base import Backend +from mopidy.backends.base.playback import PlaybackController from mopidy.frontends.base import BaseFrontend logger = logging.getLogger('mopidy.frontends.mpris') @@ -85,8 +86,19 @@ class MprisFrontend(ThreadingActor, BaseFrontend): class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.0/spec/""" - properties = { - ROOT_IFACE: { + properties = None + + def __init__(self): + self._backend = None + self.properties = { + ROOT_IFACE: self._get_root_iface_properties(), + PLAYER_IFACE: self._get_player_iface_properties(), + } + bus_name = self._connect_to_dbus() + super(MprisObject, self).__init__(bus_name, OBJECT_PATH) + + def _get_root_iface_properties(self): + return { 'CanQuit': (True, None), 'CanRaise': (False, None), # TODO Add track list support @@ -97,10 +109,11 @@ class MprisObject(dbus.service.Object): 'SupportedUriSchemes': (dbus.Array([], signature='s'), None), # TODO Return MIME types supported by local backend if active 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), - }, - PLAYER_IFACE: { - # TODO Get backend.playback.state - 'PlaybackStatus': ('Stopped', None), + } + + def _get_player_iface_properties(self): + return { + 'PlaybackStatus': (self.get_PlaybackStatus, None), # TODO Get/set loop status 'LoopStatus': ('None', None), 'Rate': (1.0, None), @@ -128,13 +141,7 @@ class MprisObject(dbus.service.Object): 'CanSeek': (False, None), # TODO Set to True when the rest is implemented 'CanControl': (False, None), - }, - } - - def __init__(self): - self._backend = None - bus_name = self._connect_to_dbus() - super(MprisObject, self).__init__(bus_name, OBJECT_PATH) + } def _connect_to_dbus(self): logger.debug(u'Connecting to D-Bus...') @@ -156,8 +163,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface, prop): - logger.debug(u'%s.Get called', dbus.PROPERTIES_IFACE) - getter, setter = self.properties[interface][prop] + logger.debug(u'%s.Get(%s, %s) called', + dbus.PROPERTIES_IFACE, repr(interface), repr(prop)) + (getter, setter) = self.properties[interface][prop] return getter() if callable(getter) else getter @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, @@ -173,7 +181,8 @@ class MprisObject(dbus.service.Object): props = player.GetAll('org.mpris.MediaPlayer2', dbus_interface='org.freedesktop.DBus.Properties') """ - logger.debug(u'%s.GetAll called', dbus.PROPERTIES_IFACE) + logger.debug(u'%s.GetAll(%s) called', + dbus.PROPERTIES_IFACE, repr(interface)) getters = {} for key, (getter, setter) in self.properties[interface].iteritems(): getters[key] = getter() if callable(getter) else getter @@ -182,7 +191,8 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ssv', out_signature='') def Set(self, interface, prop, value): - logger.debug(u'%s.Set called', dbus.PROPERTIES_IFACE) + logger.debug(u'%s.Set(%s, %s, %s) called', + dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value)) getter, setter = self.properties[interface][prop] if setter is not None: setter(value) @@ -221,6 +231,15 @@ class MprisObject(dbus.service.Object): ### Player interface + def get_PlaybackStatus(self): + state = self.backend.playback.state.get() + if state == PlaybackController.PLAYING: + return 'Playing' + elif state == PlaybackController.PAUSED: + return 'Paused' + elif state == PlaybackController.STOPPED: + return 'Stopped' + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index d44196a0..d3a671b0 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -4,6 +4,7 @@ import unittest from pykka.registry import ActorRegistry from mopidy.backends.base import Backend +from mopidy.backends.base.playback import PlaybackController from mopidy.frontends import mpris class PlayerInterfaceTest(unittest.TestCase): @@ -14,6 +15,21 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris_object = mpris.MprisObject() self.mpris_object._backend = self.backend + def test_playback_status_is_playing_when_playing(self): + self.backend.playback.state.get.return_value = PlaybackController.PLAYING + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + self.assertEqual('Playing', result) + + def test_playback_status_is_paused_when_paused(self): + self.backend.playback.state.get.return_value = PlaybackController.PAUSED + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + self.assertEqual('Paused', result) + + def test_playback_status_is_stopped_when_stopped(self): + self.backend.playback.state.get.return_value = PlaybackController.STOPPED + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + self.assertEqual('Stopped', result) + def test_next_should_call_next_on_backend(self): self.mpris_object.Next() self.assert_(self.backend.playback.next.called) From 7f20cf4e83403b8a1e6e8f5106d44d032a516e2c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 02:21:36 +0200 Subject: [PATCH 025/350] Implement getting of LoopStatus --- mopidy/frontends/mpris.py | 15 +++++++++++++-- tests/frontends/mpris/player_interface_test.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 9a1194c7..8fdf9c0e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -114,8 +114,8 @@ class MprisObject(dbus.service.Object): def _get_player_iface_properties(self): return { 'PlaybackStatus': (self.get_PlaybackStatus, None), - # TODO Get/set loop status - 'LoopStatus': ('None', None), + # TODO Set loop status + 'LoopStatus': (self.get_LoopStatus, None), 'Rate': (1.0, None), # TODO Get/set backend.playback.random 'Shuffle': (False, None), @@ -240,6 +240,17 @@ class MprisObject(dbus.service.Object): elif state == PlaybackController.STOPPED: return 'Stopped' + def get_LoopStatus(self): + single = self.backend.playback.single.get() + repeat = self.backend.playback.repeat.get() + if not repeat: + return 'None' + else: + if single: + return 'Track' + else: + return 'Playlist' + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index d3a671b0..c2970626 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -30,6 +30,24 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Stopped', result) + def test_loop_status_is_none_when_not_looping(self): + self.backend.playback.repeat.get.return_value = False + self.backend.playback.single.get.return_value = False + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + self.assertEqual('None', result) + + def test_loop_status_is_track_when_looping_a_single_track(self): + self.backend.playback.repeat.get.return_value = True + self.backend.playback.single.get.return_value = True + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + self.assertEqual('Track', result) + + def test_loop_status_is_playlist_when_looping_the_current_playlist(self): + self.backend.playback.repeat.get.return_value = True + self.backend.playback.single.get.return_value = False + result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + self.assertEqual('Playlist', result) + def test_next_should_call_next_on_backend(self): self.mpris_object.Next() self.assert_(self.backend.playback.next.called) From 9c0e139c1754f02cd0db22701fd2b8937f773d83 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 14:10:14 +0200 Subject: [PATCH 026/350] Switch from mocking the backend to using DummyBackend --- .../frontends/mpris/player_interface_test.py | 75 ++++++++++++------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index c2970626..e0bbc27f 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -3,63 +3,88 @@ import unittest from pykka.registry import ActorRegistry -from mopidy.backends.base import Backend +from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends import mpris +from mopidy.models import Track + +PLAYING = PlaybackController.PLAYING +PAUSED = PlaybackController.PAUSED +STOPPED = PlaybackController.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) mpris.MprisObject._connect_to_dbus = mock.Mock() - self.backend = mock.Mock(spec=Backend) + self.backend = DummyBackend.start().proxy() self.mpris_object = mpris.MprisObject() self.mpris_object._backend = self.backend - def test_playback_status_is_playing_when_playing(self): - self.backend.playback.state.get.return_value = PlaybackController.PLAYING + def tearDown(self): + self.backend.stop() + + def test_get_playback_status_is_playing_when_playing(self): + self.backend.playback.state = PLAYING result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Playing', result) - def test_playback_status_is_paused_when_paused(self): - self.backend.playback.state.get.return_value = PlaybackController.PAUSED + def test_get_playback_status_is_paused_when_paused(self): + self.backend.playback.state = PAUSED result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Paused', result) - def test_playback_status_is_stopped_when_stopped(self): - self.backend.playback.state.get.return_value = PlaybackController.STOPPED + def test_get_playback_status_is_stopped_when_stopped(self): + self.backend.playback.state = STOPPED result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Stopped', result) - def test_loop_status_is_none_when_not_looping(self): - self.backend.playback.repeat.get.return_value = False - self.backend.playback.single.get.return_value = False + def test_get_loop_status_is_none_when_not_looping(self): + self.backend.playback.repeat = False + self.backend.playback.single = False result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('None', result) - def test_loop_status_is_track_when_looping_a_single_track(self): - self.backend.playback.repeat.get.return_value = True - self.backend.playback.single.get.return_value = True + def test_get_loop_status_is_track_when_looping_a_single_track(self): + self.backend.playback.repeat = True + self.backend.playback.single = True result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Track', result) - def test_loop_status_is_playlist_when_looping_the_current_playlist(self): - self.backend.playback.repeat.get.return_value = True - self.backend.playback.single.get.return_value = False + def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self): + self.backend.playback.repeat = True + self.backend.playback.single = False result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Playlist', result) - def test_next_should_call_next_on_backend(self): + def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), PLAYING) self.mpris_object.Next() - self.assert_(self.backend.playback.next.called) + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), PLAYING) - def test_pause_should_call_pause_on_backend(self): + def test_pause_when_playing_should_pause_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) self.mpris_object.Pause() - self.assert_(self.backend.playback.pause.called) + self.assertEquals(self.backend.playback.state.get(), PAUSED) - def test_previous_should_call_previous_on_backend(self): + def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), PLAYING) self.mpris_object.Previous() - self.assert_(self.backend.playback.previous.called) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), PLAYING) - def test_stop_should_call_stop_on_backend(self): + def test_stop_when_playing_should_stop_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) self.mpris_object.Stop() - self.assert_(self.backend.playback.stop.called) + self.assertEquals(self.backend.playback.state.get(), STOPPED) From e407d8a4bb2522558720755e265472464478949d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 14:13:12 +0200 Subject: [PATCH 027/350] Implement setting of LoopStatus --- mopidy/frontends/mpris.py | 16 +++++++++++++--- tests/frontends/mpris/player_interface_test.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 8fdf9c0e..c17b7c19 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -114,8 +114,7 @@ class MprisObject(dbus.service.Object): def _get_player_iface_properties(self): return { 'PlaybackStatus': (self.get_PlaybackStatus, None), - # TODO Set loop status - 'LoopStatus': (self.get_LoopStatus, None), + 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus), 'Rate': (1.0, None), # TODO Get/set backend.playback.random 'Shuffle': (False, None), @@ -241,8 +240,8 @@ class MprisObject(dbus.service.Object): return 'Stopped' def get_LoopStatus(self): - single = self.backend.playback.single.get() repeat = self.backend.playback.repeat.get() + single = self.backend.playback.single.get() if not repeat: return 'None' else: @@ -251,6 +250,17 @@ class MprisObject(dbus.service.Object): else: return 'Playlist' + def set_LoopStatus(self, value): + if value == 'None': + self.backend.playback.repeat = False + self.backend.playback.single = False + elif value == 'Track': + self.backend.playback.repeat = True + self.backend.playback.single = True + elif value == 'Playlist': + self.backend.playback.repeat = True + self.backend.playback.single = False + @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index e0bbc27f..11823a98 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -56,6 +56,21 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Playlist', result) + def test_set_loop_status_to_none_unsets_repeat_and_single(self): + self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') + self.assertEquals(self.backend.playback.repeat.get(), False) + self.assertEquals(self.backend.playback.single.get(), False) + + def test_set_loop_status_to_track_sets_repeat_and_single(self): + self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Track') + self.assertEquals(self.backend.playback.repeat.get(), True) + self.assertEquals(self.backend.playback.single.get(), True) + + def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self): + self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Playlist') + self.assertEquals(self.backend.playback.repeat.get(), True) + self.assertEquals(self.backend.playback.single.get(), False) + def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 167384af60c6a986940ebdf8555eee28d3da20dc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 14:14:50 +0200 Subject: [PATCH 028/350] Use 'mpris' instead of 'mpris_object' in tests --- .../frontends/mpris/player_interface_test.py | 30 +++++++++---------- tests/frontends/mpris/root_interface_test.py | 22 +++++++------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 11823a98..e59886ac 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -17,57 +17,57 @@ class PlayerInterfaceTest(unittest.TestCase): mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) mpris.MprisObject._connect_to_dbus = mock.Mock() self.backend = DummyBackend.start().proxy() - self.mpris_object = mpris.MprisObject() - self.mpris_object._backend = self.backend + self.mpris = mpris.MprisObject() + self.mpris._backend = self.backend def tearDown(self): self.backend.stop() def test_get_playback_status_is_playing_when_playing(self): self.backend.playback.state = PLAYING - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Playing', result) def test_get_playback_status_is_paused_when_paused(self): self.backend.playback.state = PAUSED - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Paused', result) def test_get_playback_status_is_stopped_when_stopped(self): self.backend.playback.state = STOPPED - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Stopped', result) def test_get_loop_status_is_none_when_not_looping(self): self.backend.playback.repeat = False self.backend.playback.single = False - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('None', result) def test_get_loop_status_is_track_when_looping_a_single_track(self): self.backend.playback.repeat = True self.backend.playback.single = True - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Track', result) def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self): self.backend.playback.repeat = True self.backend.playback.single = False - result = self.mpris_object.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Playlist', result) def test_set_loop_status_to_none_unsets_repeat_and_single(self): - self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') + self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEquals(self.backend.playback.repeat.get(), False) self.assertEquals(self.backend.playback.single.get(), False) def test_set_loop_status_to_track_sets_repeat_and_single(self): - self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Track') + self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Track') self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), True) def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self): - self.mpris_object.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Playlist') + self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Playlist') self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), False) @@ -76,7 +76,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.playback.play() self.assertEquals(self.backend.playback.current_track.get().uri, 'a') self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris_object.Next() + self.mpris.Next() self.assertEquals(self.backend.playback.current_track.get().uri, 'b') self.assertEquals(self.backend.playback.state.get(), PLAYING) @@ -84,7 +84,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris_object.Pause() + self.mpris.Pause() self.assertEquals(self.backend.playback.state.get(), PAUSED) def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self): @@ -93,7 +93,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.playback.next() self.assertEquals(self.backend.playback.current_track.get().uri, 'b') self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris_object.Previous() + self.mpris.Previous() self.assertEquals(self.backend.playback.current_track.get().uri, 'a') self.assertEquals(self.backend.playback.state.get(), PLAYING) @@ -101,5 +101,5 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris_object.Stop() + self.mpris.Stop() self.assertEquals(self.backend.playback.state.get(), STOPPED) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 74ac2cf2..f088d4dd 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -9,42 +9,42 @@ class RootInterfaceTest(unittest.TestCase): def setUp(self): mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) mpris.MprisObject._connect_to_dbus = mock.Mock() - self.mpris_object = mpris.MprisObject() + self.mpris = mpris.MprisObject() def test_constructor_connects_to_dbus(self): - self.assert_(self.mpris_object._connect_to_dbus.called) + self.assert_(self.mpris._connect_to_dbus.called) def test_can_raise_returns_false(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'CanRaise') + result = self.mpris.Get(mpris.ROOT_IFACE, 'CanRaise') self.assertFalse(result) def test_raise_does_nothing(self): - self.mpris_object.Raise() + self.mpris.Raise() def test_can_quit_returns_true(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'CanQuit') + result = self.mpris.Get(mpris.ROOT_IFACE, 'CanQuit') self.assertTrue(result) def test_quit_should_stop_all_actors(self): - self.mpris_object.Quit() + self.mpris.Quit() self.assert_(mpris.ActorRegistry.stop_all.called) def test_has_track_list_returns_false(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'HasTrackList') + result = self.mpris.Get(mpris.ROOT_IFACE, 'HasTrackList') self.assertFalse(result) def test_identify_is_mopidy(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'Identity') + result = self.mpris.Get(mpris.ROOT_IFACE, 'Identity') self.assertEquals('Mopidy', result) def test_desktop_entry_is_mopidy(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'DesktopEntry') + result = self.mpris.Get(mpris.ROOT_IFACE, 'DesktopEntry') self.assertEquals('mopidy', result) def test_supported_uri_schemes_is_empty(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') + result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') self.assertEquals(0, len(result)) def test_supported_mime_types_is_empty(self): - result = self.mpris_object.Get(mpris.ROOT_IFACE, 'SupportedMimeTypes') + result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedMimeTypes') self.assertEquals(0, len(result)) From 3e18254c1834279550702fc6a41fbe0c1d441bd6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 15:43:33 +0200 Subject: [PATCH 029/350] Change playback.{next,prev} to not imply play() --- docs/changes.rst | 12 +++++++ mopidy/backends/base/playback.py | 57 +++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f8f01129..5e102ece 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -24,6 +24,18 @@ No description yet. - Support passing options to GStreamer. See :option:`--help-gst` for a list of available options. (Fixes: :issue:`95`) +- Backends: + + - Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next` + and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no + longer implies that playback should be started. The playback state--whether + playing, paused or stopped--will now be kept. + + - The method + :meth:`mopidy.backends.base.playback.PlaybackController.change_track` + has been added. Like ``next()``, and ``prev()``, it changes the current + track without changing the playback state. + 0.4.1 (2011-05-06) ================== diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 88ae141d..4eedbcb9 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -312,6 +312,26 @@ class PlaybackController(object): def _current_wall_time(self): return int(time.time() * 1000) + def change_track(self, cp_track, on_error_step=1): + """ + Change to the given track, keeping the current playback state. + + :param cp_track: track to change to + :type cp_track: two-tuple (CPID integer, :class:`mopidy.models.Track`) + or :class:`None` + :param on_error_step: direction to step at play error, 1 for next + track (default), -1 for previous track + :type on_error_step: int, -1 or 1 + + """ + old_state = self.state + self.stop() + self.current_cp_track = cp_track + if old_state == self.PLAYING: + self.play(on_error_step=on_error_step) + elif old_state == self.PAUSED: + self.pause() + def on_end_of_track(self): """ Tell the playback controller that end of track is reached. @@ -325,7 +345,6 @@ class PlaybackController(object): original_cp_track = self.current_cp_track if self.cp_track_at_eot: - self._trigger_stopped_playing_event() self.play(self.cp_track_at_eot) else: self.stop(clear_current_track=True) @@ -348,13 +367,14 @@ class PlaybackController(object): self.stop(clear_current_track=True) def next(self): - """Play the next track.""" - if self.state == self.STOPPED: - return + """ + Change to the next track. + The current playback state will be kept. If it was playing, playing + will continue. If it was paused, it will still be paused, etc. + """ if self.cp_track_at_next: - self._trigger_stopped_playing_event() - self.play(self.cp_track_at_next) + self.change_track(self.cp_track_at_next) else: self.stop(clear_current_track=True) @@ -378,15 +398,16 @@ class PlaybackController(object): if cp_track is not None: assert cp_track in self.backend.current_playlist.cp_tracks - - if cp_track is None and self.current_cp_track is None: - cp_track = self.cp_track_at_next - - if cp_track is None and self.state == self.PAUSED: - self.resume() + elif cp_track is None: + if self.state == self.PAUSED: + return self.resume() + elif self.current_cp_track is not None: + cp_track = self.current_cp_track + elif self.current_cp_track is None: + cp_track = self.cp_track_at_next if cp_track is not None: - self.state = self.STOPPED + self.stop() self.current_cp_track = cp_track self.state = self.PLAYING if not self.provider.play(cp_track[1]): @@ -404,13 +425,17 @@ class PlaybackController(object): self._trigger_started_playing_event() def previous(self): - """Play the previous track.""" + """ + Change to the previous track. + + The current playback state will be kept. If it was playing, playing + will continue. If it was paused, it will still be paused, etc. + """ if self.cp_track_at_previous is None: return if self.state == self.STOPPED: return - self._trigger_stopped_playing_event() - self.play(self.cp_track_at_previous, on_error_step=-1) + self.change_track(self.cp_track_at_previous, on_error_step=-1) def resume(self): """If paused, resume playing the current track.""" From 136daac6a28cd42f757a71f2c1ea4eaeb6319a5f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 15:49:15 +0200 Subject: [PATCH 030/350] Test state changes on mpris.Next() --- mopidy/frontends/mpris.py | 1 - .../frontends/mpris/player_interface_test.py | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index c17b7c19..53ee3dcd 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -264,7 +264,6 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) - # TODO keep playback.state unchanged self.backend.playback.next().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index e59886ac..df6988c8 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -80,6 +80,35 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.current_track.get().uri, 'b') self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_next_when_at_end_of_list_should_stop_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Next() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + + def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.pause() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), PAUSED) + self.mpris.Next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), PAUSED) + + def test_next_when_stopped_should_skip_to_next_track_and_stay_stopped(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.stop() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.Next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_pause_when_playing_should_pause_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 3eecfac9f186b3bcfe9a9f3df050d556f5803e72 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 15:56:24 +0200 Subject: [PATCH 031/350] Test state changes on mpris.Previous() --- mopidy/frontends/mpris.py | 11 +++-- .../frontends/mpris/player_interface_test.py | 44 ++++++++++++++++--- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 53ee3dcd..02628c7e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -266,6 +266,11 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Next called', PLAYER_IFACE) self.backend.playback.next().get() + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Previous(self): + logger.debug(u'%s.Previous called', PLAYER_IFACE) + self.backend.playback.previous().get() + @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): logger.debug(u'%s.OpenUri called', PLAYER_IFACE) @@ -302,12 +307,6 @@ class MprisObject(dbus.service.Object): # XXX Proof of concept only. Throw away, write tests, reimplement: self.backend.playback.pause().get() - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Previous(self): - logger.debug(u'%s.Previous called', PLAYER_IFACE) - # TODO keep playback.state unchanged - self.backend.playback.previous().get() - @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): logger.debug(u'%s.Seek called', PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index df6988c8..3f61fbbe 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -109,13 +109,6 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.current_track.get().uri, 'b') self.assertEquals(self.backend.playback.state.get(), STOPPED) - def test_pause_when_playing_should_pause_playback(self): - self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.backend.playback.play() - self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris.Pause() - self.assertEquals(self.backend.playback.state.get(), PAUSED) - def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() @@ -126,6 +119,43 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.current_track.get().uri, 'a') self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_previous_when_at_start_of_list_should_stop_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Previous() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + + def test_previous_when_paused_should_skip_to_previous_track_and_stay_paused(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + self.backend.playback.pause() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), PAUSED) + self.mpris.Previous() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), PAUSED) + + def test_previous_when_stopped_should_skip_to_previous_track_and_stay_stopped(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + self.backend.playback.stop() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.Previous() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.assertEquals(self.backend.playback.state.get(), STOPPED) + + def test_pause_when_playing_should_pause_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + def test_stop_when_playing_should_stop_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 271228cd664f11721283825c91ae0234d690d39f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 5 Jun 2011 15:59:42 +0200 Subject: [PATCH 032/350] Order MPRIS methods in the same order as the spec --- mopidy/frontends/mpris.py | 166 ++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 80 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 02628c7e..9fd4ede9 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -206,7 +206,7 @@ class MprisObject(dbus.service.Object): pass - ### Root interface + ### Root interface methods @dbus.service.method(dbus_interface=ROOT_IFACE) def Raise(self): @@ -228,7 +228,91 @@ class MprisObject(dbus.service.Object): ActorRegistry.stop_all() - ### Player interface + ### Player interface methods + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Next(self): + logger.debug(u'%s.Next called', PLAYER_IFACE) + self.backend.playback.next().get() + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Previous(self): + logger.debug(u'%s.Previous called', PLAYER_IFACE) + self.backend.playback.previous().get() + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Pause(self): + logger.debug(u'%s.Pause called', PLAYER_IFACE) + self.backend.playback.pause().get() + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def PlayPause(self): + logger.debug(u'%s.PlayPause called', PLAYER_IFACE) + + # TODO Pseudo code: + # if playback.state == playback.PLAYING: playback.pause() + # elif playback.state == playback.PAUSED: playback.resume() + # elif playback.state == playback.STOPPED: playback.play() + + # XXX Proof of concept only. Throw away, write tests, reimplement: + self.backend.playback.pause().get() + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Stop(self): + logger.debug(u'%s.Stop called', PLAYER_IFACE) + self.backend.playback.stop().get() + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Play(self): + logger.debug(u'%s.Play called', PLAYER_IFACE) + # TODO Pseudo code: + # if playback.state == playback.PAUSED: playback.resume() + # elif playback.state == playback.STOPPED: playback.play() + pass + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def Seek(self, offset): + logger.debug(u'%s.Seek called', PLAYER_IFACE) + # TODO Pseudo code: + # new_position = playback.time_position + offset + # if new_position > playback.current_track.length: + # playback.next() + # return + # if new_position < 0: new_position = 0 + # playback.seek(new_position) + pass + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def SetPosition(self, track_id, position): + logger.debug(u'%s.SetPosition called', PLAYER_IFACE) + # TODO Pseudo code: + # if track_id != playback.current_track.track_id: return + # if not 0 <= position <= playback.current_track.length: return + # playback.seek(position) + pass + + @dbus.service.method(dbus_interface=PLAYER_IFACE) + def OpenUri(self, uri): + logger.debug(u'%s.OpenUri called', PLAYER_IFACE) + # TODO Pseudo code: + # if uri.scheme not in SupportedUriSchemes: return + # if uri.mime_type not in SupportedMimeTypes: return + # track = library.lookup(uri) + # cp_track = current_playlist.add(track) + # playback.play(cp_track) + pass + + + ### Player interface signals + + @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') + def Seeked(self, position): + logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) + # TODO What should we do here? + pass + + + ### Player interface properties def get_PlaybackStatus(self): state = self.backend.playback.state.get() @@ -260,81 +344,3 @@ class MprisObject(dbus.service.Object): elif value == 'Playlist': self.backend.playback.repeat = True self.backend.playback.single = False - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Next(self): - logger.debug(u'%s.Next called', PLAYER_IFACE) - self.backend.playback.next().get() - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Previous(self): - logger.debug(u'%s.Previous called', PLAYER_IFACE) - self.backend.playback.previous().get() - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def OpenUri(self, uri): - logger.debug(u'%s.OpenUri called', PLAYER_IFACE) - # TODO Pseudo code: - # if uri.scheme not in SupportedUriSchemes: return - # if uri.mime_type not in SupportedMimeTypes: return - # track = library.lookup(uri) - # cp_track = current_playlist.add(track) - # playback.play(cp_track) - pass - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Pause(self): - logger.debug(u'%s.Pause called', PLAYER_IFACE) - self.backend.playback.pause().get() - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Play(self): - logger.debug(u'%s.Play called', PLAYER_IFACE) - # TODO Pseudo code: - # if playback.state == playback.PAUSED: playback.resume() - # elif playback.state == playback.STOPPED: playback.play() - pass - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def PlayPause(self): - logger.debug(u'%s.PlayPause called', PLAYER_IFACE) - - # TODO Pseudo code: - # if playback.state == playback.PLAYING: playback.pause() - # elif playback.state == playback.PAUSED: playback.resume() - # elif playback.state == playback.STOPPED: playback.play() - - # XXX Proof of concept only. Throw away, write tests, reimplement: - self.backend.playback.pause().get() - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Seek(self, offset): - logger.debug(u'%s.Seek called', PLAYER_IFACE) - # TODO Pseudo code: - # new_position = playback.time_position + offset - # if new_position > playback.current_track.length: - # playback.next() - # return - # if new_position < 0: new_position = 0 - # playback.seek(new_position) - pass - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def SetPosition(self, track_id, position): - logger.debug(u'%s.SetPosition called', PLAYER_IFACE) - # TODO Pseudo code: - # if track_id != playback.current_track.track_id: return - # if not 0 <= position <= playback.current_track.length: return - # playback.seek(position) - pass - - @dbus.service.method(dbus_interface=PLAYER_IFACE) - def Stop(self): - logger.debug(u'%s.Stop called', PLAYER_IFACE) - self.backend.playback.stop().get() - - @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') - def Seeked(self, position): - logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) - # TODO What should we do here? - pass From b99b68209e2c9e7b4290e6aa24159b4421641bf6 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 6 Jun 2011 11:07:26 +0200 Subject: [PATCH 033/350] Test mpris.Pause() --- .../frontends/mpris/player_interface_test.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 3f61fbbe..9b80be14 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -156,6 +156,29 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Pause() self.assertEquals(self.backend.playback.state.get(), PAUSED) + def test_pause_when_paused_has_no_effect(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + self.mpris.Pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + + def test_play_after_pause_resumes_from_same_position(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + + before_pause = self.backend.playback.time_position.get() + self.assert_(before_pause >= 0) + + self.mpris.Pause() + at_pause = self.backend.playback.time_position.get() + self.assert_(at_pause >= before_pause) + + self.mpris.Play() + after_pause = self.backend.playback.time_position.get() + self.assert_(after_pause >= at_pause) + def test_stop_when_playing_should_stop_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 9de9d9102085323b3a27b2e5db7d4a2c7d055463 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 6 Jun 2011 23:19:32 +0200 Subject: [PATCH 034/350] Remove unused import and mock --- tests/frontends/mpris/player_interface_test.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 9b80be14..c3ee4f5d 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -1,8 +1,6 @@ import mock import unittest -from pykka.registry import ActorRegistry - from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends import mpris @@ -14,7 +12,6 @@ STOPPED = PlaybackController.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): - mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) mpris.MprisObject._connect_to_dbus = mock.Mock() self.backend = DummyBackend.start().proxy() self.mpris = mpris.MprisObject() From 5f7e905603fe51d153ab80f6f7a5ee48ed72a3c0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 6 Jun 2011 23:24:59 +0200 Subject: [PATCH 035/350] Add missing __init__.py in MPRIS test dir --- tests/frontends/mpris/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/frontends/mpris/__init__.py diff --git a/tests/frontends/mpris/__init__.py b/tests/frontends/mpris/__init__.py new file mode 100644 index 00000000..e69de29b From 1e73e7bbf70f9fe544f92da7996d06da164e3a8e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 00:07:42 +0200 Subject: [PATCH 036/350] Make mpris.Previous() state change tests pass --- mopidy/backends/base/playback.py | 9 ++++----- tests/backends/base/playback.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 4eedbcb9..42070a40 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -380,7 +380,7 @@ class PlaybackController(object): def pause(self): """Pause playback.""" - if self.state == self.PLAYING and self.provider.pause(): + if self.provider.pause(): self.state = self.PAUSED def play(self, cp_track=None, on_error_step=1): @@ -432,10 +432,9 @@ class PlaybackController(object): will continue. If it was paused, it will still be paused, etc. """ if self.cp_track_at_previous is None: - return - if self.state == self.STOPPED: - return - self.change_track(self.cp_track_at_previous, on_error_step=-1) + self.stop() + else: + self.change_track(self.cp_track_at_previous, on_error_step=-1) def resume(self): """If paused, resume playing the current track.""" diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 2d455225..47a14e3c 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -555,7 +555,7 @@ class PlaybackControllerTest(object): @populate_playlist def test_pause_when_stopped(self): self.playback.pause() - self.assertEqual(self.playback.state, self.playback.STOPPED) + self.assertEqual(self.playback.state, self.playback.PAUSED) @populate_playlist def test_pause_when_playing(self): From 8bea5485183f622d7df284cadce86a04b8a768dd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 00:43:07 +0200 Subject: [PATCH 037/350] Test and implement mpris.PlayPause() --- mopidy/frontends/mpris.py | 15 +++++----- .../frontends/mpris/player_interface_test.py | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 9fd4ede9..80a64c17 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -248,14 +248,13 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def PlayPause(self): logger.debug(u'%s.PlayPause called', PLAYER_IFACE) - - # TODO Pseudo code: - # if playback.state == playback.PLAYING: playback.pause() - # elif playback.state == playback.PAUSED: playback.resume() - # elif playback.state == playback.STOPPED: playback.play() - - # XXX Proof of concept only. Throw away, write tests, reimplement: - self.backend.playback.pause().get() + state = self.backend.playback.state.get() + if state == PlaybackController.PLAYING: + self.backend.playback.pause().get() + elif state == PlaybackController.PAUSED: + self.backend.playback.resume().get() + elif state == PlaybackController.STOPPED: + self.backend.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Stop(self): diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index c3ee4f5d..80b1f678 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -161,6 +161,34 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Pause() self.assertEquals(self.backend.playback.state.get(), PAUSED) + def test_playpause_when_playing_should_pause_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.PlayPause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + + def test_playpause_when_paused_should_resume_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.pause() + + self.assertEquals(self.backend.playback.state.get(), PAUSED) + at_pause = self.backend.playback.time_position.get() + self.assert_(at_pause >= 0) + + self.mpris.PlayPause() + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + after_pause = self.backend.playback.time_position.get() + self.assert_(after_pause >= at_pause) + + def test_playpause_when_stopped_should_start_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.PlayPause() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_play_after_pause_resumes_from_same_position(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() From 25d0b3d262b6f94da1914b2bd57bf1b9398a0a4d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Jun 2011 00:48:19 +0200 Subject: [PATCH 038/350] Test mpris.Stop() --- .../frontends/mpris/player_interface_test.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 80b1f678..db39d015 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -189,6 +189,21 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.PlayPause() self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_stop_when_playing_should_stop_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Stop() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + + def test_stop_when_paused_should_stop_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + self.mpris.Stop() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_play_after_pause_resumes_from_same_position(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() @@ -203,10 +218,3 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Play() after_pause = self.backend.playback.time_position.get() self.assert_(after_pause >= at_pause) - - def test_stop_when_playing_should_stop_playback(self): - self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.backend.playback.play() - self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris.Stop() - self.assertEquals(self.backend.playback.state.get(), STOPPED) From 387d72ef67b1203a831124e1b6288bb405864012 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 7 Jun 2011 16:05:26 +0200 Subject: [PATCH 039/350] Remove self.set_reuse_addr from asyncore code --- mopidy/frontends/mpd/server.py | 1 - mopidy/utils/network.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 927e2a00..b3aa0481 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -21,7 +21,6 @@ class MpdServer(asyncore.dispatcher): """Start MPD server.""" try: self.socket = network.create_socket() - self.set_reuse_addr() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1dedf7d7..80a51c77 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -27,6 +27,7 @@ def create_socket(): sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock def format_hostname(hostname): From 1a6d577ed5b5b65475645627aab68b97b5d11576 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 19:39:36 +0200 Subject: [PATCH 040/350] Test and implement mpris.Play() --- mopidy/frontends/mpris.py | 9 +++++---- tests/frontends/mpris/player_interface_test.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 80a64c17..d1107020 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -264,10 +264,11 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Play(self): logger.debug(u'%s.Play called', PLAYER_IFACE) - # TODO Pseudo code: - # if playback.state == playback.PAUSED: playback.resume() - # elif playback.state == playback.STOPPED: playback.play() - pass + state = self.backend.playback.state.get() + if state == PlaybackController.PAUSED: + self.backend.playback.resume().get() + else: + self.backend.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index db39d015..d8514abd 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -204,6 +204,12 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Stop() self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_play_when_stopped_starts_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.Play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_play_after_pause_resumes_from_same_position(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() @@ -212,9 +218,17 @@ class PlayerInterfaceTest(unittest.TestCase): self.assert_(before_pause >= 0) self.mpris.Pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) at_pause = self.backend.playback.time_position.get() self.assert_(at_pause >= before_pause) self.mpris.Play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) after_pause = self.backend.playback.time_position.get() self.assert_(after_pause >= at_pause) + + def test_play_when_there_is_no_track_has_no_effect(self): + self.backend.current_playlist.clear() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.Play() + self.assertEquals(self.backend.playback.state.get(), STOPPED) From 4404e34a798dab4a489a461ab7564cac0ef24738 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 19:55:59 +0200 Subject: [PATCH 041/350] Test and implement mpris.Seek() --- mopidy/frontends/mpris.py | 12 +-- .../frontends/mpris/player_interface_test.py | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index d1107020..50e4e49a 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -273,14 +273,10 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): logger.debug(u'%s.Seek called', PLAYER_IFACE) - # TODO Pseudo code: - # new_position = playback.time_position + offset - # if new_position > playback.current_track.length: - # playback.next() - # return - # if new_position < 0: new_position = 0 - # playback.seek(new_position) - pass + offset_in_milliseconds = offset // 1000 + current_position = self.backend.playback.time_position.get() + new_position = current_position + offset_in_milliseconds + self.backend.playback.seek(new_position) @dbus.service.method(dbus_interface=PLAYER_IFACE) def SetPosition(self, track_id, position): diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index d8514abd..ef84ce3c 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -232,3 +232,82 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.state.get(), STOPPED) self.mpris.Play() self.assertEquals(self.backend.playback.state.get(), STOPPED) + + def test_seek_seeks_given_microseconds_forward_in_the_current_track(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + + before_seek = self.backend.playback.time_position.get() + self.assert_(before_seek >= 0) + + milliseconds_to_seek = 10000 + microseconds_to_seek = milliseconds_to_seek * 1000 + + self.mpris.Seek(microseconds_to_seek) + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + + after_seek = self.backend.playback.time_position.get() + self.assert_(after_seek >= (before_seek + milliseconds_to_seek)) + + def test_seek_seeks_given_microseconds_backward_if_negative(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_seek = self.backend.playback.time_position.get() + self.assert_(before_seek >= 20000) + + milliseconds_to_seek = -10000 + microseconds_to_seek = milliseconds_to_seek * 1000 + + self.mpris.Seek(microseconds_to_seek) + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + + after_seek = self.backend.playback.time_position.get() + self.assert_(after_seek >= (before_seek + milliseconds_to_seek)) + self.assert_(after_seek < before_seek) + + def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_seek = self.backend.playback.time_position.get() + self.assert_(before_seek >= 20000) + + milliseconds_to_seek = -30000 + microseconds_to_seek = milliseconds_to_seek * 1000 + + self.mpris.Seek(microseconds_to_seek) + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + + after_seek = self.backend.playback.time_position.get() + self.assert_(after_seek >= (before_seek + milliseconds_to_seek)) + self.assert_(after_seek < before_seek) + self.assert_(after_seek >= 0) + + def test_seek_skips_to_next_track_if_new_position_larger_than_track_length(self): + self.backend.current_playlist.append([Track(uri='a', length=40000), + Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_seek = self.backend.playback.time_position.get() + self.assert_(before_seek >= 20000) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + milliseconds_to_seek = 50000 + microseconds_to_seek = milliseconds_to_seek * 1000 + + self.mpris.Seek(microseconds_to_seek) + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + + after_seek = self.backend.playback.time_position.get() + self.assert_(after_seek >= 0) + self.assert_(after_seek < before_seek) From 5d1da4eeafa7c27fbcbca2dcec3e24ee2557b534 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 20:21:55 +0200 Subject: [PATCH 042/350] Test and implement mpris.SetPosition() --- mopidy/frontends/mpris.py | 17 ++-- .../frontends/mpris/player_interface_test.py | 89 +++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 50e4e49a..4a15e9df 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -281,11 +281,18 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def SetPosition(self, track_id, position): logger.debug(u'%s.SetPosition called', PLAYER_IFACE) - # TODO Pseudo code: - # if track_id != playback.current_track.track_id: return - # if not 0 <= position <= playback.current_track.length: return - # playback.seek(position) - pass + position = position // 1000 + current_track = self.backend.playback.current_track.get() + # TODO Currently the ID is assumed to be the URI of the track. This + # should be changed to a D-Bus object ID than can be mapped to the CPID + # and URI of the track. + if current_track and current_track.uri != track_id: + return + if position < 0: + return + if current_track and current_track.length < position: + return + self.backend.playback.seek(position) @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index ef84ce3c..3c753c08 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -311,3 +311,92 @@ class PlayerInterfaceTest(unittest.TestCase): after_seek = self.backend.playback.time_position.get() self.assert_(after_seek >= 0) self.assert_(after_seek < before_seek) + + def test_set_position_sets_the_current_track_position_in_microsecs(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + + before_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position <= 5000) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + + track_id = 'a' + + position_to_set_in_milliseconds = 20000 + position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 + + self.mpris.SetPosition(track_id, position_to_set_in_microseconds) + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + + after_set_position = self.backend.playback.time_position.get() + self.assert_(after_set_position >= position_to_set_in_milliseconds) + + def test_set_position_does_nothing_if_the_position_is_negative(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position >= 20000) + self.assert_(before_set_position <= 25000) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + track_id = 'a' + + position_to_set_in_milliseconds = -1000 + position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 + + self.mpris.SetPosition(track_id, position_to_set_in_microseconds) + + after_set_position = self.backend.playback.time_position.get() + self.assert_(after_set_position >= before_set_position) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + def test_set_position_does_nothing_if_position_is_larger_than_track_length(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position >= 20000) + self.assert_(before_set_position <= 25000) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + track_id = 'a' + + position_to_set_in_milliseconds = 50000 + position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 + + self.mpris.SetPosition(track_id, position_to_set_in_microseconds) + + after_set_position = self.backend.playback.time_position.get() + self.assert_(after_set_position >= before_set_position) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + def test_set_position_does_nothing_if_track_id_does_not_match_current_track(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(20000) + + before_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position >= 20000) + self.assert_(before_set_position <= 25000) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + track_id = 'b' + + position_to_set_in_milliseconds = 0 + position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 + + self.mpris.SetPosition(track_id, position_to_set_in_microseconds) + + after_set_position = self.backend.playback.time_position.get() + self.assert_(after_set_position >= before_set_position) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') From e1fc403435d59d73d0ac4839ba0ae389406cb4ca Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 20:35:45 +0200 Subject: [PATCH 043/350] Test and implement mpris.Rate property --- mopidy/frontends/mpris.py | 6 ++++- .../frontends/mpris/player_interface_test.py | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 4a15e9df..ec711713 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -115,7 +115,7 @@ class MprisObject(dbus.service.Object): return { 'PlaybackStatus': (self.get_PlaybackStatus, None), 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus), - 'Rate': (1.0, None), + 'Rate': (1.0, self.set_Rate), # TODO Get/set backend.playback.random 'Shuffle': (False, None), # TODO Get meta data @@ -347,3 +347,7 @@ class MprisObject(dbus.service.Object): elif value == 'Playlist': self.backend.playback.repeat = True self.backend.playback.single = False + + def set_Rate(self, value): + if value == 0: + self.Pause() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 3c753c08..60c36288 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -68,6 +68,31 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), False) + def test_get_rate_is_greater_or_equal_than_minimum_rate(self): + rate = self.mpris.Get(mpris.PLAYER_IFACE, 'Rate') + minimum_rate = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') + self.assert_(rate >= minimum_rate) + + def test_get_rate_is_less_or_equal_than_maximum_rate(self): + rate = self.mpris.Get(mpris.PLAYER_IFACE, 'Rate') + maximum_rate = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') + self.assert_(rate >= maximum_rate) + + def test_set_rate_to_zero_pauses_playback(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) + self.assertEquals(self.backend.playback.state.get(), PAUSED) + + def test_get_minimum_rate_is_one_or_less(self): + result = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') + self.assert_(result <= 1.0) + + def test_get_maximum_rate_is_one_or_more(self): + result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') + self.assert_(result >= 1.0) + def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 3df3527c73435bc56d6afd9fca11ca9e5f0189a8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 20:42:34 +0200 Subject: [PATCH 044/350] Test and implement mpris.Shuffle property --- mopidy/frontends/mpris.py | 12 ++++++++-- .../frontends/mpris/player_interface_test.py | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index ec711713..95ce9bcd 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -116,8 +116,7 @@ class MprisObject(dbus.service.Object): 'PlaybackStatus': (self.get_PlaybackStatus, None), 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus), 'Rate': (1.0, self.set_Rate), - # TODO Get/set backend.playback.random - 'Shuffle': (False, None), + 'Shuffle': (self.get_Shuffle, self.set_Shuffle), # TODO Get meta data 'Metadata': ({ 'mpris:trackid': '', # TODO Use (cpid, track.uri) @@ -351,3 +350,12 @@ class MprisObject(dbus.service.Object): def set_Rate(self, value): if value == 0: self.Pause() + + def get_Shuffle(self): + return self.backend.playback.shuffle.get() + + def set_Shuffle(self, value): + if value: + self.backend.playback.shuffle = True + else: + self.backend.playback.shuffle = False diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 60c36288..7e434017 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -85,6 +85,28 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) self.assertEquals(self.backend.playback.state.get(), PAUSED) + def test_get_shuffle_returns_true_if_shuffle_is_active(self): + self.backend.playback.shuffle = True + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') + self.assertTrue(result) + + def test_get_shuffle_returns_false_if_shuffle_is_inactive(self): + self.backend.playback.shuffle = False + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') + self.assertFalse(result) + + def test_set_shuffle_to_true_activates_shuffle_mode(self): + self.backend.playback.shuffle = False + self.assertFalse(self.backend.playback.shuffle.get()) + result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) + self.assertTrue(self.backend.playback.shuffle.get()) + + def test_set_shuffle_to_false_deactivates_shuffle_mode(self): + self.backend.playback.shuffle = True + self.assertTrue(self.backend.playback.shuffle.get()) + result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', False) + self.assertFalse(self.backend.playback.shuffle.get()) + def test_get_minimum_rate_is_one_or_less(self): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') self.assert_(result <= 1.0) From 84ac7b3e6ac68fe7adcc1531a357dd526c763d5d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Jun 2011 00:01:42 +0200 Subject: [PATCH 045/350] Test and implement mpris.Volume property --- mopidy/frontends/mpris.py | 28 ++++++++++++++++-- .../frontends/mpris/player_interface_test.py | 29 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 95ce9bcd..1ac9f097 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -15,6 +15,7 @@ from pykka.registry import ActorRegistry from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends.base import BaseFrontend +from mopidy.mixers.base import BaseMixer logger = logging.getLogger('mopidy.frontends.mpris') @@ -90,6 +91,7 @@ class MprisObject(dbus.service.Object): def __init__(self): self._backend = None + self._mixer = None self.properties = { ROOT_IFACE: self._get_root_iface_properties(), PLAYER_IFACE: self._get_player_iface_properties(), @@ -121,8 +123,7 @@ class MprisObject(dbus.service.Object): 'Metadata': ({ 'mpris:trackid': '', # TODO Use (cpid, track.uri) }, None), - # TODO Get/set volume - 'Volume': (1.0, None), + 'Volume': (self.get_Volume, self.set_Volume), # TODO Get backend.playback.time_position 'Position': (0, None), 'MinimumRate': (1.0, None), @@ -155,6 +156,14 @@ class MprisObject(dbus.service.Object): self._backend = backend_refs[0].proxy() return self._backend + @property + def mixer(self): + if self._mixer is None: + mixer_refs = ActorRegistry.get_by_class(BaseMixer) + assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' + self._mixer = mixer_refs[0].proxy() + return self._mixer + ### Properties interface @@ -359,3 +368,18 @@ class MprisObject(dbus.service.Object): self.backend.playback.shuffle = True else: self.backend.playback.shuffle = False + + def get_Volume(self): + volume = self.mixer.volume.get() + if volume is not None: + return volume / 100.0 + + def set_Volume(self, value): + if value is None: + return + elif value < 0: + self.mixer.volume = 0 + elif value > 1: + self.mixer.volume = 100 + elif 0 <= value <= 1: + self.mixer.volume = int(value * 100) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 7e434017..5d84b6d8 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -4,6 +4,7 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends import mpris +from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track PLAYING = PlaybackController.PLAYING @@ -13,12 +14,14 @@ STOPPED = PlaybackController.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): mpris.MprisObject._connect_to_dbus = mock.Mock() + self.mixer = DummyMixer.start().proxy() self.backend = DummyBackend.start().proxy() self.mpris = mpris.MprisObject() self.mpris._backend = self.backend def tearDown(self): self.backend.stop() + self.mixer.stop() def test_get_playback_status_is_playing_when_playing(self): self.backend.playback.state = PLAYING @@ -107,6 +110,32 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', False) self.assertFalse(self.backend.playback.shuffle.get()) + def test_get_volume_should_return_volume_between_zero_and_one(self): + self.mixer.volume = 0 + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + self.assertEquals(result, 0) + + self.mixer.volume = 50 + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + self.assertEquals(result, 0.5) + + self.mixer.volume = 100 + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + self.assertEquals(result, 1) + + def test_set_volume_to_one_should_set_mixer_volume_to_100(self): + self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 1.0) + self.assertEquals(self.mixer.volume.get(), 100) + + def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self): + self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 2.0) + self.assertEquals(self.mixer.volume.get(), 100) + + def test_set_volume_to_anything_not_a_number_does_not_change_volume(self): + self.mixer.volume = 10 + self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', None) + self.assertEquals(self.mixer.volume.get(), 10) + def test_get_minimum_rate_is_one_or_less(self): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') self.assert_(result <= 1.0) From 1b075ac4a4154853ada060c59abdde157e65ae75 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Jun 2011 00:07:18 +0200 Subject: [PATCH 046/350] Test and implement mpris.Position property --- mopidy/frontends/mpris.py | 6 ++++-- tests/frontends/mpris/player_interface_test.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 1ac9f097..04aaeca5 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -124,8 +124,7 @@ class MprisObject(dbus.service.Object): 'mpris:trackid': '', # TODO Use (cpid, track.uri) }, None), 'Volume': (self.get_Volume, self.set_Volume), - # TODO Get backend.playback.time_position - 'Position': (0, None), + 'Position': (self.get_Position, None), 'MinimumRate': (1.0, None), 'MaximumRate': (1.0, None), # TODO True if CanControl and backend.playback.track_at_next @@ -383,3 +382,6 @@ class MprisObject(dbus.service.Object): self.mixer.volume = 100 elif 0 <= value <= 1: self.mixer.volume = int(value * 100) + + def get_Position(self): + return self.backend.playback.time_position.get() * 1000 diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 5d84b6d8..817a5fbc 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -136,6 +136,19 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', None) self.assertEquals(self.mixer.volume.get(), 10) + def test_get_position_returns_time_position_in_microseconds(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + self.backend.playback.seek(10000) + result_in_microseconds = self.mpris.Get(mpris.PLAYER_IFACE, 'Position') + result_in_milliseconds = result_in_microseconds // 1000 + self.assert_(result_in_milliseconds >= 10000) + + def test_get_position_when_no_current_track_should_be_zero(self): + result_in_microseconds = self.mpris.Get(mpris.PLAYER_IFACE, 'Position') + result_in_milliseconds = result_in_microseconds // 1000 + self.assertEquals(result_in_milliseconds, 0) + def test_get_minimum_rate_is_one_or_less(self): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') self.assert_(result <= 1.0) From 24dbba2fa9d3f2dc76657a2d631e8f6f15232a7f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 16 Jun 2011 23:17:37 +0200 Subject: [PATCH 047/350] Switch to async globject based loop --- mopidy/frontends/mpd/server.py | 53 +++++++++---- mopidy/frontends/mpd/session.py | 58 --------------- mopidy/utils/network.py | 127 ++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 71 deletions(-) delete mode 100644 mopidy/frontends/mpd/session.py diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 62e443fb..1e5f5d4a 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -1,14 +1,17 @@ -import asyncore import logging import sys +import gobject + from mopidy import settings from mopidy.utils import network -from .session import MpdSession +from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION +from mopidy.utils.log import indent logger = logging.getLogger('mopidy.frontends.mpd.server') -class MpdServer(asyncore.dispatcher): +class MpdServer(object): """ The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession` for each client connection. @@ -17,22 +20,46 @@ class MpdServer(asyncore.dispatcher): def start(self): """Start MPD server.""" try: - self.set_socket(network.create_socket()) - self.set_reuse_addr() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) - self.bind((hostname, port)) - self.listen(1) + network.Listener((hostname, port), handler=MpdHandler) logger.info(u'MPD server running at [%s]:%s', hostname, port) except IOError, e: logger.error(u'MPD server startup failed: %s' % str(e).decode('utf-8')) sys.exit(1) - def handle_accept(self): - """Called by asyncore when a new client connects.""" - (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) + +class MpdHandler(network.BaseHandler): + """ + The MPD client session. Keeps track of a single client session. Any + requests from the client is passed on to the MPD request dispatcher. + """ + + terminator = LINE_TERMINATOR + + def __init__(self, (sock, addr)): + super(MpdHandler, self).__init__((sock, addr)) + self.dispatcher = MpdDispatcher(session=self) + self.send_response([u'OK MPD %s' % VERSION]) + + def recv(self, line): + """Handle the request using the MPD command handlers.""" + request = line.decode(ENCODING) + logger.debug(u'Request from [%s]:%s: %s', self.addr[0], + self.addr[1], indent(request)) + self.send_response(self.dispatcher.handle_request(request)) + + def send_response(self, response): + """ + Format a response from the MPD command handlers and send it to the + client. + """ + if response: + response = LINE_TERMINATOR.join(response) + logger.debug(u'Response to [%s]:%s: %s', self.addr[0], + self.addr[1], indent(response)) + response = u'%s%s' % (response, LINE_TERMINATOR) + data = response.encode(ENCODING) + self.send(data) diff --git a/mopidy/frontends/mpd/session.py b/mopidy/frontends/mpd/session.py deleted file mode 100644 index ce5d3be7..00000000 --- a/mopidy/frontends/mpd/session.py +++ /dev/null @@ -1,58 +0,0 @@ -import asynchat -import logging - -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION -from mopidy.utils.log import indent - -logger = logging.getLogger('mopidy.frontends.mpd.session') - -class MpdSession(asynchat.async_chat): - """ - 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): - 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.input_buffer = [] - self.authenticated = False - self.set_terminator(LINE_TERMINATOR.encode(ENCODING)) - self.dispatcher = MpdDispatcher(session=self) - self.send_response([u'OK MPD %s' % VERSION]) - - def collect_incoming_data(self, data): - """Called by asynchat when new data arrives.""" - self.input_buffer.append(data) - - def found_terminator(self): - """Called by asynchat when a terminator is found in incoming data.""" - data = ''.join(self.input_buffer).strip() - self.input_buffer = [] - try: - self.send_response(self.handle_request(data)) - except UnicodeDecodeError as e: - logger.warning(u'Received invalid data: %s', e) - - def handle_request(self, request): - """Handle the request using the MPD command handlers.""" - request = request.decode(ENCODING) - logger.debug(u'Request from [%s]:%s: %s', self.client_address, - self.client_port, indent(request)) - return self.dispatcher.handle_request(request) - - def send_response(self, response): - """ - Format a response from the MPD command handlers and send it to the - client. - """ - if response: - response = LINE_TERMINATOR.join(response) - logger.debug(u'Response to [%s]:%s: %s', self.client_address, - self.client_port, indent(response)) - response = u'%s%s' % (response, LINE_TERMINATOR) - data = response.encode(ENCODING) - self.push(data) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 80a51c77..a5680466 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -1,6 +1,7 @@ import logging import re import socket +import gobject logger = logging.getLogger('mopidy.utils.server') @@ -35,3 +36,129 @@ def format_hostname(hostname): if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % hostname return hostname + +class BaseHandler(object): + """Buffered lined based client, subclass for use.""" + + #: Line terminator to use in parse_line, can be overridden by subclasses. + terminator = '\n' + + def __init__(self, (sock, addr)): + logger.debug('Established connection from %s', addr) + + self.sock, self.addr = sock, addr + self.receiver = None + self.sender = None + self.recv_buffer = '' + self.send_buffer = '' + + self.sock.setblocking(0) + self.add_recv_watch() + + def add_recv_watch(self): + """Register recv and error handling of socket.""" + if self.receiver is None: + self.receiver = gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN + | gobject.IO_ERR | gobject.IO_HUP, self.handle) + + def clear_recv_watch(self): + if self.receiver is not None: + gobject.source_remove(self.receiver) + self.receiver = None + + def add_send_watch(self): + """Register send handling if it has not already been done.""" + if self.sender is None: + self.sender = gobject.io_add_watch(self.sock.fileno(), + gobject.IO_OUT, self.handle) + + def clear_send_watch(self): + """Remove send watcher if it is set.""" + if self.sender is not None: + gobject.source_remove(self.sender) + self.sender = None + + def handle(self, fd, flags): + """Dispatch based on current flags.""" + if flags & (gobject.IO_ERR | gobject.IO_HUP): + return self.close() + if flags & gobject.IO_IN: + return self.io_in() + if flags & gobject.IO_OUT: + return self.io_out() + logger.error('Unknown flag: %s', flags) + return False + + def io_in(self): + """Record any incoming data to buffer and parse lines.""" + data = self.sock.recv(1024) + self.recv_buffer += data # XXX limit buffer size? + if data: + return self.parse_lines() + else: + return self.close() + + def io_out(self): + """Send as much of outgoing buffer as possible.""" + if self.send_buffer: + sent = self.sock.send(self.send_buffer) + self.send_buffer = self.send_buffer[sent:] + if not self.send_buffer: + self.clear_send_watch() + return True + + def close(self): + """Close connection.""" + logger.debug('Closing connection from %s', self.addr) + self.clear_send_watch() + self.sock.close() + return False + + def release(self): + """Forget about socket so that other loop can take over FD. + + Note that other code will still need to keep a ref to the socket in + order to prevent GC cleanup closing it. + """ + self.clear_recv_watch() + self.clear_send_watch() + return self.sock + + def send(self, data): + """Add raw data to send to outbound buffer.""" + self.add_send_watch() + self.send_buffer += data # XXX limit buffer size? + + def recv(self, line): + """Recv one and one line of request. Must be sub-classed.""" + raise NotImplementedError + + def parse_lines(self): + """Parse lines by splitting at terminator.""" + while self.terminator in self.recv_buffer: + line, self.recv_buffer = self.recv_buffer.split(self.terminator, 1) + self.recv(line) + return True + +class EchoHandler(BaseHandler): + """Basic handler used for debuging of Listener and Handler code itself.""" + def recv(self, line): + print repr(line) + self.send(line) + +class Listener(object): + """Setup listener and register it with gobject loop.""" + def __init__(self, addr, handler=EchoHandler): + self.handler = handler + self.sock = create_socket() + self.sock.setblocking(0) + self.sock.bind(addr) + self.sock.listen(5) + + gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN, self.handle) + logger.debug('Listening on %s using %s', addr, self.handler) + + def handle(self, fd, flags): + self.handler(self.sock.accept()) + return True + From 9df16e071630a3e1f9af94e624d539a05e0343dc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 17 Jun 2011 00:01:55 +0200 Subject: [PATCH 048/350] Get rid of custom async client code in favour of blocking IOChannel in ThreadingActors --- mopidy/frontends/mpd/__init__.py | 1 - mopidy/frontends/mpd/server.py | 39 ++++++---- mopidy/utils/network.py | 119 ++----------------------------- 3 files changed, 31 insertions(+), 128 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 175aa0ee..b6088a41 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -44,4 +44,3 @@ class MpdThread(BaseThread): logger.debug(u'Starting MPD server thread') server = MpdServer() server.start() - asyncore.loop() diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py index 1e5f5d4a..12e6a92a 100644 --- a/mopidy/frontends/mpd/server.py +++ b/mopidy/frontends/mpd/server.py @@ -3,6 +3,8 @@ import sys import gobject +from pykka.actor import ThreadingActor + from mopidy import settings from mopidy.utils import network from mopidy.frontends.mpd.dispatcher import MpdDispatcher @@ -23,7 +25,7 @@ class MpdServer(object): hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) - network.Listener((hostname, port), handler=MpdHandler) + network.Listener((hostname, port), session=MpdSession) logger.info(u'MPD server running at [%s]:%s', hostname, port) except IOError, e: logger.error(u'MPD server startup failed: %s' % @@ -31,25 +33,33 @@ class MpdServer(object): sys.exit(1) -class MpdHandler(network.BaseHandler): +class MpdSession(ThreadingActor): """ The MPD client session. Keeps track of a single client session. Any requests from the client is passed on to the MPD request dispatcher. """ - terminator = LINE_TERMINATOR - - def __init__(self, (sock, addr)): - super(MpdHandler, self).__init__((sock, addr)) + def __init__(self, sock, addr): + self.sock = sock # Prevent premature GC + self.addr = addr + self.channel = gobject.IOChannel(sock.fileno()) self.dispatcher = MpdDispatcher(session=self) - self.send_response([u'OK MPD %s' % VERSION]) - def recv(self, line): - """Handle the request using the MPD command handlers.""" - request = line.decode(ENCODING) - logger.debug(u'Request from [%s]:%s: %s', self.addr[0], - self.addr[1], indent(request)) - self.send_response(self.dispatcher.handle_request(request)) + def on_start(self): + try: + self.send_response([u'OK MPD %s' % VERSION]) + self.request_loop() + except gobject.GError, e: + self.stop() + + def request_loop(self): + while True: + logger.debug('Trying to readline') + request = self.channel.readline()[:-1].decode(ENCODING) + logger.debug(u'Request from [%s]:%s: %s', self.addr[0], + self.addr[1], indent(request)) + response = self.dispatcher.handle_request(request) + self.send_response(response) def send_response(self, response): """ @@ -62,4 +72,5 @@ class MpdHandler(network.BaseHandler): self.addr[1], indent(response)) response = u'%s%s' % (response, LINE_TERMINATOR) data = response.encode(ENCODING) - self.send(data) + self.channel.write(data) + self.channel.flush() diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index a5680466..df4f9292 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -37,128 +37,21 @@ def format_hostname(hostname): hostname = '::ffff:%s' % hostname return hostname -class BaseHandler(object): - """Buffered lined based client, subclass for use.""" - - #: Line terminator to use in parse_line, can be overridden by subclasses. - terminator = '\n' - - def __init__(self, (sock, addr)): - logger.debug('Established connection from %s', addr) - - self.sock, self.addr = sock, addr - self.receiver = None - self.sender = None - self.recv_buffer = '' - self.send_buffer = '' - - self.sock.setblocking(0) - self.add_recv_watch() - - def add_recv_watch(self): - """Register recv and error handling of socket.""" - if self.receiver is None: - self.receiver = gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN - | gobject.IO_ERR | gobject.IO_HUP, self.handle) - - def clear_recv_watch(self): - if self.receiver is not None: - gobject.source_remove(self.receiver) - self.receiver = None - - def add_send_watch(self): - """Register send handling if it has not already been done.""" - if self.sender is None: - self.sender = gobject.io_add_watch(self.sock.fileno(), - gobject.IO_OUT, self.handle) - - def clear_send_watch(self): - """Remove send watcher if it is set.""" - if self.sender is not None: - gobject.source_remove(self.sender) - self.sender = None - - def handle(self, fd, flags): - """Dispatch based on current flags.""" - if flags & (gobject.IO_ERR | gobject.IO_HUP): - return self.close() - if flags & gobject.IO_IN: - return self.io_in() - if flags & gobject.IO_OUT: - return self.io_out() - logger.error('Unknown flag: %s', flags) - return False - - def io_in(self): - """Record any incoming data to buffer and parse lines.""" - data = self.sock.recv(1024) - self.recv_buffer += data # XXX limit buffer size? - if data: - return self.parse_lines() - else: - return self.close() - - def io_out(self): - """Send as much of outgoing buffer as possible.""" - if self.send_buffer: - sent = self.sock.send(self.send_buffer) - self.send_buffer = self.send_buffer[sent:] - if not self.send_buffer: - self.clear_send_watch() - return True - - def close(self): - """Close connection.""" - logger.debug('Closing connection from %s', self.addr) - self.clear_send_watch() - self.sock.close() - return False - - def release(self): - """Forget about socket so that other loop can take over FD. - - Note that other code will still need to keep a ref to the socket in - order to prevent GC cleanup closing it. - """ - self.clear_recv_watch() - self.clear_send_watch() - return self.sock - - def send(self, data): - """Add raw data to send to outbound buffer.""" - self.add_send_watch() - self.send_buffer += data # XXX limit buffer size? - - def recv(self, line): - """Recv one and one line of request. Must be sub-classed.""" - raise NotImplementedError - - def parse_lines(self): - """Parse lines by splitting at terminator.""" - while self.terminator in self.recv_buffer: - line, self.recv_buffer = self.recv_buffer.split(self.terminator, 1) - self.recv(line) - return True - -class EchoHandler(BaseHandler): - """Basic handler used for debuging of Listener and Handler code itself.""" - def recv(self, line): - print repr(line) - self.send(line) - class Listener(object): """Setup listener and register it with gobject loop.""" - def __init__(self, addr, handler=EchoHandler): - self.handler = handler + def __init__(self, addr, session): + self.session = session self.sock = create_socket() self.sock.setblocking(0) self.sock.bind(addr) self.sock.listen(5) gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN, self.handle) - logger.debug('Listening on %s using %s', addr, self.handler) + logger.debug('Listening on %s using %s', addr, self.session) def handle(self, fd, flags): - self.handler(self.sock.accept()) + sock, addr = self.sock.accept() + logger.debug('Got connection from %s', addr) + self.session.start(sock, addr) return True From 74aa96b300f9e498529fc9323f12c96bfb0aa6cc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 17 Jun 2011 01:44:22 +0200 Subject: [PATCH 049/350] Moved mpd session to mopidy.frontends.mpd --- mopidy/frontends/mpd/__init__.py | 78 +++++++++++++++++----- mopidy/frontends/mpd/server.py | 76 --------------------- mopidy/utils/network.py | 6 +- tests/frontends/mpd/authentication_test.py | 2 +- tests/frontends/mpd/connection_test.py | 2 +- tests/frontends/mpd/server_test.py | 23 ------- 6 files changed, 66 insertions(+), 121 deletions(-) delete mode 100644 mopidy/frontends/mpd/server.py delete mode 100644 tests/frontends/mpd/server_test.py diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index b6088a41..d0ca761e 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,14 +1,19 @@ -import asyncore +import gobject import logging +import sys from pykka.actor import ThreadingActor from mopidy.frontends.base import BaseFrontend -from mopidy.frontends.mpd.server import MpdServer -from mopidy.utils.process import BaseThread +from mopidy import settings +from mopidy.utils import network +from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR +from mopidy.utils.log import indent logger = logging.getLogger('mopidy.frontends.mpd') +# FIXME no real need for frontend to be threading actor class MpdFrontend(ThreadingActor, BaseFrontend): """ The MPD frontend. @@ -25,22 +30,61 @@ class MpdFrontend(ThreadingActor, BaseFrontend): """ def __init__(self): - self._thread = None + hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) + port = settings.MPD_SERVER_PORT + + try: + network.Listener(hostname, port, session=MpdSession) + except IOError, e: + logger.error(u'MPD server startup failed: %s', e) + sys.exit(1) + + logger.info(u'MPD server running at [%s]:%s', hostname, port) + + +class MpdSession(ThreadingActor): + """ + 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, sock, addr): + self.sock = sock # Prevent premature GC of socket closing it + self.addr = addr + self.channel = gobject.IOChannel(sock.fileno()) + self.dispatcher = MpdDispatcher() def on_start(self): - self._thread = MpdThread() - self._thread.start() + try: + self.send_response([u'OK MPD %s' % VERSION]) + self.request_loop() + except gobject.GError: + self.stop() - def on_receive(self, message): - pass # Ignore any messages + def close(self): + self.channel.close() + def request_loop(self): + while True: + data = self.channel.readline() + if not data: + return self.close() + request = data.rstrip(LINE_TERMINATOR).decode(ENCODING) + logger.debug(u'Request from [%s]:%s: %s', self.addr[0], + self.addr[1], indent(request)) + response = self.dispatcher.handle_request(request) + self.send_response(response) -class MpdThread(BaseThread): - def __init__(self): - super(MpdThread, self).__init__() - self.name = u'MpdThread' - - def run_inside_try(self): - logger.debug(u'Starting MPD server thread') - server = MpdServer() - server.start() + def send_response(self, response): + """ + Format a response from the MPD command handlers and send it to the + client. + """ + if response: + response = LINE_TERMINATOR.join(response) + logger.debug(u'Response to [%s]:%s: %s', self.addr[0], + self.addr[1], indent(response)) + response = u'%s%s' % (response, LINE_TERMINATOR) + data = response.encode(ENCODING) + self.channel.write(data) + self.channel.flush() diff --git a/mopidy/frontends/mpd/server.py b/mopidy/frontends/mpd/server.py deleted file mode 100644 index 12e6a92a..00000000 --- a/mopidy/frontends/mpd/server.py +++ /dev/null @@ -1,76 +0,0 @@ -import logging -import sys - -import gobject - -from pykka.actor import ThreadingActor - -from mopidy import settings -from mopidy.utils import network -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION -from mopidy.utils.log import indent - -logger = logging.getLogger('mopidy.frontends.mpd.server') - -class MpdServer(object): - """ - The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession` - for each client connection. - """ - - def start(self): - """Start MPD server.""" - try: - hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) - port = settings.MPD_SERVER_PORT - logger.debug(u'MPD server is binding to [%s]:%s', hostname, port) - network.Listener((hostname, port), session=MpdSession) - logger.info(u'MPD server running at [%s]:%s', hostname, port) - except IOError, e: - logger.error(u'MPD server startup failed: %s' % - str(e).decode('utf-8')) - sys.exit(1) - - -class MpdSession(ThreadingActor): - """ - 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, sock, addr): - self.sock = sock # Prevent premature GC - self.addr = addr - self.channel = gobject.IOChannel(sock.fileno()) - self.dispatcher = MpdDispatcher(session=self) - - def on_start(self): - try: - self.send_response([u'OK MPD %s' % VERSION]) - self.request_loop() - except gobject.GError, e: - self.stop() - - def request_loop(self): - while True: - logger.debug('Trying to readline') - request = self.channel.readline()[:-1].decode(ENCODING) - logger.debug(u'Request from [%s]:%s: %s', self.addr[0], - self.addr[1], indent(request)) - response = self.dispatcher.handle_request(request) - self.send_response(response) - - def send_response(self, response): - """ - Format a response from the MPD command handlers and send it to the - client. - """ - if response: - response = LINE_TERMINATOR.join(response) - logger.debug(u'Response to [%s]:%s: %s', self.addr[0], - self.addr[1], indent(response)) - response = u'%s%s' % (response, LINE_TERMINATOR) - data = response.encode(ENCODING) - self.channel.write(data) - self.channel.flush() diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index df4f9292..d1536afb 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -39,15 +39,15 @@ def format_hostname(hostname): class Listener(object): """Setup listener and register it with gobject loop.""" - def __init__(self, addr, session): + def __init__(self, host, port, session): self.session = session self.sock = create_socket() self.sock.setblocking(0) - self.sock.bind(addr) + self.sock.bind((host, port)) self.sock.listen(5) gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN, self.handle) - logger.debug('Listening on %s using %s', addr, self.session) + logger.debug('Listening on [%s]:%s using %s', host, port, self.session) def handle(self, fd, flags): sock, addr = self.sock.accept() diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/authentication_test.py index d795d726..7d340071 100644 --- a/tests/frontends/mpd/authentication_test.py +++ b/tests/frontends/mpd/authentication_test.py @@ -3,7 +3,7 @@ import unittest from mopidy import settings from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.session import MpdSession +from mopidy.frontends.mpd import MpdSession class AuthenticationTest(unittest.TestCase): def setUp(self): diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index bc995a5e..3f6b00f9 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -4,7 +4,7 @@ import unittest from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.session import MpdSession +from mopidy.frontends.mpd import MpdSession from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): diff --git a/tests/frontends/mpd/server_test.py b/tests/frontends/mpd/server_test.py deleted file mode 100644 index b2e27559..00000000 --- a/tests/frontends/mpd/server_test.py +++ /dev/null @@ -1,23 +0,0 @@ -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 MpdSessionTest(unittest.TestCase): - def setUp(self): - 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): - # Pressing Ctrl+C in a telnet session sends a 0xff byte to the server. - self.session.input_buffer = ['\xff'] - self.session.found_terminator() - self.assertEqual(len(self.session.input_buffer), 0) From 43e4952e5fd7b1208ce932cd77fed4dfca8b54f0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 19 Jun 2011 23:40:24 +0300 Subject: [PATCH 050/350] Move examples from random methods to frontend docs --- mopidy/frontends/mpris.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 04aaeca5..7845e27f 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -45,6 +45,26 @@ class MprisFrontend(ThreadingActor, BaseFrontend): - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the Ubuntu Sound Menu. The package is named ``python-indicate`` in Ubuntu/Debian. + + **Testing the frontend** + + To test, start Mopidy, and then run the following in a Python shell:: + + import dbus + bus = dbus.SessionBus() + player = bus.get_object('org.mpris.MediaPlayer2.mopidy', + '/org/mpris/MediaPlayer2') + + Now you can control Mopidy through the player object. Examples: + + - To get some properties from Mopidy, run:: + + props = player.GetAll('org.mpris.MediaPlayer2', + dbus_interface='org.freedesktop.DBus.Properties') + + - To quit Mopidy through D-Bus, run:: + + player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be @@ -177,16 +197,6 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface): - """ - To test, start Mopidy and then run the following in a Python shell:: - - import dbus - bus = dbus.SessionBus() - player = bus.get_object('org.mpris.MediaPlayer2.mopidy', - '/org/mpris/MediaPlayer2') - props = player.GetAll('org.mpris.MediaPlayer2', - dbus_interface='org.freedesktop.DBus.Properties') - """ logger.debug(u'%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface)) getters = {} @@ -222,15 +232,6 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=ROOT_IFACE) def Quit(self): - """ - To test, start Mopidy and then run the following in a Python shell:: - - import dbus - bus = dbus.SessionBus() - player = bus.get_object('org.mpris.MediaPlayer2.mopidy', - '/org/mpris/MediaPlayer2') - player.Quit(dbus_interface='org.mpris.MediaPlayer2') - """ logger.debug(u'%s.Quit called', ROOT_IFACE) ActorRegistry.stop_all() From 2115706998437daaeaac1df036b79e2ed5788fc9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 19 Jun 2011 23:41:01 +0300 Subject: [PATCH 051/350] The MPRIS spec has been updated from 2.0 to 2.1 while I've been working on this --- mopidy/frontends/mpris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 7845e27f..c9cf9545 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -105,7 +105,7 @@ class MprisFrontend(ThreadingActor, BaseFrontend): class MprisObject(dbus.service.Object): - """Implements http://www.mpris.org/2.0/spec/""" + """Implements http://www.mpris.org/2.1/spec/""" properties = None From 22cba6f75c2f32af1c8b16378bd476c94d9fa8d5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 00:15:13 +0300 Subject: [PATCH 052/350] Test and implement mpris.OpenUri --- mopidy/frontends/mpris.py | 14 +++--- .../frontends/mpris/player_interface_test.py | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index c9cf9545..b995978a 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -305,13 +305,13 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): logger.debug(u'%s.OpenUri called', PLAYER_IFACE) - # TODO Pseudo code: - # if uri.scheme not in SupportedUriSchemes: return - # if uri.mime_type not in SupportedMimeTypes: return - # track = library.lookup(uri) - # cp_track = current_playlist.add(track) - # playback.play(cp_track) - pass + # TODO Check if URI is known scheme and has known MIME type. + track = self.backend.library.lookup(uri).get() + if track is not None: + cp_track = self.backend.current_playlist.add(track).get() + self.backend.playback.play(cp_track) + else: + logger.debug(u'Track with URI "%s" not found in library.', uri) ### Player interface signals diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 817a5fbc..7316e78a 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -489,3 +489,51 @@ class PlayerInterfaceTest(unittest.TestCase): self.assert_(after_set_position >= before_set_position) self.assertEquals(self.backend.playback.state.get(), PLAYING) self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + def test_open_uri_adds_uri_to_current_playlist(self): + self.backend.library.provider.dummy_library = [ + Track(uri='dummy:/test/uri')] + self.mpris.OpenUri('dummy:/test/uri') + self.assertEquals(self.backend.current_playlist.tracks.get()[0].uri, + 'dummy:/test/uri') + + def test_open_uri_starts_playback_of_new_track_if_stopped(self): + self.backend.library.provider.dummy_library = [ + Track(uri='dummy:/test/uri')] + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEquals(self.backend.playback.state.get(), STOPPED) + + self.mpris.OpenUri('dummy:/test/uri') + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, + 'dummy:/test/uri') + + def test_open_uri_starts_playback_of_new_track_if_paused(self): + self.backend.library.provider.dummy_library = [ + Track(uri='dummy:/test/uri')] + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.pause() + self.assertEquals(self.backend.playback.state.get(), PAUSED) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + self.mpris.OpenUri('dummy:/test/uri') + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, + 'dummy:/test/uri') + + def test_open_uri_starts_playback_of_new_track_if_playing(self): + self.backend.library.provider.dummy_library = [ + Track(uri='dummy:/test/uri')] + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + + self.mpris.OpenUri('dummy:/test/uri') + + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.assertEquals(self.backend.playback.current_track.get().uri, + 'dummy:/test/uri') From a4d73a8d7ec72e2f91296b8a5efc4ddb0fe67984 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 00:33:57 +0300 Subject: [PATCH 053/350] Test and implement mpris.CanControl property --- mopidy/frontends/mpris.py | 7 +++++-- tests/frontends/mpris/player_interface_test.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index b995978a..5232a385 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -157,8 +157,7 @@ class MprisObject(dbus.service.Object): 'CanPause': (False, None), # TODO Set to True when the rest is implemented 'CanSeek': (False, None), - # TODO Set to True when the rest is implemented - 'CanControl': (False, None), + 'CanControl': (self.get_CanControl, None), } def _connect_to_dbus(self): @@ -386,3 +385,7 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + + def get_CanControl(self): + # TODO This could be a setting for the end user to change. + return True diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 7316e78a..32562fca 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -157,6 +157,10 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_control_is_true(self): + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanControl') + self.assertTrue(result) + def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From c8bc52b4c6b0fdd904b4f7e5e8c9b71d052e0360 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 13:43:20 +0300 Subject: [PATCH 054/350] Test and implement all direct checks of CanControl==true before doing the designated action --- mopidy/frontends/mpris.py | 12 ++++++++ .../frontends/mpris/player_interface_test.py | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 5232a385..a515412c 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -266,6 +266,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Stop(self): logger.debug(u'%s.Stop called', PLAYER_IFACE) + if not self.get_CanControl(): + logger.debug(u'%s.Stop not allowed', PLAYER_IFACE) + return # TODO Raise error self.backend.playback.stop().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -345,6 +348,9 @@ class MprisObject(dbus.service.Object): return 'Playlist' def set_LoopStatus(self, value): + if not self.get_CanControl(): + logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE) + return # TODO Raise error if value == 'None': self.backend.playback.repeat = False self.backend.playback.single = False @@ -363,6 +369,9 @@ class MprisObject(dbus.service.Object): return self.backend.playback.shuffle.get() def set_Shuffle(self, value): + if not self.get_CanControl(): + logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE) + return # TODO Raise error if value: self.backend.playback.shuffle = True else: @@ -374,6 +383,9 @@ class MprisObject(dbus.service.Object): return volume / 100.0 def set_Volume(self, value): + if not self.get_CanControl(): + logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE) + return # TODO Raise error if value is None: return elif value < 0: diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 32562fca..26e46184 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -56,6 +56,14 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Playlist', result) + def test_set_loop_status_is_ignored_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.playback.repeat = True + self.backend.playback.single = True + self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') + self.assertEquals(self.backend.playback.repeat.get(), True) + self.assertEquals(self.backend.playback.single.get(), True) + def test_set_loop_status_to_none_unsets_repeat_and_single(self): self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEquals(self.backend.playback.repeat.get(), False) @@ -98,6 +106,12 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') self.assertFalse(result) + def test_set_shuffle_is_ignored_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.playback.shuffle = False + result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) + self.assertFalse(self.backend.playback.shuffle.get()) + def test_set_shuffle_to_true_activates_shuffle_mode(self): self.backend.playback.shuffle = False self.assertFalse(self.backend.playback.shuffle.get()) @@ -123,6 +137,12 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') self.assertEquals(result, 1) + def test_set_volume_is_ignored_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.mixer.volume = 0 + self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 1.0) + self.assertEquals(self.mixer.volume.get(), 0) + def test_set_volume_to_one_should_set_mixer_volume_to_100(self): self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 1.0) self.assertEquals(self.mixer.volume.get(), 100) @@ -282,6 +302,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.PlayPause() self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_stop_is_ignored_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Stop() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_stop_when_playing_should_stop_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 7c2d3cd5418bd199b4e0862c2c61591e4c98a85d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:26:10 +0300 Subject: [PATCH 055/350] Test and implement mpris.CanSeek property --- mopidy/frontends/mpris.py | 10 ++++++++-- tests/frontends/mpris/player_interface_test.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index a515412c..b3c2af31 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -155,8 +155,7 @@ class MprisObject(dbus.service.Object): 'CanPlay': (False, None), # TODO True if CanControl and backend.playback.current_track 'CanPause': (False, None), - # TODO Set to True when the rest is implemented - 'CanSeek': (False, None), + 'CanSeek': (self.get_CanSeek, None), 'CanControl': (self.get_CanControl, None), } @@ -398,6 +397,13 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + def get_CanSeek(self): + if not self.get_CanControl(): + return False + # XXX Should be changed to vary based on capabilities of the current + # track if Mopidy starts supporting non-seekable media, like streams. + return True + def get_CanControl(self): # TODO This could be a setting for the end user to change. return True diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 26e46184..8c8fb36b 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -177,6 +177,16 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_seek_is_true_if_can_control_is_true(self): + self.mpris.get_CanControl = lambda *_: True + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanSeek') + self.assertTrue(result) + + def test_can_seek_is_false_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanSeek') + self.assertFalse(result) + def test_can_control_is_true(self): result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanControl') self.assertTrue(result) From 10eeb894ccfbfbcb29c0b8e15d237a270991abae Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:35:12 +0300 Subject: [PATCH 056/350] Test and implement all direct checks of CanSeek==true --- mopidy/frontends/mpris.py | 6 ++++ .../frontends/mpris/player_interface_test.py | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index b3c2af31..16f0bc5c 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -282,6 +282,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): logger.debug(u'%s.Seek called', PLAYER_IFACE) + if not self.get_CanSeek(): + logger.debug(u'%s.Seek not allowed', PLAYER_IFACE) + return offset_in_milliseconds = offset // 1000 current_position = self.backend.playback.time_position.get() new_position = current_position + offset_in_milliseconds @@ -290,6 +293,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def SetPosition(self, track_id, position): logger.debug(u'%s.SetPosition called', PLAYER_IFACE) + if not self.get_CanSeek(): + logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE) + return position = position // 1000 current_track = self.backend.playback.current_track.get() # TODO Currently the ID is assumed to be the URI of the track. This diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 8c8fb36b..42deebb6 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -364,6 +364,23 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Play() self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_seek_is_ignored_if_can_seek_is_false(self): + self.mpris.get_CanSeek = lambda *_: False + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + + before_seek = self.backend.playback.time_position.get() + self.assert_(before_seek >= 0) + + milliseconds_to_seek = 10000 + microseconds_to_seek = milliseconds_to_seek * 1000 + + self.mpris.Seek(microseconds_to_seek) + + after_seek = self.backend.playback.time_position.get() + self.assert_(before_seek <= after_seek < ( + before_seek + milliseconds_to_seek)) + def test_seek_seeks_given_microseconds_forward_in_the_current_track(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() @@ -443,6 +460,25 @@ class PlayerInterfaceTest(unittest.TestCase): self.assert_(after_seek >= 0) self.assert_(after_seek < before_seek) + def test_set_position_is_ignored_if_can_seek_is_false(self): + self.mpris.get_CanSeek = lambda *_: False + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + + before_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position <= 5000) + + track_id = 'a' + + position_to_set_in_milliseconds = 20000 + position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 + + self.mpris.SetPosition(track_id, position_to_set_in_microseconds) + + after_set_position = self.backend.playback.time_position.get() + self.assert_(before_set_position <= after_set_position < + position_to_set_in_milliseconds) + def test_set_position_sets_the_current_track_position_in_microsecs(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() From c8d574a895b9a10c235a8eee09a5d61e7466d419 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:45:13 +0300 Subject: [PATCH 057/350] Formatting --- mopidy/frontends/mpris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 16f0bc5c..c35f88be 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -67,8 +67,8 @@ class MprisFrontend(ThreadingActor, BaseFrontend): player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ - # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to be - # running too. This is not enforced in any way by the code. + # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to + # be running too. This is not enforced in any way by the code. def __init__(self): self.indicate_server = None From 27c4b68e0fc18ab3e36efc2c354cec443e4db7b5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:49:22 +0300 Subject: [PATCH 058/350] Test and implement mpris.CanPause property --- mopidy/frontends/mpris.py | 10 ++++++++-- tests/frontends/mpris/player_interface_test.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index c35f88be..7cc1a9a6 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -153,8 +153,7 @@ class MprisObject(dbus.service.Object): 'CanGoPrevious': (False, None), # TODO True if CanControl and backend.playback.current_track 'CanPlay': (False, None), - # TODO True if CanControl and backend.playback.current_track - 'CanPause': (False, None), + 'CanPause': (self.get_CanPause, None), 'CanSeek': (self.get_CanSeek, None), 'CanControl': (self.get_CanControl, None), } @@ -403,6 +402,13 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + def get_CanPause(self): + if not self.get_CanControl(): + return False + # XXX Should be changed to vary based on capabilities of the current + # track if Mopidy starts supporting non-seekable media, like streams. + return True + def get_CanSeek(self): if not self.get_CanControl(): return False diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 42deebb6..085aefd8 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -177,6 +177,16 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self): + self.mpris.get_CanControl = lambda *_: True + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPause') + self.assertTrue(result) + + def test_can_pause_if_false_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPause') + self.assertFalse(result) + def test_can_seek_is_true_if_can_control_is_true(self): self.mpris.get_CanControl = lambda *_: True result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanSeek') From 97111d710fea90b8149597f6ba82ed0f3f634715 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:53:11 +0300 Subject: [PATCH 059/350] Test and implement all direct checks of CanPause==true --- mopidy/frontends/mpris.py | 6 ++++++ tests/frontends/mpris/player_interface_test.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 7cc1a9a6..db77c845 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -248,11 +248,17 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Pause(self): logger.debug(u'%s.Pause called', PLAYER_IFACE) + if not self.get_CanPause(): + logger.debug(u'%s.Pause not allowed', PLAYER_IFACE) + return self.backend.playback.pause().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def PlayPause(self): logger.debug(u'%s.PlayPause called', PLAYER_IFACE) + if not self.get_CanPause(): + logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) + return # TODO Raise error state = self.backend.playback.state.get() if state == PlaybackController.PLAYING: self.backend.playback.pause().get() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 085aefd8..c1590d92 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -279,6 +279,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.current_track.get().uri, 'a') self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_pause_is_ignored_if_can_pause_is_false(self): + self.mpris.get_CanPause = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Pause() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_pause_when_playing_should_pause_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() @@ -294,6 +302,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Pause() self.assertEquals(self.backend.playback.state.get(), PAUSED) + def test_playpause_is_ignored_if_can_pause_is_false(self): + self.mpris.get_CanPause = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.PlayPause() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_playpause_when_playing_should_pause_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 4013a2ec9afd9a08ad0230f5fb71fe0750deae3e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 14:58:29 +0300 Subject: [PATCH 060/350] Test and implement mpris.CanPlay property --- mopidy/frontends/mpris.py | 9 +++++++-- .../frontends/mpris/player_interface_test.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index db77c845..e00075a6 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -151,8 +151,7 @@ class MprisObject(dbus.service.Object): 'CanGoNext': (False, None), # TODO True if CanControl and backend.playback.track_at_previous 'CanGoPrevious': (False, None), - # TODO True if CanControl and backend.playback.current_track - 'CanPlay': (False, None), + 'CanPlay': (self.get_CanPlay, None), 'CanPause': (self.get_CanPause, None), 'CanSeek': (self.get_CanSeek, None), 'CanControl': (self.get_CanControl, None), @@ -408,6 +407,12 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + def get_CanPlay(self): + if not self.get_CanControl(): + return False + return (self.backend.playback.current_track.get() is not None + or self.backend.playback.track_at_next.get() is not None) + def get_CanPause(self): if not self.get_CanControl(): return False diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index c1590d92..f44b61ee 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -177,6 +177,25 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_play_is_true_if_can_control_and_current_track(self): + self.mpris.get_CanControl = lambda *_: True + self.backend.current_playlist.append([Track(uri='a')]) + self.backend.playback.play() + self.assertTrue(self.backend.playback.current_track.get()) + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + self.assertTrue(result) + + def test_can_play_is_false_if_no_current_track(self): + self.mpris.get_CanControl = lambda *_: True + self.assertFalse(self.backend.playback.current_track.get()) + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + self.assertFalse(result) + + def test_can_play_if_false_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + self.assertFalse(result) + def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self): self.mpris.get_CanControl = lambda *_: True result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPause') From 4de7c242b7336a104f4eae7c1253171730f023b7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 15:03:00 +0300 Subject: [PATCH 061/350] Test and implement all direct checks of CanPlay==true --- mopidy/frontends/mpris.py | 3 +++ tests/frontends/mpris/player_interface_test.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index e00075a6..83d5fbde 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -277,6 +277,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Play(self): logger.debug(u'%s.Play called', PLAYER_IFACE) + if not self.get_CanPlay(): + logger.debug(u'%s.Play not allowed', PLAYER_IFACE) + return state = self.backend.playback.state.get() if state == PlaybackController.PAUSED: self.backend.playback.resume().get() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index f44b61ee..4fb5c91e 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -380,6 +380,13 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Stop() self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_play_is_ignored_if_can_play_is_false(self): + self.mpris.get_CanPlay = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEquals(self.backend.playback.state.get(), STOPPED) + self.mpris.Play() + self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_play_when_stopped_starts_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.assertEquals(self.backend.playback.state.get(), STOPPED) From 70139e0b7b521f2cdf341c258a53f1d0904fb295 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 15:11:11 +0300 Subject: [PATCH 062/350] Test and implement mpris.CanGoPrevious property --- mopidy/frontends/mpris.py | 9 +++++-- .../frontends/mpris/player_interface_test.py | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 83d5fbde..18969fc5 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -149,8 +149,7 @@ class MprisObject(dbus.service.Object): 'MaximumRate': (1.0, None), # TODO True if CanControl and backend.playback.track_at_next 'CanGoNext': (False, None), - # TODO True if CanControl and backend.playback.track_at_previous - 'CanGoPrevious': (False, None), + 'CanGoPrevious': (self.get_CanGoPrevious, None), 'CanPlay': (self.get_CanPlay, None), 'CanPause': (self.get_CanPause, None), 'CanSeek': (self.get_CanSeek, None), @@ -410,6 +409,12 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + def get_CanGoPrevious(self): + if not self.get_CanControl(): + return False + return (self.backend.playback.cp_track_at_previous.get() != + self.backend.playback.current_cp_track.get()) + def get_CanPlay(self): if not self.get_CanControl(): return False diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 4fb5c91e..db5ebcc7 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -177,6 +177,30 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self): + self.mpris.get_CanControl = lambda *_: True + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + self.assertTrue(result) + + def test_can_go_previous_is_false_if_previous_track_is_the_same(self): + self.mpris.get_CanControl = lambda *_: True + self.backend.current_playlist.append([Track(uri='a')]) + self.backend.playback.repeat = True + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + self.assertFalse(result) + + def test_can_go_previous_is_false_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + self.assertFalse(result) + def test_can_play_is_true_if_can_control_and_current_track(self): self.mpris.get_CanControl = lambda *_: True self.backend.current_playlist.append([Track(uri='a')]) From 8f59b0fae870b2440cf08044e205334a8f408389 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 15:21:44 +0300 Subject: [PATCH 063/350] Test and implement all direct checks of CanGoPrevious==true --- mopidy/frontends/mpris.py | 3 +++ tests/frontends/mpris/player_interface_test.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 18969fc5..afd8a234 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -241,6 +241,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Previous(self): logger.debug(u'%s.Previous called', PLAYER_IFACE) + if not self.get_CanGoPrevious(): + logger.debug(u'%s.Previous not allowed', PLAYER_IFACE) + return self.backend.playback.previous().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index db5ebcc7..84946030 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -282,6 +282,15 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.current_track.get().uri, 'b') self.assertEquals(self.backend.playback.state.get(), STOPPED) + def test_previous_is_ignored_if_can_go_previous_is_false(self): + self.mpris.get_CanGoPrevious = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.backend.playback.next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + self.mpris.Previous() + self.assertEquals(self.backend.playback.current_track.get().uri, 'b') + def test_previous_when_playing_should_skip_to_prev_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 7f64ba3e722aa82c909f4dbc37d8a581f210b71a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 15:27:21 +0300 Subject: [PATCH 064/350] Test and implement mpris.CanGoNext property --- mopidy/frontends/mpris.py | 9 ++++++-- .../frontends/mpris/player_interface_test.py | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index afd8a234..7f085410 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -147,8 +147,7 @@ class MprisObject(dbus.service.Object): 'Position': (self.get_Position, None), 'MinimumRate': (1.0, None), 'MaximumRate': (1.0, None), - # TODO True if CanControl and backend.playback.track_at_next - 'CanGoNext': (False, None), + 'CanGoNext': (self.get_CanGoNext, None), 'CanGoPrevious': (self.get_CanGoPrevious, None), 'CanPlay': (self.get_CanPlay, None), 'CanPause': (self.get_CanPause, None), @@ -412,6 +411,12 @@ class MprisObject(dbus.service.Object): def get_Position(self): return self.backend.playback.time_position.get() * 1000 + def get_CanGoNext(self): + if not self.get_CanControl(): + return False + return (self.backend.playback.cp_track_at_next.get() != + self.backend.playback.current_cp_track.get()) + def get_CanGoPrevious(self): if not self.get_CanControl(): return False diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 84946030..4f475728 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -177,6 +177,28 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) + def test_can_go_next_is_true_if_can_control_and_other_next_track(self): + self.mpris.get_CanControl = lambda *_: True + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + self.assertTrue(result) + + def test_can_go_next_is_false_if_next_track_is_the_same(self): + self.mpris.get_CanControl = lambda *_: True + self.backend.current_playlist.append([Track(uri='a')]) + self.backend.playback.repeat = True + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + self.assertFalse(result) + + def test_can_go_next_is_false_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + self.assertFalse(result) + def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self): self.mpris.get_CanControl = lambda *_: True self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) From 26b7f5e8b5c76ad8c832f6a05ed849e2515b51dc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 15:29:22 +0300 Subject: [PATCH 065/350] Test and implement all direct checks of CanGoNext==true --- mopidy/frontends/mpris.py | 3 +++ tests/frontends/mpris/player_interface_test.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 7f085410..a34fddce 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -235,6 +235,9 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug(u'%s.Next called', PLAYER_IFACE) + if not self.get_CanGoNext(): + logger.debug(u'%s.Next not allowed', PLAYER_IFACE) + return self.backend.playback.next().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 4f475728..76cfabec 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -266,6 +266,14 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanControl') self.assertTrue(result) + def test_next_is_ignored_if_can_go_next_is_false(self): + self.mpris.get_CanGoNext = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + self.mpris.Next() + self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + def test_next_when_playing_should_skip_to_next_track_and_keep_playing(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From cc05db157c9cf4dc770e3af53b604245b1bc59a4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 18:12:00 +0300 Subject: [PATCH 066/350] Ignore messages to the MPRIS frontend --- mopidy/frontends/mpris.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index a34fddce..a75531f9 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -78,6 +78,9 @@ class MprisFrontend(ThreadingActor, BaseFrontend): self.dbus_objects.append(MprisObject()) self.send_startup_notification() + def on_receive(self, message): + pass # Ignore incoming messages for know + def on_stop(self): for dbus_object in self.dbus_objects: dbus_object.remove_from_connection() From a2f90a7418e94cdc04a716b4f3700e69d01eaf8e Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:07:09 +0300 Subject: [PATCH 067/350] Signals are intentionally left empty --- mopidy/frontends/mpris.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index a75531f9..6f6b09a5 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -340,7 +340,6 @@ class MprisObject(dbus.service.Object): @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') def Seeked(self, position): logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) - # TODO What should we do here? pass From 182f074222ef7d97a43d5dc77a8de6a548df55b7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:14:34 +0300 Subject: [PATCH 068/350] Use new exit_process() function in mpris.Quit() --- mopidy/frontends/mpris.py | 3 ++- tests/frontends/mpris/root_interface_test.py | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 6f6b09a5..8ec8c0a7 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -16,6 +16,7 @@ from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends.base import BaseFrontend from mopidy.mixers.base import BaseMixer +from mopidy.utils.process import exit_process logger = logging.getLogger('mopidy.frontends.mpris') @@ -230,7 +231,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=ROOT_IFACE) def Quit(self): logger.debug(u'%s.Quit called', ROOT_IFACE) - ActorRegistry.stop_all() + exit_process() ### Player interface methods diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index f088d4dd..622f3414 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,13 +1,11 @@ import mock import unittest -from pykka.registry import ActorRegistry - from mopidy.frontends import mpris class RootInterfaceTest(unittest.TestCase): def setUp(self): - mpris.ActorRegistry = mock.Mock(spec=ActorRegistry) + mpris.exit_process = mock.Mock() mpris.MprisObject._connect_to_dbus = mock.Mock() self.mpris = mpris.MprisObject() @@ -27,7 +25,7 @@ class RootInterfaceTest(unittest.TestCase): def test_quit_should_stop_all_actors(self): self.mpris.Quit() - self.assert_(mpris.ActorRegistry.stop_all.called) + self.assert_(mpris.exit_process.called) def test_has_track_list_returns_false(self): result = self.mpris.Get(mpris.ROOT_IFACE, 'HasTrackList') From 3f325c936d060732246758720b9e33e4755ce883 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:28:58 +0300 Subject: [PATCH 069/350] Change uri_handlers to uri_schemes on backends --- docs/changes.rst | 4 +++- mopidy/backends/base/__init__.py | 4 ++-- mopidy/backends/dummy/__init__.py | 2 +- mopidy/backends/local/__init__.py | 2 +- mopidy/backends/spotify/__init__.py | 2 +- mopidy/frontends/mpd/protocol/current_playlist.py | 4 ++-- mopidy/frontends/mpd/protocol/reflection.py | 3 ++- tests/backends/local/playback_test.py | 4 ++-- tests/frontends/mpd/reflection_test.py | 2 +- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index a2fd73d5..07212cc7 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,7 +10,9 @@ v0.6.0 (in development) **Changes** -- None yet +- Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with + :attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part + up to the colon of an URI, and not any prefix. v0.5.0 (2011-06-15) diff --git a/mopidy/backends/base/__init__.py b/mopidy/backends/base/__init__.py index 038e2d7b..76c7f078 100644 --- a/mopidy/backends/base/__init__.py +++ b/mopidy/backends/base/__init__.py @@ -25,5 +25,5 @@ class Backend(object): #: :class:`mopidy.backends.base.StoredPlaylistsController`. stored_playlists = None - #: List of URI prefixes this backend can handle. - uri_handlers = [] + #: List of URI schemes this backend can handle. + uri_schemes = [] diff --git a/mopidy/backends/dummy/__init__.py b/mopidy/backends/dummy/__init__.py index 90c87dac..70efb028 100644 --- a/mopidy/backends/dummy/__init__.py +++ b/mopidy/backends/dummy/__init__.py @@ -32,7 +32,7 @@ class DummyBackend(ThreadingActor, Backend): self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) - self.uri_handlers = [u'dummy:'] + self.uri_schemes = [u'dummy'] class DummyLibraryProvider(BaseLibraryProvider): diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 93cf3534..af80a8eb 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -52,7 +52,7 @@ class LocalBackend(ThreadingActor, Backend): self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) - self.uri_handlers = [u'file://'] + self.uri_schemes = [u'file'] self.gstreamer = None diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 66bcffd4..02ccd802 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -67,7 +67,7 @@ class SpotifyBackend(ThreadingActor, Backend): self.stored_playlists = StoredPlaylistsController(backend=self, provider=stored_playlists_provider) - self.uri_handlers = [u'spotify:', u'http://open.spotify.com/'] + self.uri_schemes = [u'spotify'] self.gstreamer = None self.spotify = None diff --git a/mopidy/frontends/mpd/protocol/current_playlist.py b/mopidy/frontends/mpd/protocol/current_playlist.py index 8e26013d..c7136804 100644 --- a/mopidy/frontends/mpd/protocol/current_playlist.py +++ b/mopidy/frontends/mpd/protocol/current_playlist.py @@ -19,8 +19,8 @@ def add(context, uri): """ if not uri: return - for handler_prefix in context.backend.uri_handlers.get(): - if uri.startswith(handler_prefix): + for uri_scheme in context.backend.uri_schemes.get(): + if uri.startswith(uri_scheme): track = context.backend.library.lookup(uri).get() if track is not None: context.backend.current_playlist.add(track) diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 920f48a5..3618f5e1 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -95,4 +95,5 @@ def urlhandlers(context): Gets a list of available URL handlers. """ - return [(u'handler', uri) for uri in context.backend.uri_handlers.get()] + return [(u'handler', uri_scheme) + for uri_scheme in context.backend.uri_schemes.get()] diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 2cdeadb9..5f80e691 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -36,8 +36,8 @@ class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): track = Track(uri=uri, length=4464) self.backend.current_playlist.add(track) - def test_uri_handler(self): - self.assert_('file://' in self.backend.uri_handlers) + def test_uri_scheme(self): + self.assertIn('file', self.backend.uri_schemes) def test_play_mp3(self): self.add_track('blank.mp3') diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py index 2abf5acc..c4fd632a 100644 --- a/tests/frontends/mpd/reflection_test.py +++ b/tests/frontends/mpd/reflection_test.py @@ -76,4 +76,4 @@ class ReflectionHandlerTest(unittest.TestCase): def test_urlhandlers(self): result = self.dispatcher.handle_request(u'urlhandlers') self.assert_(u'OK' in result) - self.assert_(u'handler: dummy:' in result) + self.assert_(u'handler: dummy' in result) From 26868401c6ae40444c15f6e61103a12abf204ef2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:37:11 +0300 Subject: [PATCH 070/350] Check if CanControl==true in set_Rate for consistency (even though the spec doesn't mention it) --- mopidy/frontends/mpris.py | 5 +++++ tests/frontends/mpris/player_interface_test.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 8ec8c0a7..3ba6fcf8 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -381,6 +381,11 @@ class MprisObject(dbus.service.Object): self.backend.playback.single = False def set_Rate(self, value): + if not self.get_CanControl(): + # NOTE The spec does not explictly require this check, but it was + # added to be consistent with all the other property setters. + logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE) + return # TODO Raise error if value == 0: self.Pause() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 76cfabec..03f04842 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -89,6 +89,14 @@ class PlayerInterfaceTest(unittest.TestCase): maximum_rate = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') self.assert_(rate >= maximum_rate) + def test_set_rate_is_ignored_if_can_control_is_false(self): + self.mpris.get_CanControl = lambda *_: False + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.backend.playback.play() + self.assertEquals(self.backend.playback.state.get(), PLAYING) + self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) + self.assertEquals(self.backend.playback.state.get(), PLAYING) + def test_set_rate_to_zero_pauses_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() From 7faed379ef27d764c3d3040b84fcc03a7274d147 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:43:21 +0300 Subject: [PATCH 071/350] Check if CanPlay==true in OpenUri for consistency (even though the spec doesn't mention it) --- mopidy/frontends/mpris.py | 5 +++++ tests/frontends/mpris/player_interface_test.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 3ba6fcf8..5619b859 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -327,6 +327,11 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): logger.debug(u'%s.OpenUri called', PLAYER_IFACE) + if not self.get_CanPlay(): + # NOTE The spec does not explictly require this check, but guarding + # the other methods doesn't help much if OpenUri is open for use. + logger.debug(u'%s.Play not allowed', PLAYER_IFACE) + return # TODO Check if URI is known scheme and has known MIME type. track = self.backend.library.lookup(uri).get() if track is not None: diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 03f04842..42533870 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -691,7 +691,15 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.state.get(), PLAYING) self.assertEquals(self.backend.playback.current_track.get().uri, 'a') + def test_open_uri_is_ignored_if_can_play_is_false(self): + self.mpris.get_CanPlay = lambda *_: False + self.backend.library.provider.dummy_library = [ + Track(uri='dummy:/test/uri')] + self.mpris.OpenUri('dummy:/test/uri') + self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0) + def test_open_uri_adds_uri_to_current_playlist(self): + self.mpris.get_CanPlay = lambda *_: True self.backend.library.provider.dummy_library = [ Track(uri='dummy:/test/uri')] self.mpris.OpenUri('dummy:/test/uri') @@ -699,6 +707,7 @@ class PlayerInterfaceTest(unittest.TestCase): 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_stopped(self): + self.mpris.get_CanPlay = lambda *_: True self.backend.library.provider.dummy_library = [ Track(uri='dummy:/test/uri')] self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -711,6 +720,7 @@ class PlayerInterfaceTest(unittest.TestCase): 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_paused(self): + self.mpris.get_CanPlay = lambda *_: True self.backend.library.provider.dummy_library = [ Track(uri='dummy:/test/uri')] self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -726,6 +736,7 @@ class PlayerInterfaceTest(unittest.TestCase): 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_playing(self): + self.mpris.get_CanPlay = lambda *_: True self.backend.library.provider.dummy_library = [ Track(uri='dummy:/test/uri')] self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) From 90ce8b21bc2d038ea19a1b8b2904a6a077c524f1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 20 Jun 2011 19:56:41 +0300 Subject: [PATCH 072/350] Test and implement mpris.SupportedUriSchemes property --- mopidy/frontends/mpris.py | 9 +++++++-- tests/frontends/mpris/root_interface_test.py | 14 ++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 5619b859..80c6eeb6 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -131,8 +131,7 @@ class MprisObject(dbus.service.Object): 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), 'DesktopEntry': ('mopidy', None), - # TODO Return URI schemes supported by backend configuration - 'SupportedUriSchemes': (dbus.Array([], signature='s'), None), + 'SupportedUriSchemes': (self.get_SupportedUriSchemes, None), # TODO Return MIME types supported by local backend if active 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), } @@ -234,6 +233,12 @@ class MprisObject(dbus.service.Object): exit_process() + ### Root interface properties + + def get_SupportedUriSchemes(self): + return dbus.Array(self.backend.uri_schemes.get(), signature='s') + + ### Player interface methods @dbus.service.method(dbus_interface=PLAYER_IFACE) diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 622f3414..f781d261 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,14 +1,19 @@ import mock import unittest +from mopidy.backends.dummy import DummyBackend from mopidy.frontends import mpris class RootInterfaceTest(unittest.TestCase): def setUp(self): mpris.exit_process = mock.Mock() mpris.MprisObject._connect_to_dbus = mock.Mock() + self.backend = DummyBackend.start().proxy() self.mpris = mpris.MprisObject() + def tearDown(self): + self.backend.stop() + def test_constructor_connects_to_dbus(self): self.assert_(self.mpris._connect_to_dbus.called) @@ -33,16 +38,17 @@ class RootInterfaceTest(unittest.TestCase): def test_identify_is_mopidy(self): result = self.mpris.Get(mpris.ROOT_IFACE, 'Identity') - self.assertEquals('Mopidy', result) + self.assertEquals(result, 'Mopidy') def test_desktop_entry_is_mopidy(self): result = self.mpris.Get(mpris.ROOT_IFACE, 'DesktopEntry') - self.assertEquals('mopidy', result) + self.assertEquals(result, 'mopidy') def test_supported_uri_schemes_is_empty(self): result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') - self.assertEquals(0, len(result)) + self.assertEquals(len(result), 1) + self.assertEquals(result[0], 'dummy') def test_supported_mime_types_is_empty(self): result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedMimeTypes') - self.assertEquals(0, len(result)) + self.assertEquals(len(result), 0) From 3259a11c8d5bf985b24dbe137900c6e757c20de3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 21 Jun 2011 23:17:03 +0300 Subject: [PATCH 073/350] Test and implement mpris.Metadata property --- mopidy/frontends/mpris.py | 34 ++++++++-- .../frontends/mpris/player_interface_test.py | 67 ++++++++++++++++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 80c6eeb6..a2413a8d 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -142,10 +142,7 @@ class MprisObject(dbus.service.Object): 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus), 'Rate': (1.0, self.set_Rate), 'Shuffle': (self.get_Shuffle, self.set_Shuffle), - # TODO Get meta data - 'Metadata': ({ - 'mpris:trackid': '', # TODO Use (cpid, track.uri) - }, None), + 'Metadata': (self.get_Metadata, None), 'Volume': (self.get_Volume, self.set_Volume), 'Position': (self.get_Position, None), 'MinimumRate': (1.0, None), @@ -411,6 +408,35 @@ class MprisObject(dbus.service.Object): else: self.backend.playback.shuffle = False + def get_Metadata(self): + current_cp_track = self.backend.playback.current_cp_track.get() + if current_cp_track is None: + return {'mpris:trackid': ''} + else: + (cpid, track) = current_cp_track + metadata = {'mpris:trackid': '/com/mopidy/track/%d' % cpid} + if track.length: + metadata['mpris:length'] = track.length * 1000 + if track.uri: + metadata['xesam:url'] = track.uri + if track.name: + metadata['xesam:title'] = track.name + if track.artists: + artists = list(track.artists) + artists.sort(key=lambda a: a.name) + metadata['xesam:artist'] = dbus.Array( + [a.name for a in artists if a.name], signature='s') + if track.album and track.album.name: + metadata['xesam:album'] = track.album.name + if track.album and track.album.artists: + artists = list(track.album.artists) + artists.sort(key=lambda a: a.name) + metadata['xesam:albumArtist'] = dbus.Array( + [a.name for a in artists if a.name], signature='s') + if track.track_no: + metadata['xesam:trackNumber'] = track.track_no + return dbus.Dictionary(metadata, signature='sv') + def get_Volume(self): volume = self.mixer.volume.get() if volume is not None: diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 42533870..b4cb5f70 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -5,7 +5,7 @@ from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController from mopidy.frontends import mpris from mopidy.mixers.dummy import DummyMixer -from mopidy.models import Track +from mopidy.models import Album, Artist, Track PLAYING = PlaybackController.PLAYING PAUSED = PlaybackController.PAUSED @@ -132,6 +132,71 @@ class PlayerInterfaceTest(unittest.TestCase): result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', False) self.assertFalse(self.backend.playback.shuffle.get()) + def test_get_metadata_has_trackid_even_when_no_current_track(self): + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assert_('mpris:trackid' in result.keys()) + self.assertEquals(result['mpris:trackid'], '') + + def test_get_metadata_has_trackid_based_on_cpid(self): + self.backend.current_playlist.append([Track(uri='a')]) + self.backend.playback.play() + (cpid, track) = self.backend.playback.current_cp_track.get() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('mpris:trackid', result.keys()) + self.assertEquals(result['mpris:trackid'], + '/com/mopidy/track/%d' % cpid) + + def test_get_metadata_has_track_length(self): + self.backend.current_playlist.append([Track(uri='a', length=40000)]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('mpris:length', result.keys()) + self.assertEquals(result['mpris:length'], 40000000) + + def test_get_metadata_has_track_uri(self): + self.backend.current_playlist.append([Track(uri='a')]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:url', result.keys()) + self.assertEquals(result['xesam:url'], 'a') + + def test_get_metadata_has_track_title(self): + self.backend.current_playlist.append([Track(name='a')]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:title', result.keys()) + self.assertEquals(result['xesam:title'], 'a') + + def test_get_metadata_has_track_artists(self): + self.backend.current_playlist.append([Track(artists=[ + Artist(name='a'), Artist(name='b'), Artist(name=None)])]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:artist', result.keys()) + self.assertEquals(result['xesam:artist'], ['a', 'b']) + + def test_get_metadata_has_track_album(self): + self.backend.current_playlist.append([Track(album=Album(name='a'))]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:album', result.keys()) + self.assertEquals(result['xesam:album'], 'a') + + def test_get_metadata_has_track_album_artists(self): + self.backend.current_playlist.append([Track(album=Album(artists=[ + Artist(name='a'), Artist(name='b'), Artist(name=None)]))]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:albumArtist', result.keys()) + self.assertEquals(result['xesam:albumArtist'], ['a', 'b']) + + def test_get_metadata_has_track_number_in_album(self): + self.backend.current_playlist.append([Track(track_no=7)]) + self.backend.playback.play() + result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + self.assertIn('xesam:trackNumber', result.keys()) + self.assertEquals(result['xesam:trackNumber'], 7) + def test_get_volume_should_return_volume_between_zero_and_one(self): self.mixer.volume = 0 result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') From 190faf745a53e02fc58745a2b941311bd8d65d76 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 21 Jun 2011 23:19:57 +0300 Subject: [PATCH 074/350] 'shuffle' should be 'random' in our backend --- mopidy/frontends/mpris.py | 6 ++-- .../frontends/mpris/player_interface_test.py | 28 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index a2413a8d..bba2c95c 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -397,16 +397,16 @@ class MprisObject(dbus.service.Object): self.Pause() def get_Shuffle(self): - return self.backend.playback.shuffle.get() + return self.backend.playback.random.get() def set_Shuffle(self, value): if not self.get_CanControl(): logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE) return # TODO Raise error if value: - self.backend.playback.shuffle = True + self.backend.playback.random = True else: - self.backend.playback.shuffle = False + self.backend.playback.random = False def get_Metadata(self): current_cp_track = self.backend.playback.current_cp_track.get() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index b4cb5f70..e1c75f88 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -104,33 +104,33 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) self.assertEquals(self.backend.playback.state.get(), PAUSED) - def test_get_shuffle_returns_true_if_shuffle_is_active(self): - self.backend.playback.shuffle = True + def test_get_shuffle_returns_true_if_random_is_active(self): + self.backend.playback.random = True result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') self.assertTrue(result) - def test_get_shuffle_returns_false_if_shuffle_is_inactive(self): - self.backend.playback.shuffle = False + def test_get_shuffle_returns_false_if_random_is_inactive(self): + self.backend.playback.random = False result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') self.assertFalse(result) def test_set_shuffle_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - self.backend.playback.shuffle = False + self.backend.playback.random = False result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) - self.assertFalse(self.backend.playback.shuffle.get()) + self.assertFalse(self.backend.playback.random.get()) - def test_set_shuffle_to_true_activates_shuffle_mode(self): - self.backend.playback.shuffle = False - self.assertFalse(self.backend.playback.shuffle.get()) + def test_set_shuffle_to_true_activates_random_mode(self): + self.backend.playback.random = False + self.assertFalse(self.backend.playback.random.get()) result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) - self.assertTrue(self.backend.playback.shuffle.get()) + self.assertTrue(self.backend.playback.random.get()) - def test_set_shuffle_to_false_deactivates_shuffle_mode(self): - self.backend.playback.shuffle = True - self.assertTrue(self.backend.playback.shuffle.get()) + def test_set_shuffle_to_false_deactivates_random_mode(self): + self.backend.playback.random = True + self.assertTrue(self.backend.playback.random.get()) result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', False) - self.assertFalse(self.backend.playback.shuffle.get()) + self.assertFalse(self.backend.playback.random.get()) def test_get_metadata_has_trackid_even_when_no_current_track(self): result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') From e5725bb26b5706cc85622fef89789295acbd2ee1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 22 Jun 2011 00:18:08 +0300 Subject: [PATCH 075/350] Update SetPosition to support real track IDs instead of URIs --- mopidy/frontends/mpris.py | 19 ++++++++++++------- .../frontends/mpris/player_interface_test.py | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index bba2c95c..e8cabf5d 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -177,6 +177,12 @@ class MprisObject(dbus.service.Object): self._mixer = mixer_refs[0].proxy() return self._mixer + def _get_track_id(self, cp_track): + return '/com/mopidy/track/%d' % cp_track.cpid + + def _get_cpid(self, track_id): + assert track_id.startswith('/com/mopidy/track/') + return track_id.split('/')[-1] ### Properties interface @@ -314,15 +320,14 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.SetPosition not allowed', PLAYER_IFACE) return position = position // 1000 - current_track = self.backend.playback.current_track.get() - # TODO Currently the ID is assumed to be the URI of the track. This - # should be changed to a D-Bus object ID than can be mapped to the CPID - # and URI of the track. - if current_track and current_track.uri != track_id: + current_cp_track = self.backend.playback.current_cp_track.get() + if current_cp_track is None: + return + if track_id != self._get_track_id(current_cp_track): return if position < 0: return - if current_track and current_track.length < position: + if current_cp_track.track.length < position: return self.backend.playback.seek(position) @@ -414,7 +419,7 @@ class MprisObject(dbus.service.Object): return {'mpris:trackid': ''} else: (cpid, track) = current_cp_track - metadata = {'mpris:trackid': '/com/mopidy/track/%d' % cpid} + metadata = {'mpris:trackid': self._get_track_id(current_cp_track)} if track.length: metadata['mpris:length'] = track.length * 1000 if track.uri: diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index e1c75f88..b568db64 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -675,7 +675,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assert_(before_set_position <= 5000) self.assertEquals(self.backend.playback.state.get(), PLAYING) - track_id = 'a' + track_id = '/com/mopidy/track/0' position_to_set_in_milliseconds = 20000 position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 @@ -698,7 +698,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.assertEquals(self.backend.playback.state.get(), PLAYING) self.assertEquals(self.backend.playback.current_track.get().uri, 'a') - track_id = 'a' + track_id = '/com/mopidy/track/0' position_to_set_in_milliseconds = -1000 position_to_set_in_microseconds = position_to_set_in_milliseconds * 1000 From 537bb1a8794caac083341bfbcecd26ac771afd7c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 22 Jun 2011 00:18:52 +0300 Subject: [PATCH 076/350] Change some TODO/XXX to NOTE as they only apply given some future initiatives/changes --- mopidy/frontends/mpris.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index e8cabf5d..0e6f0e03 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -127,7 +127,7 @@ class MprisObject(dbus.service.Object): return { 'CanQuit': (True, None), 'CanRaise': (False, None), - # TODO Add track list support + # NOTE Change if adding optional track list support 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), 'DesktopEntry': ('mopidy', None), @@ -484,17 +484,17 @@ class MprisObject(dbus.service.Object): def get_CanPause(self): if not self.get_CanControl(): return False - # XXX Should be changed to vary based on capabilities of the current + # NOTE Should be changed to vary based on capabilities of the current # track if Mopidy starts supporting non-seekable media, like streams. return True def get_CanSeek(self): if not self.get_CanControl(): return False - # XXX Should be changed to vary based on capabilities of the current + # NOTE Should be changed to vary based on capabilities of the current # track if Mopidy starts supporting non-seekable media, like streams. return True def get_CanControl(self): - # TODO This could be a setting for the end user to change. + # NOTE This could be a setting for the end user to change. return True From 93f00ce7f29ba72afb42f1153c2ad3e0cec16f45 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 22 Jun 2011 00:28:51 +0300 Subject: [PATCH 077/350] Add check of URI schema to OpenUri --- mopidy/frontends/mpris.py | 5 ++++- tests/frontends/mpris/player_interface_test.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 0e6f0e03..8cf73359 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -339,7 +339,10 @@ class MprisObject(dbus.service.Object): # the other methods doesn't help much if OpenUri is open for use. logger.debug(u'%s.Play not allowed', PLAYER_IFACE) return - # TODO Check if URI is known scheme and has known MIME type. + # TODO Check if URI has known MIME type. + uri_schemes = self.backend.uri_schemes.get() + if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]): + return track = self.backend.library.lookup(uri).get() if track is not None: cp_track = self.backend.current_playlist.add(track).get() diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index b568db64..1ddd23fe 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -763,6 +763,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.mpris.OpenUri('dummy:/test/uri') self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0) + def test_open_uri_ignores_uris_with_unknown_uri_scheme(self): + self.assertListEqual(self.backend.uri_schemes.get(), ['dummy']) + self.mpris.get_CanPlay = lambda *_: True + self.backend.library.provider.dummy_library = [ + Track(uri='notdummy:/test/uri')] + self.mpris.OpenUri('notdummy:/test/uri') + self.assertEquals(len(self.backend.current_playlist.tracks.get()), 0) + def test_open_uri_adds_uri_to_current_playlist(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.provider.dummy_library = [ From 54f09b0157ea9747b8b9bc8b623fa47748b46231 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Jun 2011 02:49:02 +0200 Subject: [PATCH 078/350] Rewrite of client part of listener - takes into account that we will be implementing idle --- mopidy/frontends/mpd/__init__.py | 50 +++---------- mopidy/utils/network.py | 116 +++++++++++++++++++++++++++---- 2 files changed, 114 insertions(+), 52 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index d0ca761e..3b6b5db1 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,4 +1,3 @@ -import gobject import logging import sys @@ -9,7 +8,6 @@ from mopidy import settings from mopidy.utils import network from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR -from mopidy.utils.log import indent logger = logging.getLogger('mopidy.frontends.mpd') @@ -34,7 +32,7 @@ class MpdFrontend(ThreadingActor, BaseFrontend): port = settings.MPD_SERVER_PORT try: - network.Listener(hostname, port, session=MpdSession) + network.Listener(hostname, port, MpdSession) except IOError, e: logger.error(u'MPD server startup failed: %s', e) sys.exit(1) @@ -42,49 +40,21 @@ class MpdFrontend(ThreadingActor, BaseFrontend): logger.info(u'MPD server running at [%s]:%s', hostname, port) -class MpdSession(ThreadingActor): +class MpdSession(network.LineProtocol): """ The MPD client session. Keeps track of a single client session. Any requests from the client is passed on to the MPD request dispatcher. """ + terminator = LINE_TERMINATOR + encoding = ENCODING + def __init__(self, sock, addr): - self.sock = sock # Prevent premature GC of socket closing it - self.addr = addr - self.channel = gobject.IOChannel(sock.fileno()) - self.dispatcher = MpdDispatcher() + super(MpdSession, self).__init__(sock, addr) + self.dispatcher = MpdDispatcher(self) def on_start(self): - try: - self.send_response([u'OK MPD %s' % VERSION]) - self.request_loop() - except gobject.GError: - self.stop() + self.send_lines([u'OK MPD %s' % VERSION]) - def close(self): - self.channel.close() - - def request_loop(self): - while True: - data = self.channel.readline() - if not data: - return self.close() - request = data.rstrip(LINE_TERMINATOR).decode(ENCODING) - logger.debug(u'Request from [%s]:%s: %s', self.addr[0], - self.addr[1], indent(request)) - response = self.dispatcher.handle_request(request) - self.send_response(response) - - def send_response(self, response): - """ - Format a response from the MPD command handlers and send it to the - client. - """ - if response: - response = LINE_TERMINATOR.join(response) - logger.debug(u'Response to [%s]:%s: %s', self.addr[0], - self.addr[1], indent(response)) - response = u'%s%s' % (response, LINE_TERMINATOR) - data = response.encode(ENCODING) - self.channel.write(data) - self.channel.flush() + def on_line_recieved(self, line): + self.send_lines(self.dispatcher.handle_request(line)) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index d1536afb..3b597e36 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -3,6 +3,10 @@ import re import socket import gobject +from pykka.actor import ThreadingActor + +from mopidy.utils.log import indent + logger = logging.getLogger('mopidy.utils.server') def _try_ipv6_socket(): @@ -39,19 +43,107 @@ def format_hostname(hostname): class Listener(object): """Setup listener and register it with gobject loop.""" - def __init__(self, host, port, session): - self.session = session - self.sock = create_socket() - self.sock.setblocking(0) - self.sock.bind((host, port)) - self.sock.listen(5) - gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN, self.handle) - logger.debug('Listening on [%s]:%s using %s', host, port, self.session) + def __init__(self, host, port, protcol): + self.protcol = protcol + self.listener = create_socket() + self.listener.setblocking(False) + self.listener.bind((host, port)) + self.listener.listen(1) + + gobject.io_add_watch( + self.listener.fileno(), gobject.IO_IN, self.handle_accept) + logger.debug('Listening on [%s]:%s using %s as protcol handler', + host, port, self.protcol.__name__) + + def handle_accept(self, fd, flags): + sock, addr = self.listener.accept() + sock.setblocking(False) + + actor_ref = self.protcol.start(sock, addr) + gobject.io_add_watch(sock.fileno(), gobject.IO_IN | gobject.IO_ERR | + gobject.IO_HUP, self.handle_client, sock, actor_ref) - def handle(self, fd, flags): - sock, addr = self.sock.accept() - logger.debug('Got connection from %s', addr) - self.session.start(sock, addr) return True + def handle_client(self, fd, flags, sock, actor_ref): + if flags & (gobject.IO_ERR | gobject.IO_HUP): + data = '' + else: + data = sock.recv(1024) + + if not data: + actor_ref.stop() + return False + + actor_ref.send_one_way({'recvieved': data}) + return True + + +class LineProtocol(ThreadingActor): + terminator = '\n' + encoding = 'utf-8' + + def __init__(self, sock, addr): + self.sock = sock + self.host, self.port = addr + self.recv_buffer = '' + + def on_line_recieved(self, line): + raise NotImplemented + + def on_receive(self, message): + if 'recvieved' not in message: + return + + for line in self.parse_lines(message['recvieved']): + line = self.encode(line) + self.log_request(line) + self.on_line_recieved(line) + + def on_stop(self): + try: + self.sock.close() + except socket.error as e: + pass + + def parse_lines(self, new_data=None): + if new_data: + self.recv_buffer += new_data + while self.terminator in self.recv_buffer: + line, self.recv_buffer = self.recv_buffer.split(self.terminator, 1) + yield line + + def log_request(self, request): + logger.debug(u'Request from [%s]:%s: %s', + self.host, self.port, indent(request)) + + def log_response(self, response): + logger.debug(u'Response to [%s]:%s: %s', + self.host, self.port, indent(response)) + + def encode(self, line): + if self.encoding: + return line.encode(self.encoding) + return line + + def decode(self, line): + if self.encoding: + return line.decode(self.encoding) + return line + + def send_lines(self, lines): + if not lines: + return + + data = self.terminator.join(lines) + self.log_response(data) + self.send_raw(self.encode(data + self.terminator)) + + def send_raw(self, data): + try: + sent = self.sock.send(data) + assert len(data) == sent, 'All data was not sent' # FIXME + except socket.error as e: # FIXME + logger.debug('send() failed with: %s', e) + self.stop() From 14a7a9bd80a07c498a7165e736165f1024b18d85 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Jun 2011 03:00:30 +0200 Subject: [PATCH 079/350] Get rid of GObjectEventThread --- mopidy/core.py | 16 +++++++--------- mopidy/utils/process.py | 25 ------------------------- 2 files changed, 7 insertions(+), 34 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index 65472a29..4420c319 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -2,7 +2,9 @@ import logging import optparse import signal import sys -import time + +import gobject +gobject.threads_init() # Extract any non-GStreamer arguments, and leave the GStreamer arguments for # processing by GStreamer. This needs to be done before GStreamer is imported, @@ -23,25 +25,23 @@ from mopidy.gstreamer import GStreamer 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 (GObjectEventThread, exit_handler, - stop_all_actors) +from mopidy.utils.process import exit_handler, stop_all_actors from mopidy.utils.settings import list_settings_optparse_callback logger = logging.getLogger('mopidy.core') def main(): signal.signal(signal.SIGTERM, exit_handler) + loop = gobject.MainLoop() try: options = parse_options() setup_logging(options.verbosity_level, options.save_debug_log) setup_settings(options.interactive) - setup_gobject_loop() setup_gstreamer() setup_mixer() setup_backend() setup_frontends() - while True: - time.sleep(1) + loop.run() except SettingsError as e: logger.error(e.message) except KeyboardInterrupt: @@ -49,6 +49,7 @@ def main(): except Exception as e: logger.exception(e) finally: + loop.quit() stop_all_actors() def parse_options(): @@ -82,9 +83,6 @@ def setup_settings(interactive): logger.error(e.message) sys.exit(1) -def setup_gobject_loop(): - GObjectEventThread().start() - def setup_gstreamer(): GStreamer.start() diff --git a/mopidy/utils/process.py b/mopidy/utils/process.py index c1d1c9f5..f9577496 100644 --- a/mopidy/utils/process.py +++ b/mopidy/utils/process.py @@ -3,9 +3,6 @@ import signal import thread import threading -import gobject -gobject.threads_init() - from pykka import ActorDeadError from pykka.registry import ActorRegistry @@ -60,25 +57,3 @@ class BaseThread(threading.Thread): def run_inside_try(self): raise NotImplementedError - - -class GObjectEventThread(BaseThread): - """ - A GObject event loop which is shared by all Mopidy components that uses - libraries that need a GObject event loop, like GStreamer and D-Bus. - - Should be started by Mopidy's core and used by - :mod:`mopidy.output.gstreamer`, :mod:`mopidy.frontend.mpris`, etc. - """ - - def __init__(self): - super(GObjectEventThread, self).__init__() - self.name = u'GObjectEventThread' - self.loop = None - - def run_inside_try(self): - self.loop = gobject.MainLoop().run() - - def destroy(self): - self.loop.quit() - super(GObjectEventThread, self).destroy() From 742ecf10ae2989e177cd97526ed32f31f336b218 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 22 Jun 2011 03:04:46 +0200 Subject: [PATCH 080/350] Fix minor test regresion --- mopidy/frontends/mpd/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 3b6b5db1..89180d0a 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -58,3 +58,6 @@ class MpdSession(network.LineProtocol): def on_line_recieved(self, line): self.send_lines(self.dispatcher.handle_request(line)) + + def close(self): + self.stop() From 34203e2ba18d3572807faf98e6cd183925135339 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 23 Jun 2011 21:40:32 +0200 Subject: [PATCH 081/350] Reignore info sent to frontend --- mopidy/frontends/mpd/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 89180d0a..0432850d 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -39,6 +39,9 @@ class MpdFrontend(ThreadingActor, BaseFrontend): logger.info(u'MPD server running at [%s]:%s', hostname, port) + def on_receive(self, message): + pass # Ignore state info that is sent to frontend. + class MpdSession(network.LineProtocol): """ From 8092ffaa349aa0a8062364f7ba2cfd45aff6395c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 24 Jun 2011 21:08:13 +0300 Subject: [PATCH 082/350] More logging during startup of MprisFrontend --- mopidy/frontends/mpris.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 8cf73359..4decfb3e 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -84,8 +84,10 @@ class MprisFrontend(ThreadingActor, BaseFrontend): def on_stop(self): for dbus_object in self.dbus_objects: + logger.debug(u'Removing %s from connection...', dbus_object) dbus_object.remove_from_connection() self.dbus_objects = [] + logger.debug(u'Removed all D-Bus objects from connection') def send_startup_notification(self): """ @@ -98,14 +100,16 @@ class MprisFrontend(ThreadingActor, BaseFrontend): """ try: import indicate + logger.debug(u'Sending startup notification...') self.indicate_server = indicate.Server() self.indicate_server.set_type('music.mopidy') # FIXME Location of .desktop file shouldn't be hardcoded self.indicate_server.set_desktop_file( '/usr/share/applications/mopidy.desktop') self.indicate_server.show() + logger.debug(u'Startup notification sent') except ImportError as e: - logger.debug(u'Startup notification was not sent. (%s)', e) + logger.debug(u'Startup notification was not sent (%s)', e) class MprisObject(dbus.service.Object): From cae4e63fdb8ef6244c94fa5cf291ee35ac99b82c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 16:39:26 +0300 Subject: [PATCH 083/350] Replace BaseFrontend with BackendListener in MPRIS frontend --- mopidy/frontends/mpris.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 4decfb3e..017016c7 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -14,7 +14,7 @@ from pykka.registry import ActorRegistry from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController -from mopidy.frontends.base import BaseFrontend +from mopidy.listeners import BackendListener from mopidy.mixers.base import BaseMixer from mopidy.utils.process import exit_process @@ -31,7 +31,7 @@ ROOT_IFACE = 'org.mpris.MediaPlayer2' PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' -class MprisFrontend(ThreadingActor, BaseFrontend): +class MprisFrontend(ThreadingActor, BackendListener): """ Frontend which lets you control Mopidy through the Media Player Remote Interfacing Specification (MPRIS) D-Bus interface. @@ -79,9 +79,6 @@ class MprisFrontend(ThreadingActor, BaseFrontend): self.dbus_objects.append(MprisObject()) self.send_startup_notification() - def on_receive(self, message): - pass # Ignore incoming messages for know - def on_stop(self): for dbus_object in self.dbus_objects: logger.debug(u'Removing %s from connection...', dbus_object) From 83863814795bf2fc6ddf12c1a45aa531afb1662c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 16:22:28 +0300 Subject: [PATCH 084/350] Test that backend actually sends the events --- tests/backends/events_test.py | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/backends/events_test.py diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py new file mode 100644 index 00000000..c988244d --- /dev/null +++ b/tests/backends/events_test.py @@ -0,0 +1,45 @@ +import threading +import unittest + +from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry + +from mopidy.backends.dummy import DummyBackend +from mopidy.listeners import BackendListener +from mopidy.models import Track + +class BackendEventsTest(unittest.TestCase): + def setUp(self): + self.events = { + 'started_playing': threading.Event(), + 'stopped_playing': threading.Event(), + } + self.backend = DummyBackend.start().proxy() + self.listener = DummyBackendListener.start(self.events).proxy() + + def tearDown(self): + ActorRegistry.stop_all() + + def test_play_sends_started_playing_event(self): + self.backend.current_playlist.add([Track(uri='a')]) + self.backend.playback.play() + self.events['started_playing'].wait(timeout=1) + self.assertTrue(self.events['started_playing'].is_set()) + + def test_stop_sends_stopped_playing_event(self): + self.backend.current_playlist.add([Track(uri='a')]) + self.backend.playback.play() + self.backend.playback.stop() + self.events['stopped_playing'].wait(timeout=1) + self.assertTrue(self.events['stopped_playing'].is_set()) + + +class DummyBackendListener(ThreadingActor, BackendListener): + def __init__(self, events): + self.events = events + + def started_playing(self, track): + self.events['started_playing'].set() + + def stopped_playing(self, track, stop_position): + self.events['stopped_playing'].set() From b23853b958fc2353065b0aed3ad4e042955d4066 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 17:29:47 +0300 Subject: [PATCH 085/350] Remove obvious docs on internal methods --- mopidy/backends/base/playback.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index e94ddf4d..a19590ba 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -485,27 +485,13 @@ class PlaybackController(object): self.current_cp_track = None def _trigger_started_playing_event(self): - """ - Notifies implementors of :class:`mopidy.listeners.BackendListener` that - a track has started playing. - - For internal use only. Should be called by the backend directly after a - track has started playing. - """ if self.current_track is None: return for listener_ref in ActorRegistry.get_by_class(BackendListener): listener_ref.proxy().started_playing(track=self.current_track) def _trigger_stopped_playing_event(self): - """ - Notifies implementors of :class:`mopidy.listeners.BackendListener` that - a track has stopped playing. - - For internal use only. Should be called by the backend before a track - is stopped playing, e.g. at the next, previous, and stop actions and at - end-of-track. - """ + # TODO Test that this is called on next/prev/end-of-track if self.current_track is None: return for listener_ref in ActorRegistry.get_by_class(BackendListener): From 0e8fb5e7ac0de3ac08082251b813d19694e0a0f8 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 17:30:26 +0300 Subject: [PATCH 086/350] Change stopped_playing event arg from stop_position to time_position --- mopidy/backends/base/playback.py | 2 +- mopidy/listeners.py | 6 +++--- tests/backends/events_test.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index a19590ba..d880cd61 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -496,7 +496,7 @@ class PlaybackController(object): return for listener_ref in ActorRegistry.get_by_class(BackendListener): listener_ref.proxy().stopped_playing( - track=self.current_track, stop_position=self.time_position) + track=self.current_track, time_position=self.time_position) class BasePlaybackProvider(object): diff --git a/mopidy/listeners.py b/mopidy/listeners.py index f6d1c67e..dfc5c60b 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -20,7 +20,7 @@ class BackendListener(object): """ pass - def stopped_playing(self, track, stop_position): + def stopped_playing(self, track, time_position): """ Called whenever playback is stopped. @@ -28,7 +28,7 @@ class BackendListener(object): :param track: the track that was played before playback stopped :type track: :class:`mopidy.models.Track` - :param stop_position: the time position when stopped in milliseconds - :type stop_position: int + :param time_position: the time position in milliseconds + :type time_position: int """ pass diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index c988244d..44529e90 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -41,5 +41,5 @@ class DummyBackendListener(ThreadingActor, BackendListener): def started_playing(self, track): self.events['started_playing'].set() - def stopped_playing(self, track, stop_position): + def stopped_playing(self, track, time_position): self.events['stopped_playing'].set() From ad246706c679d96a472f4f2483df8ecdea6f7c85 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 16:28:27 +0300 Subject: [PATCH 087/350] Add paused_playing and resumed_playing events --- mopidy/backends/base/playback.py | 16 ++++++++++++++++ mopidy/listeners.py | 27 +++++++++++++++++++++++++++ tests/backends/events_test.py | 23 +++++++++++++++++++++++ tests/listeners_test.py | 6 ++++++ 4 files changed, 72 insertions(+) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index d880cd61..5155418f 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -383,6 +383,7 @@ class PlaybackController(object): """Pause playback.""" if self.provider.pause(): self.state = self.PAUSED + self._trigger_paused_playing_event() def play(self, cp_track=None, on_error_step=1): """ @@ -441,6 +442,7 @@ class PlaybackController(object): """If paused, resume playing the current track.""" if self.state == self.PAUSED and self.provider.resume(): self.state = self.PLAYING + self._trigger_resumed_playing_event() def seek(self, time_position): """ @@ -484,6 +486,20 @@ class PlaybackController(object): if clear_current_track: self.current_cp_track = None + def _trigger_paused_playing_event(self): + if self.current_track is None: + return + for listener_ref in ActorRegistry.get_by_class(BackendListener): + listener_ref.proxy().paused_playing( + track=self.current_track, time_position=self.time_position) + + def _trigger_resumed_playing_event(self): + if self.current_track is None: + return + for listener_ref in ActorRegistry.get_by_class(BackendListener): + listener_ref.proxy().resumed_playing( + track=self.current_track, time_position=self.time_position) + def _trigger_started_playing_event(self): if self.current_track is None: return diff --git a/mopidy/listeners.py b/mopidy/listeners.py index dfc5c60b..263afd36 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -9,6 +9,33 @@ class BackendListener(object): interested in all events. """ + def paused_playing(self, track, time_position): + """ + Called whenever playback is paused. + + *MAY* be implemented by actor. + + :param track: the track that was playing when playback paused + :type track: :class:`mopidy.models.Track` + :param time_position: the time position in milliseconds + :type time_position: int + """ + pass + + def resumed_playing(self, track, time_position): + """ + Called whenever playback is resumed. + + *MAY* be implemented by actor. + + :param track: the track that was playing when playback resumed + :type track: :class:`mopidy.models.Track` + :param time_position: the time position in milliseconds + :type time_position: int + """ + pass + + def started_playing(self, track): """ Called whenever a new track starts playing. diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index 44529e90..c2e4d28a 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -11,6 +11,8 @@ from mopidy.models import Track class BackendEventsTest(unittest.TestCase): def setUp(self): self.events = { + 'paused_playing': threading.Event(), + 'resumed_playing': threading.Event(), 'started_playing': threading.Event(), 'stopped_playing': threading.Event(), } @@ -20,6 +22,21 @@ class BackendEventsTest(unittest.TestCase): def tearDown(self): ActorRegistry.stop_all() + def test_pause_sends_paused_playing_event(self): + self.backend.current_playlist.add([Track(uri='a')]) + self.backend.playback.play() + self.backend.playback.pause() + self.events['paused_playing'].wait(timeout=1) + self.assertTrue(self.events['paused_playing'].is_set()) + + def test_resume_sends_resumed_playing_event(self): + self.backend.current_playlist.add([Track(uri='a')]) + self.backend.playback.play() + self.backend.playback.pause() + self.backend.playback.resume() + self.events['resumed_playing'].wait(timeout=1) + self.assertTrue(self.events['resumed_playing'].is_set()) + def test_play_sends_started_playing_event(self): self.backend.current_playlist.add([Track(uri='a')]) self.backend.playback.play() @@ -38,6 +55,12 @@ class DummyBackendListener(ThreadingActor, BackendListener): def __init__(self, events): self.events = events + def paused_playing(self, track, time_position): + self.events['paused_playing'].set() + + def resumed_playing(self, track, time_position): + self.events['resumed_playing'].set() + def started_playing(self, track): self.events['started_playing'].set() diff --git a/tests/listeners_test.py b/tests/listeners_test.py index 761aff4f..b51202d3 100644 --- a/tests/listeners_test.py +++ b/tests/listeners_test.py @@ -7,6 +7,12 @@ class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() + def test_listener_has_default_impl_for_the_paused_playing_event(self): + self.listener.paused_playing(Track(), 0) + + def test_listener_has_default_impl_for_the_resumed_playing_event(self): + self.listener.resumed_playing(Track(), 0) + def test_listener_has_default_impl_for_the_started_playing_event(self): self.listener.started_playing(Track()) From 2812e7ad4543700bd97ae27793ceac93c60e093f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 17:06:10 +0300 Subject: [PATCH 088/350] Update MPRIS' PlaybackStatus on play/stop/pause/resume --- mopidy/frontends/mpris.py | 52 ++++++++++++++++++++++------ tests/frontends/mpris/events_test.py | 43 +++++++++++++++++++++++ 2 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 tests/frontends/mpris/events_test.py diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 017016c7..e99360a0 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -73,18 +73,17 @@ class MprisFrontend(ThreadingActor, BackendListener): def __init__(self): self.indicate_server = None - self.dbus_objects = [] + self.mpris_object = None def on_start(self): - self.dbus_objects.append(MprisObject()) + self.mpris_object = MprisObject() self.send_startup_notification() def on_stop(self): - for dbus_object in self.dbus_objects: - logger.debug(u'Removing %s from connection...', dbus_object) - dbus_object.remove_from_connection() - self.dbus_objects = [] - logger.debug(u'Removed all D-Bus objects from connection') + logger.debug(u'Removing MPRIS object from D-Bus connection...') + self.mpris_object.remove_from_connection() + self.mpris_object = None + logger.debug(u'Removed MPRIS object from D-Bus connection') def send_startup_notification(self): """ @@ -108,6 +107,38 @@ class MprisFrontend(ThreadingActor, BackendListener): except ImportError as e: logger.debug(u'Startup notification was not sent (%s)', e) + def paused_playing(self, track, time_position): + if self.mpris_object is None: + return + self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'PlaybackStatus': + self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), + }, []) + + def resumed_playing(self, track, time_position): + if self.mpris_object is None: + return + self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'PlaybackStatus': + self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), + }, []) + + def started_playing(self, track): + if self.mpris_object is None: + return + self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'PlaybackStatus': + self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), + }, []) + + def stopped_playing(self, track, time_position): + if self.mpris_object is None: + return + self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'PlaybackStatus': + self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), + }, []) + class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.1/spec/""" @@ -219,9 +250,10 @@ class MprisObject(dbus.service.Object): @dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE, signature='sa{sv}as') def PropertiesChanged(self, interface, changed_properties, - invalidated_properties): - logger.debug(u'%s.PropertiesChanged signaled', dbus.PROPERTIES_IFACE) - pass + invalidated_properties): + logger.debug(u'%s.PropertiesChanged(%s, %s, %s) signaled', + dbus.PROPERTIES_IFACE, interface, changed_properties, + invalidated_properties) ### Root interface methods diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py new file mode 100644 index 00000000..9abca16c --- /dev/null +++ b/tests/frontends/mpris/events_test.py @@ -0,0 +1,43 @@ +import mock +import unittest + +from mopidy.frontends.mpris import MprisFrontend, MprisObject, PLAYER_IFACE +from mopidy.models import Track + +class BackendEventsTest(unittest.TestCase): + def setUp(self): + self.mpris_frontend = MprisFrontend() # As a plain class, not an actor + self.mpris_object = mock.Mock(spec=MprisObject) + self.mpris_frontend.mpris_object = self.mpris_object + + def test_paused_playing_event_changes_playback_status(self): + self.mpris_object.Get.return_value = 'Paused' + self.mpris_frontend.paused_playing(Track(), 0) + self.mpris_object.Get.assert_called_with( + PLAYER_IFACE, 'PlaybackStatus') + self.mpris_object.PropertiesChanged.assert_called_with( + PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, []) + + def test_resumed_playing_event_changes_playback_status(self): + self.mpris_object.Get.return_value = 'Playing' + self.mpris_frontend.resumed_playing(Track(), 0) + self.mpris_object.Get.assert_called_with( + PLAYER_IFACE, 'PlaybackStatus') + self.mpris_object.PropertiesChanged.assert_called_with( + PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) + + def test_started_playing_event_changes_playback_status(self): + self.mpris_object.Get.return_value = 'Playing' + self.mpris_frontend.started_playing(Track()) + self.mpris_object.Get.assert_called_with( + PLAYER_IFACE, 'PlaybackStatus') + self.mpris_object.PropertiesChanged.assert_called_with( + PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) + + def test_stopped_playing_event_changes_playback_status(self): + self.mpris_object.Get.return_value = 'Stopped' + self.mpris_frontend.stopped_playing(Track(), 0) + self.mpris_object.Get.assert_called_with( + PLAYER_IFACE, 'PlaybackStatus') + self.mpris_object.PropertiesChanged.assert_called_with( + PLAYER_IFACE, {'PlaybackStatus': 'Stopped'}, []) From 8a49b1f3255f4f0e48836da561d2a7a7e2a18c4c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 29 Jun 2011 18:10:27 +0300 Subject: [PATCH 089/350] Update MPRIS' Metadata on started_playing/stopped_playing --- mopidy/frontends/mpris.py | 2 ++ tests/frontends/mpris/events_test.py | 34 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index e99360a0..8291a226 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -127,6 +127,7 @@ class MprisFrontend(ThreadingActor, BackendListener): if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'Metadata': self.mpris_object.Get(PLAYER_IFACE, 'Metadata'), 'PlaybackStatus': self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) @@ -135,6 +136,7 @@ class MprisFrontend(ThreadingActor, BackendListener): if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'Metadata': self.mpris_object.Get(PLAYER_IFACE, 'Metadata'), 'PlaybackStatus': self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 9abca16c..b9a6ba77 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -13,31 +13,37 @@ class BackendEventsTest(unittest.TestCase): def test_paused_playing_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Paused' self.mpris_frontend.paused_playing(Track(), 0) - self.mpris_object.Get.assert_called_with( - PLAYER_IFACE, 'PlaybackStatus') + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, []) def test_resumed_playing_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Playing' self.mpris_frontend.resumed_playing(Track(), 0) - self.mpris_object.Get.assert_called_with( - PLAYER_IFACE, 'PlaybackStatus') + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) - def test_started_playing_event_changes_playback_status(self): - self.mpris_object.Get.return_value = 'Playing' + def test_started_playing_event_changes_playback_status_and_metadata(self): + self.mpris_object.Get.return_value = '...' self.mpris_frontend.started_playing(Track()) - self.mpris_object.Get.assert_called_with( - PLAYER_IFACE, 'PlaybackStatus') + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'Metadata'), {}), + ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) + PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) - def test_stopped_playing_event_changes_playback_status(self): - self.mpris_object.Get.return_value = 'Stopped' + def test_stopped_playing_event_changes_playback_status_and_metadata(self): + self.mpris_object.Get.return_value = '...' self.mpris_frontend.stopped_playing(Track(), 0) - self.mpris_object.Get.assert_called_with( - PLAYER_IFACE, 'PlaybackStatus') + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'Metadata'), {}), + ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'PlaybackStatus': 'Stopped'}, []) + PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) From b3e2e13a8db67030a44a05f2f012a81b27c6cd59 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 3 Jul 2011 22:37:45 +0200 Subject: [PATCH 090/350] sock.accept() on IPv6 systems is different --- mopidy/utils/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 3b597e36..04efac86 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -86,7 +86,7 @@ class LineProtocol(ThreadingActor): def __init__(self, sock, addr): self.sock = sock - self.host, self.port = addr + self.host, self.port = addr[:2] self.recv_buffer = '' def on_line_recieved(self, line): From 77c140f466023a1b26d9992444bcc1ca0c7073d3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 3 Jul 2011 22:38:16 +0200 Subject: [PATCH 091/350] More low-level debug logging --- mopidy/utils/network.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 04efac86..e23e1d2f 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -96,6 +96,9 @@ class LineProtocol(ThreadingActor): if 'recvieved' not in message: return + logger.debug('Got %s from event-loop in %s', + repr(message['recvieved']), self.actor_urn) + for line in self.parse_lines(message['recvieved']): line = self.encode(line) self.log_request(line) @@ -115,12 +118,12 @@ class LineProtocol(ThreadingActor): yield line def log_request(self, request): - logger.debug(u'Request from [%s]:%s: %s', - self.host, self.port, indent(request)) + logger.debug(u'Request from [%s]:%s %s: %s', + self.host, self.port, self.actor_urn, indent(request)) def log_response(self, response): - logger.debug(u'Response to [%s]:%s: %s', - self.host, self.port, indent(response)) + logger.debug(u'Response to [%s]:%s %s: %s', + self.host, self.port, self.actor_urn, indent(response)) def encode(self, line): if self.encoding: From a3d72351d93cc89407df3470420179edd3604c30 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 00:34:06 +0200 Subject: [PATCH 092/350] Stop all sesions when mpd frontend is asked to stop --- mopidy/frontends/mpd/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 1e18f6e2..3e6080e5 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -5,6 +5,7 @@ from pykka.actor import ThreadingActor from mopidy import settings from mopidy.utils import network +from mopidy.utils.process import stop_actors_by_class from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR @@ -40,6 +41,9 @@ class MpdFrontend(ThreadingActor): def on_receive(self, message): pass # Ignore state info that is sent to frontend. + def on_stop(self): + stop_actors_by_class(MpdSession) + class MpdSession(network.LineProtocol): """ From 6e0d9905ed06bf25ea907f5885baca6d0dcc601f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 00:40:50 +0200 Subject: [PATCH 093/350] Sort imports --- mopidy/frontends/mpd/__init__.py | 4 ++-- tests/frontends/mpd/authentication_test.py | 2 +- tests/frontends/mpd/connection_test.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 3e6080e5..3feb7478 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -4,10 +4,10 @@ import sys from pykka.actor import ThreadingActor from mopidy import settings -from mopidy.utils import network -from mopidy.utils.process import stop_actors_by_class from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR +from mopidy.utils import network +from mopidy.utils.process import stop_actors_by_class logger = logging.getLogger('mopidy.frontends.mpd') diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/authentication_test.py index 7d340071..fb32ea54 100644 --- a/tests/frontends/mpd/authentication_test.py +++ b/tests/frontends/mpd/authentication_test.py @@ -2,8 +2,8 @@ import mock import unittest from mopidy import settings -from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd import MpdSession +from mopidy.frontends.mpd.dispatcher import MpdDispatcher class AuthenticationTest(unittest.TestCase): def setUp(self): diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py index 3f6b00f9..82debabb 100644 --- a/tests/frontends/mpd/connection_test.py +++ b/tests/frontends/mpd/connection_test.py @@ -3,8 +3,8 @@ import unittest from mopidy import settings from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd import MpdSession +from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.mixers.dummy import DummyMixer class ConnectionHandlerTest(unittest.TestCase): From e0ecc76e98068928677d0a6e2920cd8885cbbb25 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 00:42:39 +0200 Subject: [PATCH 094/350] Import modules --- mopidy/frontends/mpd/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 3feb7478..64591e6a 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -4,10 +4,10 @@ import sys from pykka.actor import ThreadingActor from mopidy import settings -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.frontends.mpd.protocol import ENCODING, VERSION, LINE_TERMINATOR +from mopidy.frontends.mpd import dispatcher +from mopidy.frontends.mpd import protocol from mopidy.utils import network -from mopidy.utils.process import stop_actors_by_class +from mopidy.utils import process logger = logging.getLogger('mopidy.frontends.mpd') @@ -42,7 +42,7 @@ class MpdFrontend(ThreadingActor): pass # Ignore state info that is sent to frontend. def on_stop(self): - stop_actors_by_class(MpdSession) + process.stop_actors_by_class(MpdSession) class MpdSession(network.LineProtocol): @@ -51,15 +51,15 @@ class MpdSession(network.LineProtocol): requests from the client is passed on to the MPD request dispatcher. """ - terminator = LINE_TERMINATOR - encoding = ENCODING + terminator = protocol.LINE_TERMINATOR + encoding = protocol.ENCODING def __init__(self, sock, addr): super(MpdSession, self).__init__(sock, addr) - self.dispatcher = MpdDispatcher(self) + self.dispatcher = dispatcher.MpdDispatcher(self) def on_start(self): - self.send_lines([u'OK MPD %s' % VERSION]) + self.send_lines([u'OK MPD %s' % protocol.VERSION]) def on_line_recieved(self, line): self.send_lines(self.dispatcher.handle_request(line)) From cfd48f8a5b8aaabaa01edc82501603ca404cd203 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 00:46:46 +0200 Subject: [PATCH 095/350] Code style fix --- mopidy/frontends/mpd/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 64591e6a..00d60b00 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -6,8 +6,7 @@ from pykka.actor import ThreadingActor from mopidy import settings from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd import protocol -from mopidy.utils import network -from mopidy.utils import process +from mopidy.utils import network, process logger = logging.getLogger('mopidy.frontends.mpd') From 224a0d1247301eb318bba81679a5bb0d4620efee Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 00:56:57 +0200 Subject: [PATCH 096/350] Remove on_recieve from frontend --- mopidy/frontends/mpd/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 00d60b00..d6ff0a86 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -37,9 +37,6 @@ class MpdFrontend(ThreadingActor): logger.info(u'MPD server running at [%s]:%s', hostname, port) - def on_receive(self, message): - pass # Ignore state info that is sent to frontend. - def on_stop(self): process.stop_actors_by_class(MpdSession) From ab653b7539fbb4999da219f408e29e60096a5630 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 10:33:59 +0200 Subject: [PATCH 097/350] Fix comments from pull-request --- mopidy/frontends/mpd/__init__.py | 3 +- mopidy/utils/network.py | 49 +++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index d6ff0a86..4599b31a 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -4,8 +4,7 @@ import sys from pykka.actor import ThreadingActor from mopidy import settings -from mopidy.frontends.mpd import dispatcher -from mopidy.frontends.mpd import protocol +from mopidy.frontends.mpd import dispatcher, protocol from mopidy.utils import network, process logger = logging.getLogger('mopidy.frontends.mpd') diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index e23e1d2f..1366e6b1 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -44,8 +44,8 @@ def format_hostname(hostname): class Listener(object): """Setup listener and register it with gobject loop.""" - def __init__(self, host, port, protcol): - self.protcol = protcol + def __init__(self, host, port, protocol): + self.protocol = protocol self.listener = create_socket() self.listener.setblocking(False) self.listener.bind((host, port)) @@ -53,14 +53,14 @@ class Listener(object): gobject.io_add_watch( self.listener.fileno(), gobject.IO_IN, self.handle_accept) - logger.debug('Listening on [%s]:%s using %s as protcol handler', - host, port, self.protcol.__name__) + logger.debug(u'Listening on [%s]:%s using %s as protocol handler', + host, port, self.protocol.__name__) def handle_accept(self, fd, flags): sock, addr = self.listener.accept() sock.setblocking(False) - actor_ref = self.protcol.start(sock, addr) + actor_ref = self.protocol.start(sock, addr) gobject.io_add_watch(sock.fileno(), gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.handle_client, sock, actor_ref) @@ -76,7 +76,7 @@ class Listener(object): actor_ref.stop() return False - actor_ref.send_one_way({'recvieved': data}) + actor_ref.send_one_way({'received': data}) return True @@ -93,13 +93,13 @@ class LineProtocol(ThreadingActor): raise NotImplemented def on_receive(self, message): - if 'recvieved' not in message: + if 'received' not in message: return - logger.debug('Got %s from event-loop in %s', - repr(message['recvieved']), self.actor_urn) + logger.debug(u'Got %s from eventloop in %s', + repr(message['received']), self.actor_urn) - for line in self.parse_lines(message['recvieved']): + for line in self.parse_lines(message['received']): line = self.encode(line) self.log_request(line) self.on_line_recieved(line) @@ -107,7 +107,7 @@ class LineProtocol(ThreadingActor): def on_stop(self): try: self.sock.close() - except socket.error as e: + except socket.error: pass def parse_lines(self, new_data=None): @@ -118,11 +118,11 @@ class LineProtocol(ThreadingActor): yield line def log_request(self, request): - logger.debug(u'Request from [%s]:%s %s: %s', + logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port, self.actor_urn, indent(request)) def log_response(self, response): - logger.debug(u'Response to [%s]:%s %s: %s', + logger.debug(u'Response to [%s]:%s %s: from %s', self.host, self.port, self.actor_urn, indent(response)) def encode(self, line): @@ -146,7 +146,24 @@ class LineProtocol(ThreadingActor): def send_raw(self, data): try: sent = self.sock.send(data) - assert len(data) == sent, 'All data was not sent' # FIXME - except socket.error as e: # FIXME - logger.debug('send() failed with: %s', e) + # FIXME we are assuming that sock send will not fail as the OS send + # buffer is big enough compared to our need. This can of course + # fail and will be caught and handeled fairly poorly with the + # following assert. + # + # Safer, and more complex way of handling this would be to ensure + # that data can be send by putting a data sender in the event loop + # and appending to its buffer. Once the buffer is empty the sender + # must be removed from the loop. This option is doable, but adds + # extra complexity. + # + # The other simpler option would be to try and recall raw_send with + # remaining data. Probably with a decrementing retry counter to + # prevent an inf. loop. + assert len(data) == sent, u'All data was not sent' + except socket.error as e: + # FIXME should this be handeled in a better maner, for instance + # retry? For instance would block errors and interupted system call + # errors would warrant a retry. + logger.debug(u'send() failed with: %s', e) self.stop() From c7617e150ade8dacb793b5d9a7f2c7dbc5e413f0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 21:08:49 +0200 Subject: [PATCH 098/350] Decode incomming data --- mopidy/utils/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1366e6b1..7ded3e27 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -100,7 +100,7 @@ class LineProtocol(ThreadingActor): repr(message['received']), self.actor_urn) for line in self.parse_lines(message['received']): - line = self.encode(line) + line = self.decode(line) self.log_request(line) self.on_line_recieved(line) From 9a8f3a141ca25e374461c580689fe50a478d5ded Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 21:09:21 +0200 Subject: [PATCH 099/350] Misplaced text in log message --- mopidy/utils/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 7ded3e27..69eaac2a 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -122,7 +122,7 @@ class LineProtocol(ThreadingActor): self.host, self.port, self.actor_urn, indent(request)) def log_response(self, response): - logger.debug(u'Response to [%s]:%s %s: from %s', + logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, self.actor_urn, indent(response)) def encode(self, line): From 3053ba09e003bb7a40988637ff5f04488b21984e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 21:13:46 +0200 Subject: [PATCH 100/350] Typo fix :) --- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/utils/network.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 4599b31a..65299e9d 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -56,7 +56,7 @@ class MpdSession(network.LineProtocol): def on_start(self): self.send_lines([u'OK MPD %s' % protocol.VERSION]) - def on_line_recieved(self, line): + def on_line_received(self, line): self.send_lines(self.dispatcher.handle_request(line)) def close(self): diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 69eaac2a..50c7bfab 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -89,7 +89,7 @@ class LineProtocol(ThreadingActor): self.host, self.port = addr[:2] self.recv_buffer = '' - def on_line_recieved(self, line): + def on_line_received(self, line): raise NotImplemented def on_receive(self, message): @@ -102,7 +102,7 @@ class LineProtocol(ThreadingActor): for line in self.parse_lines(message['received']): line = self.decode(line) self.log_request(line) - self.on_line_recieved(line) + self.on_line_received(line) def on_stop(self): try: From 094850fe2071c5d83627a9f27dcac8be89a06db9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 4 Jul 2011 21:24:09 +0200 Subject: [PATCH 101/350] Fix reference to mopidy.frontends.mpd.MpdSession --- mopidy/frontends/mpd/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 18f994de..0f0f0299 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -178,7 +178,7 @@ class MpdContext(object): #: The current :class:`MpdDispatcher`. dispatcher = None - #: The current :class:`mopidy.frontends.mpd.session.MpdSession`. + #: The current :class:`mopidy.frontends.mpd.MpdSession`. session = None def __init__(self, dispatcher, session=None): From b311e4284054c3828d41559cfb6dbb042d142c30 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 5 Jul 2011 00:55:35 +0200 Subject: [PATCH 102/350] Try to document new server helper --- mopidy/utils/network.py | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 50c7bfab..f89867ba 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -57,7 +57,7 @@ class Listener(object): host, port, self.protocol.__name__) def handle_accept(self, fd, flags): - sock, addr = self.listener.accept() + sock, addr = self.listener.accept() # FIXME this might fail is some rare cases. sock.setblocking(False) actor_ref = self.protocol.start(sock, addr) @@ -67,10 +67,16 @@ class Listener(object): return True def handle_client(self, fd, flags, sock, actor_ref): + """ + Read client data when possible. + + Returns false when reading failed in order to deregister with the event + loop. + """ if flags & (gobject.IO_ERR | gobject.IO_HUP): data = '' else: - data = sock.recv(1024) + data = sock.recv(1024) # FIXME there are cases where this might fail. if not data: actor_ref.stop() @@ -81,6 +87,16 @@ class Listener(object): class LineProtocol(ThreadingActor): + """ + Base class for handling line based protocols. + + Takes care of receiving new data from listener's client code, decoding and + then splitting data along line boundaries. + + Attributes ``terminator``and ``encoding`` can be set in case subclasses + want to split by another terminator or use another encoding. + """ + terminator = '\n' encoding = 'utf-8' @@ -90,13 +106,19 @@ class LineProtocol(ThreadingActor): self.recv_buffer = '' def on_line_received(self, line): + """ + Called whenever a new line is found. + + Should be implemented by subclasses. + """ raise NotImplemented def on_receive(self, message): + """Handle messages with new data from listener.""" if 'received' not in message: return - logger.debug(u'Got %s from eventloop in %s', + logger.debug(u'Got %s from event loop in %s', repr(message['received']), self.actor_urn) for line in self.parse_lines(message['received']): @@ -105,12 +127,14 @@ class LineProtocol(ThreadingActor): self.on_line_received(line) def on_stop(self): + """Ensure that socket is closed when actor stops.""" try: self.sock.close() except socket.error: pass def parse_lines(self, new_data=None): + """Consume new data and yield any lines found.""" if new_data: self.recv_buffer += new_data while self.terminator in self.recv_buffer: @@ -118,24 +142,50 @@ class LineProtocol(ThreadingActor): yield line def log_request(self, request): + """ + Log request for debug purposes. + + Can be overridden by subclasses to change logging behaviour. + """ logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port, self.actor_urn, indent(request)) def log_response(self, response): + """ + Log response for debug purposes. + + Can be overridden by subclasses to change logging behaviour. + """ logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, self.actor_urn, indent(response)) def encode(self, line): + """ + Handle encoding of line. + + Can be overridden by subclasses to change encoding behaviour. + """ if self.encoding: return line.encode(self.encoding) return line def decode(self, line): + """ + Handle decoding of line. + + Can be overridden by subclasses to change decoding behaviour. + """ if self.encoding: return line.decode(self.encoding) return line def send_lines(self, lines): + """ + Send array of lines to client. + + Join lines using the terminator that is set for this class, encode it + and send it to the client. + """ if not lines: return @@ -144,11 +194,12 @@ class LineProtocol(ThreadingActor): self.send_raw(self.encode(data + self.terminator)) def send_raw(self, data): + """Send data to client exactly as is.""" try: sent = self.sock.send(data) # FIXME we are assuming that sock send will not fail as the OS send # buffer is big enough compared to our need. This can of course - # fail and will be caught and handeled fairly poorly with the + # fail and will be caught and handled fairly poorly with the # following assert. # # Safer, and more complex way of handling this would be to ensure @@ -162,8 +213,8 @@ class LineProtocol(ThreadingActor): # prevent an inf. loop. assert len(data) == sent, u'All data was not sent' except socket.error as e: - # FIXME should this be handeled in a better maner, for instance - # retry? For instance would block errors and interupted system call + # FIXME should this be handled in a better manner, for instance + # retry? For instance would block errors and interrupted system call # errors would warrant a retry. logger.debug(u'send() failed with: %s', e) self.stop() From 79e46ab4fa4eb3b293e85516a2283212416e725c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 5 Jul 2011 00:56:29 +0200 Subject: [PATCH 103/350] Rename listemer to server --- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/utils/network.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 65299e9d..8dbbf3db 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -29,7 +29,7 @@ class MpdFrontend(ThreadingActor): port = settings.MPD_SERVER_PORT try: - network.Listener(hostname, port, MpdSession) + network.Server(hostname, port, protocol=MpdSession) except IOError, e: logger.error(u'MPD server startup failed: %s', e) sys.exit(1) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index f89867ba..6e8b5b44 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -41,7 +41,7 @@ def format_hostname(hostname): hostname = '::ffff:%s' % hostname return hostname -class Listener(object): +class Server(object): """Setup listener and register it with gobject loop.""" def __init__(self, host, port, protocol): From cb2f0df5d6a0d88af1d8a49a59e279583eb7f2fe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 5 Jul 2011 01:13:12 +0200 Subject: [PATCH 104/350] Extract logging of raw data to method --- mopidy/utils/network.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 6e8b5b44..1fed885a 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -118,8 +118,7 @@ class LineProtocol(ThreadingActor): if 'received' not in message: return - logger.debug(u'Got %s from event loop in %s', - repr(message['received']), self.actor_urn) + self.log_raw_data(message['received']) for line in self.parse_lines(message['received']): line = self.decode(line) @@ -141,6 +140,15 @@ class LineProtocol(ThreadingActor): line, self.recv_buffer = self.recv_buffer.split(self.terminator, 1) yield line + def log_raw_data(self, data): + """ + Log raw data from event loopfor debug purposes. + + Can be overridden by subclasses to change logging behaviour. + """ + logger.debug(u'Got %s from event loop in %s', + repr(data), self.actor_urn) + def log_request(self, request): """ Log request for debug purposes. From a12e9779e39ae9075299df3f0d2dd7679e0f1dec Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 5 Jul 2011 23:47:23 +0200 Subject: [PATCH 105/350] Add some better error handling for accept call --- mopidy/utils/network.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1fed885a..13bbd2bf 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -57,7 +57,13 @@ class Server(object): host, port, self.protocol.__name__) def handle_accept(self, fd, flags): - sock, addr = self.listener.accept() # FIXME this might fail is some rare cases. + try: + sock, addr = self.listener.accept() + except socket.error as e: + if e.errno in (errno.EAGAIN, errno.EINTR): + return True # i.e. retry + raise + sock.setblocking(False) actor_ref = self.protocol.start(sock, addr) From 52087bd5b4cf47cb15927e0a4ff197ed527d4c40 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Jul 2011 00:23:18 +0200 Subject: [PATCH 106/350] Cleanup recv code a bit --- mopidy/utils/network.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 13bbd2bf..542baaa0 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -80,9 +80,16 @@ class Server(object): loop. """ if flags & (gobject.IO_ERR | gobject.IO_HUP): - data = '' - else: - data = sock.recv(1024) # FIXME there are cases where this might fail. + actor_ref.stop() + return False + + try: + data = sock.recv(4096) + except socket.error as e: + if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): + return True + actor_ref.stop() + return False if not data: actor_ref.stop() @@ -148,7 +155,7 @@ class LineProtocol(ThreadingActor): def log_raw_data(self, data): """ - Log raw data from event loopfor debug purposes. + Log raw data from event loop for debug purposes. Can be overridden by subclasses to change logging behaviour. """ From a0f6ba7dc451e552628ff493ba3b1b39fdef66ac Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 6 Jul 2011 00:23:54 +0200 Subject: [PATCH 107/350] Switch to thread safe send queue and use event loop to send data --- mopidy/utils/network.py | 49 +++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 542baaa0..89d573a3 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -2,6 +2,7 @@ import logging import re import socket import gobject +import Queue as queue from pykka.actor import ThreadingActor @@ -115,8 +116,9 @@ class LineProtocol(ThreadingActor): def __init__(self, sock, addr): self.sock = sock - self.host, self.port = addr[:2] + self.host, self.port = addr[:2] # IPv6 has larger addr self.recv_buffer = '' + self.send_queue = queue.Queue() def on_line_received(self, line): """ @@ -216,26 +218,31 @@ class LineProtocol(ThreadingActor): def send_raw(self, data): """Send data to client exactly as is.""" + start_sender = self.send_queue.empty() + + self.send_queue.put(data) + + if start_sender: + gobject.io_add_watch(self.sock.fileno(), gobject.IO_OUT | + gobject.IO_ERR | gobject.IO_HUP, self._send) + + def _send(self, fd, flags): + # NOTE: This code is _not_ run in the actor's thread, but in the same + # one as the event loop. If this blocks, rest of gobject code will + # likely be blocked as well... try: + data = self.send_queue.get_nowait() sent = self.sock.send(data) - # FIXME we are assuming that sock send will not fail as the OS send - # buffer is big enough compared to our need. This can of course - # fail and will be caught and handled fairly poorly with the - # following assert. - # - # Safer, and more complex way of handling this would be to ensure - # that data can be send by putting a data sender in the event loop - # and appending to its buffer. Once the buffer is empty the sender - # must be removed from the loop. This option is doable, but adds - # extra complexity. - # - # The other simpler option would be to try and recall raw_send with - # remaining data. Probably with a decrementing retry counter to - # prevent an inf. loop. - assert len(data) == sent, u'All data was not sent' + except queue.Empty: + return False # No more data to send, remove callback except socket.error as e: - # FIXME should this be handled in a better manner, for instance - # retry? For instance would block errors and interrupted system call - # errors would warrant a retry. - logger.debug(u'send() failed with: %s', e) - self.stop() + if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): + self.send_queue.put(data) + return True + self.actor_ref.stop() + return False + + if len(data) != sent: # Retry remaining data + self.send_queue.put(data[sent:]) + + return not self.send_queue.empty() From 4cd6f5f66cb5d0a0854438cf054739116b921057 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Jul 2011 21:50:03 +0200 Subject: [PATCH 108/350] Switch to lock based protection of send buffer, queue use was flawed --- mopidy/utils/network.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 89d573a3..76bebc1d 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -1,8 +1,8 @@ +import gobject import logging import re import socket -import gobject -import Queue as queue +import threading from pykka.actor import ThreadingActor @@ -117,8 +117,9 @@ class LineProtocol(ThreadingActor): def __init__(self, sock, addr): self.sock = sock self.host, self.port = addr[:2] # IPv6 has larger addr + self.send_lock = threading.Lock() self.recv_buffer = '' - self.send_queue = queue.Queue() + self.send_buffer = '' def on_line_received(self, line): """ @@ -218,11 +219,12 @@ class LineProtocol(ThreadingActor): def send_raw(self, data): """Send data to client exactly as is.""" - start_sender = self.send_queue.empty() + self.send_lock.acquire(True) + should_register_sender = len(self.send_buffer) == 0 + self.send_buffer += data + self.send_lock.release() - self.send_queue.put(data) - - if start_sender: + if should_register_sender: gobject.io_add_watch(self.sock.fileno(), gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self._send) @@ -230,19 +232,20 @@ class LineProtocol(ThreadingActor): # NOTE: This code is _not_ run in the actor's thread, but in the same # one as the event loop. If this blocks, rest of gobject code will # likely be blocked as well... + + # If with can't get the lock, simply try again next time socket is + # ready for sending. + if not self.send_lock.acquire(False): + return True + try: - data = self.send_queue.get_nowait() - sent = self.sock.send(data) - except queue.Empty: - return False # No more data to send, remove callback + sent = self.sock.send(self.send_buffer) + self.send_buffer = self.send_buffer[sent:] + return bool(self.send_buffer) except socket.error as e: if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): - self.send_queue.put(data) return True self.actor_ref.stop() return False - - if len(data) != sent: # Retry remaining data - self.send_queue.put(data[sent:]) - - return not self.send_queue.empty() + finally: + self.send_lock.release() From 2449308758b27dda3b83adf11151553dc54c83e6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Jul 2011 23:47:28 +0200 Subject: [PATCH 109/350] Use re for finding terminator and splitting as it does not assume unicode --- mopidy/utils/network.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 76bebc1d..a9cde77d 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -120,6 +120,7 @@ class LineProtocol(ThreadingActor): self.send_lock = threading.Lock() self.recv_buffer = '' self.send_buffer = '' + self.terminator_re = re.compile(self.terminator) def on_line_received(self, line): """ @@ -152,8 +153,9 @@ class LineProtocol(ThreadingActor): """Consume new data and yield any lines found.""" if new_data: self.recv_buffer += new_data - while self.terminator in self.recv_buffer: - line, self.recv_buffer = self.recv_buffer.split(self.terminator, 1) + while self.terminator_re.search(self.recv_buffer): + line, self.recv_buffer = self.terminator_re.split( + self.recv_buffer, 1) yield line def log_raw_data(self, data): From 12633e0d4ab94671f8958b14c67dc1b94346ed11 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Jul 2011 23:52:38 +0200 Subject: [PATCH 110/350] Add log_error to LineProtocol --- mopidy/utils/network.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index a9cde77d..1de582b8 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -185,6 +185,15 @@ class LineProtocol(ThreadingActor): logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, self.actor_urn, indent(response)) + def log_error(self, error): + """ + Log error for debug purposes. + + Can be overridden by subclasses to change logging behaviour. + """ + logger.warning('Problem with connection to [%s]:%s in %s: %s', + self.host, self.port, self.actor_urn, error) + def encode(self, line): """ Handle encoding of line. @@ -247,6 +256,7 @@ class LineProtocol(ThreadingActor): except socket.error as e: if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): return True + self.log_error(e) self.actor_ref.stop() return False finally: From 7c2ccbbaa13299e5858cfc066c63add22fa1a016 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Jul 2011 23:55:54 +0200 Subject: [PATCH 111/350] Document attributes correctly --- mopidy/utils/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1de582b8..1bee3ba1 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -106,12 +106,12 @@ class LineProtocol(ThreadingActor): Takes care of receiving new data from listener's client code, decoding and then splitting data along line boundaries. - - Attributes ``terminator``and ``encoding`` can be set in case subclasses - want to split by another terminator or use another encoding. """ + #: What terminator to use to split lines. terminator = '\n' + + #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' def __init__(self, sock, addr): From 26155d1d402f05b915e7831df1c35b6c65372d4f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 7 Jul 2011 23:57:20 +0200 Subject: [PATCH 112/350] Fix s/listener/server/ in docs --- mopidy/utils/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1bee3ba1..69033cea 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -43,7 +43,7 @@ def format_hostname(hostname): return hostname class Server(object): - """Setup listener and register it with gobject loop.""" + """Setup listener and register it with gobject's event loop.""" def __init__(self, host, port, protocol): self.protocol = protocol @@ -104,7 +104,7 @@ class LineProtocol(ThreadingActor): """ Base class for handling line based protocols. - Takes care of receiving new data from listener's client code, decoding and + Takes care of receiving new data from server's client code, decoding and then splitting data along line boundaries. """ @@ -131,7 +131,7 @@ class LineProtocol(ThreadingActor): raise NotImplemented def on_receive(self, message): - """Handle messages with new data from listener.""" + """Handle messages with new data from server.""" if 'received' not in message: return From 8cc1aa07d97e2894214c8fe8790f794907c7b679 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Jul 2011 00:07:14 +0200 Subject: [PATCH 113/350] Forgot to include errno --- mopidy/utils/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 69033cea..f539b84d 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -1,3 +1,4 @@ +import errno import gobject import logging import re From 63244b9af8dcdf34bf3071dab9b230a6105585c1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Jul 2011 00:08:07 +0200 Subject: [PATCH 114/350] Limit number of allowed connections --- mopidy/utils/network.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index f539b84d..4497c479 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -6,6 +6,7 @@ import socket import threading from pykka.actor import ThreadingActor +from pykka.registry import ActorRegistry from mopidy.utils.log import indent @@ -46,8 +47,9 @@ def format_hostname(hostname): class Server(object): """Setup listener and register it with gobject's event loop.""" - def __init__(self, host, port, protocol): + def __init__(self, host, port, protocol, max_connections=15): self.protocol = protocol + self.max_connections = max_connections self.listener = create_socket() self.listener.setblocking(False) self.listener.bind((host, port)) @@ -66,6 +68,15 @@ class Server(object): return True # i.e. retry raise + num_connections = len(ActorRegistry.get_by_class(self.protocol)) + if self.max_connections and num_connections >= self.max_connections: + logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1]) + try: + sock.close() + except socket.error: + pass + return True + sock.setblocking(False) actor_ref = self.protocol.start(sock, addr) @@ -192,7 +203,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.warning('Problem with connection to [%s]:%s in %s: %s', + logger.warning(u'Problem with connection to [%s]:%s in %s: %s', self.host, self.port, self.actor_urn, error) def encode(self, line): From 7f77fe38d5fd5ba05e39476b5f10e27fae5d0740 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 8 Jul 2011 00:28:01 +0200 Subject: [PATCH 115/350] Add timeout support to LineProtocol --- mopidy/utils/network.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 4497c479..fb4b6f4d 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -126,6 +126,10 @@ class LineProtocol(ThreadingActor): #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' + #: How long to wait before disconnecting client due to inactivity in + #: milliseconds. + timeout = 30000 + def __init__(self, sock, addr): self.sock = sock self.host, self.port = addr[:2] # IPv6 has larger addr @@ -133,6 +137,9 @@ class LineProtocol(ThreadingActor): self.recv_buffer = '' self.send_buffer = '' self.terminator_re = re.compile(self.terminator) + self.timeout_id = None + + self.enable_timeout() def on_line_received(self, line): """ @@ -147,6 +154,7 @@ class LineProtocol(ThreadingActor): if 'received' not in message: return + self.disable_timeout() self.log_raw_data(message['received']) for line in self.parse_lines(message['received']): @@ -154,13 +162,26 @@ class LineProtocol(ThreadingActor): self.log_request(line) self.on_line_received(line) + self.enable_timeout() + def on_stop(self): - """Ensure that socket is closed when actor stops.""" + """Ensure that cleanup when actor stops.""" + self.disable_timeout() try: self.sock.close() except socket.error: pass + def disable_timeout(self): + """Deactivate timeout mechanism.""" + if self.timeout_id: + gobject.source_remove(self.timeout_id) + + def enable_timeout(self): + """Reactivate timeout mechanism.""" + self.disable_timeout() + self.timeout_id = gobject.timeout_add(self.timeout, self._timeout) + def parse_lines(self, new_data=None): """Consume new data and yield any lines found.""" if new_data: @@ -206,6 +227,15 @@ class LineProtocol(ThreadingActor): logger.warning(u'Problem with connection to [%s]:%s in %s: %s', self.host, self.port, self.actor_urn, error) + def log_timeout(self): + """ + Log timeout for debug purposes. + + Can be overridden by subclasses to change logging behaviour. + """ + logger.debug(u'Closing connection to [%s]:%s in %s due to timeout.', + self.host, self.port, self.actor_urn) + def encode(self, line): """ Handle encoding of line. @@ -273,3 +303,9 @@ class LineProtocol(ThreadingActor): return False finally: self.send_lock.release() + + def _timeout(self): + # NOTE: This code is _not_ run in the actor's thread... + self.log_timeout() + self.actor_ref.stop() + return False From cdb68d61f5b92e38f72cab9b957ef0c6c7d5f67c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Jul 2011 00:46:56 +0200 Subject: [PATCH 116/350] Use timeout_add_seconds which is less accurate but more efficient --- mopidy/utils/network.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index fb4b6f4d..ed9c7161 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -127,8 +127,8 @@ class LineProtocol(ThreadingActor): encoding = 'utf-8' #: How long to wait before disconnecting client due to inactivity in - #: milliseconds. - timeout = 30000 + #: seconds. + timeout = 30 def __init__(self, sock, addr): self.sock = sock @@ -180,7 +180,8 @@ class LineProtocol(ThreadingActor): def enable_timeout(self): """Reactivate timeout mechanism.""" self.disable_timeout() - self.timeout_id = gobject.timeout_add(self.timeout, self._timeout) + self.timeout_id = gobject.timeout_add_seconds(self.timeout, + self._timeout) def parse_lines(self, new_data=None): """Consume new data and yield any lines found.""" From 22ebb1bc297bbbb37f7cb3c31bd259129d042412 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Jul 2011 14:28:35 +0200 Subject: [PATCH 117/350] Move recv code to LineProtocol and add source removal Fixes problem where timed out sockets where not being removed from event loop causing excess CPU usage. --- mopidy/utils/network.py | 99 +++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index ed9c7161..d949eb40 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -77,38 +77,7 @@ class Server(object): pass return True - sock.setblocking(False) - - actor_ref = self.protocol.start(sock, addr) - gobject.io_add_watch(sock.fileno(), gobject.IO_IN | gobject.IO_ERR | - gobject.IO_HUP, self.handle_client, sock, actor_ref) - - return True - - def handle_client(self, fd, flags, sock, actor_ref): - """ - Read client data when possible. - - Returns false when reading failed in order to deregister with the event - loop. - """ - if flags & (gobject.IO_ERR | gobject.IO_HUP): - actor_ref.stop() - return False - - try: - data = sock.recv(4096) - except socket.error as e: - if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): - return True - actor_ref.stop() - return False - - if not data: - actor_ref.stop() - return False - - actor_ref.send_one_way({'received': data}) + self.protocol.start(sock, addr) return True @@ -131,14 +100,21 @@ class LineProtocol(ThreadingActor): timeout = 30 def __init__(self, sock, addr): + sock.setblocking(False) + self.sock = sock self.host, self.port = addr[:2] # IPv6 has larger addr self.send_lock = threading.Lock() self.recv_buffer = '' self.send_buffer = '' self.terminator_re = re.compile(self.terminator) + self.send_id = None + self.recv_id = None self.timeout_id = None + self.sock.setblocking(False) + + self.enable_recv() self.enable_timeout() def on_line_received(self, line): @@ -167,6 +143,8 @@ class LineProtocol(ThreadingActor): def on_stop(self): """Ensure that cleanup when actor stops.""" self.disable_timeout() + self.disable_recv() + self.disable_send() try: self.sock.close() except socket.error: @@ -174,8 +152,33 @@ class LineProtocol(ThreadingActor): def disable_timeout(self): """Deactivate timeout mechanism.""" - if self.timeout_id: + if self.timeout_id is not None: gobject.source_remove(self.timeout_id) + self.timeout_id = None + + def disable_recv(self): + """Deactivate recv mechanism.""" + if self.recv_id is not None: + gobject.source_remove(self.recv_id) + self.recv_id = None + + def disable_send(self): + """Deactivate send mechanism.""" + if self.send_id: + gobject.source_remove(self.send_id) + self.send_id = None + + def enable_recv(self): + """Reactivate recv mechanism.""" + if self.recv_id is None: + self.recv_id = gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN | + gobject.IO_ERR | gobject.IO_HUP, self._recv) + + def enable_send(self): + """Reactivate send mechanism.""" + if self.send_id is None: + self.send_id = gobject.io_add_watch(self.sock.fileno(), + gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self._send) def enable_timeout(self): """Reactivate timeout mechanism.""" @@ -274,19 +277,37 @@ class LineProtocol(ThreadingActor): def send_raw(self, data): """Send data to client exactly as is.""" self.send_lock.acquire(True) - should_register_sender = len(self.send_buffer) == 0 self.send_buffer += data self.send_lock.release() + self.enable_send() - if should_register_sender: - gobject.io_add_watch(self.sock.fileno(), gobject.IO_OUT | - gobject.IO_ERR | gobject.IO_HUP, self._send) - - def _send(self, fd, flags): + def _recv(self, fd, flags): # NOTE: This code is _not_ run in the actor's thread, but in the same # one as the event loop. If this blocks, rest of gobject code will # likely be blocked as well... + if flags & (gobject.IO_ERR | gobject.IO_HUP): + actor_ref.stop() + return False + + try: + data = self.sock.recv(4096) + except socket.error as e: + if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): + return True + self.actor_ref.stop() + return False + + if not data: + self.actor_ref.stop() + return False + + self.actor_ref.send_one_way({'received': data}) + return True + + def _send(self, fd, flags): + # NOTE: This code is _not_ run in the actor's thread... + # If with can't get the lock, simply try again next time socket is # ready for sending. if not self.send_lock.acquire(False): From 34cd3008d9c21213039327d18645442311fbd2da Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 9 Jul 2011 22:44:11 +0200 Subject: [PATCH 118/350] Extract gobject/network code to new Client class This implies that the Server class is in charge of just listening and starting up new clients. Clients are expected to run in the event loop thread, so they only deal with minimal IO/network concerns. Each client has a protocol actor that does the actual work. --- mopidy/frontends/mpd/__init__.py | 4 +- mopidy/utils/network.py | 280 ++++++++++++++++--------------- 2 files changed, 148 insertions(+), 136 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 8dbbf3db..e0ad28fc 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -49,8 +49,8 @@ class MpdSession(network.LineProtocol): terminator = protocol.LINE_TERMINATOR encoding = protocol.ENCODING - def __init__(self, sock, addr): - super(MpdSession, self).__init__(sock, addr) + def __init__(self, client): + super(MpdSession, self).__init__(client) self.dispatcher = dispatcher.MpdDispatcher(self) def on_start(self): diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index d949eb40..15ddb98e 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -47,9 +47,11 @@ def format_hostname(hostname): class Server(object): """Setup listener and register it with gobject's event loop.""" - def __init__(self, host, port, protocol, max_connections=15): + def __init__(self, host, port, protocol, max_connections=5, timeout=30): self.protocol = protocol self.max_connections = max_connections + self.timeout = timeout + self.listener = create_socket() self.listener.setblocking(False) self.listener.bind((host, port)) @@ -57,6 +59,7 @@ class Server(object): gobject.io_add_watch( self.listener.fileno(), gobject.IO_IN, self.handle_accept) + logger.debug(u'Listening on [%s]:%s using %s as protocol handler', host, port, self.protocol.__name__) @@ -77,10 +80,135 @@ class Server(object): pass return True - self.protocol.start(sock, addr) + client = Client(self.protocol, sock, addr, self.timeout) + client.start() + return True +class Client(object): + def __init__(self, protocol, sock, addr, timeout): + sock.setblocking(False) + + self._sock = sock + self.host, self.port = addr[:2] # IPv6 has larger addr + self._protocol = protocol + self._timeout_time = timeout + + self._send_lock = threading.Lock() + self._send_buffer = '' + + self._actor_ref = None + + self._recv_id = None + self._send_id = None + self._timeout_id = None + + def start(self): + self._actor_ref = self._protocol.start(self) + self._enable_recv() + self.enable_timeout() + + def stop(self): + self._actor_ref.stop() + self.disable_timeout() + self._disable_recv() + self._disable_send() + try: + self._sock.close() + except socket.error: + pass + return False + + def send(self, data): + """Send data to client exactly as is.""" + self._send_lock.acquire(True) + self._send_buffer += data + self._send_lock.release() + self._enable_send() + + def enable_timeout(self): + """Reactivate timeout mechanism.""" + self.disable_timeout() + if self._timeout_time > 0: + self._timeout_id = gobject.timeout_add_seconds( + self._timeout_time, self._timeout) + + def disable_timeout(self): + """Deactivate timeout mechanism.""" + if self._timeout_id is not None: + gobject.source_remove(self._timeout_id) + self._timeout_id = None + + def _enable_recv(self): + if self._recv_id is None: + self._recv_id = gobject.io_add_watch(self._sock.fileno(), + gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self._recv) + + def _disable_recv(self): + if self._recv_id is not None: + gobject.source_remove(self._recv_id) + self._recv_id = None + + def _enable_send(self): + if self._send_id is None: + self._send_id = gobject.io_add_watch(self._sock.fileno(), + gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self._send) + + def _disable_send(self): + if self._send_id: + gobject.source_remove(self._send_id) + self._send_id = None + + def _recv(self, fd, flags): + # NOTE: This code is _not_ run in the actor's thread, but in the same + # one as the event loop. If this blocks, rest of gobject code will + # likely be blocked as well... + + if flags & (gobject.IO_ERR | gobject.IO_HUP): + return self.stop() + + try: + data = self._sock.recv(4096) + except socket.error as e: + if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): + return True + return self.stop() + + if not data: + return self.stop() + + self._actor_ref.send_one_way({'received': data}) + return True + + def _send(self, fd, flags): + # NOTE: This code is _not_ run in the actor's thread... + + # If with can't get the lock, simply try again next time socket is + # ready for sending. + if not self._send_lock.acquire(False): + return True + + try: + sent = self._sock.send(self._send_buffer) + self._send_buffer = self._send_buffer[sent:] + if not self._send_buffer: + self._disable_send() + except socket.error as e: + if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK): + #self.log_error(e) # FIXME log error + return self.stop() + finally: + self._send_lock.release() + + return True + + def _timeout(self): + # NOTE: This code is _not_ run in the actor's thread... + #self.log_timeout() # FIXME log this + return self.stop() + + class LineProtocol(ThreadingActor): """ Base class for handling line based protocols. @@ -95,27 +223,11 @@ class LineProtocol(ThreadingActor): #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' - #: How long to wait before disconnecting client due to inactivity in - #: seconds. - timeout = 30 + def __init__(self, client): + self.client = client - def __init__(self, sock, addr): - sock.setblocking(False) - - self.sock = sock - self.host, self.port = addr[:2] # IPv6 has larger addr - self.send_lock = threading.Lock() self.recv_buffer = '' - self.send_buffer = '' self.terminator_re = re.compile(self.terminator) - self.send_id = None - self.recv_id = None - self.timeout_id = None - - self.sock.setblocking(False) - - self.enable_recv() - self.enable_timeout() def on_line_received(self, line): """ @@ -130,7 +242,7 @@ class LineProtocol(ThreadingActor): if 'received' not in message: return - self.disable_timeout() + self.client.disable_timeout() self.log_raw_data(message['received']) for line in self.parse_lines(message['received']): @@ -138,53 +250,11 @@ class LineProtocol(ThreadingActor): self.log_request(line) self.on_line_received(line) - self.enable_timeout() + self.client.enable_timeout() def on_stop(self): """Ensure that cleanup when actor stops.""" - self.disable_timeout() - self.disable_recv() - self.disable_send() - try: - self.sock.close() - except socket.error: - pass - - def disable_timeout(self): - """Deactivate timeout mechanism.""" - if self.timeout_id is not None: - gobject.source_remove(self.timeout_id) - self.timeout_id = None - - def disable_recv(self): - """Deactivate recv mechanism.""" - if self.recv_id is not None: - gobject.source_remove(self.recv_id) - self.recv_id = None - - def disable_send(self): - """Deactivate send mechanism.""" - if self.send_id: - gobject.source_remove(self.send_id) - self.send_id = None - - def enable_recv(self): - """Reactivate recv mechanism.""" - if self.recv_id is None: - self.recv_id = gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN | - gobject.IO_ERR | gobject.IO_HUP, self._recv) - - def enable_send(self): - """Reactivate send mechanism.""" - if self.send_id is None: - self.send_id = gobject.io_add_watch(self.sock.fileno(), - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self._send) - - def enable_timeout(self): - """Reactivate timeout mechanism.""" - self.disable_timeout() - self.timeout_id = gobject.timeout_add_seconds(self.timeout, - self._timeout) + self.client.stop() def parse_lines(self, new_data=None): """Consume new data and yield any lines found.""" @@ -201,8 +271,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Got %s from event loop in %s', - repr(data), self.actor_urn) + logger.debug(u'Got %s from event loop in %s', repr(data), + self.actor_urn) def log_request(self, request): """ @@ -210,8 +280,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Request from [%s]:%s to %s: %s', - self.host, self.port, self.actor_urn, indent(request)) + logger.debug(u'Request from %s to %s: %s', self.client, self.actor_urn, + indent(request)) def log_response(self, response): """ @@ -219,8 +289,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Response to [%s]:%s from %s: %s', - self.host, self.port, self.actor_urn, indent(response)) + logger.debug(u'Response to %s from %s: %s', self.client, + self.actor_urn, indent(response)) def log_error(self, error): """ @@ -228,8 +298,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.warning(u'Problem with connection to [%s]:%s in %s: %s', - self.host, self.port, self.actor_urn, error) + logger.warning(u'Problem with connection to %s in %s: %s', + self.client, self.actor_urn, error) def log_timeout(self): """ @@ -237,8 +307,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Closing connection to [%s]:%s in %s due to timeout.', - self.host, self.port, self.actor_urn) + logger.debug(u'Closing connection to %s in %s due to timeout.', + self.client, self.actor_urn) def encode(self, line): """ @@ -272,62 +342,4 @@ class LineProtocol(ThreadingActor): data = self.terminator.join(lines) self.log_response(data) - self.send_raw(self.encode(data + self.terminator)) - - def send_raw(self, data): - """Send data to client exactly as is.""" - self.send_lock.acquire(True) - self.send_buffer += data - self.send_lock.release() - self.enable_send() - - def _recv(self, fd, flags): - # NOTE: This code is _not_ run in the actor's thread, but in the same - # one as the event loop. If this blocks, rest of gobject code will - # likely be blocked as well... - - if flags & (gobject.IO_ERR | gobject.IO_HUP): - actor_ref.stop() - return False - - try: - data = self.sock.recv(4096) - except socket.error as e: - if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): - return True - self.actor_ref.stop() - return False - - if not data: - self.actor_ref.stop() - return False - - self.actor_ref.send_one_way({'received': data}) - return True - - def _send(self, fd, flags): - # NOTE: This code is _not_ run in the actor's thread... - - # If with can't get the lock, simply try again next time socket is - # ready for sending. - if not self.send_lock.acquire(False): - return True - - try: - sent = self.sock.send(self.send_buffer) - self.send_buffer = self.send_buffer[sent:] - return bool(self.send_buffer) - except socket.error as e: - if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): - return True - self.log_error(e) - self.actor_ref.stop() - return False - finally: - self.send_lock.release() - - def _timeout(self): - # NOTE: This code is _not_ run in the actor's thread... - self.log_timeout() - self.actor_ref.stop() - return False + self.client.send(self.encode(data + self.terminator)) From 91270ef535920530a7a65056cbda0d916c859587 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Jul 2011 18:53:49 +0200 Subject: [PATCH 119/350] Refactor network.Server to improve testability --- mopidy/utils/network.py | 103 ++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 15ddb98e..9280e772 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -12,6 +12,9 @@ from mopidy.utils.log import indent logger = logging.getLogger('mopidy.utils.server') +class ShouldRetrySocketCall(Exception): + """Indicate that attempted socket call should be retried""" + def _try_ipv6_socket(): """Determine if system really supports IPv6""" if not socket.has_ipv6: @@ -51,61 +54,79 @@ class Server(object): self.protocol = protocol self.max_connections = max_connections self.timeout = timeout + self.server_socket = self.create_server_socket(host, port) - self.listener = create_socket() - self.listener.setblocking(False) - self.listener.bind((host, port)) - self.listener.listen(1) - - gobject.io_add_watch( - self.listener.fileno(), gobject.IO_IN, self.handle_accept) + self.register_server_socket(self.server_socket.fileno()) logger.debug(u'Listening on [%s]:%s using %s as protocol handler', - host, port, self.protocol.__name__) + host, port, self.protocol) - def handle_accept(self, fd, flags): + def create_server_socket(self, host, port): + sock = create_socket() + sock.setblocking(False) + sock.bind((host, port)) + sock.listen(1) + return sock + + def register_server_socket(self, fileno): + gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection) + + def handle_connection(self, fd, flags): try: - sock, addr = self.listener.accept() - except socket.error as e: - if e.errno in (errno.EAGAIN, errno.EINTR): - return True # i.e. retry - raise - - num_connections = len(ActorRegistry.get_by_class(self.protocol)) - if self.max_connections and num_connections >= self.max_connections: - logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1]) - try: - sock.close() - except socket.error: - pass + sock, addr = self.accept_connection() + except ShouldRetrySocketCall: return True - client = Client(self.protocol, sock, addr, self.timeout) - client.start() - + if self.maximum_connections_exceeded(): + self.reject_connection(sock, addr) + else: + self.init_connection(sock, addr) return True + def accept_connection(self): + try: + return self.server_socket.accept() + except socket.error as e: + if e.errno in (errno.EAGAIN, errno.EINTR): + raise ShouldRetrySocketCall + raise -class Client(object): + def maximum_connections_exceeded(self): + return (self.max_connections is not None and + self.number_of_connections() >= self.max_connections) + + def number_of_connections(self): + return len(ActorRegistry.get_by_class(self.protocol)) + + def reject_connection(self, sock, addr): + logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1]) + try: + sock.close() + except socket.error: + pass + + def init_connection(self, sock, addr): + Connection(self.protocol, sock, addr, self.timeout) + +class Connection(object): def __init__(self, protocol, sock, addr, timeout): sock.setblocking(False) - self._sock = sock self.host, self.port = addr[:2] # IPv6 has larger addr + + self._sock = sock self._protocol = protocol self._timeout_time = timeout self._send_lock = threading.Lock() self._send_buffer = '' - self._actor_ref = None - self._recv_id = None self._send_id = None self._timeout_id = None - def start(self): self._actor_ref = self._protocol.start(self) + self._enable_recv() self.enable_timeout() @@ -223,8 +244,8 @@ class LineProtocol(ThreadingActor): #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' - def __init__(self, client): - self.client = client + def __init__(self, connection): + self.connection = connection self.recv_buffer = '' self.terminator_re = re.compile(self.terminator) @@ -242,7 +263,7 @@ class LineProtocol(ThreadingActor): if 'received' not in message: return - self.client.disable_timeout() + self.connection.disable_timeout() self.log_raw_data(message['received']) for line in self.parse_lines(message['received']): @@ -250,11 +271,11 @@ class LineProtocol(ThreadingActor): self.log_request(line) self.on_line_received(line) - self.client.enable_timeout() + self.connection.enable_timeout() def on_stop(self): """Ensure that cleanup when actor stops.""" - self.client.stop() + self.connection.stop() def parse_lines(self, new_data=None): """Consume new data and yield any lines found.""" @@ -280,7 +301,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Request from %s to %s: %s', self.client, self.actor_urn, + logger.debug(u'Request from %s to %s: %s', self.connection, self.actor_urn, indent(request)) def log_response(self, response): @@ -289,7 +310,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Response to %s from %s: %s', self.client, + logger.debug(u'Response to %s from %s: %s', self.connection, self.actor_urn, indent(response)) def log_error(self, error): @@ -299,7 +320,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ logger.warning(u'Problem with connection to %s in %s: %s', - self.client, self.actor_urn, error) + self.connection, self.actor_urn, error) def log_timeout(self): """ @@ -308,7 +329,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ logger.debug(u'Closing connection to %s in %s due to timeout.', - self.client, self.actor_urn) + self.connection, self.actor_urn) def encode(self, line): """ @@ -332,7 +353,7 @@ class LineProtocol(ThreadingActor): def send_lines(self, lines): """ - Send array of lines to client. + Send array of lines to client via connection. Join lines using the terminator that is set for this class, encode it and send it to the client. @@ -342,4 +363,4 @@ class LineProtocol(ThreadingActor): data = self.terminator.join(lines) self.log_response(data) - self.client.send(self.encode(data + self.terminator)) + self.connection.send(self.encode(data + self.terminator)) From 471ab6802adbcb3b1282447eaddfbcdb4bb6b874 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Jul 2011 18:54:08 +0200 Subject: [PATCH 120/350] Add tests that backed the network.Server refactor --- tests/utils/network_test.py | 179 +++++++++++++++++++++++++++++++++--- 1 file changed, 166 insertions(+), 13 deletions(-) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 66229036..6fee59d1 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -1,52 +1,54 @@ -import mock +import errno +import gobject import socket import unittest +from mock import patch, sentinel, Mock from mopidy.utils import network from tests import SkipTest class FormatHostnameTest(unittest.TestCase): - @mock.patch('mopidy.utils.network.has_ipv6', True) + @patch('mopidy.utils.network.has_ipv6', True) def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): network.has_ipv6 = True self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') - @mock.patch('mopidy.utils.network.has_ipv6', False) + @patch('mopidy.utils.network.has_ipv6', False) def test_format_hostname_does_nothing_when_only_ipv4_available(self): network.has_ipv6 = False self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') class TryIPv6SocketTest(unittest.TestCase): - @mock.patch('socket.has_ipv6', False) + @patch('socket.has_ipv6', False) def test_system_that_claims_no_ipv6_support(self): self.assertFalse(network._try_ipv6_socket()) - @mock.patch('socket.has_ipv6', True) - @mock.patch('socket.socket') + @patch('socket.has_ipv6', True) + @patch('socket.socket') def test_system_with_broken_ipv6(self, socket_mock): socket_mock.side_effect = IOError() self.assertFalse(network._try_ipv6_socket()) - @mock.patch('socket.has_ipv6', True) - @mock.patch('socket.socket') + @patch('socket.has_ipv6', True) + @patch('socket.socket') def test_with_working_ipv6(self, socket_mock): - socket_mock.return_value = mock.Mock() + socket_mock.return_value = Mock() self.assertTrue(network._try_ipv6_socket()) class CreateSocketTest(unittest.TestCase): - @mock.patch('mopidy.utils.network.has_ipv6', False) - @mock.patch('socket.socket') + @patch('mopidy.utils.network.has_ipv6', False) + @patch('socket.socket') def test_ipv4_socket(self, socket_mock): network.create_socket() self.assertEqual(socket_mock.call_args[0], (socket.AF_INET, socket.SOCK_STREAM)) - @mock.patch('mopidy.utils.network.has_ipv6', True) - @mock.patch('socket.socket') + @patch('mopidy.utils.network.has_ipv6', True) + @patch('socket.socket') def test_ipv6_socket(self, socket_mock): network.create_socket() self.assertEqual(socket_mock.call_args[0], @@ -55,3 +57,154 @@ class CreateSocketTest(unittest.TestCase): @SkipTest def test_ipv6_only_is_set(self): pass + +class ServerTest(unittest.TestCase): + def setUp(self): + self.protocol = network.LineProtocol + self.addr = (sentinel.host, sentinel.port) + self.host, self.port = self.addr + + self.create_server_socket_patchter = patch.object( + network.Server, 'create_server_socket', new=Mock()) + self.register_server_socket_patcher = patch.object( + network.Server, 'register_server_socket', new=Mock()) + + self.create_server_socket_patchter.start() + self.register_server_socket_patcher.start() + + def tearDown(self): + self.create_server_socket_patchter.stop() + self.register_server_socket_patcher.stop() + + def create_server(self): + return network.Server(sentinel.host, sentinel.port, self.protocol) + + def test_init_creates_socket_and_registers_it(self): + server = self.create_server() + sock = server.create_server_socket.return_value + fileno = sock.fileno.return_value + + server.create_server_socket.assert_called_once_with(self.host, self.port) + server.register_server_socket.assert_called_once_with(fileno) + + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_socket_sets_up_listener(self, create_socket): + self.create_server_socket_patchter.stop() + + try: + server = self.create_server() + sock = create_socket.return_value + + sock.setblocking.assert_called_once_with(False) + sock.bind.assert_called_once_with(self.addr) + self.assertEqual(1, sock.listen.call_count) + self.assertEqual(sock, server.server_socket) + finally: + self.create_server_socket_patchter.start() + + @SkipTest + def test_create_server_socket_fails(self): + # FIXME define what should happen in this case, let the error propegate + # or do something else? + pass + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_register_server_socket_sets_up_io_watch(self): + self.register_server_socket_patcher.stop() + + try: + server = self.create_server() + sock = server.create_server_socket.return_value + fileno = sock.fileno.return_value + + gobject.io_add_watch.assert_called_once_with( + fileno, gobject.IO_IN, server.handle_connection) + finally: + self.register_server_socket_patcher.start() + + @patch.object(network.Server, 'accept_connection', new=Mock()) + @patch.object(network.Server, 'maximum_connections_exceeded', new=Mock()) + @patch.object(network.Server, 'reject_connection', new=Mock()) + @patch.object(network.Server, 'init_connection', new=Mock()) + def test_handle_connection(self): + server = self.create_server() + server.accept_connection.return_value = (sentinel.sock, self.addr) + server.maximum_connections_exceeded.return_value = False + + server.handle_connection(sentinel.fileno, gobject.IO_IN) + + server.accept_connection.assert_called_once_with() + server.maximum_connections_exceeded.assert_called_once_with() + server.init_connection.assert_called_once_with(sentinel.sock, self.addr) + self.assertEquals(0, server.reject_connection.call_count) + + @patch.object(network.Server, 'accept_connection', new=Mock()) + @patch.object(network.Server, 'maximum_connections_exceeded', new=Mock()) + @patch.object(network.Server, 'reject_connection', new=Mock()) + @patch.object(network.Server, 'init_connection', new=Mock()) + def test_handle_connection_exceeded_connections(self): + server = self.create_server() + server.accept_connection.return_value = (sentinel.sock, self.addr) + server.maximum_connections_exceeded.return_value = True + + server.handle_connection(sentinel.fileno, gobject.IO_IN) + + server.accept_connection.assert_called_once_with() + server.maximum_connections_exceeded.assert_called_once_with() + server.reject_connection.assert_called_once_with(sentinel.sock, self.addr) + self.assertEquals(0, server.init_connection.call_count) + + def test_accept_connection(self): + server = self.create_server() + sock = server.create_server_socket.return_value + sock.accept.return_value = (sentinel.sock, self.addr) + + self.assertEquals((sentinel.sock, self.addr), server.accept_connection()) + + def test_accept_connection_recoverable_error(self): + server = self.create_server() + sock = server.create_server_socket.return_value + + sock.accept.side_effect = socket.error(errno.EAGAIN, '') + self.assertRaises(network.ShouldRetrySocketCall, server.accept_connection) + + sock.accept.side_effect = socket.error(errno.EINTR, '') + self.assertRaises(network.ShouldRetrySocketCall, server.accept_connection) + + def test_accept_connection_recoverable_error(self): + server = self.create_server() + sock = server.create_server_socket.return_value + + sock.accept.side_effect = socket.error() + self.assertRaises(socket.error, server.accept_connection) + + @patch.object(network.Server, 'number_of_connections', new=Mock()) + def test_maximum_connections_exceeded(self): + server = self.create_server() + maximum_connections = server.max_connections + + server.number_of_connections.return_value = maximum_connections + 1 + self.assertTrue(server.maximum_connections_exceeded()) + + server.number_of_connections.return_value = maximum_connections + self.assertTrue(server.maximum_connections_exceeded()) + + server.number_of_connections.return_value = maximum_connections - 1 + self.assertFalse(server.maximum_connections_exceeded()) + + @patch('pykka.registry.ActorRegistry.get_by_class') + def test_number_of_connections(self, get_by_class): + server = self.create_server() + + get_by_class.return_value = [1, 2, 3] + self.assertEqual(3, server.number_of_connections()) + + get_by_class.return_value = [] + self.assertEqual(0, server.number_of_connections()) + + @patch.object(network, 'Connection', new=Mock()) + def test_init_connection(self): + server = self.create_server() + server.init_connection(sentinel.sock, self.addr) + + network.Connection.assert_called_once_with(server.protocol, sentinel.sock, self.addr, server.timeout) From cef53b9e7d55b971fdf77d6ea1f2077160c1a675 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Jul 2011 19:07:22 +0200 Subject: [PATCH 121/350] Cleanup of connection class --- mopidy/utils/network.py | 114 ++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 9280e772..6d6c9472 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -109,88 +109,89 @@ class Server(object): Connection(self.protocol, sock, addr, self.timeout) class Connection(object): + # NOTE: the callback code is _not_ run in the actor's thread, but in the + # same one as the event loop. If code in the callbacks blocks, the rest of + # gobject code will likely be blocked as well... + def __init__(self, protocol, sock, addr, timeout): sock.setblocking(False) self.host, self.port = addr[:2] # IPv6 has larger addr - self._sock = sock - self._protocol = protocol - self._timeout_time = timeout + self.sock = sock + self.protocol = protocol + self.timeout = timeout - self._send_lock = threading.Lock() - self._send_buffer = '' + self.send_lock = threading.Lock() + self.send_buffer = '' - self._recv_id = None - self._send_id = None - self._timeout_id = None + self.recv_id = None + self.send_id = None + self.timeout_id = None - self._actor_ref = self._protocol.start(self) + self.actor_ref = self.protocol.start(self) - self._enable_recv() + self.enable_recv() self.enable_timeout() def stop(self): - self._actor_ref.stop() + self.actor_ref.stop() self.disable_timeout() - self._disable_recv() - self._disable_send() + self.disable_recv() + self.disable_send() try: - self._sock.close() + self.sock.close() except socket.error: pass return False def send(self, data): """Send data to client exactly as is.""" - self._send_lock.acquire(True) - self._send_buffer += data - self._send_lock.release() - self._enable_send() + self.send_lock.acquire(True) + self.send_buffer += data + self.send_lock.release() + self.enable_send() def enable_timeout(self): """Reactivate timeout mechanism.""" self.disable_timeout() - if self._timeout_time > 0: - self._timeout_id = gobject.timeout_add_seconds( - self._timeout_time, self._timeout) + if self.timeout > 0: + self.timeout_id = gobject.timeout_add_seconds( + self.timeout, self.timeout_callback) def disable_timeout(self): """Deactivate timeout mechanism.""" - if self._timeout_id is not None: - gobject.source_remove(self._timeout_id) - self._timeout_id = None + if self.timeout_id is not None: + gobject.source_remove(self.timeout_id) + self.timeout_id = None - def _enable_recv(self): - if self._recv_id is None: - self._recv_id = gobject.io_add_watch(self._sock.fileno(), - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self._recv) + def enable_recv(self): + if self.recv_id is None: + self.recv_id = gobject.io_add_watch(self.sock.fileno(), + gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.recv_callback) - def _disable_recv(self): - if self._recv_id is not None: - gobject.source_remove(self._recv_id) - self._recv_id = None + def disable_recv(self): + if self.recv_id is not None: + gobject.source_remove(self.recv_id) + self.recv_id = None - def _enable_send(self): - if self._send_id is None: - self._send_id = gobject.io_add_watch(self._sock.fileno(), - gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self._send) + def enable_send(self): + if self.send_id is None: + self.send_id = gobject.io_add_watch(self.sock.fileno(), + gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + self.send_callback) - def _disable_send(self): - if self._send_id: - gobject.source_remove(self._send_id) - self._send_id = None - - def _recv(self, fd, flags): - # NOTE: This code is _not_ run in the actor's thread, but in the same - # one as the event loop. If this blocks, rest of gobject code will - # likely be blocked as well... + def disable_send(self): + if self.send_id is not None: + gobject.source_remove(self.send_id) + self.send_id = None + def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): return self.stop() try: - data = self._sock.recv(4096) + data = self.sock.recv(4096) except socket.error as e: if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): return True @@ -199,33 +200,30 @@ class Connection(object): if not data: return self.stop() - self._actor_ref.send_one_way({'received': data}) + self.actor_ref.send_one_way({'received': data}) return True - def _send(self, fd, flags): - # NOTE: This code is _not_ run in the actor's thread... - + def send_callback(self, fd, flags): # If with can't get the lock, simply try again next time socket is # ready for sending. - if not self._send_lock.acquire(False): + if not self.send_lock.acquire(False): return True try: - sent = self._sock.send(self._send_buffer) - self._send_buffer = self._send_buffer[sent:] - if not self._send_buffer: - self._disable_send() + sent = self.sock.send(self.send_buffer) + self.send_buffer = self.send_buffer[sent:] + if not self.send_buffer: + self.disable_send() except socket.error as e: if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK): #self.log_error(e) # FIXME log error return self.stop() finally: - self._send_lock.release() + self.send_lock.release() return True - def _timeout(self): - # NOTE: This code is _not_ run in the actor's thread... + def timeout_callback(self): #self.log_timeout() # FIXME log this return self.stop() From 9b41eb17c5ebc8534bda627ad4a0891c4f3fad2e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 11 Jul 2011 23:55:33 +0200 Subject: [PATCH 122/350] Lint fixing --- mopidy/utils/network.py | 13 +++++++------ requirements/tests.txt | 2 +- tests/utils/network_test.py | 25 ++++++++++++++++--------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 6d6c9472..7767377b 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -15,7 +15,7 @@ logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): """Indicate that attempted socket call should be retried""" -def _try_ipv6_socket(): +def try_ipv6_socket(): """Determine if system really supports IPv6""" if not socket.has_ipv6: return False @@ -28,7 +28,7 @@ def _try_ipv6_socket(): return False #: Boolean value that indicates if creating an IPv6 socket will succeed. -has_ipv6 = _try_ipv6_socket() +has_ipv6 = try_ipv6_socket() def create_socket(): """Create a TCP socket with or without IPv6 depending on system support""" @@ -168,7 +168,8 @@ class Connection(object): def enable_recv(self): if self.recv_id is None: self.recv_id = gobject.io_add_watch(self.sock.fileno(), - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.recv_callback) + gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + self.recv_callback) def disable_recv(self): if self.recv_id is not None: @@ -254,7 +255,7 @@ class LineProtocol(ThreadingActor): Should be implemented by subclasses. """ - raise NotImplemented + raise NotImplementedError def on_receive(self, message): """Handle messages with new data from server.""" @@ -299,8 +300,8 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change logging behaviour. """ - logger.debug(u'Request from %s to %s: %s', self.connection, self.actor_urn, - indent(request)) + logger.debug(u'Request from %s to %s: %s', self.connection, + self.actor_urn, indent(request)) def log_response(self, response): """ diff --git a/requirements/tests.txt b/requirements/tests.txt index f8cf2eb3..0bc8380f 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,4 @@ coverage -mock +mock >= 0.7 nose tox diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 6fee59d1..e7767689 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -24,19 +24,19 @@ class FormatHostnameTest(unittest.TestCase): class TryIPv6SocketTest(unittest.TestCase): @patch('socket.has_ipv6', False) def test_system_that_claims_no_ipv6_support(self): - self.assertFalse(network._try_ipv6_socket()) + self.assertFalse(network.try_ipv6_socket()) @patch('socket.has_ipv6', True) @patch('socket.socket') def test_system_with_broken_ipv6(self, socket_mock): socket_mock.side_effect = IOError() - self.assertFalse(network._try_ipv6_socket()) + self.assertFalse(network.try_ipv6_socket()) @patch('socket.has_ipv6', True) @patch('socket.socket') def test_with_working_ipv6(self, socket_mock): socket_mock.return_value = Mock() - self.assertTrue(network._try_ipv6_socket()) + self.assertTrue(network.try_ipv6_socket()) class CreateSocketTest(unittest.TestCase): @@ -84,7 +84,8 @@ class ServerTest(unittest.TestCase): sock = server.create_server_socket.return_value fileno = sock.fileno.return_value - server.create_server_socket.assert_called_once_with(self.host, self.port) + server.create_server_socket.assert_called_once_with( + self.host, self.port) server.register_server_socket.assert_called_once_with(fileno) @patch.object(network, 'create_socket', spec=socket.SocketType) @@ -151,7 +152,8 @@ class ServerTest(unittest.TestCase): server.accept_connection.assert_called_once_with() server.maximum_connections_exceeded.assert_called_once_with() - server.reject_connection.assert_called_once_with(sentinel.sock, self.addr) + server.reject_connection.assert_called_once_with( + sentinel.sock, self.addr) self.assertEquals(0, server.init_connection.call_count) def test_accept_connection(self): @@ -159,17 +161,20 @@ class ServerTest(unittest.TestCase): sock = server.create_server_socket.return_value sock.accept.return_value = (sentinel.sock, self.addr) - self.assertEquals((sentinel.sock, self.addr), server.accept_connection()) + self.assertEquals((sentinel.sock, self.addr), + server.accept_connection()) def test_accept_connection_recoverable_error(self): server = self.create_server() sock = server.create_server_socket.return_value sock.accept.side_effect = socket.error(errno.EAGAIN, '') - self.assertRaises(network.ShouldRetrySocketCall, server.accept_connection) + self.assertRaises(network.ShouldRetrySocketCall, + server.accept_connection) sock.accept.side_effect = socket.error(errno.EINTR, '') - self.assertRaises(network.ShouldRetrySocketCall, server.accept_connection) + self.assertRaises(network.ShouldRetrySocketCall, + server.accept_connection) def test_accept_connection_recoverable_error(self): server = self.create_server() @@ -207,4 +212,6 @@ class ServerTest(unittest.TestCase): server = self.create_server() server.init_connection(sentinel.sock, self.addr) - network.Connection.assert_called_once_with(server.protocol, sentinel.sock, self.addr, server.timeout) + network.Connection.assert_called_once_with(server.protocol, + sentinel.sock, self.addr, server.timeout) + From 6d7575a2dbf0c3152b5fb76846019433e216225a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 01:32:14 +0200 Subject: [PATCH 123/350] Changed test strategy to use mocks in better way, i.e. rewrote ServerTest --- tests/utils/network_test.py | 175 ++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 99 deletions(-) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index e7767689..b686e20c 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -60,48 +60,44 @@ class CreateSocketTest(unittest.TestCase): class ServerTest(unittest.TestCase): def setUp(self): - self.protocol = network.LineProtocol - self.addr = (sentinel.host, sentinel.port) - self.host, self.port = self.addr + self.mock = Mock(spec=network.Server) - self.create_server_socket_patchter = patch.object( - network.Server, 'create_server_socket', new=Mock()) - self.register_server_socket_patcher = patch.object( - network.Server, 'register_server_socket', new=Mock()) + def test_init_calls_create_server_socket(self): + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) + self.mock.create_server_socket.assert_called_once_with( + sentinel.host, sentinel.port) - self.create_server_socket_patchter.start() - self.register_server_socket_patcher.start() + def test_init_calls_register_server(self): + sock = Mock(spec=socket.SocketType) + sock.fileno.return_value = sentinel.fileno + self.mock.create_server_socket.return_value = sock - def tearDown(self): - self.create_server_socket_patchter.stop() - self.register_server_socket_patcher.stop() + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) + self.mock.register_server_socket.assert_called_once_with(sentinel.fileno) - def create_server(self): - return network.Server(sentinel.host, sentinel.port, self.protocol) + def test_init_stores_values_in_attributes(self): + sock = Mock(spec=socket.SocketType) + self.mock.create_server_socket.return_value = sock - def test_init_creates_socket_and_registers_it(self): - server = self.create_server() - sock = server.create_server_socket.return_value - fileno = sock.fileno.return_value - - server.create_server_socket.assert_called_once_with( - self.host, self.port) - server.register_server_socket.assert_called_once_with(fileno) + network.Server.__init__(self.mock, sentinel.host, sentinel.port, + sentinel.protocol, max_connections=sentinel.max_connections, + timeout=sentinel.timeout) + self.assertEqual(sentinel.protocol, self.mock.protocol) + self.assertEqual(sentinel.max_connections, self.mock.max_connections) + self.assertEqual(sentinel.timeout, self.mock.timeout) + self.assertEqual(sock, self.mock.server_socket) @patch.object(network, 'create_socket', spec=socket.SocketType) def test_create_server_socket_sets_up_listener(self, create_socket): - self.create_server_socket_patchter.stop() + sock = create_socket.return_value - try: - server = self.create_server() - sock = create_socket.return_value - - sock.setblocking.assert_called_once_with(False) - sock.bind.assert_called_once_with(self.addr) - self.assertEqual(1, sock.listen.call_count) - self.assertEqual(sock, server.server_socket) - finally: - self.create_server_socket_patchter.start() + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + sock.setblocking.assert_called_once_with(False) + sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) + self.assertEqual(1, sock.listen.call_count) @SkipTest def test_create_server_socket_fails(self): @@ -111,107 +107,88 @@ class ServerTest(unittest.TestCase): @patch.object(gobject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): - self.register_server_socket_patcher.stop() + network.Server.register_server_socket(self.mock, sentinel.fileno) + gobject.io_add_watch.assert_called_once_with(sentinel.fileno, + gobject.IO_IN, self.mock.handle_connection) - try: - server = self.create_server() - sock = server.create_server_socket.return_value - fileno = sock.fileno.return_value - - gobject.io_add_watch.assert_called_once_with( - fileno, gobject.IO_IN, server.handle_connection) - finally: - self.register_server_socket_patcher.start() - - @patch.object(network.Server, 'accept_connection', new=Mock()) - @patch.object(network.Server, 'maximum_connections_exceeded', new=Mock()) - @patch.object(network.Server, 'reject_connection', new=Mock()) - @patch.object(network.Server, 'init_connection', new=Mock()) def test_handle_connection(self): - server = self.create_server() - server.accept_connection.return_value = (sentinel.sock, self.addr) - server.maximum_connections_exceeded.return_value = False + self.mock.accept_connection.return_value = (sentinel.sock, sentinel.addr) + self.mock.maximum_connections_exceeded.return_value = False - server.handle_connection(sentinel.fileno, gobject.IO_IN) + network.Server.handle_connection(self.mock, sentinel.fileno, gobject.IO_IN) + self.mock.accept_connection.assert_called_once_with() + self.mock.maximum_connections_exceeded.assert_called_once_with() + self.mock.init_connection.assert_called_once_with(sentinel.sock, sentinel.addr) + self.assertEquals(0, self.mock.reject_connection.call_count) - server.accept_connection.assert_called_once_with() - server.maximum_connections_exceeded.assert_called_once_with() - server.init_connection.assert_called_once_with(sentinel.sock, self.addr) - self.assertEquals(0, server.reject_connection.call_count) - - @patch.object(network.Server, 'accept_connection', new=Mock()) - @patch.object(network.Server, 'maximum_connections_exceeded', new=Mock()) - @patch.object(network.Server, 'reject_connection', new=Mock()) - @patch.object(network.Server, 'init_connection', new=Mock()) def test_handle_connection_exceeded_connections(self): - server = self.create_server() - server.accept_connection.return_value = (sentinel.sock, self.addr) - server.maximum_connections_exceeded.return_value = True + self.mock.accept_connection.return_value = (sentinel.sock, sentinel.addr) + self.mock.maximum_connections_exceeded.return_value = True - server.handle_connection(sentinel.fileno, gobject.IO_IN) - - server.accept_connection.assert_called_once_with() - server.maximum_connections_exceeded.assert_called_once_with() - server.reject_connection.assert_called_once_with( - sentinel.sock, self.addr) - self.assertEquals(0, server.init_connection.call_count) + network.Server.handle_connection(self.mock, sentinel.fileno, gobject.IO_IN) + self.mock.accept_connection.assert_called_once_with() + self.mock.maximum_connections_exceeded.assert_called_once_with() + self.mock.reject_connection.assert_called_once_with(sentinel.sock, sentinel.addr) + self.assertEquals(0, self.mock.init_connection.call_count) def test_accept_connection(self): - server = self.create_server() - sock = server.create_server_socket.return_value - sock.accept.return_value = (sentinel.sock, self.addr) + sock = Mock(spec=socket.SocketType) + sock.accept.return_value = (sentinel.sock, sentinel.addr) + self.mock.server_socket = sock - self.assertEquals((sentinel.sock, self.addr), - server.accept_connection()) + sock, addr = network.Server.accept_connection(self.mock) + self.assertEquals(sentinel.sock, sock) + self.assertEquals(sentinel.addr, addr) def test_accept_connection_recoverable_error(self): - server = self.create_server() - sock = server.create_server_socket.return_value + sock = Mock(spec=socket.SocketType) + self.mock.server_socket = sock sock.accept.side_effect = socket.error(errno.EAGAIN, '') self.assertRaises(network.ShouldRetrySocketCall, - server.accept_connection) + network.Server.accept_connection, self.mock) sock.accept.side_effect = socket.error(errno.EINTR, '') self.assertRaises(network.ShouldRetrySocketCall, - server.accept_connection) + network.Server.accept_connection, self.mock) - def test_accept_connection_recoverable_error(self): - server = self.create_server() - sock = server.create_server_socket.return_value + def test_accept_connection_unrecoverable_error(self): + sock = Mock(spec=socket.SocketType) + self.mock.server_socket = sock sock.accept.side_effect = socket.error() - self.assertRaises(socket.error, server.accept_connection) + self.assertRaises(socket.error, + network.Server.accept_connection, self.mock) @patch.object(network.Server, 'number_of_connections', new=Mock()) def test_maximum_connections_exceeded(self): - server = self.create_server() - maximum_connections = server.max_connections + self.mock.max_connections = 10 - server.number_of_connections.return_value = maximum_connections + 1 - self.assertTrue(server.maximum_connections_exceeded()) + self.mock.number_of_connections.return_value = 11 + self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) - server.number_of_connections.return_value = maximum_connections - self.assertTrue(server.maximum_connections_exceeded()) + self.mock.number_of_connections.return_value = 10 + self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) - server.number_of_connections.return_value = maximum_connections - 1 - self.assertFalse(server.maximum_connections_exceeded()) + self.mock.number_of_connections.return_value = 9 + self.assertFalse(network.Server.maximum_connections_exceeded(self.mock)) @patch('pykka.registry.ActorRegistry.get_by_class') def test_number_of_connections(self, get_by_class): - server = self.create_server() + self.mock.protocol = sentinel.protocol get_by_class.return_value = [1, 2, 3] - self.assertEqual(3, server.number_of_connections()) + self.assertEqual(3, network.Server.number_of_connections(self.mock)) get_by_class.return_value = [] - self.assertEqual(0, server.number_of_connections()) + self.assertEqual(0, network.Server.number_of_connections(self.mock)) @patch.object(network, 'Connection', new=Mock()) def test_init_connection(self): - server = self.create_server() - server.init_connection(sentinel.sock, self.addr) + self.mock.protocol = sentinel.protocol + self.mock.timeout = sentinel.timeout - network.Connection.assert_called_once_with(server.protocol, - sentinel.sock, self.addr, server.timeout) + network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr) + network.Connection.assert_called_once_with(sentinel.protocol, + sentinel.sock, sentinel.addr, sentinel.timeout) From d9406420e3ce6462e2eb0b878b00dc06a135ef9b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 01:34:02 +0200 Subject: [PATCH 124/350] Add missing reject_connection_test --- tests/utils/network_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index b686e20c..00032884 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -192,3 +192,17 @@ class ServerTest(unittest.TestCase): network.Connection.assert_called_once_with(sentinel.protocol, sentinel.sock, sentinel.addr, sentinel.timeout) + def test_reject_connection(self): + sock = Mock(spec=socket.SocketType) + + network.Server.reject_connection(self.mock, sock, + (sentinel.host, sentinel.port)) + sock.close.assert_called_once_with() + + def test_reject_connection_error(self): + sock = Mock(spec=socket.SocketType) + sock.close.side_effect = socket.error() + + network.Server.reject_connection(self.mock, sock, + (sentinel.host, sentinel.port)) + sock.close.assert_called_once_with() From b14e019d98b953fe88df4745cb68e04cdb285bb0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 02:10:42 +0200 Subject: [PATCH 125/350] Write up most of ConnectionTest, only callbacks to go --- tests/utils/network_test.py | 189 ++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 00032884..23ed0247 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -206,3 +206,192 @@ class ServerTest(unittest.TestCase): network.Server.reject_connection(self.mock, sock, (sentinel.host, sentinel.port)) sock.close.assert_called_once_with() + +class ConnectionTest(unittest.TestCase): + def setUp(self): + self.mock = Mock(spec=network.Connection) + + def test_init_ensure_nonblocking_io(self): + sock = Mock(spec=socket.SocketType) + + network.Connection.__init__(self.mock, Mock(), sock, + (sentinel.host, sentinel.port), sentinel.timeout) + sock.setblocking.assert_called_once_with(False) + + def test_init_starts_actor(self): + protocol = Mock(spec=network.LineProtocol) + + network.Connection.__init__(self.mock, protocol, Mock(), + (sentinel.host, sentinel.port), sentinel.timeout) + protocol.start.assert_called_once_with(self.mock) + + def test_init_enables_recv_and_timeout(self): + network.Connection.__init__(self.mock, Mock(), Mock(), + (sentinel.host, sentinel.port), sentinel.timeout) + self.mock.enable_recv.assert_called_once_with() + self.mock.enable_timeout.assert_called_once_with() + + def test_init_stores_values_in_attributes(self): + protocol = Mock(spec=network.LineProtocol) + sock = Mock(spec=socket.SocketType) + + network.Connection.__init__(self.mock, protocol, sock, + (sentinel.host, sentinel.port), sentinel.timeout) + self.assertEqual(sock, self.mock.sock) + self.assertEqual(protocol, self.mock.protocol) + self.assertEqual(sentinel.timeout, self.mock.timeout) + self.assertEqual(sentinel.host, self.mock.host) + self.assertEqual(sentinel.port, self.mock.port) + + def test_stop_disables_recv_send_and_timeout(self): + network.Connection.stop(self.mock) + self.mock.disable_timeout.assert_called_once_with() + self.mock.disable_recv.assert_called_once_with() + self.mock.disable_timeout.assert_called_once_with() + + def test_stop_closes_socket(self): + sock = Mock(spec=socket.SocketType) + self.mock.sock = sock + + network.Connection.stop(self.mock) + sock.close.assert_called_once_with() + + def test_stop_closes_socket_error(self): + sock = Mock(spec=socket.SocketType) + sock.close.side_effect = socket.error() + self.mock.sock = sock + + network.Connection.stop(self.mock) + sock.close.assert_called_once_with() + + def test_stop_return_false(self): + self.assertFalse(network.Connection.stop(self.mock)) + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_enable_recv_registers_with_gobject(self): + self.mock.recv_id = None + self.mock.sock.fileno.return_value = sentinel.fileno + gobject.io_add_watch.return_value = sentinel.tag + + network.Connection.enable_recv(self.mock) + gobject.io_add_watch.assert_called_once_with(sentinel.fileno, + gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + self.mock.recv_callback) + self.assertEqual(sentinel.tag, self.mock.recv_id) + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_enable_recv_already_registered(self): + self.mock.recv_id = sentinel.tag + + network.Connection.enable_recv(self.mock) + self.assertEqual(0, gobject.io_add_watch.call_count) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_recv_deregisters(self): + self.mock.recv_id = sentinel.tag + + network.Connection.disable_recv(self.mock) + gobject.source_remove.assert_called_once_with(sentinel.tag) + self.assertEqual(None, self.mock.recv_id) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_recv_already_deregistered(self): + self.mock.recv_id = None + + network.Connection.disable_recv(self.mock) + self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(None, self.mock.recv_id) + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_enable_send_registers_with_gobject(self): + self.mock.send_id = None + self.mock.sock.fileno.return_value = sentinel.fileno + gobject.io_add_watch.return_value = sentinel.tag + + network.Connection.enable_send(self.mock) + gobject.io_add_watch.assert_called_once_with(sentinel.fileno, + gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, + self.mock.send_callback) + self.assertEqual(sentinel.tag, self.mock.send_id) + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_enable_send_already_registered(self): + self.mock.send_id = sentinel.tag + + network.Connection.enable_send(self.mock) + self.assertEqual(0, gobject.io_add_watch.call_count) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_send_deregisters(self): + self.mock.send_id = sentinel.tag + + network.Connection.disable_send(self.mock) + gobject.source_remove.assert_called_once_with(sentinel.tag) + self.assertEqual(None, self.mock.send_id) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_send_already_deregistered(self): + self.mock.send_id = None + + network.Connection.disable_send(self.mock) + self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(None, self.mock.send_id) + + @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + def test_enable_timeout_clears_existing_timeouts(self): + self.mock.timeout = 10 + + network.Connection.enable_timeout(self.mock) + self.mock.disable_timeout.assert_called_once_with() + + @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + def test_enable_timeout_add_gobject_timeout(self): + self.mock.timeout = 10 + gobject.timeout_add_seconds.return_value = sentinel.tag + + network.Connection.enable_timeout(self.mock) + gobject.timeout_add_seconds.assert_called_once_with(10, + self.mock.timeout_callback) + self.assertEqual(sentinel.tag, self.mock.timeout_id) + + @patch.object(gobject, 'timeout_add_seconds', new=Mock()) + def test_enable_timeout_does_not_add_timeout(self): + self.mock.timeout = 0 + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, gobject.timeout_add_seconds.call_count) + + self.mock.timeout = -1 + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, gobject.timeout_add_seconds.call_count) + + self.mock.timeout = None + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, gobject.timeout_add_seconds.call_count) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_timeout_deregisters(self): + self.mock.timeout_id = sentinel.tag + + network.Connection.disable_timeout(self.mock) + gobject.source_remove.assert_called_once_with(sentinel.tag) + self.assertEqual(None, self.mock.timeout_id) + + @patch.object(gobject, 'source_remove', new=Mock()) + def test_disable_timeout_already_deregistered(self): + self.mock.timeout_id = None + + network.Connection.disable_timeout(self.mock) + self.assertEqual(0, gobject.source_remove.call_count) + self.assertEqual(None, self.mock.timeout_id) + + @SkipTest + def test_recv_callback(self): + pass + + @SkipTest + def test_send_callback(self): + pass + + @SkipTest + def test_timeout_callback(self): + pass From 8c9fc735503ca383768f8b1eaaf0fe4175b1bd01 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 03:15:30 +0200 Subject: [PATCH 126/350] Implement rest of connection tests --- mopidy/utils/network.py | 16 +-- tests/utils/network_test.py | 201 +++++++++++++++++++++++++++++++----- 2 files changed, 183 insertions(+), 34 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 7767377b..27eb3ed8 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -143,7 +143,6 @@ class Connection(object): self.sock.close() except socket.error: pass - return False def send(self, data): """Send data to client exactly as is.""" @@ -189,17 +188,20 @@ class Connection(object): def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): - return self.stop() + self.stop() + return False try: data = self.sock.recv(4096) except socket.error as e: - if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN): + if e.errno in (errno.EWOULDBLOCK, errno.EINTR): return True - return self.stop() + self.stop() + return False if not data: - return self.stop() + self.stop() + return False self.actor_ref.send_one_way({'received': data}) return True @@ -216,9 +218,9 @@ class Connection(object): if not self.send_buffer: self.disable_send() except socket.error as e: - if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK): + if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): #self.log_error(e) # FIXME log error - return self.stop() + self.stop() finally: self.send_lock.release() diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 23ed0247..8fd345c6 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -18,7 +18,7 @@ class FormatHostnameTest(unittest.TestCase): @patch('mopidy.utils.network.has_ipv6', False) def test_format_hostname_does_nothing_when_only_ipv4_available(self): network.has_ipv6 = False - self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0') + self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0') class TryIPv6SocketTest(unittest.TestCase): @@ -119,7 +119,7 @@ class ServerTest(unittest.TestCase): self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.init_connection.assert_called_once_with(sentinel.sock, sentinel.addr) - self.assertEquals(0, self.mock.reject_connection.call_count) + self.assertEqual(0, self.mock.reject_connection.call_count) def test_handle_connection_exceeded_connections(self): self.mock.accept_connection.return_value = (sentinel.sock, sentinel.addr) @@ -129,7 +129,7 @@ class ServerTest(unittest.TestCase): self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.reject_connection.assert_called_once_with(sentinel.sock, sentinel.addr) - self.assertEquals(0, self.mock.init_connection.call_count) + self.assertEqual(0, self.mock.init_connection.call_count) def test_accept_connection(self): sock = Mock(spec=socket.SocketType) @@ -137,20 +137,17 @@ class ServerTest(unittest.TestCase): self.mock.server_socket = sock sock, addr = network.Server.accept_connection(self.mock) - self.assertEquals(sentinel.sock, sock) - self.assertEquals(sentinel.addr, addr) + self.assertEqual(sentinel.sock, sock) + self.assertEqual(sentinel.addr, addr) def test_accept_connection_recoverable_error(self): sock = Mock(spec=socket.SocketType) self.mock.server_socket = sock - sock.accept.side_effect = socket.error(errno.EAGAIN, '') - self.assertRaises(network.ShouldRetrySocketCall, - network.Server.accept_connection, self.mock) - - sock.accept.side_effect = socket.error(errno.EINTR, '') - self.assertRaises(network.ShouldRetrySocketCall, - network.Server.accept_connection, self.mock) + for error in (errno.EAGAIN, errno.EINTR): + sock.accept.side_effect = socket.error(error, '') + self.assertRaises(network.ShouldRetrySocketCall, + network.Server.accept_connection, self.mock) def test_accept_connection_unrecoverable_error(self): sock = Mock(spec=socket.SocketType) @@ -244,32 +241,40 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(sentinel.port, self.mock.port) def test_stop_disables_recv_send_and_timeout(self): + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + network.Connection.stop(self.mock) self.mock.disable_timeout.assert_called_once_with() self.mock.disable_recv.assert_called_once_with() self.mock.disable_timeout.assert_called_once_with() def test_stop_closes_socket(self): - sock = Mock(spec=socket.SocketType) - self.mock.sock = sock + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock) - sock.close.assert_called_once_with() + self.mock.sock.close.assert_called_once_with() def test_stop_closes_socket_error(self): - sock = Mock(spec=socket.SocketType) - sock.close.side_effect = socket.error() - self.mock.sock = sock + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.close.side_effect = socket.error() network.Connection.stop(self.mock) - sock.close.assert_called_once_with() + self.mock.sock.close.assert_called_once_with() - def test_stop_return_false(self): - self.assertFalse(network.Connection.stop(self.mock)) + def test_stop_stops_actor(self): + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock) + self.mock.actor_ref.stop.assert_called_once_with() @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None + self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno gobject.io_add_watch.return_value = sentinel.tag @@ -305,6 +310,7 @@ class ConnectionTest(unittest.TestCase): @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_send_registers_with_gobject(self): self.mock.send_id = None + self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno gobject.io_add_watch.return_value = sentinel.tag @@ -384,14 +390,155 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.timeout_id) - @SkipTest - def test_recv_callback(self): - pass + def test_send_acquires_and_releases_lock(self): + self.mock.send_lock = Mock() + self.mock.send_buffer = '' + + network.Connection.send(self.mock, 'data') + self.mock.send_lock.acquire.assert_called_once_with(True) + self.mock.send_lock.release.assert_called_once_with() + + def test_send_appends_to_send_buffer(self): + self.mock.send_lock = Mock() + self.mock.send_buffer = '' + + network.Connection.send(self.mock, 'abc') + self.assertEqual('abc', self.mock.send_buffer) + + network.Connection.send(self.mock, 'def') + self.assertEqual('abcdef', self.mock.send_buffer) + + network.Connection.send(self.mock, '') + self.assertEqual('abcdef', self.mock.send_buffer) + + def test_send_calls_enable_send(self): + self.mock.send_lock = Mock() + self.mock.send_buffer = '' + + network.Connection.send(self.mock, 'data') + self.mock.enable_send.assert_called_once_with() + + def test_recv_callback_respects_io_err(self): + self.assertFalse(network.Connection.recv_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock.stop.assert_called_once_with() + + def test_recv_callback_respects_io_hup(self): + self.assertFalse(network.Connection.recv_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock.stop.assert_called_once_with() + + def test_recv_callback_respects_io_hup_and_io_err(self): + self.assertFalse(network.Connection.recv_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + self.mock.stop.assert_called_once_with() + + def test_recv_callback_gets_data(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.recv.return_value = 'data' + self.mock.actor_ref = Mock() + + self.assertTrue(network.Connection.recv_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.actor_ref.send_one_way.assert_called_once_with( + {'received': 'data'}) + + def test_recv_callback_gets_no_data(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.recv.return_value = '' + + self.assertFalse(network.Connection.recv_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.stop.assert_called_once_with() + + def test_recv_callback_recoverable_error(self): + self.mock.sock = Mock(spec=socket.SocketType) + + for error in (errno.EWOULDBLOCK, errno.EINTR): + self.mock.sock.recv.side_effect = socket.error(error, '') + self.assertTrue(network.Connection.recv_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + + def test_recv_callback_unrecoverable_error(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.recv.side_effect = socket.error() + + self.assertFalse(network.Connection.recv_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.stop.assert_called_once_with() @SkipTest - def test_send_callback(self): + def test_send_callback_respects_flags(self): + # stop self pass - @SkipTest + def test_send_callback_acquires_and_releases_lock(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = True + self.mock.send_buffer = '' + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 0 + + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.send_lock.acquire.assert_called_once_with(False) + self.mock.send_lock.release.assert_called_once_with() + + def test_send_callback_fails_to_acquire_lock(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = False + + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.send_lock.acquire.assert_called_once_with(False) + + def test_send_callback_sends_all_data(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = True + self.mock.send_buffer = 'data' + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 4 + + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.disable_send.assert_called_once_with() + self.mock.sock.send.assert_called_once_with('data') + self.assertEqual('', self.mock.send_buffer) + + def test_send_callback_sends_partial_data(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = True + self.mock.send_buffer = 'data' + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 2 + + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.sock.send.assert_called_once_with('data') + self.assertEqual('ta', self.mock.send_buffer) + + def test_send_callback_recoverable_error(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = True + self.mock.send_buffer = 'data' + self.mock.sock = Mock(spec=socket.SocketType) + + for error in (errno.EWOULDBLOCK, errno.EINTR): + self.mock.sock.send.side_effect = socket.error(error, '') + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + + def test_send_callback_unrecoverable_error(self): + self.mock.send_lock = Mock() + self.mock.send_lock.acquire.return_value = True + self.mock.send_buffer = 'data' + self.mock.sock = Mock(spec=socket.SocketType) + + self.mock.sock.send.side_effect = socket.error() + self.assertTrue(network.Connection.send_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.stop.assert_called_once_with() + def test_timeout_callback(self): - pass + network.Connection.timeout_callback(self.mock) + self.mock.stop.assert_called_once_with() From b5c6bc044263f280a742be35998e7cf729864b90 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 03:18:07 +0200 Subject: [PATCH 127/350] Allways return true from recv_callback, rely on activly removing sources instead --- mopidy/utils/network.py | 14 ++++++-------- tests/utils/network_test.py | 10 +++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 27eb3ed8..c56690a3 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -189,21 +189,19 @@ class Connection(object): def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): self.stop() - return False + return True try: data = self.sock.recv(4096) except socket.error as e: - if e.errno in (errno.EWOULDBLOCK, errno.EINTR): - return True - self.stop() - return False + if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): + self.stop() + return True if not data: self.stop() - return False - - self.actor_ref.send_one_way({'received': data}) + else: + self.actor_ref.send_one_way({'received': data}) return True def send_callback(self, fd, flags): diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 8fd345c6..f280c0d3 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -419,17 +419,17 @@ class ConnectionTest(unittest.TestCase): self.mock.enable_send.assert_called_once_with() def test_recv_callback_respects_io_err(self): - self.assertFalse(network.Connection.recv_callback(self.mock, + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock.stop.assert_called_once_with() def test_recv_callback_respects_io_hup(self): - self.assertFalse(network.Connection.recv_callback(self.mock, + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock.stop.assert_called_once_with() def test_recv_callback_respects_io_hup_and_io_err(self): - self.assertFalse(network.Connection.recv_callback(self.mock, + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) self.mock.stop.assert_called_once_with() @@ -447,7 +447,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = '' - self.assertFalse(network.Connection.recv_callback( + self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with() @@ -463,7 +463,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.side_effect = socket.error() - self.assertFalse(network.Connection.recv_callback( + self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with() From b9286fb9ee190520276b706a17f505077316a889 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 03:29:04 +0200 Subject: [PATCH 128/350] Log why we are stopping --- mopidy/utils/network.py | 18 ++++++++------- tests/utils/network_test.py | 44 +++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index c56690a3..26565577 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -10,6 +10,7 @@ from pykka.registry import ActorRegistry from mopidy.utils.log import indent +# FIXME setup logger with extra={...} logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): @@ -134,11 +135,14 @@ class Connection(object): self.enable_recv() self.enable_timeout() - def stop(self): + def stop(self, reason, level=logging.DEBUG): + logger.log(level, reason) + self.actor_ref.stop() self.disable_timeout() self.disable_recv() self.disable_send() + try: self.sock.close() except socket.error: @@ -188,18 +192,18 @@ class Connection(object): def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): - self.stop() + self.stop(u'Bad client flags: %s' % flags) return True try: data = self.sock.recv(4096) except socket.error as e: if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): - self.stop() + self.stop(u'Unexpected client error: %s' % e) return True if not data: - self.stop() + self.stop(u'Client most likely disconnected.') else: self.actor_ref.send_one_way({'received': data}) return True @@ -217,16 +221,14 @@ class Connection(object): self.disable_send() except socket.error as e: if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): - #self.log_error(e) # FIXME log error - self.stop() + self.stop(u'Unexpected client error: %s' % e) finally: self.send_lock.release() return True def timeout_callback(self): - #self.log_timeout() # FIXME log this - return self.stop() + return self.stop(u'Client timeout out after %s seconds' % self.timeout) class LineProtocol(ThreadingActor): diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index f280c0d3..e4dcec70 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -2,6 +2,7 @@ import errno import gobject import socket import unittest +import logging from mock import patch, sentinel, Mock from mopidy.utils import network @@ -244,7 +245,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) - network.Connection.stop(self.mock) + network.Connection.stop(self.mock, sentinel.reason) self.mock.disable_timeout.assert_called_once_with() self.mock.disable_recv.assert_called_once_with() self.mock.disable_timeout.assert_called_once_with() @@ -253,7 +254,7 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) - network.Connection.stop(self.mock) + network.Connection.stop(self.mock, sentinel.reason) self.mock.sock.close.assert_called_once_with() def test_stop_closes_socket_error(self): @@ -261,16 +262,35 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.close.side_effect = socket.error() - network.Connection.stop(self.mock) + network.Connection.stop(self.mock, sentinel.reason) self.mock.sock.close.assert_called_once_with() def test_stop_stops_actor(self): self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) - network.Connection.stop(self.mock) + network.Connection.stop(self.mock, sentinel.reason) self.mock.actor_ref.stop.assert_called_once_with() + @patch.object(network.logger, 'log', new=Mock()) + def test_stop_logs_reason(self): + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + network.logger.log.assert_called_once_with( + logging.DEBUG, sentinel.reason) + + @patch.object(network.logger, 'log', new=Mock()) + def test_stop_logs_reason_with_level(self): + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason, + level=sentinel.level) + network.logger.log.assert_called_once_with( + sentinel.level, sentinel.reason) + @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None @@ -421,17 +441,17 @@ class ConnectionTest(unittest.TestCase): def test_recv_callback_respects_io_err(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) def test_recv_callback_respects_io_hup(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) def test_recv_callback_respects_io_hup_and_io_err(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) def test_recv_callback_gets_data(self): self.mock.sock = Mock(spec=socket.SocketType) @@ -449,7 +469,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) def test_recv_callback_recoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) @@ -465,7 +485,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) @SkipTest def test_send_callback_respects_flags(self): @@ -537,8 +557,10 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.side_effect = socket.error() self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) def test_timeout_callback(self): + self.mock.timeout = 10 + network.Connection.timeout_callback(self.mock) - self.mock.stop.assert_called_once_with() + self.assertEqual(1, self.mock.stop.call_count) From 2f1d32ba80816e3880a464a63d8f3f549a2be9e2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 22:02:50 +0200 Subject: [PATCH 129/350] Add IsA helper to tests to provde any_int, any_str and any_unicode --- tests/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 1d4d2e3d..663b89ec 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -22,3 +22,22 @@ def path_to_data_dir(name): path = os.path.abspath(path) return os.path.join(path, name) +class IsA(object): + def __init__(self, klass): + self.klass = klass + + def __eq__(self, rhs): + try: + return isinstance(rhs, self.klass) + except TypeError: + return type(rhs) == type(self.klass) + + def __ne__(self, rhs): + return not self.__eq__(rhs) + + def __repr__(self): + return str(self.klass) + +any_int = IsA(int) +any_str = IsA(str) +any_unicode = IsA(unicode) From 51190c510a089d73bdb216e4a17fd94807fc2dae Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 12 Jul 2011 22:02:56 +0200 Subject: [PATCH 130/350] Switch to more robust checking of stop calls --- tests/utils/network_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index e4dcec70..e1f1bbed 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -7,7 +7,7 @@ from mock import patch, sentinel, Mock from mopidy.utils import network -from tests import SkipTest +from tests import SkipTest, any_int, any_unicode class FormatHostnameTest(unittest.TestCase): @patch('mopidy.utils.network.has_ipv6', True) @@ -98,7 +98,7 @@ class ServerTest(unittest.TestCase): sentinel.host, sentinel.port) sock.setblocking.assert_called_once_with(False) sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) - self.assertEqual(1, sock.listen.call_count) + sock.listen.assert_called_once_with(any_int) @SkipTest def test_create_server_socket_fails(self): @@ -441,17 +441,17 @@ class ConnectionTest(unittest.TestCase): def test_recv_callback_respects_io_err(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup_and_io_err(self): self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_gets_data(self): self.mock.sock = Mock(spec=socket.SocketType) @@ -469,7 +469,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_recoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) @@ -485,7 +485,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) @SkipTest def test_send_callback_respects_flags(self): @@ -557,10 +557,10 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.side_effect = socket.error() self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) def test_timeout_callback(self): self.mock.timeout = 10 network.Connection.timeout_callback(self.mock) - self.assertEqual(1, self.mock.stop.call_count) + self.mock.stop.assert_called_once_with(any_unicode) From 05b169930fa2b85f47dfebfbb5abb403e1846953 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jul 2011 11:48:22 +0200 Subject: [PATCH 131/350] Add missing stop explanation --- mopidy/utils/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 26565577..52fc04eb 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -276,7 +276,7 @@ class LineProtocol(ThreadingActor): def on_stop(self): """Ensure that cleanup when actor stops.""" - self.connection.stop() + self.connection.stop(u'Actor is shuting down.') def parse_lines(self, new_data=None): """Consume new data and yield any lines found.""" From a49855abfa5431711369cf3a94c0a213a0b92e92 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jul 2011 22:32:35 +0200 Subject: [PATCH 132/350] Improve error handling in connection code --- mopidy/utils/network.py | 28 ++++++++++++++++++++---- tests/utils/network_test.py | 43 ++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 52fc04eb..1d63f6e6 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -5,6 +5,7 @@ import re import socket import threading +from pykka import ActorDeadError from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry @@ -138,7 +139,11 @@ class Connection(object): def stop(self, reason, level=logging.DEBUG): logger.log(level, reason) - self.actor_ref.stop() + try: + self.actor_ref.stop() + except ActorDeadError: + pass + self.disable_timeout() self.disable_recv() self.disable_send() @@ -169,10 +174,15 @@ class Connection(object): self.timeout_id = None def enable_recv(self): - if self.recv_id is None: + if self.recv_id is not None: + return + + try: self.recv_id = gobject.io_add_watch(self.sock.fileno(), gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.recv_callback) + except socket.error as e: + self.stop(u'Problem with connection: %s' % e) def disable_recv(self): if self.recv_id is not None: @@ -180,10 +190,15 @@ class Connection(object): self.recv_id = None def enable_send(self): - if self.send_id is None: + if self.send_id is not None: + return + + try: self.send_id = gobject.io_add_watch(self.sock.fileno(), gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self.send_callback) + except socket.error as e: + self.stop(u'Problem with connection: %s' % e) def disable_send(self): if self.send_id is not None: @@ -204,8 +219,13 @@ class Connection(object): if not data: self.stop(u'Client most likely disconnected.') - else: + return True + + try: self.actor_ref.send_one_way({'received': data}) + except ActorDeadError: + self.stop(u'Actor is dead.') + return True def send_callback(self, fd, flags): diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index e1f1bbed..9e41472f 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -1,12 +1,13 @@ import errno import gobject +import logging +import pykka import socket import unittest -import logging -from mock import patch, sentinel, Mock from mopidy.utils import network +from mock import patch, sentinel, Mock from tests import SkipTest, any_int, any_unicode class FormatHostnameTest(unittest.TestCase): @@ -59,6 +60,7 @@ class CreateSocketTest(unittest.TestCase): def test_ipv6_only_is_set(self): pass + class ServerTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.Server) @@ -205,6 +207,7 @@ class ServerTest(unittest.TestCase): (sentinel.host, sentinel.port)) sock.close.assert_called_once_with() + class ConnectionTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.Connection) @@ -272,6 +275,13 @@ class ConnectionTest(unittest.TestCase): network.Connection.stop(self.mock, sentinel.reason) self.mock.actor_ref.stop.assert_called_once_with() + def test_stop_handles_actor_already_being_stopped(self): + self.mock.actor_ref = Mock() + self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason(self): self.mock.actor_ref = Mock() @@ -327,6 +337,15 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.recv_id) + def test_enable_recv_on_closed_socket(self): + self.mock.recv_id = None + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '') + + network.Connection.enable_recv(self.mock) + self.mock.stop.assert_called_once_with(any_unicode) + self.assertEqual(None, self.mock.recv_id) + @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_send_registers_with_gobject(self): self.mock.send_id = None @@ -363,6 +382,14 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.send_id) + def test_enable_send_on_closed_socket(self): + self.mock.send_id = None + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '') + + network.Connection.enable_send(self.mock) + self.assertEqual(None, self.mock.send_id) + @patch.object(gobject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_clears_existing_timeouts(self): self.mock.timeout = 10 @@ -453,7 +480,7 @@ class ConnectionTest(unittest.TestCase): sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) - def test_recv_callback_gets_data(self): + def test_recv_callback_sends_data_to_actor(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = 'data' self.mock.actor_ref = Mock() @@ -463,6 +490,16 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref.send_one_way.assert_called_once_with( {'received': 'data'}) + def test_recv_callback_handles_dead_actors(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.recv.return_value = 'data' + self.mock.actor_ref = Mock() + self.mock.actor_ref.send_one_way.side_effect = pykka.ActorDeadError() + + self.assertTrue(network.Connection.recv_callback( + self.mock, sentinel.fd, gobject.IO_IN)) + self.mock.stop.assert_called_once_with(any_unicode) + def test_recv_callback_gets_no_data(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = '' From ee6f5a651bf5061683558a524e1640fab2f0a02e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jul 2011 22:43:57 +0200 Subject: [PATCH 133/350] Try to prevent recursive calls to stop --- mopidy/utils/network.py | 8 ++++++++ tests/utils/network_test.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 1d63f6e6..acb074d1 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -127,6 +127,8 @@ class Connection(object): self.send_lock = threading.Lock() self.send_buffer = '' + self.stopping = False + self.recv_id = None self.send_id = None self.timeout_id = None @@ -137,6 +139,12 @@ class Connection(object): self.enable_timeout() def stop(self, reason, level=logging.DEBUG): + if self.stopping: + logger.log(level, 'Already stopping: %s' % reason) + return + else: + self.stopping = True + logger.log(level, reason) try: diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 9e41472f..52d8c51b 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -245,6 +245,7 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(sentinel.port, self.mock.port) def test_stop_disables_recv_send_and_timeout(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) @@ -254,6 +255,7 @@ class ConnectionTest(unittest.TestCase): self.mock.disable_timeout.assert_called_once_with() def test_stop_closes_socket(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) @@ -261,6 +263,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.close.assert_called_once_with() def test_stop_closes_socket_error(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.close.side_effect = socket.error() @@ -269,6 +272,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.close.assert_called_once_with() def test_stop_stops_actor(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) @@ -276,14 +280,42 @@ class ConnectionTest(unittest.TestCase): self.mock.actor_ref.stop.assert_called_once_with() def test_stop_handles_actor_already_being_stopped(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) + def test_stop_sets_stopping_to_true(self): + self.mock.stopping = False + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + self.assertEqual(True, self.mock.stopping) + + def test_stop_does_not_proceed_when_already_stopping(self): + self.mock.stopping = True + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + self.assertEqual(0, self.mock.actor_ref.stop.call_count) + self.assertEqual(0, self.mock.sock.close.call_count) + + @patch.object(network.logger, 'log', new=Mock()) + def test_stop_logs_that_it_is_calling_itself(self): + self.mock.stopping = True + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + network.logger.log(any_int, any_unicode) + @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) @@ -293,6 +325,7 @@ class ConnectionTest(unittest.TestCase): @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason_with_level(self): + self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) From 66a89918c8480430541fa93eaa998393fdc8d9f2 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jul 2011 23:11:28 +0200 Subject: [PATCH 134/350] Add LineProtocol tests --- mopidy/utils/network.py | 30 +++--- tests/utils/network_test.py | 203 ++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 16 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index acb074d1..7233e65b 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -275,9 +275,7 @@ class LineProtocol(ThreadingActor): def __init__(self, connection): self.connection = connection - self.recv_buffer = '' - self.terminator_re = re.compile(self.terminator) def on_line_received(self, line): """ @@ -294,8 +292,9 @@ class LineProtocol(ThreadingActor): self.connection.disable_timeout() self.log_raw_data(message['received']) + self.recv_buffer += message['received'] - for line in self.parse_lines(message['received']): + for line in self.parse_lines(): line = self.decode(line) self.log_request(line) self.on_line_received(line) @@ -306,12 +305,10 @@ class LineProtocol(ThreadingActor): """Ensure that cleanup when actor stops.""" self.connection.stop(u'Actor is shuting down.') - def parse_lines(self, new_data=None): + def parse_lines(self): """Consume new data and yield any lines found.""" - if new_data: - self.recv_buffer += new_data - while self.terminator_re.search(self.recv_buffer): - line, self.recv_buffer = self.terminator_re.split( + while re.search(self.terminator, self.recv_buffer): + line, self.recv_buffer = re.split(self.terminator, self.recv_buffer, 1) yield line @@ -366,9 +363,7 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change encoding behaviour. """ - if self.encoding: - return line.encode(self.encoding) - return line + return line.encode(self.encoding) def decode(self, line): """ @@ -376,9 +371,12 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change decoding behaviour. """ - if self.encoding: - return line.decode(self.encoding) - return line + return line.decode(self.encoding) + + def join_lines(self, lines): + if not lines: + return u'' + return self.terminator.join(lines) + self.terminator def send_lines(self, lines): """ @@ -390,6 +388,6 @@ class LineProtocol(ThreadingActor): if not lines: return - data = self.terminator.join(lines) + data = self.join_lines(lines) self.log_response(data) - self.connection.send(self.encode(data + self.terminator)) + self.connection.send(self.encode(data)) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 52d8c51b..5553af1a 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -1,3 +1,5 @@ +#encoding: utf-8 + import errno import gobject import logging @@ -634,3 +636,204 @@ class ConnectionTest(unittest.TestCase): network.Connection.timeout_callback(self.mock) self.mock.stop.assert_called_once_with(any_unicode) + + +class LineProtocolTest(unittest.TestCase): + def setUp(self): + self.mock = Mock(spec=network.LineProtocol) + self.mock.terminator = network.LineProtocol.terminator + self.mock.encoding = network.LineProtocol.encoding + + def test_init_stores_values_in_attributes(self): + network.LineProtocol.__init__(self.mock, sentinel.connection) + self.assertEqual(sentinel.connection, self.mock.connection) + self.assertEqual('', self.mock.recv_buffer) + + def test_on_receive_no_new_lines_adds_to_recv_buffer(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.assertEqual('data', self.mock.recv_buffer) + self.mock.parse_lines.assert_called_once_with() + self.assertEqual(0, self.mock.on_line_received.call_count) + + def test_on_receive_no_new_lines_toggles_timeout(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.mock.connection.disable_timeout.assert_called_once_with() + self.mock.connection.enable_timeout.assert_called_once_with() + + def test_on_receive_no_new_lines_calls_parse_lines(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.mock.parse_lines.assert_called_once_with() + self.assertEqual(0, self.mock.on_line_received.call_count) + + def test_on_receive_with_new_line_calls_decode(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [sentinel.line] + + network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) + self.mock.parse_lines.assert_called_once_with() + self.mock.decode.assert_called_once_with(sentinel.line) + + def test_on_receive_with_new_line_calls_on_recieve(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [sentinel.line] + self.mock.decode.return_value = sentinel.decoded + + network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) + self.mock.on_line_received.assert_called_once_with(sentinel.decoded) + + def test_parse_lines_emtpy_buffer(self): + self.mock.recv_buffer = '' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + + def test_parse_lines_no_terminator(self): + self.mock.recv_buffer = 'data' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + + def test_parse_lines_termintor(self): + self.mock.recv_buffer = 'data\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_no_data_before_terminator(self): + self.mock.recv_buffer = '\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_extra_data_after_terminator(self): + self.mock.recv_buffer = 'data1\ndata2' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data1', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('data2', self.mock.recv_buffer) + + def test_parse_lines_unicode(self): + self.mock.recv_buffer = u'æøå\n'.encode('utf-8') + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual(u'æøå'.encode('utf-8'), lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_multiple_lines(self): + self.mock.recv_buffer = 'abc\ndef\nghi\njkl' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('abc', lines.next()) + self.assertEqual('def', lines.next()) + self.assertEqual('ghi', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('jkl', self.mock.recv_buffer) + + def test_parse_lines_multiple_calls(self): + self.mock.recv_buffer = 'data' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('data', self.mock.recv_buffer) + + self.mock.recv_buffer += '\n' + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_send_lines_called_with_no_lines(self): + self.mock.connection = Mock(spec=network.Connection) + + network.LineProtocol.send_lines(self.mock, []) + self.assertEqual(0, self.mock.encode.call_count) + self.assertEqual(0, self.mock.connection.send.call_count) + + def test_send_lines_calls_join_lines(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = 'lines' + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.join_lines.assert_called_once_with(sentinel.lines) + + def test_send_line_encodes_joined_lines_with_final_terminator(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = u'lines\n' + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.encode.assert_called_once_with(u'lines\n') + + def test_send_lines_sends_encoded_string(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = 'lines' + self.mock.encode.return_value = sentinel.data + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.connection.send.assert_called_once_with(sentinel.data) + + def test_join_lines_returns_empty_string_for_no_lines(self): + self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, [])) + + def test_join_lines_returns_joined_lines(self): + self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines( + self.mock, [u'1', u'2'])) + + def test_decode_calls_decode_on_string(self): + string = Mock() + + network.LineProtocol.decode(self.mock, string) + string.decode.assert_called_once_with(self.mock.encoding) + + def test_decode_plain_ascii(self): + self.assertEqual(u'abc', network.LineProtocol.decode(self.mock, 'abc')) + + def test_decode_utf8(self): + self.assertEqual(u'æøå', network.LineProtocol.decode( + self.mock, u'æøå'.encode('utf-8'))) + + @SkipTest + def test_decode_invalid_data(self): + string = Mock() + string.decode.side_effect = UnicodeError + + network.LineProtocol.decode(self.mock, string) + + def test_encode_calls_encode_on_string(self): + string = Mock() + + network.LineProtocol.encode(self.mock, string) + string.encode.assert_called_once_with(self.mock.encoding) + + def test_encode_plain_ascii(self): + self.assertEqual('abc', network.LineProtocol.encode(self.mock, u'abc')) + + def test_encode_utf8(self): + self.assertEqual(u'æøå'.encode('utf-8'), + network.LineProtocol.encode(self.mock, u'æøå')) + + @SkipTest + def test_encode_invalid_data(self): + string = Mock() + string.encode.side_effect = UnicodeError + + network.LineProtocol.encode(self.mock, string) From cf48faad986ea57c0bd3542f7a22a11ce5333838 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 13 Jul 2011 23:57:00 +0200 Subject: [PATCH 135/350] Remove log_* method from LineProtocol --- mopidy/frontends/mpd/__init__.py | 15 ++++++-- mopidy/utils/network.py | 59 +++++--------------------------- tests/utils/network_test.py | 8 +++++ 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index e0ad28fc..37877386 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -5,7 +5,7 @@ from pykka.actor import ThreadingActor from mopidy import settings from mopidy.frontends.mpd import dispatcher, protocol -from mopidy.utils import network, process +from mopidy.utils import network, process, log logger = logging.getLogger('mopidy.frontends.mpd') @@ -54,10 +54,21 @@ class MpdSession(network.LineProtocol): self.dispatcher = dispatcher.MpdDispatcher(self) def on_start(self): + logger.info(u'New connection from [%s]:%s', self.host, self.port) self.send_lines([u'OK MPD %s' % protocol.VERSION]) def on_line_received(self, line): - self.send_lines(self.dispatcher.handle_request(line)) + logger.debug(u'Request from [%s]:%s to %s: %s', self.host, self.port, + self.actor_urn, line) + + response = self.dispatcher.handle_request(line) + if not response: + return + + logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, + self.actor_urn, log.indent(self.terminator.join(response))) + + self.send_lines(response) def close(self): self.stop() diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 7233e65b..936aab41 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -60,9 +60,6 @@ class Server(object): self.register_server_socket(self.server_socket.fileno()) - logger.debug(u'Listening on [%s]:%s using %s as protocol handler', - host, port, self.protocol) - def create_server_socket(self, host, port): sock = create_socket() sock.setblocking(False) @@ -277,6 +274,14 @@ class LineProtocol(ThreadingActor): self.connection = connection self.recv_buffer = '' + @property + def host(self): + return self.connection.host + + @property + def port(self): + return self.connection.port + def on_line_received(self, line): """ Called whenever a new line is found. @@ -291,12 +296,10 @@ class LineProtocol(ThreadingActor): return self.connection.disable_timeout() - self.log_raw_data(message['received']) self.recv_buffer += message['received'] for line in self.parse_lines(): line = self.decode(line) - self.log_request(line) self.on_line_received(line) self.connection.enable_timeout() @@ -312,51 +315,6 @@ class LineProtocol(ThreadingActor): self.recv_buffer, 1) yield line - def log_raw_data(self, data): - """ - Log raw data from event loop for debug purposes. - - Can be overridden by subclasses to change logging behaviour. - """ - logger.debug(u'Got %s from event loop in %s', repr(data), - self.actor_urn) - - def log_request(self, request): - """ - Log request for debug purposes. - - Can be overridden by subclasses to change logging behaviour. - """ - logger.debug(u'Request from %s to %s: %s', self.connection, - self.actor_urn, indent(request)) - - def log_response(self, response): - """ - Log response for debug purposes. - - Can be overridden by subclasses to change logging behaviour. - """ - logger.debug(u'Response to %s from %s: %s', self.connection, - self.actor_urn, indent(response)) - - def log_error(self, error): - """ - Log error for debug purposes. - - Can be overridden by subclasses to change logging behaviour. - """ - logger.warning(u'Problem with connection to %s in %s: %s', - self.connection, self.actor_urn, error) - - def log_timeout(self): - """ - Log timeout for debug purposes. - - Can be overridden by subclasses to change logging behaviour. - """ - logger.debug(u'Closing connection to %s in %s due to timeout.', - self.connection, self.actor_urn) - def encode(self, line): """ Handle encoding of line. @@ -389,5 +347,4 @@ class LineProtocol(ThreadingActor): return data = self.join_lines(lines) - self.log_response(data) self.connection.send(self.encode(data)) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 5553af1a..369d3a44 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -837,3 +837,11 @@ class LineProtocolTest(unittest.TestCase): string.encode.side_effect = UnicodeError network.LineProtocol.encode(self.mock, string) + + @SkipTest + def test_host_property(self): + pass + + @SkipTest(self): + def test_port_property + pass From fe6e4a65f5764c6cf2b55897f56d10677c8f726f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 14 Jul 2011 11:09:04 +0200 Subject: [PATCH 136/350] Fix syntax error --- mopidy/utils/network.py | 2 -- tests/utils/network_test.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 936aab41..33d8efc0 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -9,8 +9,6 @@ from pykka import ActorDeadError from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy.utils.log import indent - # FIXME setup logger with extra={...} logger = logging.getLogger('mopidy.utils.server') diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 369d3a44..f1b23418 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -842,6 +842,6 @@ class LineProtocolTest(unittest.TestCase): def test_host_property(self): pass - @SkipTest(self): - def test_port_property + @SkipTest + def test_port_property(self): pass From 96ebb4eed45eba08b8a571673f25d1a910094732 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 14 Jul 2011 14:02:36 +0200 Subject: [PATCH 137/350] Indicate connection type in log message --- 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 37877386..4deb7b89 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -54,7 +54,7 @@ class MpdSession(network.LineProtocol): self.dispatcher = dispatcher.MpdDispatcher(self) def on_start(self): - logger.info(u'New connection from [%s]:%s', self.host, self.port) + logger.info(u'New MPD connection from [%s]:%s', self.host, self.port) self.send_lines([u'OK MPD %s' % protocol.VERSION]) def on_line_received(self, line): From e23476cc6fdc54e65a5fa382caadc238866b305d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 14 Jul 2011 23:05:08 +0200 Subject: [PATCH 138/350] Cleanup some tests --- tests/utils/network_test.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index f1b23418..f3619314 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -80,7 +80,16 @@ class ServerTest(unittest.TestCase): network.Server.__init__(self.mock, sentinel.host, sentinel.port, sentinel.protocol) - self.mock.register_server_socket.assert_called_once_with(sentinel.fileno) + self. mock.register_server_socket.assert_called_once_with(sentinel.fileno) + + @SkipTest + def test_init_fails_on_fileno_call(self): + sock = Mock(spec=socket.SocketType) + sock.fileno.side_effect = socket.error + self.mock.create_server_socket.return_value = sock + + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) def test_init_stores_values_in_attributes(self): sock = Mock(spec=socket.SocketType) @@ -105,10 +114,11 @@ class ServerTest(unittest.TestCase): sock.listen.assert_called_once_with(any_int) @SkipTest + @patch.object(network, 'create_socket') def test_create_server_socket_fails(self): - # FIXME define what should happen in this case, let the error propegate - # or do something else? - pass + create_socket.side_effect = socket.error + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): @@ -157,12 +167,10 @@ class ServerTest(unittest.TestCase): def test_accept_connection_unrecoverable_error(self): sock = Mock(spec=socket.SocketType) self.mock.server_socket = sock - sock.accept.side_effect = socket.error() self.assertRaises(socket.error, network.Server.accept_connection, self.mock) - @patch.object(network.Server, 'number_of_connections', new=Mock()) def test_maximum_connections_exceeded(self): self.mock.max_connections = 10 @@ -755,8 +763,8 @@ class LineProtocolTest(unittest.TestCase): lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) self.assertEqual('data', self.mock.recv_buffer) - self.mock.recv_buffer += '\n' + lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('data', lines.next()) self.assertRaises(StopIteration, lines.next) From 805a6fefd0015cfc6f35ca0d889e06bf82772a30 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 14 Jul 2011 23:14:51 +0200 Subject: [PATCH 139/350] Lint fixing --- mopidy/utils/network.py | 1 - tests/utils/network_test.py | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 33d8efc0..e1f2d8c4 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -9,7 +9,6 @@ from pykka import ActorDeadError from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -# FIXME setup logger with extra={...} logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index f3619314..9b7abff3 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -80,7 +80,8 @@ class ServerTest(unittest.TestCase): network.Server.__init__(self.mock, sentinel.host, sentinel.port, sentinel.protocol) - self. mock.register_server_socket.assert_called_once_with(sentinel.fileno) + self.mock.register_server_socket.assert_called_once_with( + sentinel.fileno) @SkipTest def test_init_fails_on_fileno_call(self): @@ -116,7 +117,7 @@ class ServerTest(unittest.TestCase): @SkipTest @patch.object(network, 'create_socket') def test_create_server_socket_fails(self): - create_socket.side_effect = socket.error + network.create_socket.side_effect = socket.error network.Server.create_server_socket(self.mock, sentinel.host, sentinel.port) @@ -127,23 +128,29 @@ class ServerTest(unittest.TestCase): gobject.IO_IN, self.mock.handle_connection) def test_handle_connection(self): - self.mock.accept_connection.return_value = (sentinel.sock, sentinel.addr) + self.mock.accept_connection.return_value = ( + sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = False - network.Server.handle_connection(self.mock, sentinel.fileno, gobject.IO_IN) + network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() - self.mock.init_connection.assert_called_once_with(sentinel.sock, sentinel.addr) + self.mock.init_connection.assert_called_once_with( + sentinel.sock, sentinel.addr) self.assertEqual(0, self.mock.reject_connection.call_count) def test_handle_connection_exceeded_connections(self): - self.mock.accept_connection.return_value = (sentinel.sock, sentinel.addr) + self.mock.accept_connection.return_value = ( + sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = True - network.Server.handle_connection(self.mock, sentinel.fileno, gobject.IO_IN) + network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() - self.mock.reject_connection.assert_called_once_with(sentinel.sock, sentinel.addr) + self.mock.reject_connection.assert_called_once_with( + sentinel.sock, sentinel.addr) self.assertEqual(0, self.mock.init_connection.call_count) def test_accept_connection(self): From e6781135ba0cd7770a89f6612774855fb89f1ed1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 15 Jul 2011 00:48:47 +0200 Subject: [PATCH 140/350] Doubled checked most network.Server/Connection/LineProtocol tests --- mopidy/utils/network.py | 37 +++++--- tests/utils/network_test.py | 168 +++++++++++++++++++++++++++++------- 2 files changed, 162 insertions(+), 43 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index e1f2d8c4..c0dd5043 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -95,6 +95,7 @@ class Server(object): return len(ActorRegistry.get_by_class(self.protocol)) def reject_connection(self, sock, addr): + # FIXME provide more context in logging? logger.warning(u'Rejected connection from [%s]:%s', addr[0], addr[1]) try: sock.close() @@ -104,6 +105,7 @@ class Server(object): def init_connection(self, sock, addr): Connection(self.protocol, sock, addr, self.timeout) + class Connection(object): # NOTE: the callback code is _not_ run in the actor's thread, but in the # same one as the event loop. If code in the callbacks blocks, the rest of @@ -164,16 +166,19 @@ class Connection(object): def enable_timeout(self): """Reactivate timeout mechanism.""" + if self.timeout <= 0: + return + self.disable_timeout() - if self.timeout > 0: - self.timeout_id = gobject.timeout_add_seconds( - self.timeout, self.timeout_callback) + self.timeout_id = gobject.timeout_add_seconds( + self.timeout, self.timeout_callback) def disable_timeout(self): """Deactivate timeout mechanism.""" - if self.timeout_id is not None: - gobject.source_remove(self.timeout_id) - self.timeout_id = None + if self.timeout_id is None: + return + gobject.source_remove(self.timeout_id) + self.timeout_id = None def enable_recv(self): if self.recv_id is not None: @@ -181,15 +186,16 @@ class Connection(object): try: self.recv_id = gobject.io_add_watch(self.sock.fileno(), - gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, + gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.recv_callback) except socket.error as e: self.stop(u'Problem with connection: %s' % e) def disable_recv(self): - if self.recv_id is not None: - gobject.source_remove(self.recv_id) - self.recv_id = None + if self.recv_id is None: + return + gobject.source_remove(self.recv_id) + self.recv_id = None def enable_send(self): if self.send_id is not None: @@ -203,9 +209,11 @@ class Connection(object): self.stop(u'Problem with connection: %s' % e) def disable_send(self): - if self.send_id is not None: - gobject.source_remove(self.send_id) - self.send_id = None + if self.send_id is None: + return + + gobject.source_remove(self.send_id) + self.send_id = None def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): @@ -250,7 +258,8 @@ class Connection(object): return True def timeout_callback(self): - return self.stop(u'Client timeout out after %s seconds' % self.timeout) + self.stop(u'Client timeout out after %s seconds' % self.timeout) + return False class LineProtocol(ThreadingActor): diff --git a/tests/utils/network_test.py b/tests/utils/network_test.py index 9b7abff3..41dacda1 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network_test.py @@ -93,6 +93,7 @@ class ServerTest(unittest.TestCase): sentinel.port, sentinel.protocol) def test_init_stores_values_in_attributes(self): + # This need to be a mock and no a sentinel as fileno() is called on it sock = Mock(spec=socket.SocketType) self.mock.create_server_socket.return_value = sock @@ -114,13 +115,31 @@ class ServerTest(unittest.TestCase): sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) sock.listen.assert_called_once_with(any_int) - @SkipTest + @SkipTest # FIXME decide behaviour @patch.object(network, 'create_socket') def test_create_server_socket_fails(self): network.create_socket.side_effect = socket.error network.Server.create_server_socket(self.mock, sentinel.host, sentinel.port) + @SkipTest # FIXME decide behaviour + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_bind_fails(self): + sock = create_socket.return_value + sock.bind.side_effect = socket.error + + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + + @SkipTest # FIXME decide behaviour + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_listen_fails(self): + sock = create_socket.return_value + sock.listen.side_effect = socket.error + + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + @patch.object(gobject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): network.Server.register_server_socket(self.mock, sentinel.fileno) @@ -132,8 +151,8 @@ class ServerTest(unittest.TestCase): sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = False - network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN) + self.assertTrue(network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.init_connection.assert_called_once_with( @@ -145,8 +164,8 @@ class ServerTest(unittest.TestCase): sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = True - network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN) + self.assertTrue(network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.reject_connection.assert_called_once_with( @@ -171,10 +190,11 @@ class ServerTest(unittest.TestCase): self.assertRaises(network.ShouldRetrySocketCall, network.Server.accept_connection, self.mock) + # FIXME decide if this should be allowed to propegate def test_accept_connection_unrecoverable_error(self): sock = Mock(spec=socket.SocketType) self.mock.server_socket = sock - sock.accept.side_effect = socket.error() + sock.accept.side_effect = socket.error self.assertRaises(socket.error, network.Server.accept_connection, self.mock) @@ -218,7 +238,7 @@ class ServerTest(unittest.TestCase): def test_reject_connection_error(self): sock = Mock(spec=socket.SocketType) - sock.close.side_effect = socket.error() + sock.close.side_effect = socket.error network.Server.reject_connection(self.mock, sock, (sentinel.host, sentinel.port)) @@ -250,17 +270,29 @@ class ConnectionTest(unittest.TestCase): self.mock.enable_timeout.assert_called_once_with() def test_init_stores_values_in_attributes(self): + addr = (sentinel.host, sentinel.port) protocol = Mock(spec=network.LineProtocol) sock = Mock(spec=socket.SocketType) - network.Connection.__init__(self.mock, protocol, sock, - (sentinel.host, sentinel.port), sentinel.timeout) + network.Connection.__init__( + self.mock, protocol, sock, addr, sentinel.timeout) self.assertEqual(sock, self.mock.sock) self.assertEqual(protocol, self.mock.protocol) self.assertEqual(sentinel.timeout, self.mock.timeout) self.assertEqual(sentinel.host, self.mock.host) self.assertEqual(sentinel.port, self.mock.port) + def test_init_handles_ipv6_addr(self): + addr = (sentinel.host, sentinel.port, + sentinel.flowinfo, sentinel.scopeid) + protocol = Mock(spec=network.LineProtocol) + sock = Mock(spec=socket.SocketType) + + network.Connection.__init__( + self.mock, protocol, sock, addr, sentinel.timeout) + self.assertEqual(sentinel.host, self.mock.host) + self.assertEqual(sentinel.port, self.mock.port) + def test_stop_disables_recv_send_and_timeout(self): self.mock.stopping = False self.mock.actor_ref = Mock() @@ -269,7 +301,7 @@ class ConnectionTest(unittest.TestCase): network.Connection.stop(self.mock, sentinel.reason) self.mock.disable_timeout.assert_called_once_with() self.mock.disable_recv.assert_called_once_with() - self.mock.disable_timeout.assert_called_once_with() + self.mock.disable_send.assert_called_once_with() def test_stop_closes_socket(self): self.mock.stopping = False @@ -283,7 +315,7 @@ class ConnectionTest(unittest.TestCase): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) - self.mock.sock.close.side_effect = socket.error() + self.mock.sock.close.side_effect = socket.error network.Connection.stop(self.mock, sentinel.reason) self.mock.sock.close.assert_called_once_with() @@ -303,6 +335,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) + self.mock.actor_ref.stop.assert_called_once_with() def test_stop_sets_stopping_to_true(self): self.mock.stopping = False @@ -321,15 +354,6 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(0, self.mock.actor_ref.stop.call_count) self.assertEqual(0, self.mock.sock.close.call_count) - @patch.object(network.logger, 'log', new=Mock()) - def test_stop_logs_that_it_is_calling_itself(self): - self.mock.stopping = True - self.mock.actor_ref = Mock() - self.mock.sock = Mock(spec=socket.SocketType) - - network.Connection.stop(self.mock, sentinel.reason) - network.logger.log(any_int, any_unicode) - @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason(self): self.mock.stopping = False @@ -351,6 +375,15 @@ class ConnectionTest(unittest.TestCase): network.logger.log.assert_called_once_with( sentinel.level, sentinel.reason) + @patch.object(network.logger, 'log', new=Mock()) + def test_stop_logs_that_it_is_calling_itself(self): + self.mock.stopping = True + self.mock.actor_ref = Mock() + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.stop(self.mock, sentinel.reason) + network.logger.log(any_int, any_unicode) + @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None @@ -366,11 +399,19 @@ class ConnectionTest(unittest.TestCase): @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_already_registered(self): + self.mock.sock = Mock(spec=socket.SocketType) self.mock.recv_id = sentinel.tag network.Connection.enable_recv(self.mock) self.assertEqual(0, gobject.io_add_watch.call_count) + def test_enable_recv_does_not_change_tag(self): + self.mock.recv_id = sentinel.tag + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.enable_recv(self.mock) + self.assertEqual(sentinel.tag, self.mock.recv_id) + @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_recv_deregisters(self): self.mock.recv_id = sentinel.tag @@ -411,11 +452,19 @@ class ConnectionTest(unittest.TestCase): @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_send_already_registered(self): + self.mock.sock = Mock(spec=socket.SocketType) self.mock.send_id = sentinel.tag network.Connection.enable_send(self.mock) self.assertEqual(0, gobject.io_add_watch.call_count) + def test_enable_send_does_not_change_tag(self): + self.mock.send_id = sentinel.tag + self.mock.sock = Mock(spec=socket.SocketType) + + network.Connection.enable_send(self.mock) + self.assertEqual(sentinel.tag, self.mock.send_id) + @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_send_deregisters(self): self.mock.send_id = sentinel.tag @@ -471,6 +520,19 @@ class ConnectionTest(unittest.TestCase): network.Connection.enable_timeout(self.mock) self.assertEqual(0, gobject.timeout_add_seconds.call_count) + def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self): + self.mock.timeout = 0 + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, self.mock.disable_timeout.call_count) + + self.mock.timeout = -1 + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, self.mock.disable_timeout.call_count) + + self.mock.timeout = None + network.Connection.enable_timeout(self.mock) + self.assertEqual(0, self.mock.disable_timeout.call_count) + @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_timeout_deregisters(self): self.mock.timeout_id = sentinel.tag @@ -516,16 +578,25 @@ class ConnectionTest(unittest.TestCase): self.mock.enable_send.assert_called_once_with() def test_recv_callback_respects_io_err(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup_and_io_err(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + self.assertTrue(network.Connection.recv_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) @@ -553,6 +624,7 @@ class ConnectionTest(unittest.TestCase): def test_recv_callback_gets_no_data(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = '' + self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) @@ -565,19 +637,42 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.recv.side_effect = socket.error(error, '') self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) + self.assertEqual(0, self.mock.stop.call_count) def test_recv_callback_unrecoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) - self.mock.sock.recv.side_effect = socket.error() + self.mock.sock.recv.side_effect = socket.error self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) - @SkipTest - def test_send_callback_respects_flags(self): - # stop self - pass + @SkipTest # FIXME decide behaviour + def test_send_callback_respects_io_err(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + + self.assertTrue(network.Connection.send_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) + self.mock.stop.assert_called_once_with(any_unicode) + + @SkipTest # FIXME decide behaviour + def test_send_callback_respects_io_hup(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + + self.assertTrue(network.Connection.send_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) + self.mock.stop.assert_called_once_with(any_unicode) + + @SkipTest # FIXME decide behaviour + def test_send_callback_respects_io_hup_and_io_err(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.actor_ref = Mock() + + self.assertTrue(network.Connection.send_callback(self.mock, + sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) + self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_acquires_and_releases_lock(self): self.mock.send_lock = Mock() @@ -594,10 +689,14 @@ class ConnectionTest(unittest.TestCase): def test_send_callback_fails_to_acquire_lock(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = False + self.mock.send_buffer = '' + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) + self.assertEqual(0, self.mock.sock.send.call_count) def test_send_callback_sends_all_data(self): self.mock.send_lock = Mock() @@ -634,6 +733,7 @@ class ConnectionTest(unittest.TestCase): self.mock.sock.send.side_effect = socket.error(error, '') self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) + self.assertEqual(0, self.mock.stop.call_count) def test_send_callback_unrecoverable_error(self): self.mock.send_lock = Mock() @@ -641,7 +741,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_buffer = 'data' self.mock.sock = Mock(spec=socket.SocketType) - self.mock.sock.send.side_effect = socket.error() + self.mock.sock.send.side_effect = socket.error self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) @@ -649,7 +749,7 @@ class ConnectionTest(unittest.TestCase): def test_timeout_callback(self): self.mock.timeout = 10 - network.Connection.timeout_callback(self.mock) + self.assertFalse(network.Connection.timeout_callback(self.mock)) self.mock.stop.assert_called_once_with(any_unicode) @@ -710,6 +810,16 @@ class LineProtocolTest(unittest.TestCase): network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) self.mock.on_line_received.assert_called_once_with(sentinel.decoded) + def test_on_receive_with_new_lines_calls_on_recieve(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = ['line1', 'line2'] + self.mock.decode.return_value = sentinel.decoded + + network.LineProtocol.on_receive(self.mock, + {'received': 'line1\nline2\n'}) + self.assertEqual(2, self.mock.on_line_received.call_count) + def test_parse_lines_emtpy_buffer(self): self.mock.recv_buffer = '' @@ -826,7 +936,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual(u'æøå', network.LineProtocol.decode( self.mock, u'æøå'.encode('utf-8'))) - @SkipTest + @SkipTest # FIXME decide behaviour def test_decode_invalid_data(self): string = Mock() string.decode.side_effect = UnicodeError @@ -846,7 +956,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual(u'æøå'.encode('utf-8'), network.LineProtocol.encode(self.mock, u'æøå')) - @SkipTest + @SkipTest # FIXME decide behaviour def test_encode_invalid_data(self): string = Mock() string.encode.side_effect = UnicodeError From 1b46dade83ee2e8ba55371e9071200d036696c55 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 15 Jul 2011 00:54:14 +0200 Subject: [PATCH 141/350] Note why source_remove return values are ignored --- mopidy/utils/network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index c0dd5043..b08a12d6 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -110,6 +110,10 @@ class Connection(object): # NOTE: the callback code is _not_ run in the actor's thread, but in the # same one as the event loop. If code in the callbacks blocks, the rest of # gobject code will likely be blocked as well... + # + # Also note that source_remove() return values are ignored on purpose, a + # false return value would only tell us that what we thought was registered + # is already gone, there is really nothing more we can do. def __init__(self, protocol, sock, addr, timeout): sock.setblocking(False) From a1c382666f70a035691a6b6a6355a03ec486071f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 15 Jul 2011 01:08:29 +0200 Subject: [PATCH 142/350] Split up tests into multiple files --- tests/utils/network/__init__.py | 0 .../connection_test.py} | 454 ------------------ tests/utils/network/lineprotocol_test.py | 226 +++++++++ tests/utils/network/server_test.py | 190 ++++++++ tests/utils/network/utils_test.py | 57 +++ 5 files changed, 473 insertions(+), 454 deletions(-) create mode 100644 tests/utils/network/__init__.py rename tests/utils/{network_test.py => network/connection_test.py} (53%) create mode 100644 tests/utils/network/lineprotocol_test.py create mode 100644 tests/utils/network/server_test.py create mode 100644 tests/utils/network/utils_test.py diff --git a/tests/utils/network/__init__.py b/tests/utils/network/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/network_test.py b/tests/utils/network/connection_test.py similarity index 53% rename from tests/utils/network_test.py rename to tests/utils/network/connection_test.py index 41dacda1..090d7e3c 100644 --- a/tests/utils/network_test.py +++ b/tests/utils/network/connection_test.py @@ -1,5 +1,3 @@ -#encoding: utf-8 - import errno import gobject import logging @@ -12,239 +10,6 @@ from mopidy.utils import network from mock import patch, sentinel, Mock from tests import SkipTest, any_int, any_unicode -class FormatHostnameTest(unittest.TestCase): - @patch('mopidy.utils.network.has_ipv6', True) - def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): - network.has_ipv6 = True - self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') - self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') - - @patch('mopidy.utils.network.has_ipv6', False) - def test_format_hostname_does_nothing_when_only_ipv4_available(self): - network.has_ipv6 = False - self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0') - - -class TryIPv6SocketTest(unittest.TestCase): - @patch('socket.has_ipv6', False) - def test_system_that_claims_no_ipv6_support(self): - self.assertFalse(network.try_ipv6_socket()) - - @patch('socket.has_ipv6', True) - @patch('socket.socket') - def test_system_with_broken_ipv6(self, socket_mock): - socket_mock.side_effect = IOError() - self.assertFalse(network.try_ipv6_socket()) - - @patch('socket.has_ipv6', True) - @patch('socket.socket') - def test_with_working_ipv6(self, socket_mock): - socket_mock.return_value = Mock() - self.assertTrue(network.try_ipv6_socket()) - - -class CreateSocketTest(unittest.TestCase): - @patch('mopidy.utils.network.has_ipv6', False) - @patch('socket.socket') - def test_ipv4_socket(self, socket_mock): - network.create_socket() - self.assertEqual(socket_mock.call_args[0], - (socket.AF_INET, socket.SOCK_STREAM)) - - @patch('mopidy.utils.network.has_ipv6', True) - @patch('socket.socket') - def test_ipv6_socket(self, socket_mock): - network.create_socket() - self.assertEqual(socket_mock.call_args[0], - (socket.AF_INET6, socket.SOCK_STREAM)) - - @SkipTest - def test_ipv6_only_is_set(self): - pass - - -class ServerTest(unittest.TestCase): - def setUp(self): - self.mock = Mock(spec=network.Server) - - def test_init_calls_create_server_socket(self): - network.Server.__init__(self.mock, sentinel.host, - sentinel.port, sentinel.protocol) - self.mock.create_server_socket.assert_called_once_with( - sentinel.host, sentinel.port) - - def test_init_calls_register_server(self): - sock = Mock(spec=socket.SocketType) - sock.fileno.return_value = sentinel.fileno - self.mock.create_server_socket.return_value = sock - - network.Server.__init__(self.mock, sentinel.host, - sentinel.port, sentinel.protocol) - self.mock.register_server_socket.assert_called_once_with( - sentinel.fileno) - - @SkipTest - def test_init_fails_on_fileno_call(self): - sock = Mock(spec=socket.SocketType) - sock.fileno.side_effect = socket.error - self.mock.create_server_socket.return_value = sock - - network.Server.__init__(self.mock, sentinel.host, - sentinel.port, sentinel.protocol) - - def test_init_stores_values_in_attributes(self): - # This need to be a mock and no a sentinel as fileno() is called on it - sock = Mock(spec=socket.SocketType) - self.mock.create_server_socket.return_value = sock - - network.Server.__init__(self.mock, sentinel.host, sentinel.port, - sentinel.protocol, max_connections=sentinel.max_connections, - timeout=sentinel.timeout) - self.assertEqual(sentinel.protocol, self.mock.protocol) - self.assertEqual(sentinel.max_connections, self.mock.max_connections) - self.assertEqual(sentinel.timeout, self.mock.timeout) - self.assertEqual(sock, self.mock.server_socket) - - @patch.object(network, 'create_socket', spec=socket.SocketType) - def test_create_server_socket_sets_up_listener(self, create_socket): - sock = create_socket.return_value - - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) - sock.setblocking.assert_called_once_with(False) - sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) - sock.listen.assert_called_once_with(any_int) - - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket') - def test_create_server_socket_fails(self): - network.create_socket.side_effect = socket.error - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) - - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket', spec=socket.SocketType) - def test_create_server_bind_fails(self): - sock = create_socket.return_value - sock.bind.side_effect = socket.error - - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) - - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket', spec=socket.SocketType) - def test_create_server_listen_fails(self): - sock = create_socket.return_value - sock.listen.side_effect = socket.error - - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) - - @patch.object(gobject, 'io_add_watch', new=Mock()) - def test_register_server_socket_sets_up_io_watch(self): - network.Server.register_server_socket(self.mock, sentinel.fileno) - gobject.io_add_watch.assert_called_once_with(sentinel.fileno, - gobject.IO_IN, self.mock.handle_connection) - - def test_handle_connection(self): - self.mock.accept_connection.return_value = ( - sentinel.sock, sentinel.addr) - self.mock.maximum_connections_exceeded.return_value = False - - self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) - self.mock.accept_connection.assert_called_once_with() - self.mock.maximum_connections_exceeded.assert_called_once_with() - self.mock.init_connection.assert_called_once_with( - sentinel.sock, sentinel.addr) - self.assertEqual(0, self.mock.reject_connection.call_count) - - def test_handle_connection_exceeded_connections(self): - self.mock.accept_connection.return_value = ( - sentinel.sock, sentinel.addr) - self.mock.maximum_connections_exceeded.return_value = True - - self.assertTrue(network.Server.handle_connection( - self.mock, sentinel.fileno, gobject.IO_IN)) - self.mock.accept_connection.assert_called_once_with() - self.mock.maximum_connections_exceeded.assert_called_once_with() - self.mock.reject_connection.assert_called_once_with( - sentinel.sock, sentinel.addr) - self.assertEqual(0, self.mock.init_connection.call_count) - - def test_accept_connection(self): - sock = Mock(spec=socket.SocketType) - sock.accept.return_value = (sentinel.sock, sentinel.addr) - self.mock.server_socket = sock - - sock, addr = network.Server.accept_connection(self.mock) - self.assertEqual(sentinel.sock, sock) - self.assertEqual(sentinel.addr, addr) - - def test_accept_connection_recoverable_error(self): - sock = Mock(spec=socket.SocketType) - self.mock.server_socket = sock - - for error in (errno.EAGAIN, errno.EINTR): - sock.accept.side_effect = socket.error(error, '') - self.assertRaises(network.ShouldRetrySocketCall, - network.Server.accept_connection, self.mock) - - # FIXME decide if this should be allowed to propegate - def test_accept_connection_unrecoverable_error(self): - sock = Mock(spec=socket.SocketType) - self.mock.server_socket = sock - sock.accept.side_effect = socket.error - self.assertRaises(socket.error, - network.Server.accept_connection, self.mock) - - def test_maximum_connections_exceeded(self): - self.mock.max_connections = 10 - - self.mock.number_of_connections.return_value = 11 - self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) - - self.mock.number_of_connections.return_value = 10 - self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) - - self.mock.number_of_connections.return_value = 9 - self.assertFalse(network.Server.maximum_connections_exceeded(self.mock)) - - @patch('pykka.registry.ActorRegistry.get_by_class') - def test_number_of_connections(self, get_by_class): - self.mock.protocol = sentinel.protocol - - get_by_class.return_value = [1, 2, 3] - self.assertEqual(3, network.Server.number_of_connections(self.mock)) - - get_by_class.return_value = [] - self.assertEqual(0, network.Server.number_of_connections(self.mock)) - - @patch.object(network, 'Connection', new=Mock()) - def test_init_connection(self): - self.mock.protocol = sentinel.protocol - self.mock.timeout = sentinel.timeout - - network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr) - network.Connection.assert_called_once_with(sentinel.protocol, - sentinel.sock, sentinel.addr, sentinel.timeout) - - def test_reject_connection(self): - sock = Mock(spec=socket.SocketType) - - network.Server.reject_connection(self.mock, sock, - (sentinel.host, sentinel.port)) - sock.close.assert_called_once_with() - - def test_reject_connection_error(self): - sock = Mock(spec=socket.SocketType) - sock.close.side_effect = socket.error - - network.Server.reject_connection(self.mock, sock, - (sentinel.host, sentinel.port)) - sock.close.assert_called_once_with() - - class ConnectionTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.Connection) @@ -751,222 +516,3 @@ class ConnectionTest(unittest.TestCase): self.assertFalse(network.Connection.timeout_callback(self.mock)) self.mock.stop.assert_called_once_with(any_unicode) - - -class LineProtocolTest(unittest.TestCase): - def setUp(self): - self.mock = Mock(spec=network.LineProtocol) - self.mock.terminator = network.LineProtocol.terminator - self.mock.encoding = network.LineProtocol.encoding - - def test_init_stores_values_in_attributes(self): - network.LineProtocol.__init__(self.mock, sentinel.connection) - self.assertEqual(sentinel.connection, self.mock.connection) - self.assertEqual('', self.mock.recv_buffer) - - def test_on_receive_no_new_lines_adds_to_recv_buffer(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = [] - - network.LineProtocol.on_receive(self.mock, {'received': 'data'}) - self.assertEqual('data', self.mock.recv_buffer) - self.mock.parse_lines.assert_called_once_with() - self.assertEqual(0, self.mock.on_line_received.call_count) - - def test_on_receive_no_new_lines_toggles_timeout(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = [] - - network.LineProtocol.on_receive(self.mock, {'received': 'data'}) - self.mock.connection.disable_timeout.assert_called_once_with() - self.mock.connection.enable_timeout.assert_called_once_with() - - def test_on_receive_no_new_lines_calls_parse_lines(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = [] - - network.LineProtocol.on_receive(self.mock, {'received': 'data'}) - self.mock.parse_lines.assert_called_once_with() - self.assertEqual(0, self.mock.on_line_received.call_count) - - def test_on_receive_with_new_line_calls_decode(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = [sentinel.line] - - network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) - self.mock.parse_lines.assert_called_once_with() - self.mock.decode.assert_called_once_with(sentinel.line) - - def test_on_receive_with_new_line_calls_on_recieve(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = [sentinel.line] - self.mock.decode.return_value = sentinel.decoded - - network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) - self.mock.on_line_received.assert_called_once_with(sentinel.decoded) - - def test_on_receive_with_new_lines_calls_on_recieve(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.recv_buffer = '' - self.mock.parse_lines.return_value = ['line1', 'line2'] - self.mock.decode.return_value = sentinel.decoded - - network.LineProtocol.on_receive(self.mock, - {'received': 'line1\nline2\n'}) - self.assertEqual(2, self.mock.on_line_received.call_count) - - def test_parse_lines_emtpy_buffer(self): - self.mock.recv_buffer = '' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertRaises(StopIteration, lines.next) - - def test_parse_lines_no_terminator(self): - self.mock.recv_buffer = 'data' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertRaises(StopIteration, lines.next) - - def test_parse_lines_termintor(self): - self.mock.recv_buffer = 'data\n' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('data', lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('', self.mock.recv_buffer) - - def test_parse_lines_no_data_before_terminator(self): - self.mock.recv_buffer = '\n' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('', lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('', self.mock.recv_buffer) - - def test_parse_lines_extra_data_after_terminator(self): - self.mock.recv_buffer = 'data1\ndata2' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('data1', lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('data2', self.mock.recv_buffer) - - def test_parse_lines_unicode(self): - self.mock.recv_buffer = u'æøå\n'.encode('utf-8') - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual(u'æøå'.encode('utf-8'), lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('', self.mock.recv_buffer) - - def test_parse_lines_multiple_lines(self): - self.mock.recv_buffer = 'abc\ndef\nghi\njkl' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('abc', lines.next()) - self.assertEqual('def', lines.next()) - self.assertEqual('ghi', lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('jkl', self.mock.recv_buffer) - - def test_parse_lines_multiple_calls(self): - self.mock.recv_buffer = 'data' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('data', self.mock.recv_buffer) - self.mock.recv_buffer += '\n' - - lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('data', lines.next()) - self.assertRaises(StopIteration, lines.next) - self.assertEqual('', self.mock.recv_buffer) - - def test_send_lines_called_with_no_lines(self): - self.mock.connection = Mock(spec=network.Connection) - - network.LineProtocol.send_lines(self.mock, []) - self.assertEqual(0, self.mock.encode.call_count) - self.assertEqual(0, self.mock.connection.send.call_count) - - def test_send_lines_calls_join_lines(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.join_lines.return_value = 'lines' - - network.LineProtocol.send_lines(self.mock, sentinel.lines) - self.mock.join_lines.assert_called_once_with(sentinel.lines) - - def test_send_line_encodes_joined_lines_with_final_terminator(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.join_lines.return_value = u'lines\n' - - network.LineProtocol.send_lines(self.mock, sentinel.lines) - self.mock.encode.assert_called_once_with(u'lines\n') - - def test_send_lines_sends_encoded_string(self): - self.mock.connection = Mock(spec=network.Connection) - self.mock.join_lines.return_value = 'lines' - self.mock.encode.return_value = sentinel.data - - network.LineProtocol.send_lines(self.mock, sentinel.lines) - self.mock.connection.send.assert_called_once_with(sentinel.data) - - def test_join_lines_returns_empty_string_for_no_lines(self): - self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, [])) - - def test_join_lines_returns_joined_lines(self): - self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines( - self.mock, [u'1', u'2'])) - - def test_decode_calls_decode_on_string(self): - string = Mock() - - network.LineProtocol.decode(self.mock, string) - string.decode.assert_called_once_with(self.mock.encoding) - - def test_decode_plain_ascii(self): - self.assertEqual(u'abc', network.LineProtocol.decode(self.mock, 'abc')) - - def test_decode_utf8(self): - self.assertEqual(u'æøå', network.LineProtocol.decode( - self.mock, u'æøå'.encode('utf-8'))) - - @SkipTest # FIXME decide behaviour - def test_decode_invalid_data(self): - string = Mock() - string.decode.side_effect = UnicodeError - - network.LineProtocol.decode(self.mock, string) - - def test_encode_calls_encode_on_string(self): - string = Mock() - - network.LineProtocol.encode(self.mock, string) - string.encode.assert_called_once_with(self.mock.encoding) - - def test_encode_plain_ascii(self): - self.assertEqual('abc', network.LineProtocol.encode(self.mock, u'abc')) - - def test_encode_utf8(self): - self.assertEqual(u'æøå'.encode('utf-8'), - network.LineProtocol.encode(self.mock, u'æøå')) - - @SkipTest # FIXME decide behaviour - def test_encode_invalid_data(self): - string = Mock() - string.encode.side_effect = UnicodeError - - network.LineProtocol.encode(self.mock, string) - - @SkipTest - def test_host_property(self): - pass - - @SkipTest - def test_port_property(self): - pass diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py new file mode 100644 index 00000000..836c3109 --- /dev/null +++ b/tests/utils/network/lineprotocol_test.py @@ -0,0 +1,226 @@ +#encoding: utf-8 + +import unittest + +from mopidy.utils import network + +from mock import sentinel, Mock +from tests import SkipTest + +class LineProtocolTest(unittest.TestCase): + def setUp(self): + self.mock = Mock(spec=network.LineProtocol) + self.mock.terminator = network.LineProtocol.terminator + self.mock.encoding = network.LineProtocol.encoding + + def test_init_stores_values_in_attributes(self): + network.LineProtocol.__init__(self.mock, sentinel.connection) + self.assertEqual(sentinel.connection, self.mock.connection) + self.assertEqual('', self.mock.recv_buffer) + + def test_on_receive_no_new_lines_adds_to_recv_buffer(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.assertEqual('data', self.mock.recv_buffer) + self.mock.parse_lines.assert_called_once_with() + self.assertEqual(0, self.mock.on_line_received.call_count) + + def test_on_receive_no_new_lines_toggles_timeout(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.mock.connection.disable_timeout.assert_called_once_with() + self.mock.connection.enable_timeout.assert_called_once_with() + + def test_on_receive_no_new_lines_calls_parse_lines(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.mock.parse_lines.assert_called_once_with() + self.assertEqual(0, self.mock.on_line_received.call_count) + + def test_on_receive_with_new_line_calls_decode(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [sentinel.line] + + network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) + self.mock.parse_lines.assert_called_once_with() + self.mock.decode.assert_called_once_with(sentinel.line) + + def test_on_receive_with_new_line_calls_on_recieve(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [sentinel.line] + self.mock.decode.return_value = sentinel.decoded + + network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) + self.mock.on_line_received.assert_called_once_with(sentinel.decoded) + + def test_on_receive_with_new_lines_calls_on_recieve(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = ['line1', 'line2'] + self.mock.decode.return_value = sentinel.decoded + + network.LineProtocol.on_receive(self.mock, + {'received': 'line1\nline2\n'}) + self.assertEqual(2, self.mock.on_line_received.call_count) + + def test_parse_lines_emtpy_buffer(self): + self.mock.recv_buffer = '' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + + def test_parse_lines_no_terminator(self): + self.mock.recv_buffer = 'data' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + + def test_parse_lines_termintor(self): + self.mock.recv_buffer = 'data\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_no_data_before_terminator(self): + self.mock.recv_buffer = '\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_extra_data_after_terminator(self): + self.mock.recv_buffer = 'data1\ndata2' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data1', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('data2', self.mock.recv_buffer) + + def test_parse_lines_unicode(self): + self.mock.recv_buffer = u'æøå\n'.encode('utf-8') + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual(u'æøå'.encode('utf-8'), lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_parse_lines_multiple_lines(self): + self.mock.recv_buffer = 'abc\ndef\nghi\njkl' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('abc', lines.next()) + self.assertEqual('def', lines.next()) + self.assertEqual('ghi', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('jkl', self.mock.recv_buffer) + + def test_parse_lines_multiple_calls(self): + self.mock.recv_buffer = 'data' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('data', self.mock.recv_buffer) + self.mock.recv_buffer += '\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + + def test_send_lines_called_with_no_lines(self): + self.mock.connection = Mock(spec=network.Connection) + + network.LineProtocol.send_lines(self.mock, []) + self.assertEqual(0, self.mock.encode.call_count) + self.assertEqual(0, self.mock.connection.send.call_count) + + def test_send_lines_calls_join_lines(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = 'lines' + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.join_lines.assert_called_once_with(sentinel.lines) + + def test_send_line_encodes_joined_lines_with_final_terminator(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = u'lines\n' + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.encode.assert_called_once_with(u'lines\n') + + def test_send_lines_sends_encoded_string(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.join_lines.return_value = 'lines' + self.mock.encode.return_value = sentinel.data + + network.LineProtocol.send_lines(self.mock, sentinel.lines) + self.mock.connection.send.assert_called_once_with(sentinel.data) + + def test_join_lines_returns_empty_string_for_no_lines(self): + self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, [])) + + def test_join_lines_returns_joined_lines(self): + self.assertEqual(u'1\n2\n', network.LineProtocol.join_lines( + self.mock, [u'1', u'2'])) + + def test_decode_calls_decode_on_string(self): + string = Mock() + + network.LineProtocol.decode(self.mock, string) + string.decode.assert_called_once_with(self.mock.encoding) + + def test_decode_plain_ascii(self): + self.assertEqual(u'abc', network.LineProtocol.decode(self.mock, 'abc')) + + def test_decode_utf8(self): + self.assertEqual(u'æøå', network.LineProtocol.decode( + self.mock, u'æøå'.encode('utf-8'))) + + @SkipTest # FIXME decide behaviour + def test_decode_invalid_data(self): + string = Mock() + string.decode.side_effect = UnicodeError + + network.LineProtocol.decode(self.mock, string) + + def test_encode_calls_encode_on_string(self): + string = Mock() + + network.LineProtocol.encode(self.mock, string) + string.encode.assert_called_once_with(self.mock.encoding) + + def test_encode_plain_ascii(self): + self.assertEqual('abc', network.LineProtocol.encode(self.mock, u'abc')) + + def test_encode_utf8(self): + self.assertEqual(u'æøå'.encode('utf-8'), + network.LineProtocol.encode(self.mock, u'æøå')) + + @SkipTest # FIXME decide behaviour + def test_encode_invalid_data(self): + string = Mock() + string.encode.side_effect = UnicodeError + + network.LineProtocol.encode(self.mock, string) + + @SkipTest + def test_host_property(self): + pass + + @SkipTest + def test_port_property(self): + pass diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py new file mode 100644 index 00000000..c844a487 --- /dev/null +++ b/tests/utils/network/server_test.py @@ -0,0 +1,190 @@ +import errno +import gobject +import socket +import unittest + +from mopidy.utils import network + +from mock import patch, sentinel, Mock +from tests import SkipTest, any_int + +class ServerTest(unittest.TestCase): + def setUp(self): + self.mock = Mock(spec=network.Server) + + def test_init_calls_create_server_socket(self): + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) + self.mock.create_server_socket.assert_called_once_with( + sentinel.host, sentinel.port) + + def test_init_calls_register_server(self): + sock = Mock(spec=socket.SocketType) + sock.fileno.return_value = sentinel.fileno + self.mock.create_server_socket.return_value = sock + + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) + self.mock.register_server_socket.assert_called_once_with( + sentinel.fileno) + + @SkipTest + def test_init_fails_on_fileno_call(self): + sock = Mock(spec=socket.SocketType) + sock.fileno.side_effect = socket.error + self.mock.create_server_socket.return_value = sock + + network.Server.__init__(self.mock, sentinel.host, + sentinel.port, sentinel.protocol) + + def test_init_stores_values_in_attributes(self): + # This need to be a mock and no a sentinel as fileno() is called on it + sock = Mock(spec=socket.SocketType) + self.mock.create_server_socket.return_value = sock + + network.Server.__init__(self.mock, sentinel.host, sentinel.port, + sentinel.protocol, max_connections=sentinel.max_connections, + timeout=sentinel.timeout) + self.assertEqual(sentinel.protocol, self.mock.protocol) + self.assertEqual(sentinel.max_connections, self.mock.max_connections) + self.assertEqual(sentinel.timeout, self.mock.timeout) + self.assertEqual(sock, self.mock.server_socket) + + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_socket_sets_up_listener(self, create_socket): + sock = create_socket.return_value + + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + sock.setblocking.assert_called_once_with(False) + sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) + sock.listen.assert_called_once_with(any_int) + + @SkipTest # FIXME decide behaviour + @patch.object(network, 'create_socket') + def test_create_server_socket_fails(self): + network.create_socket.side_effect = socket.error + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + + @SkipTest # FIXME decide behaviour + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_bind_fails(self): + sock = create_socket.return_value + sock.bind.side_effect = socket.error + + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + + @SkipTest # FIXME decide behaviour + @patch.object(network, 'create_socket', spec=socket.SocketType) + def test_create_server_listen_fails(self): + sock = create_socket.return_value + sock.listen.side_effect = socket.error + + network.Server.create_server_socket(self.mock, + sentinel.host, sentinel.port) + + @patch.object(gobject, 'io_add_watch', new=Mock()) + def test_register_server_socket_sets_up_io_watch(self): + network.Server.register_server_socket(self.mock, sentinel.fileno) + gobject.io_add_watch.assert_called_once_with(sentinel.fileno, + gobject.IO_IN, self.mock.handle_connection) + + def test_handle_connection(self): + self.mock.accept_connection.return_value = ( + sentinel.sock, sentinel.addr) + self.mock.maximum_connections_exceeded.return_value = False + + self.assertTrue(network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock.accept_connection.assert_called_once_with() + self.mock.maximum_connections_exceeded.assert_called_once_with() + self.mock.init_connection.assert_called_once_with( + sentinel.sock, sentinel.addr) + self.assertEqual(0, self.mock.reject_connection.call_count) + + def test_handle_connection_exceeded_connections(self): + self.mock.accept_connection.return_value = ( + sentinel.sock, sentinel.addr) + self.mock.maximum_connections_exceeded.return_value = True + + self.assertTrue(network.Server.handle_connection( + self.mock, sentinel.fileno, gobject.IO_IN)) + self.mock.accept_connection.assert_called_once_with() + self.mock.maximum_connections_exceeded.assert_called_once_with() + self.mock.reject_connection.assert_called_once_with( + sentinel.sock, sentinel.addr) + self.assertEqual(0, self.mock.init_connection.call_count) + + def test_accept_connection(self): + sock = Mock(spec=socket.SocketType) + sock.accept.return_value = (sentinel.sock, sentinel.addr) + self.mock.server_socket = sock + + sock, addr = network.Server.accept_connection(self.mock) + self.assertEqual(sentinel.sock, sock) + self.assertEqual(sentinel.addr, addr) + + def test_accept_connection_recoverable_error(self): + sock = Mock(spec=socket.SocketType) + self.mock.server_socket = sock + + for error in (errno.EAGAIN, errno.EINTR): + sock.accept.side_effect = socket.error(error, '') + self.assertRaises(network.ShouldRetrySocketCall, + network.Server.accept_connection, self.mock) + + # FIXME decide if this should be allowed to propegate + def test_accept_connection_unrecoverable_error(self): + sock = Mock(spec=socket.SocketType) + self.mock.server_socket = sock + sock.accept.side_effect = socket.error + self.assertRaises(socket.error, + network.Server.accept_connection, self.mock) + + def test_maximum_connections_exceeded(self): + self.mock.max_connections = 10 + + self.mock.number_of_connections.return_value = 11 + self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) + + self.mock.number_of_connections.return_value = 10 + self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) + + self.mock.number_of_connections.return_value = 9 + self.assertFalse(network.Server.maximum_connections_exceeded(self.mock)) + + @patch('pykka.registry.ActorRegistry.get_by_class') + def test_number_of_connections(self, get_by_class): + self.mock.protocol = sentinel.protocol + + get_by_class.return_value = [1, 2, 3] + self.assertEqual(3, network.Server.number_of_connections(self.mock)) + + get_by_class.return_value = [] + self.assertEqual(0, network.Server.number_of_connections(self.mock)) + + @patch.object(network, 'Connection', new=Mock()) + def test_init_connection(self): + self.mock.protocol = sentinel.protocol + self.mock.timeout = sentinel.timeout + + network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr) + network.Connection.assert_called_once_with(sentinel.protocol, + sentinel.sock, sentinel.addr, sentinel.timeout) + + def test_reject_connection(self): + sock = Mock(spec=socket.SocketType) + + network.Server.reject_connection(self.mock, sock, + (sentinel.host, sentinel.port)) + sock.close.assert_called_once_with() + + def test_reject_connection_error(self): + sock = Mock(spec=socket.SocketType) + sock.close.side_effect = socket.error + + network.Server.reject_connection(self.mock, sock, + (sentinel.host, sentinel.port)) + sock.close.assert_called_once_with() diff --git a/tests/utils/network/utils_test.py b/tests/utils/network/utils_test.py new file mode 100644 index 00000000..ada1de01 --- /dev/null +++ b/tests/utils/network/utils_test.py @@ -0,0 +1,57 @@ +import socket +import unittest + +from mopidy.utils import network + +from mock import patch, Mock +from tests import SkipTest + +class FormatHostnameTest(unittest.TestCase): + @patch('mopidy.utils.network.has_ipv6', True) + def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): + network.has_ipv6 = True + self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') + self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') + + @patch('mopidy.utils.network.has_ipv6', False) + def test_format_hostname_does_nothing_when_only_ipv4_available(self): + network.has_ipv6 = False + self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0') + + +class TryIPv6SocketTest(unittest.TestCase): + @patch('socket.has_ipv6', False) + def test_system_that_claims_no_ipv6_support(self): + self.assertFalse(network.try_ipv6_socket()) + + @patch('socket.has_ipv6', True) + @patch('socket.socket') + def test_system_with_broken_ipv6(self, socket_mock): + socket_mock.side_effect = IOError() + self.assertFalse(network.try_ipv6_socket()) + + @patch('socket.has_ipv6', True) + @patch('socket.socket') + def test_with_working_ipv6(self, socket_mock): + socket_mock.return_value = Mock() + self.assertTrue(network.try_ipv6_socket()) + + +class CreateSocketTest(unittest.TestCase): + @patch('mopidy.utils.network.has_ipv6', False) + @patch('socket.socket') + def test_ipv4_socket(self, socket_mock): + network.create_socket() + self.assertEqual(socket_mock.call_args[0], + (socket.AF_INET, socket.SOCK_STREAM)) + + @patch('mopidy.utils.network.has_ipv6', True) + @patch('socket.socket') + def test_ipv6_socket(self, socket_mock): + network.create_socket() + self.assertEqual(socket_mock.call_args[0], + (socket.AF_INET6, socket.SOCK_STREAM)) + + @SkipTest + def test_ipv6_only_is_set(self): + pass From 91b450bd6ba03ba2a9bc5af51734e097697dd92d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 16 Jul 2011 22:56:43 +0200 Subject: [PATCH 143/350] Add tests for line protocol host and port properties --- tests/utils/network/lineprotocol_test.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index 836c3109..d339890c 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -217,10 +217,16 @@ class LineProtocolTest(unittest.TestCase): network.LineProtocol.encode(self.mock, string) - @SkipTest def test_host_property(self): - pass + mock = Mock(spec=network.Connection) + mock.host = sentinel.host + + lineprotocol = network.LineProtocol(mock) + self.assertEqual(sentinel.host, lineprotocol.host) - @SkipTest def test_port_property(self): - pass + mock = Mock(spec=network.Connection) + mock.port = sentinel.port + + lineprotocol = network.LineProtocol(mock) + self.assertEqual(sentinel.port, lineprotocol.port) From ffd4ae5045fd783472fedfab41bffdefb1ce416d Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 00:06:10 +0200 Subject: [PATCH 144/350] Some more test cleanup and improvement --- tests/utils/network/lineprotocol_test.py | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index d339890c..360b5c68 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -28,7 +28,7 @@ class LineProtocolTest(unittest.TestCase): self.mock.parse_lines.assert_called_once_with() self.assertEqual(0, self.mock.on_line_received.call_count) - def test_on_receive_no_new_lines_toggles_timeout(self): + def test_on_receive_toggles_timeout(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [] @@ -129,17 +129,18 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual('jkl', self.mock.recv_buffer) def test_parse_lines_multiple_calls(self): - self.mock.recv_buffer = 'data' + self.mock.recv_buffer = 'data1' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) - self.assertEqual('data', self.mock.recv_buffer) - self.mock.recv_buffer += '\n' + self.assertEqual('data1', self.mock.recv_buffer) + + self.mock.recv_buffer += '\ndata2' lines = network.LineProtocol.parse_lines(self.mock) - self.assertEqual('data', lines.next()) + self.assertEqual('data1', lines.next()) self.assertRaises(StopIteration, lines.next) - self.assertEqual('', self.mock.recv_buffer) + self.assertEqual('data2', self.mock.recv_buffer) def test_send_lines_called_with_no_lines(self): self.mock.connection = Mock(spec=network.Connection) @@ -184,11 +185,15 @@ class LineProtocolTest(unittest.TestCase): string.decode.assert_called_once_with(self.mock.encoding) def test_decode_plain_ascii(self): - self.assertEqual(u'abc', network.LineProtocol.decode(self.mock, 'abc')) + result = network.LineProtocol.decode(self.mock, 'abc') + self.assertEqual(u'abc', result) + self.assertEqual(unicode, type(result)) def test_decode_utf8(self): - self.assertEqual(u'æøå', network.LineProtocol.decode( - self.mock, u'æøå'.encode('utf-8'))) + result = network.LineProtocol.decode( + self.mock, u'æøå'.encode('utf-8')) + self.assertEqual(u'æøå', result) + self.assertEqual(unicode, type(result)) @SkipTest # FIXME decide behaviour def test_decode_invalid_data(self): @@ -204,11 +209,14 @@ class LineProtocolTest(unittest.TestCase): string.encode.assert_called_once_with(self.mock.encoding) def test_encode_plain_ascii(self): - self.assertEqual('abc', network.LineProtocol.encode(self.mock, u'abc')) + result = network.LineProtocol.encode(self.mock, u'abc') + self.assertEqual('abc', result) + self.assertEqual(str, type(result)) def test_encode_utf8(self): - self.assertEqual(u'æøå'.encode('utf-8'), - network.LineProtocol.encode(self.mock, u'æøå')) + result = network.LineProtocol.encode(self.mock, u'æøå') + self.assertEqual(u'æøå'.encode('utf-8'), result) + self.assertEqual(str, type(result)) @SkipTest # FIXME decide behaviour def test_encode_invalid_data(self): From c773998fd8f9187423216ca93acbd232f167585a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 00:26:33 +0200 Subject: [PATCH 145/350] Stop actor if decode or encode fails --- mopidy/utils/network.py | 10 ++++++++-- tests/utils/network/lineprotocol_test.py | 5 ++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index b08a12d6..9de688cc 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -331,7 +331,10 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change encoding behaviour. """ - return line.encode(self.encoding) + try: + return line.encode(self.encoding) + except UnicodeError: # FIXME log this? + self.stop() def decode(self, line): """ @@ -339,7 +342,10 @@ class LineProtocol(ThreadingActor): Can be overridden by subclasses to change decoding behaviour. """ - return line.decode(self.encoding) + try: + return line.decode(self.encoding) + except UnicodeError: # FIXME log this? + self.stop() def join_lines(self, lines): if not lines: diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index 360b5c68..a87f461c 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -5,7 +5,6 @@ import unittest from mopidy.utils import network from mock import sentinel, Mock -from tests import SkipTest class LineProtocolTest(unittest.TestCase): def setUp(self): @@ -195,12 +194,12 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual(u'æøå', result) self.assertEqual(unicode, type(result)) - @SkipTest # FIXME decide behaviour def test_decode_invalid_data(self): string = Mock() string.decode.side_effect = UnicodeError network.LineProtocol.decode(self.mock, string) + self.mock.stop.assert_called_once_with() def test_encode_calls_encode_on_string(self): string = Mock() @@ -218,12 +217,12 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual(u'æøå'.encode('utf-8'), result) self.assertEqual(str, type(result)) - @SkipTest # FIXME decide behaviour def test_encode_invalid_data(self): string = Mock() string.encode.side_effect = UnicodeError network.LineProtocol.encode(self.mock, string) + self.mock.stop.assert_called_once_with() def test_host_property(self): mock = Mock(spec=network.Connection) From d2a9e3d1ecda18c454d1253638c62154487b567b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 00:32:50 +0200 Subject: [PATCH 146/350] Make send_callback respect flags from gobject --- mopidy/utils/network.py | 4 ++++ tests/utils/network/connection_test.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 9de688cc..852b6ca6 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -243,6 +243,10 @@ class Connection(object): return True def send_callback(self, fd, flags): + if flags & (gobject.IO_ERR | gobject.IO_HUP): + self.stop(u'Bad client flags: %s' % flags) + return True + # If with can't get the lock, simply try again next time socket is # ready for sending. if not self.send_lock.acquire(False): diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 090d7e3c..6e68f250 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -8,7 +8,7 @@ import unittest from mopidy.utils import network from mock import patch, sentinel, Mock -from tests import SkipTest, any_int, any_unicode +from tests import any_int, any_unicode class ConnectionTest(unittest.TestCase): def setUp(self): @@ -412,28 +412,34 @@ class ConnectionTest(unittest.TestCase): self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) - @SkipTest # FIXME decide behaviour def test_send_callback_respects_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 1 + self.mock.send_lock = Mock() self.mock.actor_ref = Mock() + self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) - @SkipTest # FIXME decide behaviour def test_send_callback_respects_io_hup(self): self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 1 + self.mock.send_lock = Mock() self.mock.actor_ref = Mock() + self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) - @SkipTest # FIXME decide behaviour def test_send_callback_respects_io_hup_and_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 1 + self.mock.send_lock = Mock() self.mock.actor_ref = Mock() + self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback(self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) From d07a758f68d38fcfc36956020ba7863f7e226098 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 00:42:13 +0200 Subject: [PATCH 147/350] Update tests to reflect that server's socket errors should simply not be handeled --- tests/utils/network/server_test.py | 32 +++++++++++++----------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py index c844a487..75b33d61 100644 --- a/tests/utils/network/server_test.py +++ b/tests/utils/network/server_test.py @@ -6,7 +6,7 @@ import unittest from mopidy.utils import network from mock import patch, sentinel, Mock -from tests import SkipTest, any_int +from tests import any_int class ServerTest(unittest.TestCase): def setUp(self): @@ -28,14 +28,13 @@ class ServerTest(unittest.TestCase): self.mock.register_server_socket.assert_called_once_with( sentinel.fileno) - @SkipTest def test_init_fails_on_fileno_call(self): sock = Mock(spec=socket.SocketType) sock.fileno.side_effect = socket.error self.mock.create_server_socket.return_value = sock - network.Server.__init__(self.mock, sentinel.host, - sentinel.port, sentinel.protocol) + self.assertRaises(socket.error, network.Server.__init__, + self.mock, sentinel.host, sentinel.port, sentinel.protocol) def test_init_stores_values_in_attributes(self): # This need to be a mock and no a sentinel as fileno() is called on it @@ -60,30 +59,27 @@ class ServerTest(unittest.TestCase): sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) sock.listen.assert_called_once_with(any_int) - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket') + @patch.object(network, 'create_socket', new=Mock()) def test_create_server_socket_fails(self): network.create_socket.side_effect = socket.error - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) + self.assertRaises(socket.error, network.Server.create_server_socket, + self.mock, sentinel.host, sentinel.port) - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket', spec=socket.SocketType) + @patch.object(network, 'create_socket', new=Mock()) def test_create_server_bind_fails(self): - sock = create_socket.return_value + sock = network.create_socket.return_value sock.bind.side_effect = socket.error - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) + self.assertRaises(socket.error, network.Server.create_server_socket, + self.mock, sentinel.host, sentinel.port) - @SkipTest # FIXME decide behaviour - @patch.object(network, 'create_socket', spec=socket.SocketType) + @patch.object(network, 'create_socket', new=Mock()) def test_create_server_listen_fails(self): - sock = create_socket.return_value + sock = network.create_socket.return_value sock.listen.side_effect = socket.error - network.Server.create_server_socket(self.mock, - sentinel.host, sentinel.port) + self.assertRaises(socket.error, network.Server.create_server_socket, + self.mock, sentinel.host, sentinel.port) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): From ee856dd06e6c6ea5936f26587011bd244b951902 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 01:41:22 +0200 Subject: [PATCH 148/350] Switch to using xdg cache folder for spotify cache --- mopidy/backends/spotify/session_manager.py | 6 +++++- mopidy/settings.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 2c6509ed..ea689a2d 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -1,3 +1,4 @@ +import glib import logging import os import threading @@ -18,12 +19,15 @@ from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') +DEFAULT_CACHE_LOCATION = os.path.join(glib.get_user_cache_dir(), 'spotify') + # pylint: disable = R0901 # SpotifySessionManager: Too many ancestors (9/7) + class SpotifySessionManager(BaseThread, PyspotifySessionManager): cache_location = settings.SPOTIFY_CACHE_PATH - settings_location = settings.SPOTIFY_CACHE_PATH + settings_location = settings.SPOTIFY_CACHE_PATH or DEFAULT_CACHE_LOCATION appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() diff --git a/mopidy/settings.py b/mopidy/settings.py index f3e012ed..f215ea42 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -237,7 +237,7 @@ SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' #: Path to the Spotify cache. #: #: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_CACHE_PATH = u'~/.mopidy/spotify_cache' +SPOTIFY_CACHE_PATH = u'' #: Your Spotify Premium username. #: From f365786c9d3b4a9a68f95813b073bff581c382cb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 01:45:21 +0200 Subject: [PATCH 149/350] Switch to get_user_config_dir() instead of .mopidy --- mopidy/utils/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 3f7593af..a4d8052b 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from copy import copy import getpass +import glib import logging import os from pprint import pformat @@ -20,7 +21,7 @@ class SettingsProxy(object): self.runtime = {} def _get_local_settings(self): - dotdir = os.path.expanduser(u'~/.mopidy/') + dotdir = os.path.join(glib.get_user_config_dir(), 'mopidy') settings_file = os.path.join(dotdir, u'settings.py') if not os.path.isfile(settings_file): return {} From f4839087618fee44fcf93c7123b6b06abe145594 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 01:56:14 +0200 Subject: [PATCH 150/350] Switch to using SETTINGS_FOLDER and SETTINGS_FILE derived from xdg --- mopidy/__init__.py | 6 ++++++ mopidy/core.py | 6 +++--- mopidy/utils/settings.py | 8 +++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 7b25c525..fdcb11d7 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -3,10 +3,16 @@ import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') +import glib +import os + from subprocess import PIPE, Popen VERSION = (0, 6, 0) +SETTINGS_FOLDER = os.path.join(glib.get_user_config_dir(), 'mopidy') +SETTINGS_FILE = os.path.join(SETTINGS_FOLDER, 'settings.py') + def get_version(): try: return get_git_version() diff --git a/mopidy/core.py b/mopidy/core.py index b3ce9070..c16f32c8 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -18,7 +18,7 @@ sys.argv[1:] = gstreamer_args from pykka.registry import ActorRegistry from mopidy import (get_version, settings, OptionalDependencyError, - SettingsError) + SettingsError, SETTINGS_FOLDER, SETTINGS_FILE) from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging @@ -78,8 +78,8 @@ def parse_options(): return parser.parse_args(args=mopidy_args)[0] def setup_settings(interactive): - get_or_create_folder('~/.mopidy/') - get_or_create_file('~/.mopidy/settings.py') + get_or_create_folder(SETTINGS_FOLDER) + get_or_create_file(SETTINGS_FILE) try: settings.validate(interactive) except SettingsError, e: diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index a4d8052b..60a02957 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -8,7 +8,7 @@ import os from pprint import pformat import sys -from mopidy import SettingsError +from mopidy import SettingsError, SETTINGS_FOLDER, SETTINGS_FILE from mopidy.utils.log import indent logger = logging.getLogger('mopidy.utils.settings') @@ -21,11 +21,9 @@ class SettingsProxy(object): self.runtime = {} def _get_local_settings(self): - dotdir = os.path.join(glib.get_user_config_dir(), 'mopidy') - settings_file = os.path.join(dotdir, u'settings.py') - if not os.path.isfile(settings_file): + if not os.path.isfile(SETTINGS_FILE): return {} - sys.path.insert(0, dotdir) + sys.path.insert(0, SETTINGS_FOLDER) # pylint: disable = F0401 import settings as local_settings_module # pylint: enable = F0401 From aa2c68d88ecd297e43cfceb01322028e5a7e5a5b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 01:56:32 +0200 Subject: [PATCH 151/350] Add simply check for old .mopidy folder --- mopidy/core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mopidy/core.py b/mopidy/core.py index c16f32c8..ac25c977 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -1,5 +1,6 @@ import logging import optparse +import os import signal import sys import time @@ -34,6 +35,7 @@ def main(): try: options = parse_options() setup_logging(options.verbosity_level, options.save_debug_log) + check_old_folders() setup_settings(options.interactive) setup_gobject_loop() setup_gstreamer() @@ -77,6 +79,16 @@ def parse_options(): help='list current settings') return parser.parse_args(args=mopidy_args)[0] +def check_old_folders(): + old_settings_folder = os.path.expanduser(u'~/.mopidy') + + if not os.path.isdir(old_settings_folder): + return + + logger.warning(u'Old settings folder found at %s, settings.py should be ' + 'moved to %s, any cache data should be deleted.', old_settings_folder, + SETTINGS_FOLDER) + def setup_settings(interactive): get_or_create_folder(SETTINGS_FOLDER) get_or_create_file(SETTINGS_FILE) From 3468feb1a9cd128f8e0544f4ada2cc4a7c7d628a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 02:16:44 +0200 Subject: [PATCH 152/350] Switch local backend over to xdg_data_dir --- mopidy/__init__.py | 1 + mopidy/backends/local/__init__.py | 9 ++++++--- mopidy/core.py | 3 ++- mopidy/settings.py | 10 +++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index fdcb11d7..9a897089 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -10,6 +10,7 @@ from subprocess import PIPE, Popen VERSION = (0, 6, 0) +DATA_FOLDER = os.path.join(glib.get_user_data_dir(), 'mopidy') SETTINGS_FOLDER = os.path.join(glib.get_user_config_dir(), 'mopidy') SETTINGS_FILE = os.path.join(SETTINGS_FOLDER, 'settings.py') diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index af80a8eb..1751c460 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -6,7 +6,7 @@ import shutil from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings +from mopidy import settings, DATA_FOLDER from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, PlaybackController, BasePlaybackProvider, StoredPlaylistsController, @@ -18,6 +18,9 @@ from .translator import parse_m3u, parse_mpd_tag_cache logger = logging.getLogger(u'mopidy.backends.local') +DEFAULT_PLAYLIST_PATH = os.path.join(DATA_FOLDER, 'playlists') +DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_FOLDER, 'tag_cache') + class LocalBackend(ThreadingActor, Backend): """ A backend for playing music from a local music archive. @@ -96,7 +99,7 @@ class LocalPlaybackProvider(BasePlaybackProvider): class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider): def __init__(self, *args, **kwargs): super(LocalStoredPlaylistsProvider, self).__init__(*args, **kwargs) - self._folder = settings.LOCAL_PLAYLIST_PATH + self._folder = settings.LOCAL_PLAYLIST_PATH or DEFAULT_PLAYLIST_PATH self.refresh() def lookup(self, uri): @@ -173,7 +176,7 @@ class LocalLibraryProvider(BaseLibraryProvider): self.refresh() def refresh(self, uri=None): - tag_cache = settings.LOCAL_TAG_CACHE_FILE + tag_cache = settings.LOCAL_TAG_CACHE_FILE or DEFAULT_TAG_CACHE_FILE music_folder = settings.LOCAL_MUSIC_PATH tracks = parse_mpd_tag_cache(tag_cache, music_folder) diff --git a/mopidy/core.py b/mopidy/core.py index ac25c977..00a6ad77 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -19,7 +19,7 @@ sys.argv[1:] = gstreamer_args from pykka.registry import ActorRegistry from mopidy import (get_version, settings, OptionalDependencyError, - SettingsError, SETTINGS_FOLDER, SETTINGS_FILE) + SettingsError, DATA_FOLDER, SETTINGS_FOLDER, SETTINGS_FILE) from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging @@ -91,6 +91,7 @@ def check_old_folders(): def setup_settings(interactive): get_or_create_folder(SETTINGS_FOLDER) + get_or_create_folder(DATA_FOLDER) get_or_create_file(SETTINGS_FILE) try: settings.validate(interactive) diff --git a/mopidy/settings.py b/mopidy/settings.py index f215ea42..9eed9c67 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -4,7 +4,7 @@ Available settings and their default values. .. warning:: Do *not* change settings directly in :mod:`mopidy.settings`. Instead, add a - file called ``~/.mopidy/settings.py`` and redefine settings there. + file called ``~/.config/mopidy/settings.py`` and redefine settings there. """ #: List of playback backends to use. See :mod:`mopidy.backends` for all @@ -87,8 +87,8 @@ LOCAL_MUSIC_PATH = u'~/music' #: #: Default:: #: -#: LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists' -LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists' +#: LOCAL_PLAYLIST_PATH = None # Implies $XDG_DATA_DIR/mopidy/playlists +LOCAL_PLAYLIST_PATH = None #: Path to tag cache for local music. #: @@ -96,8 +96,8 @@ LOCAL_PLAYLIST_PATH = u'~/.mopidy/playlists' #: #: Default:: #: -#: LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache' -LOCAL_TAG_CACHE_FILE = u'~/.mopidy/tag_cache' +#: LOCAL_TAG_CACHE_FILE = None # Implies $XDG_DATA_DIR/mopidy/tag_cache +LOCAL_TAG_CACHE_FILE = None #: Sound mixer to use. See :mod:`mopidy.mixers` for all available mixers. #: From c112e6d5dfbfcd10e251fd5cfa74c5824bc9cd69 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 02:19:18 +0200 Subject: [PATCH 153/350] Fix conversion of spotify backend to xdg locations --- mopidy/backends/spotify/session_manager.py | 3 ++- mopidy/settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index ea689a2d..cdea0c5c 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -20,13 +20,14 @@ from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') DEFAULT_CACHE_LOCATION = os.path.join(glib.get_user_cache_dir(), 'spotify') +DEFAULT_SETTINGS_LOCATION = DEFAULT_CACHE_LOCATION # pylint: disable = R0901 # SpotifySessionManager: Too many ancestors (9/7) class SpotifySessionManager(BaseThread, PyspotifySessionManager): - cache_location = settings.SPOTIFY_CACHE_PATH + cache_location = settings.SPOTIFY_CACHE_PATH or DEFAULT_SETTINGS_LOCATION settings_location = settings.SPOTIFY_CACHE_PATH or DEFAULT_CACHE_LOCATION appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() diff --git a/mopidy/settings.py b/mopidy/settings.py index 9eed9c67..6e012a05 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -237,7 +237,7 @@ SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320' #: Path to the Spotify cache. #: #: Used by :mod:`mopidy.backends.spotify`. -SPOTIFY_CACHE_PATH = u'' +SPOTIFY_CACHE_PATH = None #: Your Spotify Premium username. #: From a0c62f8245203e4bb25888bc73fc56a78e386c4b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 02:25:13 +0200 Subject: [PATCH 154/350] Use glib.get_user_special_dir(USER_DIRECTORY_MUSIC) to try and determine LOCAL_MUSIC_PATH --- mopidy/backends/local/__init__.py | 8 +++++++- mopidy/settings.py | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 1751c460..05ea02dd 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -1,4 +1,5 @@ import glob +import glib import logging import os import shutil @@ -20,6 +21,11 @@ logger = logging.getLogger(u'mopidy.backends.local') DEFAULT_PLAYLIST_PATH = os.path.join(DATA_FOLDER, 'playlists') DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_FOLDER, 'tag_cache') +DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC) + +if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'): + DEFAULT_MUSIC_PATH = os.path.expanduser(u'~/music') + class LocalBackend(ThreadingActor, Backend): """ @@ -177,7 +183,7 @@ class LocalLibraryProvider(BaseLibraryProvider): def refresh(self, uri=None): tag_cache = settings.LOCAL_TAG_CACHE_FILE or DEFAULT_TAG_CACHE_FILE - music_folder = settings.LOCAL_MUSIC_PATH + music_folder = settings.LOCAL_MUSIC_PATH or DEFAULT_MUSIC_PATH tracks = parse_mpd_tag_cache(tag_cache, music_folder) diff --git a/mopidy/settings.py b/mopidy/settings.py index 6e012a05..9909973e 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -78,8 +78,9 @@ LASTFM_PASSWORD = u'' #: #: Default:: #: -#: LOCAL_MUSIC_PATH = u'~/music' -LOCAL_MUSIC_PATH = u'~/music' +#: # Defaults to asking glib where music is stored, fallback is ~/music +#: LOCAL_MUSIC_PATH = None +LOCAL_MUSIC_PATH = None #: Path to playlist folder with m3u files for local music. #: From c5b7d5a35edcfca0aeb2073202def51b5c55fcbd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 02:33:21 +0200 Subject: [PATCH 155/350] Note XDG change in changes file --- docs/changes.rst | 12 ++++++++++++ mopidy/core.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 5d2ab57d..563eac53 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,18 @@ v0.6.0 (in development) - Pykka 0.12.3 or greater is required. +- All config, data and cache locations are now based on the XDG spec. + + - This means that your settings file will need to be moved from + `~/.mopidy/settings.py` to `~/.config/mopidy/settings.py`. + - Your Spotify cache will now be stored in `~/.cache/spotify` instead of + `~/.mopidy/spotify_cache`, this matches Spotify's own behaviour for their + Linux client. + - Localbackends tag_cache should now be in `~/.local/share/mopidy/tag_cache`, + likewise your playlists will be in `~/.local/share/mopidy/playlists`. + - The local client now tries to lookup where your music is via XDG, it will + fall-back to `~/music` or use whatever setting you set manually. + **Changes** - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with diff --git a/mopidy/core.py b/mopidy/core.py index 00a6ad77..ea6da28c 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -86,8 +86,8 @@ def check_old_folders(): return logger.warning(u'Old settings folder found at %s, settings.py should be ' - 'moved to %s, any cache data should be deleted.', old_settings_folder, - SETTINGS_FOLDER) + 'moved to %s, any cache data should be deleted. See release notes ' + 'for further instructions.', old_settings_folder, SETTINGS_FOLDER) def setup_settings(interactive): get_or_create_folder(SETTINGS_FOLDER) From 0f976fe4e03ca9c85edd61b6a6a3232df1521ad9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 02:38:41 +0200 Subject: [PATCH 156/350] Add changes notes about network code changes --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 5d2ab57d..21011139 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -20,6 +20,9 @@ v0.6.0 (in development) - Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors wanting to receive events from the backend. This is a formalization of the ad hoc events the Last.fm scrobbler has already been using for some time. +- Replaced all of the MPD network code that was provided by asyncore with + custom stack. This change was made to facilitate the future support of the + `idle` command, and to reduce the number of event loops being used. v0.5.0 (2011-06-15) From bc6162ca05b81267687cf6fa2cae107bc509eedd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 03:16:48 +0200 Subject: [PATCH 157/350] Remove outdated refrence to mopidy.utils.process.GObjectEventThread --- mopidy/gstreamer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 166c487e..43c5ea2b 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -43,9 +43,6 @@ class GStreamer(ThreadingActor): self._handlers = {} def on_start(self): - # **Warning:** :class:`GStreamer` requires - # :class:`mopidy.utils.process.GObjectEventThread` to be running. This - # is not enforced by :class:`GStreamer` itself. self._setup_pipeline() self._setup_outputs() self._setup_message_processor() From 23775dfe1add8f00d4095ab71687a0394f9b286f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 23:44:00 +0200 Subject: [PATCH 158/350] Fix up last comments regarding typo and more logging --- mopidy/utils/network.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 852b6ca6..b7f808c9 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -320,7 +320,7 @@ class LineProtocol(ThreadingActor): def on_stop(self): """Ensure that cleanup when actor stops.""" - self.connection.stop(u'Actor is shuting down.') + self.connection.stop(u'Actor is shutting down.') def parse_lines(self): """Consume new data and yield any lines found.""" @@ -337,7 +337,9 @@ class LineProtocol(ThreadingActor): """ try: return line.encode(self.encoding) - except UnicodeError: # FIXME log this? + except UnicodeError: + logger.warning(u'Stoping actor due to encode problem, data ' + 'supplied by client was not valid %s', self.encoding) self.stop() def decode(self, line): @@ -348,7 +350,9 @@ class LineProtocol(ThreadingActor): """ try: return line.decode(self.encoding) - except UnicodeError: # FIXME log this? + except UnicodeError: + logger.warning(u'Stoping actor due to decode problem, data ' + 'supplied by client was not valid %s', self.encoding) self.stop() def join_lines(self, lines): From 6cf5deb21651b403d1719f18d2af77ee26537b8f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 17 Jul 2011 23:52:32 +0200 Subject: [PATCH 159/350] Typo fix :) --- mopidy/utils/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index b7f808c9..b7cc144d 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -338,7 +338,7 @@ class LineProtocol(ThreadingActor): try: return line.encode(self.encoding) except UnicodeError: - logger.warning(u'Stoping actor due to encode problem, data ' + logger.warning(u'Stopping actor due to encode problem, data ' 'supplied by client was not valid %s', self.encoding) self.stop() @@ -351,7 +351,7 @@ class LineProtocol(ThreadingActor): try: return line.decode(self.encoding) except UnicodeError: - logger.warning(u'Stoping actor due to decode problem, data ' + logger.warning(u'Stopping actor due to decode problem, data ' 'supplied by client was not valid %s', self.encoding) self.stop() From 0479adf7fc31f457f7c06d531d0463b5007b85b7 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 18 Jul 2011 00:15:58 +0200 Subject: [PATCH 160/350] Fix s/_FOLDER/_PATH/, move cache to mopidy instead of spotify folder and fix docs. --- docs/changes.rst | 6 +++--- mopidy/__init__.py | 7 ++++--- mopidy/backends/local/__init__.py | 6 +++--- mopidy/backends/spotify/session_manager.py | 9 +++------ mopidy/core.py | 8 ++++---- mopidy/utils/settings.py | 4 ++-- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 6cda6668..9dbcaf61 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,14 +12,14 @@ v0.6.0 (in development) - Pykka 0.12.3 or greater is required. -- All config, data and cache locations are now based on the XDG spec. +- All config, data, and cache locations are now based on the XDG spec. - This means that your settings file will need to be moved from `~/.mopidy/settings.py` to `~/.config/mopidy/settings.py`. - - Your Spotify cache will now be stored in `~/.cache/spotify` instead of + - Your Spotify cache will now be stored in `~/.cache/mopidy` instead of `~/.mopidy/spotify_cache`, this matches Spotify's own behaviour for their Linux client. - - Localbackends tag_cache should now be in `~/.local/share/mopidy/tag_cache`, + - The local backend's `tag_cache` should now be in `~/.local/share/mopidy/tag_cache`, likewise your playlists will be in `~/.local/share/mopidy/playlists`. - The local client now tries to lookup where your music is via XDG, it will fall-back to `~/music` or use whatever setting you set manually. diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 9a897089..1d820fd0 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -10,9 +10,10 @@ from subprocess import PIPE, Popen VERSION = (0, 6, 0) -DATA_FOLDER = os.path.join(glib.get_user_data_dir(), 'mopidy') -SETTINGS_FOLDER = os.path.join(glib.get_user_config_dir(), 'mopidy') -SETTINGS_FILE = os.path.join(SETTINGS_FOLDER, 'settings.py') +DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy') +CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy') +SETTINGS_PATH = os.path.join(glib.get_user_config_dir(), 'mopidy') +SETTINGS_FILE = os.path.join(SETTINGS_PATH, 'settings.py') def get_version(): try: diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index 05ea02dd..e689f666 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -7,7 +7,7 @@ import shutil from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry -from mopidy import settings, DATA_FOLDER +from mopidy import settings, DATA_PATH from mopidy.backends.base import (Backend, CurrentPlaylistController, LibraryController, BaseLibraryProvider, PlaybackController, BasePlaybackProvider, StoredPlaylistsController, @@ -19,8 +19,8 @@ from .translator import parse_m3u, parse_mpd_tag_cache logger = logging.getLogger(u'mopidy.backends.local') -DEFAULT_PLAYLIST_PATH = os.path.join(DATA_FOLDER, 'playlists') -DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_FOLDER, 'tag_cache') +DEFAULT_PLAYLIST_PATH = os.path.join(DATA_PATH, 'playlists') +DEFAULT_TAG_CACHE_FILE = os.path.join(DATA_PATH, 'tag_cache') DEFAULT_MUSIC_PATH = glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC) if not DEFAULT_MUSIC_PATH or DEFAULT_MUSIC_PATH == os.path.expanduser(u'~'): diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index cdea0c5c..9c8853e6 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -7,7 +7,7 @@ from spotify.manager import SpotifySessionManager as PyspotifySessionManager from pykka.registry import ActorRegistry -from mopidy import get_version, settings +from mopidy import get_version, settings, CACHE_PATH from mopidy.backends.base import Backend from mopidy.backends.spotify import BITRATES from mopidy.backends.spotify.container_manager import SpotifyContainerManager @@ -19,16 +19,13 @@ from mopidy.utils.process import BaseThread logger = logging.getLogger('mopidy.backends.spotify.session_manager') -DEFAULT_CACHE_LOCATION = os.path.join(glib.get_user_cache_dir(), 'spotify') -DEFAULT_SETTINGS_LOCATION = DEFAULT_CACHE_LOCATION - # pylint: disable = R0901 # SpotifySessionManager: Too many ancestors (9/7) class SpotifySessionManager(BaseThread, PyspotifySessionManager): - cache_location = settings.SPOTIFY_CACHE_PATH or DEFAULT_SETTINGS_LOCATION - settings_location = settings.SPOTIFY_CACHE_PATH or DEFAULT_CACHE_LOCATION + cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH + settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() diff --git a/mopidy/core.py b/mopidy/core.py index 97b4ed5c..bf794655 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -21,7 +21,7 @@ sys.argv[1:] = gstreamer_args from pykka.registry import ActorRegistry from mopidy import (get_version, settings, OptionalDependencyError, - SettingsError, DATA_FOLDER, SETTINGS_FOLDER, SETTINGS_FILE) + SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) from mopidy.gstreamer import GStreamer from mopidy.utils import get_class from mopidy.utils.log import setup_logging @@ -89,11 +89,11 @@ def check_old_folders(): logger.warning(u'Old settings folder found at %s, settings.py should be ' 'moved to %s, any cache data should be deleted. See release notes ' - 'for further instructions.', old_settings_folder, SETTINGS_FOLDER) + 'for further instructions.', old_settings_folder, SETTINGS_PATH) def setup_settings(interactive): - get_or_create_folder(SETTINGS_FOLDER) - get_or_create_folder(DATA_FOLDER) + get_or_create_folder(SETTINGS_PATH) + get_or_create_folder(DATA_PATH) get_or_create_file(SETTINGS_FILE) try: settings.validate(interactive) diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index 60a02957..fca4f337 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -8,7 +8,7 @@ import os from pprint import pformat import sys -from mopidy import SettingsError, SETTINGS_FOLDER, SETTINGS_FILE +from mopidy import SettingsError, SETTINGS_PATH, SETTINGS_FILE from mopidy.utils.log import indent logger = logging.getLogger('mopidy.utils.settings') @@ -23,7 +23,7 @@ class SettingsProxy(object): def _get_local_settings(self): if not os.path.isfile(SETTINGS_FILE): return {} - sys.path.insert(0, SETTINGS_FOLDER) + sys.path.insert(0, SETTINGS_PATH) # pylint: disable = F0401 import settings as local_settings_module # pylint: enable = F0401 From 2ec9570a4c020a497feef0a98652fac34772b4ff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 18 Jul 2011 10:17:59 +0200 Subject: [PATCH 161/350] Formatting --- docs/changes.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 9dbcaf61..0a9ab925 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -15,14 +15,15 @@ v0.6.0 (in development) - All config, data, and cache locations are now based on the XDG spec. - This means that your settings file will need to be moved from - `~/.mopidy/settings.py` to `~/.config/mopidy/settings.py`. - - Your Spotify cache will now be stored in `~/.cache/mopidy` instead of - `~/.mopidy/spotify_cache`, this matches Spotify's own behaviour for their + ``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``. + - Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of + ``~/.mopidy/spotify_cache``, this matches Spotify's own behaviour for their Linux client. - - The local backend's `tag_cache` should now be in `~/.local/share/mopidy/tag_cache`, - likewise your playlists will be in `~/.local/share/mopidy/playlists`. + - The local backend's ``tag_cache`` should now be in + ``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in + ``~/.local/share/mopidy/playlists``. - The local client now tries to lookup where your music is via XDG, it will - fall-back to `~/music` or use whatever setting you set manually. + fall-back to ``~/music`` or use whatever setting you set manually. **Changes** @@ -34,7 +35,7 @@ v0.6.0 (in development) ad hoc events the Last.fm scrobbler has already been using for some time. - Replaced all of the MPD network code that was provided by asyncore with custom stack. This change was made to facilitate the future support of the - `idle` command, and to reduce the number of event loops being used. + ``idle`` command, and to reduce the number of event loops being used. - Fix metadata update in Shoutcast streaming (Fixes: :issue:`122`) From 0ccb5dec23e32747eb4e59fb8742e930a36e2601 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 26 Jun 2011 17:37:22 +0200 Subject: [PATCH 162/350] Add debug tool that inspects MPD's idle behaviour --- tools/idle.py | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 tools/idle.py diff --git a/tools/idle.py b/tools/idle.py new file mode 100644 index 00000000..aa56dce2 --- /dev/null +++ b/tools/idle.py @@ -0,0 +1,201 @@ +#! /usr/bin/env python + +# This script is helper to systematicly test the behaviour of MPD's idle +# command. It is simply provided as a quick hack, expect nothing more. + +import logging +import pprint +import socket + +host = '' +port = 6601 + +url = "13 - a-ha - White Canvas.mp3" +artist = "a-ha" + +data = {'id': None, 'id2': None, 'url': url, 'artist': artist} + +# Commands to run before test requests to coerce MPD into right state +setup_requests = [ + 'clear', + 'add "%(url)s"', + 'add "%(url)s"', + 'add "%(url)s"', + 'play', +# 'pause', # Uncomment to test paused idle behaviour +# 'stop', # Uncomment to test stopped idle behaviour +] + +# List of commands to test for idle behaviour. Ordering of list is important in +# order to keep MPD state as intended. Commands that are obviously +# informational only or "harmfull" have been excluded. +test_requests = [ + 'add "%(url)s"', + 'addid "%(url)s" "1"', + 'clear', +# 'clearerror', +# 'close', +# 'commands', + 'consume "1"', + 'consume "0"', +# 'count', + 'crossfade "1"', + 'crossfade "0"', +# 'currentsong', +# 'delete "1:2"', + 'delete "0"', + 'deleteid "%(id)s"', + 'disableoutput "0"', + 'enableoutput "0"', +# 'find', +# 'findadd "artist" "%(artist)s"', +# 'idle', +# 'kill', +# 'list', +# 'listall', +# 'listallinfo', +# 'listplaylist', +# 'listplaylistinfo', +# 'listplaylists', +# 'lsinfo', + 'move "0:1" "2"', + 'move "0" "1"', + 'moveid "%(id)s" "1"', + 'next', +# 'notcommands', +# 'outputs', +# 'password', + 'pause', +# 'ping', + 'play', + 'playid "%(id)s"', +# 'playlist', + 'playlistadd "foo" "%(url)s"', + 'playlistclear "foo"', + 'playlistadd "foo" "%(url)s"', + 'playlistdelete "foo" "0"', +# 'playlistfind', +# 'playlistid', +# 'playlistinfo', + 'playlistadd "foo" "%(url)s"', + 'playlistadd "foo" "%(url)s"', + 'playlistmove "foo" "0" "1"', +# 'playlistsearch', +# 'plchanges', +# 'plchangesposid', + 'previous', + 'random "1"', + 'random "0"', + 'rm "bar"', + 'rename "foo" "bar"', + 'repeat "0"', + 'rm "bar"', + 'save "bar"', + 'load "bar"', +# 'search', + 'seek "1" "10"', + 'seekid "%(id)s" "10"', +# 'setvol "10"', + 'shuffle', + 'shuffle "0:1"', + 'single "1"', + 'single "0"', +# 'stats', +# 'status', + 'stop', + 'swap "1" "2"', + 'swapid "%(id)s" "%(id2)s"', +# 'tagtypes', +# 'update', +# 'urlhandlers', +# 'volume', +] + + +def create_socketfile(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, port)) + sock.settimeout(0.5) + fd = sock.makefile('rw', 1) # 1 = line buffered + fd.readline() # Read banner + return fd + + +def wait(fd, prefix=None, collect=None): + while True: + line = fd.readline().rstrip() + if prefix: + logging.debug('%s: %s', prefix, repr(line)) + if line.split()[0] in ('OK', 'ACK'): + break + + +def collect_ids(fd): + fd.write('playlistinfo\n') + + ids = [] + while True: + line = fd.readline() + if line.split()[0] == 'OK': + break + if line.split()[0] == 'Id:': + ids.append(line.split()[1]) + return ids + + +def main(): + subsystems = {} + + command = create_socketfile() + + for test in test_requests: + # Remove any old ids + del data['id'] + del data['id2'] + + # Run setup code to force MPD into known state + for setup in setup_requests: + command.write(setup % data + '\n') + wait(command) + + data['id'], data['id2'] = collect_ids(command)[:2] + + # This connection needs to be make after setup commands are done or + # else they will cause idle events. + idle = create_socketfile() + + # Wait for new idle events + idle.write('idle\n') + + test = test % data + + logging.debug('idle: %s', repr('idle')) + logging.debug('command: %s', repr(test)) + + command.write(test + '\n') + wait(command, prefix='command') + + while True: + try: + line = idle.readline().rstrip() + except socket.timeout: + # Abort try if we time out. + idle.write('noidle\n') + break + + logging.debug('idle: %s', repr(line)) + + if line == 'OK': + break + + request_type = test.split()[0] + subsystem = line.split()[1] + subsystems.setdefault(request_type, set()).add(subsystem) + + logging.debug('---') + + pprint.pprint(subsystems) + + +if __name__ == '__main__': + main() From e919dcf627de3f3eb8951cafc410ec2d347f2dd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 00:49:43 +0200 Subject: [PATCH 163/350] Create helper for sending events to BackendListeners --- mopidy/backends/base/playback.py | 20 +++++--------------- mopidy/frontends/mpd/__init__.py | 2 +- mopidy/listeners.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 088a5ad4..78e5057d 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -464,27 +464,17 @@ class PlaybackController(object): logger.debug(u'Triggering started playing event') if self.current_track is None: return - ActorRegistry.broadcast({ - 'command': 'pykka_call', - 'attr_path': ('started_playing',), - 'args': [], - 'kwargs': {'track': self.current_track}, - }, target_class=BackendListener) + BackendListener.send('started_playing', + track=self.current_track) def _trigger_stopped_playing_event(self): # TODO Test that this is called on next/prev/end-of-track logger.debug(u'Triggering stopped playing event') if self.current_track is None: return - ActorRegistry.broadcast({ - 'command': 'pykka_call', - 'attr_path': ('stopped_playing',), - 'args': [], - 'kwargs': { - 'track': self.current_track, - 'time_position': self.time_position, - }, - }, target_class=BackendListener) + BackendListener.send('stopped_playing', + track=self.current_track, + time_position=self.time_position) class BasePlaybackProvider(object): diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 4deb7b89..561f9295 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -67,7 +67,7 @@ class MpdSession(network.LineProtocol): logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, self.actor_urn, log.indent(self.terminator.join(response))) - + self.send_lines(response) def close(self): diff --git a/mopidy/listeners.py b/mopidy/listeners.py index dfc5c60b..9977d5da 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -1,3 +1,5 @@ +from pykka import registry + class BackendListener(object): """ Marker interface for recipients of events sent by the backend. @@ -9,6 +11,16 @@ class BackendListener(object): interested in all events. """ + @staticmethod + def send(event, **kwargs): + """Helper to allow calling of backend listener events""" + registry.ActorRegistry.broadcast({ + 'command': 'pykka_call', + 'attr_path': (event,), + 'args': [], + 'kwargs': kwargs + }, target_class=BackendListener) + def started_playing(self, track): """ Called whenever a new track starts playing. From 215ed61b0b9b52078b15a93d35d374012ce355a0 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 02:29:40 +0200 Subject: [PATCH 164/350] Update existing listener events to reflect that they only notify about track changes --- mopidy/backends/base/playback.py | 23 +++++++++++------------ mopidy/frontends/lastfm.py | 4 ++-- mopidy/listeners.py | 6 +++--- tests/backends/events_test.py | 24 ++++++++++++------------ tests/listeners_test.py | 8 ++++---- 5 files changed, 32 insertions(+), 33 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 78e5057d..7b697781 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -326,7 +326,7 @@ class PlaybackController(object): original_cp_track = self.current_cp_track if self.cp_track_at_eot: - self._trigger_stopped_playing_event() + self._trigger_track_playback_ended() self.play(self.cp_track_at_eot) else: self.stop(clear_current_track=True) @@ -354,7 +354,7 @@ class PlaybackController(object): return if self.cp_track_at_next: - self._trigger_stopped_playing_event() + self._trigger_track_playback_ended() self.play(self.cp_track_at_next) else: self.stop(clear_current_track=True) @@ -402,7 +402,7 @@ class PlaybackController(object): if self.random and self.current_cp_track in self._shuffled: self._shuffled.remove(self.current_cp_track) - self._trigger_started_playing_event() + self._trigger_track_playback_started() def previous(self): """Play the previous track.""" @@ -410,7 +410,7 @@ class PlaybackController(object): return if self.state == self.STOPPED: return - self._trigger_stopped_playing_event() + self._trigger_track_playback_ended() self.play(self.cp_track_at_previous, on_error_step=-1) def resume(self): @@ -454,25 +454,24 @@ class PlaybackController(object): :type clear_current_track: boolean """ if self.state != self.STOPPED: - self._trigger_stopped_playing_event() if self.provider.stop(): + self._trigger_track_playback_ended() self.state = self.STOPPED if clear_current_track: self.current_cp_track = None - def _trigger_started_playing_event(self): - logger.debug(u'Triggering started playing event') + def _trigger_track_playback_started(self): + logger.debug(u'Triggering track playback started event') if self.current_track is None: return - BackendListener.send('started_playing', + BackendListener.send('track_playback_started', track=self.current_track) - def _trigger_stopped_playing_event(self): - # TODO Test that this is called on next/prev/end-of-track - logger.debug(u'Triggering stopped playing event') + def _trigger_track_playback_ended(self): + logger.debug(u'Triggering track playback ended event') if self.current_track is None: return - BackendListener.send('stopped_playing', + BackendListener.send('track_playback_ended', track=self.current_track, time_position=self.time_position) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index d50f8dd8..125457cd 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -57,7 +57,7 @@ class LastfmFrontend(ThreadingActor, BackendListener): logger.error(u'Error during Last.fm setup: %s', e) self.stop() - def started_playing(self, track): + def track_playback_started(self, track): artists = ', '.join([a.name for a in track.artists]) duration = track.length and track.length // 1000 or 0 self.last_start_time = int(time.time()) @@ -74,7 +74,7 @@ class LastfmFrontend(ThreadingActor, BackendListener): pylast.MalformedResponseError, pylast.WSError) as e: logger.warning(u'Error submitting playing track to Last.fm: %s', e) - def stopped_playing(self, track, time_position): + def track_playback_ended(self, track, time_position): artists = ', '.join([a.name for a in track.artists]) duration = track.length and track.length // 1000 or 0 time_position = time_position // 1000 diff --git a/mopidy/listeners.py b/mopidy/listeners.py index 9977d5da..bcce0c40 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -21,7 +21,7 @@ class BackendListener(object): 'kwargs': kwargs }, target_class=BackendListener) - def started_playing(self, track): + def track_playback_started(self, track): """ Called whenever a new track starts playing. @@ -32,9 +32,9 @@ class BackendListener(object): """ pass - def stopped_playing(self, track, time_position): + def track_playback_ended(self, track, time_position): """ - Called whenever playback is stopped. + Called whenever playback of a track ends. *MAY* be implemented by actor. diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index 44529e90..bc39ac00 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -11,8 +11,8 @@ from mopidy.models import Track class BackendEventsTest(unittest.TestCase): def setUp(self): self.events = { - 'started_playing': threading.Event(), - 'stopped_playing': threading.Event(), + 'track_playback_started': threading.Event(), + 'track_playback_ended': threading.Event(), } self.backend = DummyBackend.start().proxy() self.listener = DummyBackendListener.start(self.events).proxy() @@ -20,26 +20,26 @@ class BackendEventsTest(unittest.TestCase): def tearDown(self): ActorRegistry.stop_all() - def test_play_sends_started_playing_event(self): + def test_play_sends_track_playback_started_event(self): self.backend.current_playlist.add([Track(uri='a')]) self.backend.playback.play() - self.events['started_playing'].wait(timeout=1) - self.assertTrue(self.events['started_playing'].is_set()) + self.events['track_playback_started'].wait(timeout=1) + self.assertTrue(self.events['track_playback_started'].is_set()) - def test_stop_sends_stopped_playing_event(self): + def test_stop_sends_track_playback_ended_event(self): self.backend.current_playlist.add([Track(uri='a')]) self.backend.playback.play() self.backend.playback.stop() - self.events['stopped_playing'].wait(timeout=1) - self.assertTrue(self.events['stopped_playing'].is_set()) + self.events['track_playback_ended'].wait(timeout=1) + self.assertTrue(self.events['track_playback_ended'].is_set()) class DummyBackendListener(ThreadingActor, BackendListener): def __init__(self, events): self.events = events - def started_playing(self, track): - self.events['started_playing'].set() + def track_playback_started(self, track): + self.events['track_playback_started'].set() - def stopped_playing(self, track, time_position): - self.events['stopped_playing'].set() + def track_playback_ended(self, track, time_position): + self.events['track_playback_ended'].set() diff --git a/tests/listeners_test.py b/tests/listeners_test.py index 761aff4f..2c31efdb 100644 --- a/tests/listeners_test.py +++ b/tests/listeners_test.py @@ -7,8 +7,8 @@ class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() - def test_listener_has_default_impl_for_the_started_playing_event(self): - self.listener.started_playing(Track()) + def test_listener_has_default_impl_for_the_track_playback_started(self): + self.listener.track_playback_started(Track()) - def test_listener_has_default_impl_for_the_stopped_playing_event(self): - self.listener.stopped_playing(Track(), 0) + def test_listener_has_default_impl_for_the_track_playback_ended(self): + self.listener.track_playback_ended(Track(), 0) From 1e5a5fb7d0843c79d32fdc0029728dbd2f25f5bf Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 02:43:48 +0200 Subject: [PATCH 165/350] Add playback state changed event --- mopidy/backends/base/playback.py | 8 +++++++- mopidy/listeners.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 7b697781..1b5e1c9f 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -276,6 +276,9 @@ 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) + + self._trigger_playback_state_changed() + # FIXME play_time stuff assumes backend does not have a better way of # handeling this stuff :/ if (old_state in (self.PLAYING, self.STOPPED) @@ -387,7 +390,6 @@ class PlaybackController(object): self.resume() if cp_track is not None: - self.state = self.STOPPED self.current_cp_track = cp_track self.state = self.PLAYING if not self.provider.play(cp_track.track): @@ -475,6 +477,10 @@ class PlaybackController(object): track=self.current_track, time_position=self.time_position) + def _trigger_playback_state_changed(self): + logger.debug(u'Triggering playback state change event') + BackendListener.send('playback_state_changed') + class BasePlaybackProvider(object): """ diff --git a/mopidy/listeners.py b/mopidy/listeners.py index bcce0c40..f6973eaf 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -44,3 +44,11 @@ class BackendListener(object): :type time_position: int """ pass + + def playback_state_changed(self): + """ + Called whenever playback state is changed. + + *MAY* be implemented by actor. + """ + pass From ce1a0118d80500a31b55de2874e258deb69c5dd1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 02:54:27 +0200 Subject: [PATCH 166/350] Add event that indicates playlist whenever version is changed --- mopidy/backends/base/current_playlist.py | 20 ++++++++++++++++---- mopidy/listeners.py | 8 ++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index 2633f166..e89c23d5 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -2,6 +2,7 @@ from copy import copy import logging import random +from mopidy.listeners import BackendListener from mopidy.models import CpTrack logger = logging.getLogger('mopidy.backends.base') @@ -16,6 +17,7 @@ class CurrentPlaylistController(object): def __init__(self, backend): self.backend = backend + self.cp_id = 0 self._cp_tracks = [] self._version = 0 @@ -53,8 +55,9 @@ class CurrentPlaylistController(object): def version(self, version): self._version = version self.backend.playback.on_current_playlist_change() + self._trigger_playlist_changed() - def add(self, track, at_position=None): + def add(self, track, at_position=None, increase_version=True): """ Add the track to the end of, or at the given position in the current playlist. @@ -68,12 +71,14 @@ class CurrentPlaylistController(object): """ assert at_position <= len(self._cp_tracks), \ u'at_position can not be greater than playlist length' - cp_track = CpTrack(self.version, track) + cp_track = CpTrack(self.cp_id, track) if at_position is not None: self._cp_tracks.insert(at_position, cp_track) else: self._cp_tracks.append(cp_track) - self.version += 1 + if increase_version: + self.version += 1 + self.cp_id += 1 return cp_track def append(self, tracks): @@ -84,7 +89,10 @@ class CurrentPlaylistController(object): :type tracks: list of :class:`mopidy.models.Track` """ for track in tracks: - self.add(track) + self.add(track, increase_version=False) + + if tracks: + self.version += 1 def clear(self): """Clear the current playlist.""" @@ -199,3 +207,7 @@ class CurrentPlaylistController(object): random.shuffle(shuffled) self._cp_tracks = before + shuffled + after self.version += 1 + + def _trigger_playlist_changed(self): + logger.debug(u'Triggering playlist changed event') + BackendListener.send('playlist_changed') diff --git a/mopidy/listeners.py b/mopidy/listeners.py index f6973eaf..397e08ea 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -52,3 +52,11 @@ class BackendListener(object): *MAY* be implemented by actor. """ pass + + def playlist_changed(self): + """ + Called whenever a playlist is changed. + + *MAY* be implemented by actor. + """ + pass From 4f124480c3998b1d0911f72fd44e535c62b03ffe Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 03:08:29 +0200 Subject: [PATCH 167/350] Add an options wrapper to magically pick up changes --- mopidy/backends/base/playback.py | 23 +++++++++++++++++++---- mopidy/listeners.py | 8 ++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 1b5e1c9f..5cab8229 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -8,6 +8,17 @@ from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.backends.base') + +def option_wrapper(name, default): + def get_option(self): + return getattr(self, name, default) + def set_option(self, value): + if getattr(self, name, default) != value: + self._trigger_options_changed() + return setattr(self, name, value) + return property(get_option, set_option) + + class PlaybackController(object): """ :param backend: the backend @@ -34,7 +45,7 @@ class PlaybackController(object): #: Tracks are removed from the playlist when they have been played. #: :class:`False` #: Tracks are not removed from the playlist. - consume = False + consume = option_wrapper('_consume', False) #: The currently playing or selected track. #: @@ -46,21 +57,21 @@ class PlaybackController(object): #: Tracks are selected at random from the playlist. #: :class:`False` #: Tracks are played in the order of the playlist. - random = False + random = option_wrapper('_random', False) #: :class:`True` #: The current playlist is played repeatedly. To repeat a single track, #: select both :attr:`repeat` and :attr:`single`. #: :class:`False` #: The current playlist is played once. - repeat = False + repeat = option_wrapper('_repeat', False) #: :class:`True` #: Playback is stopped after current song, unless in :attr:`repeat` #: mode. #: :class:`False` #: Playback continues after current song. - single = False + single = option_wrapper('_single', False) def __init__(self, backend, provider): self.backend = backend @@ -481,6 +492,10 @@ class PlaybackController(object): logger.debug(u'Triggering playback state change event') BackendListener.send('playback_state_changed') + def _trigger_options_changed(self): + logger.debug(u'Triggering options changed event') + BackendListener.send('options_changed') + class BasePlaybackProvider(object): """ diff --git a/mopidy/listeners.py b/mopidy/listeners.py index 397e08ea..c0453a2b 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -60,3 +60,11 @@ class BackendListener(object): *MAY* be implemented by actor. """ pass + + def options_changed(self): + """ + Called whenever an option is changed. + + *MAY* be implemented by actor. + """ + pass From 8ae0381cd8d54b81e31fea994397d62347a44a18 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 03:44:34 +0200 Subject: [PATCH 168/350] Start adding idle to frontend, mpd-session and dispatcher --- mopidy/frontends/mpd/__init__.py | 28 +++++++++++++++++++++++++--- mopidy/frontends/mpd/dispatcher.py | 3 +++ mopidy/listeners.py | 4 +++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 561f9295..6f6e3bfc 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -1,15 +1,15 @@ import logging import sys -from pykka.actor import ThreadingActor +from pykka import registry, actor -from mopidy import settings +from mopidy import listeners, settings from mopidy.frontends.mpd import dispatcher, protocol from mopidy.utils import network, process, log logger = logging.getLogger('mopidy.frontends.mpd') -class MpdFrontend(ThreadingActor): +class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): """ The MPD frontend. @@ -39,6 +39,25 @@ class MpdFrontend(ThreadingActor): def on_stop(self): process.stop_actors_by_class(MpdSession) + def send_idle(self, subsystem): + # FIXME this should be updated once pykka supports non-blocking calls + # on proxies or some similar solution + registry.ActorRegistry.broadcast({ + 'command': 'pykka_call', + 'attr_path': ('on_idle',), + 'args': [subsystem], + 'kwargs': {}, + }, target_class=MpdSession) + + def playback_state_changed(self): + self.send_idle('player') + + def playlist_changed(self): + self.send_idle('playlist') + + def options_changed(self): + self.send_idle('options') + class MpdSession(network.LineProtocol): """ @@ -70,5 +89,8 @@ class MpdSession(network.LineProtocol): self.send_lines(response) + def on_idle(self, subsystem): + self.dispatcher.handle_idle(subsystem) + def close(self): self.stop() diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 0f0f0299..6cc05bec 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -47,6 +47,9 @@ class MpdDispatcher(object): ] return self._call_next_filter(request, response, filter_chain) + def handle_idle(self, subsystem): + logger.debug(u'Got idle event for %s', subsystem) + def _call_next_filter(self, request, response, filter_chain): if filter_chain: next_filter = filter_chain.pop(0) diff --git a/mopidy/listeners.py b/mopidy/listeners.py index c0453a2b..5fbccff5 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -14,11 +14,13 @@ class BackendListener(object): @staticmethod def send(event, **kwargs): """Helper to allow calling of backend listener events""" + # FIXME this should be updated once pykka supports non-blocking calls + # on proxies or some similar solution registry.ActorRegistry.broadcast({ 'command': 'pykka_call', 'attr_path': (event,), 'args': [], - 'kwargs': kwargs + 'kwargs': kwargs, }, target_class=BackendListener) def track_playback_started(self, track): From da3b4c4b93fbd51b58a8ccd7974306253d1ec8d8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 11:07:36 +0200 Subject: [PATCH 169/350] Add changed event for volume and updated MpdSession to regcognise it. --- mopidy/frontends/mpd/__init__.py | 3 +++ mopidy/listeners.py | 8 ++++++++ mopidy/mixers/base.py | 11 ++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 6f6e3bfc..ff239458 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -58,6 +58,9 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): def options_changed(self): self.send_idle('options') + def volume_changed(self): + self.send_idle('mixer') + class MpdSession(network.LineProtocol): """ diff --git a/mopidy/listeners.py b/mopidy/listeners.py index 5fbccff5..590f0ad0 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -70,3 +70,11 @@ class BackendListener(object): *MAY* be implemented by actor. """ pass + + def volume_changed(self): + """ + Called whenever the volume is changed. + + *MAY* be implemented by actor. + """ + pass diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index ec3d8ae5..8798076a 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -1,4 +1,8 @@ -from mopidy import settings +import logging + +from mopidy import listeners, settings + +logger = logging.getLogger('mopdy.mixers') class BaseMixer(object): """ @@ -30,6 +34,7 @@ class BaseMixer(object): elif volume > 100: volume = 100 self.set_volume(volume) + self._trigger_volume_changed() def get_volume(self): """ @@ -46,3 +51,7 @@ class BaseMixer(object): *MUST be implemented by subclass.* """ raise NotImplementedError + + def _trigger_volume_changed(self): + logger.debug(u'Triggering volume changed event') + listeners.BackendListener.send('volume_changed') From e050c1325154992a9776db9f18d40c42338809c9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 21:08:14 +0200 Subject: [PATCH 170/350] Remove most of pykka logging which we don't need unless debuging --- mopidy/core.py | 2 +- mopidy/utils/log.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mopidy/core.py b/mopidy/core.py index e831fc55..cec8aef9 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -69,7 +69,7 @@ def parse_options(): 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', + action='count', default=1, dest='verbosity_level', help='more output (debug level)') parser.add_option('--save-debug-log', action='store_true', dest='save_debug_log', diff --git a/mopidy/utils/log.py b/mopidy/utils/log.py index 03b85b48..0e5dfc29 100644 --- a/mopidy/utils/log.py +++ b/mopidy/utils/log.py @@ -20,7 +20,7 @@ def setup_console_logging(verbosity_level): if verbosity_level == 0: log_level = logging.WARNING log_format = settings.CONSOLE_LOG_FORMAT - elif verbosity_level == 2: + elif verbosity_level >= 2: log_level = logging.DEBUG log_format = settings.DEBUG_LOG_FORMAT else: @@ -33,6 +33,9 @@ def setup_console_logging(verbosity_level): root = logging.getLogger('') root.addHandler(handler) + if verbosity_level < 3: + logging.getLogger('pykka').setLevel(logging.INFO) + def setup_debug_logging_to_file(): formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT) handler = logging.handlers.RotatingFileHandler( From da9b6470ba739d6197f732966d4311ba9ed3f225 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Tue, 19 Jul 2011 22:41:58 +0200 Subject: [PATCH 171/350] Switched playback test over to testing via MpdSession slightly higher level testing --- mopidy/frontends/mpd/__init__.py | 6 +- tests/frontends/mpd/protocol/__init__.py | 43 +++ .../mpd/{ => protocol}/playback_test.py | 311 +++++++++--------- 3 files changed, 203 insertions(+), 157 deletions(-) create mode 100644 tests/frontends/mpd/protocol/__init__.py rename tests/frontends/mpd/{ => protocol}/playback_test.py (55%) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 4deb7b89..8b6d3770 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -49,8 +49,8 @@ class MpdSession(network.LineProtocol): terminator = protocol.LINE_TERMINATOR encoding = protocol.ENCODING - def __init__(self, client): - super(MpdSession, self).__init__(client) + def __init__(self, connection): + super(MpdSession, self).__init__(connection) self.dispatcher = dispatcher.MpdDispatcher(self) def on_start(self): @@ -67,7 +67,7 @@ class MpdSession(network.LineProtocol): logger.debug(u'Response to [%s]:%s from %s: %s', self.host, self.port, self.actor_urn, log.indent(self.terminator.join(response))) - + self.send_lines(response) def close(self): diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py new file mode 100644 index 00000000..a3705c48 --- /dev/null +++ b/tests/frontends/mpd/protocol/__init__.py @@ -0,0 +1,43 @@ +import unittest +import mock + +from mopidy.backends import dummy as backend +from mopidy.frontends import mpd +from mopidy.frontends.mpd import dispatcher +from mopidy.mixers import dummy as mixer +from mopidy.utils import network + + +class MockConnetion(mock.Mock): + def __init__(self, *args, **kwargs): + super(MockConnetion, self).__init__(*args, **kwargs) + self.host = mock.sentinel.host + self.port = mock.sentinel.port + self.response = [] + + def send(self, data): + self.response.extend(data.split('\n')) + + +class BaseTestCase(unittest.TestCase): + def setUp(self): + self.backend = backend.DummyBackend.start().proxy() + self.mixer = mixer.DummyMixer.start().proxy() + self.dispatcher = dispatcher.MpdDispatcher() + + self.connection = MockConnetion() + self.session = mpd.MpdSession(self.connection) + + def tearDown(self): + self.backend.stop().get() + self.mixer.stop().get() + + def sendRequest(self, request, clear=False): + self.connection.response = [] + self.session.on_line_received(request) + + def assertResponse(self, value, index=None): + if index is not None: + self.assertEqual(value, self.connection.response[index]) + else: + self.assert_(value in self.connection.response) diff --git a/tests/frontends/mpd/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py similarity index 55% rename from tests/frontends/mpd/playback_test.py rename to tests/frontends/mpd/protocol/playback_test.py index e80943d6..b9129688 100644 --- a/tests/frontends/mpd/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,163 +1,150 @@ -import unittest - from mopidy.backends.base import PlaybackController -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track from tests import SkipTest +from tests.frontends.mpd import protocol PAUSED = PlaybackController.PAUSED PLAYING = PlaybackController.PLAYING STOPPED = PlaybackController.STOPPED -class PlaybackOptionsHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() +class PlaybackOptionsHandlerTest(protocol.BaseTestCase): def test_consume_off(self): - result = self.dispatcher.handle_request(u'consume "0"') + self.sendRequest(u'consume "0"') self.assertFalse(self.backend.playback.consume.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_consume_off_without_quotes(self): - result = self.dispatcher.handle_request(u'consume 0') + self.sendRequest(u'consume 0') self.assertFalse(self.backend.playback.consume.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_consume_on(self): - result = self.dispatcher.handle_request(u'consume "1"') + self.sendRequest(u'consume "1"') self.assertTrue(self.backend.playback.consume.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_consume_on_without_quotes(self): - result = self.dispatcher.handle_request(u'consume 1') + self.sendRequest(u'consume 1') self.assertTrue(self.backend.playback.consume.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_crossfade(self): - result = self.dispatcher.handle_request(u'crossfade "10"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'crossfade "10"') + self.assertResponse(u'ACK [0@0] {} Not implemented') def test_random_off(self): - result = self.dispatcher.handle_request(u'random "0"') + self.sendRequest(u'random "0"') self.assertFalse(self.backend.playback.random.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_random_off_without_quotes(self): - result = self.dispatcher.handle_request(u'random 0') + self.sendRequest(u'random 0') self.assertFalse(self.backend.playback.random.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_random_on(self): - result = self.dispatcher.handle_request(u'random "1"') + self.sendRequest(u'random "1"') self.assertTrue(self.backend.playback.random.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_random_on_without_quotes(self): - result = self.dispatcher.handle_request(u'random 1') + self.sendRequest(u'random 1') self.assertTrue(self.backend.playback.random.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_repeat_off(self): - result = self.dispatcher.handle_request(u'repeat "0"') + self.sendRequest(u'repeat "0"') self.assertFalse(self.backend.playback.repeat.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_repeat_off_without_quotes(self): - result = self.dispatcher.handle_request(u'repeat 0') + self.sendRequest(u'repeat 0') self.assertFalse(self.backend.playback.repeat.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_repeat_on(self): - result = self.dispatcher.handle_request(u'repeat "1"') + self.sendRequest(u'repeat "1"') self.assertTrue(self.backend.playback.repeat.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_repeat_on_without_quotes(self): - result = self.dispatcher.handle_request(u'repeat 1') + self.sendRequest(u'repeat 1') self.assertTrue(self.backend.playback.repeat.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_setvol_below_min(self): - result = self.dispatcher.handle_request(u'setvol "-10"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "-10"') self.assertEqual(0, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_min(self): - result = self.dispatcher.handle_request(u'setvol "0"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "0"') self.assertEqual(0, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_middle(self): - result = self.dispatcher.handle_request(u'setvol "50"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "50"') self.assertEqual(50, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_max(self): - result = self.dispatcher.handle_request(u'setvol "100"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "100"') self.assertEqual(100, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_above_max(self): - result = self.dispatcher.handle_request(u'setvol "110"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "110"') self.assertEqual(100, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_plus_is_ignored(self): - result = self.dispatcher.handle_request(u'setvol "+10"') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol "+10"') self.assertEqual(10, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_setvol_without_quotes(self): - result = self.dispatcher.handle_request(u'setvol 50') - self.assert_(u'OK' in result) + self.sendRequest(u'setvol 50') self.assertEqual(50, self.mixer.volume.get()) + self.assertResponse(u'OK') def test_single_off(self): - result = self.dispatcher.handle_request(u'single "0"') + self.sendRequest(u'single "0"') self.assertFalse(self.backend.playback.single.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_single_off_without_quotes(self): - result = self.dispatcher.handle_request(u'single 0') + self.sendRequest(u'single 0') self.assertFalse(self.backend.playback.single.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_single_on(self): - result = self.dispatcher.handle_request(u'single "1"') + self.sendRequest(u'single "1"') self.assertTrue(self.backend.playback.single.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_single_on_without_quotes(self): - result = self.dispatcher.handle_request(u'single 1') + self.sendRequest(u'single 1') self.assertTrue(self.backend.playback.single.get()) - self.assert_(u'OK' in result) + self.assertResponse(u'OK') def test_replay_gain_mode_off(self): - result = self.dispatcher.handle_request(u'replay_gain_mode "off"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'replay_gain_mode "off"') + self.assertResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_mode_track(self): - result = self.dispatcher.handle_request(u'replay_gain_mode "track"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'replay_gain_mode "track"') + self.assertResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_mode_album(self): - result = self.dispatcher.handle_request(u'replay_gain_mode "album"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'replay_gain_mode "album"') + self.assertResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_status_default(self): - expected = u'off' - result = self.dispatcher.handle_request(u'replay_gain_status') - self.assert_(u'OK' in result) - self.assert_(expected in result) + self.sendRequest(u'replay_gain_status') + self.assertResponse(u'OK') + self.assertResponse(u'off') def test_replay_gain_status_off(self): raise SkipTest # TODO @@ -169,79 +156,80 @@ class PlaybackOptionsHandlerTest(unittest.TestCase): raise SkipTest # TODO -class PlaybackControlHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - +class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_next(self): - result = self.dispatcher.handle_request(u'next') - self.assert_(u'OK' in result) + self.sendRequest(u'next') + self.assertResponse(u'OK') def test_pause_off(self): self.backend.current_playlist.append([Track()]) - self.dispatcher.handle_request(u'play "0"') - self.dispatcher.handle_request(u'pause "1"') - result = self.dispatcher.handle_request(u'pause "0"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "0"') + self.sendRequest(u'pause "1"') + self.sendRequest(u'pause "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_pause_on(self): self.backend.current_playlist.append([Track()]) - self.dispatcher.handle_request(u'play "0"') - result = self.dispatcher.handle_request(u'pause "1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "0"') + self.sendRequest(u'pause "1"') self.assertEqual(PAUSED, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_pause_toggle(self): self.backend.current_playlist.append([Track()]) - result = self.dispatcher.handle_request(u'play "0"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'pause') - self.assert_(u'OK' in result) + self.assertResponse(u'OK') + + self.sendRequest(u'pause') self.assertEqual(PAUSED, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'pause') - self.assert_(u'OK' in result) + self.assertResponse(u'OK') + + self.sendRequest(u'pause') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_play_without_pos(self): self.backend.current_playlist.append([Track()]) self.backend.playback.state = PAUSED - result = self.dispatcher.handle_request(u'play') - self.assert_(u'OK' in result) + + self.sendRequest(u'play') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_play_with_pos(self): self.backend.current_playlist.append([Track()]) - result = self.dispatcher.handle_request(u'play "0"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_play_with_pos_without_quotes(self): self.backend.current_playlist.append([Track()]) - result = self.dispatcher.handle_request(u'play 0') - self.assert_(u'OK' in result) + + self.sendRequest(u'play 0') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_play_with_pos_out_of_bounds(self): self.backend.current_playlist.append([]) - result = self.dispatcher.handle_request(u'play "0"') - self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index') + + self.sendRequest(u'play "0"') self.assertEqual(STOPPED, self.backend.playback.state.get()) + self.assertResponse(u'ACK [2@0] {play} Bad song index', index=0) def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.backend.playback.current_track.get(), None) self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) - result = self.dispatcher.handle_request(u'play "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get().uri, 'a') + self.assertEqual('a', self.backend.playback.current_track.get().uri) + self.assertResponse(u'OK') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -250,27 +238,30 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.backend.playback.next() self.backend.playback.stop() self.assertNotEqual(self.backend.playback.current_track.get(), None) - result = self.dispatcher.handle_request(u'play "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get().uri, 'b') + self.assertEqual('b', self.backend.playback.current_track.get().uri) + self.assertResponse(u'OK') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.backend.current_playlist.clear() - result = self.dispatcher.handle_request(u'play "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "-1"') self.assertEqual(STOPPED, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get(), None) + self.assertEqual(None, self.backend.playback.current_track.get()) + self.assertResponse(u'OK') def test_play_minus_is_ignored_if_playing(self): self.backend.current_playlist.append([Track(length=40000)]) self.backend.playback.seek(30000) self.assert_(self.backend.playback.time_position.get() >= 30000) self.assertEquals(PLAYING, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'play "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_play_minus_one_resumes_if_paused(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -279,24 +270,27 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEquals(PLAYING, self.backend.playback.state.get()) self.backend.playback.pause() self.assertEquals(PAUSED, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'play "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_playid(self): self.backend.current_playlist.append([Track()]) - result = self.dispatcher.handle_request(u'playid "0"') - self.assert_(u'OK' in result) + + self.sendRequest(u'playid "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) + self.assertResponse(u'OK') def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.backend.playback.current_track.get(), None) self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) - result = self.dispatcher.handle_request(u'playid "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get().uri, 'a') + self.assertEqual('a', self.backend.playback.current_track.get().uri) + self.assertResponse(u'OK') def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -304,28 +298,31 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.backend.playback.play() self.backend.playback.next() self.backend.playback.stop() - self.assertNotEqual(self.backend.playback.current_track.get(), None) - result = self.dispatcher.handle_request(u'playid "-1"') - self.assert_(u'OK' in result) + self.assertNotEqual(None, self.backend.playback.current_track.get()) + + self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get().uri, 'b') + self.assertEqual('b', self.backend.playback.current_track.get().uri) + self.assertResponse(u'OK') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.backend.current_playlist.clear() - result = self.dispatcher.handle_request(u'playid "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'playid "-1"') self.assertEqual(STOPPED, self.backend.playback.state.get()) - self.assertEqual(self.backend.playback.current_track.get(), None) + self.assertEqual(None, self.backend.playback.current_track.get()) + self.assertResponse(u'OK') def test_playid_minus_is_ignored_if_playing(self): self.backend.current_playlist.append([Track(length=40000)]) self.backend.playback.seek(30000) self.assert_(self.backend.playback.time_position.get() >= 30000) self.assertEquals(PLAYING, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'playid "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_playid_minus_one_resumes_if_paused(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -334,58 +331,64 @@ class PlaybackControlHandlerTest(unittest.TestCase): self.assertEquals(PLAYING, self.backend.playback.state.get()) self.backend.playback.pause() self.assertEquals(PAUSED, self.backend.playback.state.get()) - result = self.dispatcher.handle_request(u'playid "-1"') - self.assert_(u'OK' in result) + + self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_playid_which_does_not_exist(self): self.backend.current_playlist.append([Track()]) - result = self.dispatcher.handle_request(u'playid "12345"') - self.assertEqual(result[0], u'ACK [50@0] {playid} No such song') + + self.sendRequest(u'playid "12345"') + self.assertResponse(u'ACK [50@0] {playid} No such song', index=0) def test_previous(self): - result = self.dispatcher.handle_request(u'previous') - self.assert_(u'OK' in result) + self.sendRequest(u'previous') + self.assertResponse(u'OK') def test_seek(self): self.backend.current_playlist.append([Track(length=40000)]) - self.dispatcher.handle_request(u'seek "0"') - result = self.dispatcher.handle_request(u'seek "0" "30"') - self.assert_(u'OK' in result) + + self.sendRequest(u'seek "0"') + self.sendRequest(u'seek "0" "30"') self.assert_(self.backend.playback.time_position >= 30000) + self.assertResponse(u'OK') def test_seek_with_songpos(self): seek_track = Track(uri='2', length=40000) self.backend.current_playlist.append( [Track(uri='1', length=40000), seek_track]) - result = self.dispatcher.handle_request(u'seek "1" "30"') - self.assert_(u'OK' in result) + + self.sendRequest(u'seek "1" "30"') self.assertEqual(self.backend.playback.current_track.get(), seek_track) + self.assertResponse(u'OK') def test_seek_without_quotes(self): self.backend.current_playlist.append([Track(length=40000)]) - self.dispatcher.handle_request(u'seek 0') - result = self.dispatcher.handle_request(u'seek 0 30') - self.assert_(u'OK' in result) + + self.sendRequest(u'seek 0') + self.sendRequest(u'seek 0 30') self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_seekid(self): self.backend.current_playlist.append([Track(length=40000)]) - result = self.dispatcher.handle_request(u'seekid "0" "30"') - self.assert_(u'OK' in result) + self.sendRequest(u'seekid "0" "30"') self.assert_(self.backend.playback.time_position.get() >= 30000) + self.assertResponse(u'OK') def test_seekid_with_cpid(self): seek_track = Track(uri='2', length=40000) self.backend.current_playlist.append( [Track(length=40000), seek_track]) - result = self.dispatcher.handle_request(u'seekid "1" "30"') - self.assert_(u'OK' in result) - self.assertEqual(self.backend.playback.current_cpid.get(), 1) - self.assertEqual(self.backend.playback.current_track.get(), seek_track) + + self.sendRequest(u'seekid "1" "30"') + self.assertEqual(1, self.backend.playback.current_cpid.get()) + self.assertEqual(seek_track, self.backend.playback.current_track.get()) + self.assertResponse(u'OK') def test_stop(self): - result = self.dispatcher.handle_request(u'stop') - self.assert_(u'OK' in result) + self.sendRequest(u'stop') self.assertEqual(STOPPED, self.backend.playback.state.get()) + self.assertResponse(u'OK') From 3d1c47586ed17e9b0cd411299c29caf7fcafdc31 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 16:14:43 +0200 Subject: [PATCH 172/350] Match assertIn behaviour and rename helper to assertInResponse --- tests/frontends/mpd/protocol/__init__.py | 8 +- tests/frontends/mpd/protocol/playback_test.py | 116 +++++++++--------- 2 files changed, 61 insertions(+), 63 deletions(-) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index a3705c48..5b3eabf2 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -36,8 +36,6 @@ class BaseTestCase(unittest.TestCase): self.connection.response = [] self.session.on_line_received(request) - def assertResponse(self, value, index=None): - if index is not None: - self.assertEqual(value, self.connection.response[index]) - else: - self.assert_(value in self.connection.response) + def assertInResponse(self, value): + self.assert_(value in self.connection.response, u'Did not find %s ' + 'in %s' % (repr(value), repr(self.connection.response))) diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index b9129688..ce561108 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -13,138 +13,138 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): def test_consume_off(self): self.sendRequest(u'consume "0"') self.assertFalse(self.backend.playback.consume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_consume_off_without_quotes(self): self.sendRequest(u'consume 0') self.assertFalse(self.backend.playback.consume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_consume_on(self): self.sendRequest(u'consume "1"') self.assertTrue(self.backend.playback.consume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_consume_on_without_quotes(self): self.sendRequest(u'consume 1') self.assertTrue(self.backend.playback.consume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_crossfade(self): self.sendRequest(u'crossfade "10"') - self.assertResponse(u'ACK [0@0] {} Not implemented') + self.assertInResponse(u'ACK [0@0] {} Not implemented') def test_random_off(self): self.sendRequest(u'random "0"') self.assertFalse(self.backend.playback.random.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_random_off_without_quotes(self): self.sendRequest(u'random 0') self.assertFalse(self.backend.playback.random.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_random_on(self): self.sendRequest(u'random "1"') self.assertTrue(self.backend.playback.random.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_random_on_without_quotes(self): self.sendRequest(u'random 1') self.assertTrue(self.backend.playback.random.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_repeat_off(self): self.sendRequest(u'repeat "0"') self.assertFalse(self.backend.playback.repeat.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_repeat_off_without_quotes(self): self.sendRequest(u'repeat 0') self.assertFalse(self.backend.playback.repeat.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_repeat_on(self): self.sendRequest(u'repeat "1"') self.assertTrue(self.backend.playback.repeat.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_repeat_on_without_quotes(self): self.sendRequest(u'repeat 1') self.assertTrue(self.backend.playback.repeat.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_below_min(self): self.sendRequest(u'setvol "-10"') self.assertEqual(0, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_min(self): self.sendRequest(u'setvol "0"') self.assertEqual(0, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_middle(self): self.sendRequest(u'setvol "50"') self.assertEqual(50, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_max(self): self.sendRequest(u'setvol "100"') self.assertEqual(100, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_above_max(self): self.sendRequest(u'setvol "110"') self.assertEqual(100, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_plus_is_ignored(self): self.sendRequest(u'setvol "+10"') self.assertEqual(10, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_setvol_without_quotes(self): self.sendRequest(u'setvol 50') self.assertEqual(50, self.mixer.volume.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_single_off(self): self.sendRequest(u'single "0"') self.assertFalse(self.backend.playback.single.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_single_off_without_quotes(self): self.sendRequest(u'single 0') self.assertFalse(self.backend.playback.single.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_single_on(self): self.sendRequest(u'single "1"') self.assertTrue(self.backend.playback.single.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_single_on_without_quotes(self): self.sendRequest(u'single 1') self.assertTrue(self.backend.playback.single.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_replay_gain_mode_off(self): self.sendRequest(u'replay_gain_mode "off"') - self.assertResponse(u'ACK [0@0] {} Not implemented') + self.assertInResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_mode_track(self): self.sendRequest(u'replay_gain_mode "track"') - self.assertResponse(u'ACK [0@0] {} Not implemented') + self.assertInResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_mode_album(self): self.sendRequest(u'replay_gain_mode "album"') - self.assertResponse(u'ACK [0@0] {} Not implemented') + self.assertInResponse(u'ACK [0@0] {} Not implemented') def test_replay_gain_status_default(self): self.sendRequest(u'replay_gain_status') - self.assertResponse(u'OK') - self.assertResponse(u'off') + self.assertInResponse(u'OK') + self.assertInResponse(u'off') def test_replay_gain_status_off(self): raise SkipTest # TODO @@ -159,7 +159,7 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_next(self): self.sendRequest(u'next') - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_pause_off(self): self.backend.current_playlist.append([Track()]) @@ -168,7 +168,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'pause "1"') self.sendRequest(u'pause "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_pause_on(self): self.backend.current_playlist.append([Track()]) @@ -176,22 +176,22 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "0"') self.sendRequest(u'pause "1"') self.assertEqual(PAUSED, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_pause_toggle(self): self.backend.current_playlist.append([Track()]) self.sendRequest(u'play "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') self.sendRequest(u'pause') self.assertEqual(PAUSED, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') self.sendRequest(u'pause') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_without_pos(self): self.backend.current_playlist.append([Track()]) @@ -199,28 +199,28 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_with_pos(self): self.backend.current_playlist.append([Track()]) self.sendRequest(u'play "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_with_pos_without_quotes(self): self.backend.current_playlist.append([Track()]) self.sendRequest(u'play 0') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_with_pos_out_of_bounds(self): self.backend.current_playlist.append([]) self.sendRequest(u'play "0"') self.assertEqual(STOPPED, self.backend.playback.state.get()) - self.assertResponse(u'ACK [2@0] {play} Bad song index', index=0) + self.assertInResponse(u'ACK [2@0] {play} Bad song index') def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.backend.playback.current_track.get(), None) @@ -229,7 +229,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assertEqual('a', self.backend.playback.current_track.get().uri) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -242,7 +242,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assertEqual('b', self.backend.playback.current_track.get().uri) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.backend.current_playlist.clear() @@ -250,7 +250,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "-1"') self.assertEqual(STOPPED, self.backend.playback.state.get()) self.assertEqual(None, self.backend.playback.current_track.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_minus_is_ignored_if_playing(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -261,7 +261,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_play_minus_one_resumes_if_paused(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -274,14 +274,14 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'play "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid(self): self.backend.current_playlist.append([Track()]) self.sendRequest(u'playid "0"') self.assertEqual(PLAYING, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.backend.playback.current_track.get(), None) @@ -290,7 +290,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assertEqual('a', self.backend.playback.current_track.get().uri) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_minus_one_plays_current_track_if_current_track_is_set(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) @@ -303,7 +303,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assertEqual('b', self.backend.playback.current_track.get().uri) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.backend.current_playlist.clear() @@ -311,7 +311,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playid "-1"') self.assertEqual(STOPPED, self.backend.playback.state.get()) self.assertEqual(None, self.backend.playback.current_track.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_minus_is_ignored_if_playing(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -322,7 +322,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_minus_one_resumes_if_paused(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -335,17 +335,17 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'playid "-1"') self.assertEqual(PLAYING, self.backend.playback.state.get()) self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_playid_which_does_not_exist(self): self.backend.current_playlist.append([Track()]) self.sendRequest(u'playid "12345"') - self.assertResponse(u'ACK [50@0] {playid} No such song', index=0) + self.assertInResponse(u'ACK [50@0] {playid} No such song') def test_previous(self): self.sendRequest(u'previous') - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_seek(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -353,7 +353,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'seek "0"') self.sendRequest(u'seek "0" "30"') self.assert_(self.backend.playback.time_position >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_seek_with_songpos(self): seek_track = Track(uri='2', length=40000) @@ -362,7 +362,7 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'seek "1" "30"') self.assertEqual(self.backend.playback.current_track.get(), seek_track) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_seek_without_quotes(self): self.backend.current_playlist.append([Track(length=40000)]) @@ -370,13 +370,13 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'seek 0') self.sendRequest(u'seek 0 30') self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_seekid(self): self.backend.current_playlist.append([Track(length=40000)]) self.sendRequest(u'seekid "0" "30"') self.assert_(self.backend.playback.time_position.get() >= 30000) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_seekid_with_cpid(self): seek_track = Track(uri='2', length=40000) @@ -386,9 +386,9 @@ class PlaybackControlHandlerTest(protocol.BaseTestCase): self.sendRequest(u'seekid "1" "30"') self.assertEqual(1, self.backend.playback.current_cpid.get()) self.assertEqual(seek_track, self.backend.playback.current_track.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') def test_stop(self): self.sendRequest(u'stop') self.assertEqual(STOPPED, self.backend.playback.state.get()) - self.assertResponse(u'OK') + self.assertInResponse(u'OK') From 6d444362a15fb3b7b0f7c85d0e155783a3abe2b4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 16:25:16 +0200 Subject: [PATCH 173/350] Cleanup imports in playback_test --- tests/frontends/mpd/protocol/playback_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index ce561108..6f93dc72 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,12 +1,12 @@ -from mopidy.backends.base import PlaybackController +from mopidy.backends import base as backend from mopidy.models import Track from tests import SkipTest from tests.frontends.mpd import protocol -PAUSED = PlaybackController.PAUSED -PLAYING = PlaybackController.PLAYING -STOPPED = PlaybackController.STOPPED +PAUSED = backend.PlaybackController.PAUSED +PLAYING = backend.PlaybackController.PLAYING +STOPPED = backend.PlaybackController.STOPPED class PlaybackOptionsHandlerTest(protocol.BaseTestCase): From 523f5eb03a467d9c5b5feaf67a65fd766edc95a4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 16:29:39 +0200 Subject: [PATCH 174/350] Migrate audo_output_test --- tests/frontends/mpd/audio_output_test.py | 30 ------------------- .../mpd/protocol/audio_output_test.py | 17 +++++++++++ 2 files changed, 17 insertions(+), 30 deletions(-) delete mode 100644 tests/frontends/mpd/audio_output_test.py create mode 100644 tests/frontends/mpd/protocol/audio_output_test.py diff --git a/tests/frontends/mpd/audio_output_test.py b/tests/frontends/mpd/audio_output_test.py deleted file mode 100644 index 82d9e203..00000000 --- a/tests/frontends/mpd/audio_output_test.py +++ /dev/null @@ -1,30 +0,0 @@ -import unittest - -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer - -class AudioOutputHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_enableoutput(self): - result = self.dispatcher.handle_request(u'enableoutput "0"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_disableoutput(self): - result = self.dispatcher.handle_request(u'disableoutput "0"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_outputs(self): - result = self.dispatcher.handle_request(u'outputs') - self.assert_(u'outputid: 0' in result) - self.assert_(u'outputname: None' in result) - self.assert_(u'outputenabled: 1' in result) - self.assert_(u'OK' in result) diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py new file mode 100644 index 00000000..f9374159 --- /dev/null +++ b/tests/frontends/mpd/protocol/audio_output_test.py @@ -0,0 +1,17 @@ +from tests.frontends.mpd import protocol + +class AudioOutputHandlerTest(protocol.BaseTestCase): + def test_enableoutput(self): + self.sendRequest(u'enableoutput "0"') + self.assertInResponse(u'ACK [0@0] {} Not implemented') + + def test_disableoutput(self): + self.sendRequest(u'disableoutput "0"') + self.assertInResponse(u'ACK [0@0] {} Not implemented') + + def test_outputs(self): + self.sendRequest(u'outputs') + self.assertInResponse(u'outputid: 0') + self.assertInResponse(u'outputname: None') + self.assertInResponse(u'outputenabled: 1') + self.assertInResponse(u'OK') From c4a1692d92290a61df87439f392cfccf1ba68b27 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 17:32:33 +0200 Subject: [PATCH 175/350] Migrate current_playlist_test --- tests/frontends/mpd/protocol/__init__.py | 12 +- .../{ => protocol}/current_playlist_test.py | 351 +++++++++--------- 2 files changed, 192 insertions(+), 171 deletions(-) rename tests/frontends/mpd/{ => protocol}/current_playlist_test.py (59%) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 5b3eabf2..fd56c32e 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -16,7 +16,8 @@ class MockConnetion(mock.Mock): self.response = [] def send(self, data): - self.response.extend(data.split('\n')) + lines = (line for line in data.split('\n') if line) + self.response.extend(lines) class BaseTestCase(unittest.TestCase): @@ -35,7 +36,16 @@ class BaseTestCase(unittest.TestCase): def sendRequest(self, request, clear=False): self.connection.response = [] self.session.on_line_received(request) + return self.connection.response def assertInResponse(self, value): self.assert_(value in self.connection.response, u'Did not find %s ' 'in %s' % (repr(value), repr(self.connection.response))) + + def assertNotInResponse(self, value): + self.assert_(value not in self.connection.response, u'Found %s in %s' % + (repr(value), repr(self.connection.response))) + + def assertEqualResponse(self, value): + self.assertEqual(1, len(self.connection.response)) + self.assertEqual(value, self.connection.response[0]) diff --git a/tests/frontends/mpd/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py similarity index 59% rename from tests/frontends/mpd/current_playlist_test.py rename to tests/frontends/mpd/protocol/current_playlist_test.py index c7f47429..1b0ae404 100644 --- a/tests/frontends/mpd/current_playlist_test.py +++ b/tests/frontends/mpd/protocol/current_playlist_test.py @@ -1,20 +1,8 @@ -import unittest - -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Track -class CurrentPlaylistHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() +from tests.frontends.mpd import protocol +class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add(self): needle = Track(uri='dummy://foo') self.backend.library.provider.dummy_library = [ @@ -22,21 +10,21 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'add "dummy://foo"') - self.assertEqual(len(result), 1) - self.assertEqual(result[0], u'OK') + + self.sendRequest(u'add "dummy://foo"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6) self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle) + self.assertEqualResponse(u'OK') def test_add_with_uri_not_found_in_library_should_ack(self): - result = self.dispatcher.handle_request(u'add "dummy://foo"') - self.assertEqual(result[0], + self.sendRequest(u'add "dummy://foo"') + self.assertEqualResponse( u'ACK [50@0] {add} directory or file not found') def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self): - result = self.dispatcher.handle_request(u'add ""') + self.sendRequest(u'add ""') # TODO check that we add all tracks (we currently don't) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') @@ -45,16 +33,17 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'addid "dummy://foo"') + + self.sendRequest(u'addid "dummy://foo"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6) self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle) - self.assert_(u'Id: %d' % - self.backend.current_playlist.cp_tracks.get()[5][0] in result) - self.assert_(u'OK' in result) + self.assertInResponse(u'Id: %d' % + self.backend.current_playlist.cp_tracks.get()[5][0]) + self.assertInResponse(u'OK') def test_addid_with_empty_uri_acks(self): - result = self.dispatcher.handle_request(u'addid ""') - self.assertEqual(result[0], u'ACK [50@0] {addid} No such song') + self.sendRequest(u'addid ""') + self.assertEqualResponse(u'ACK [50@0] {addid} No such song') def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') @@ -63,12 +52,13 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"') + + self.sendRequest(u'addid "dummy://foo" "3"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6) self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle) - self.assert_(u'Id: %d' % - self.backend.current_playlist.cp_tracks.get()[3][0] in result) - self.assert_(u'OK' in result) + self.assertInResponse(u'Id: %d' % + self.backend.current_playlist.cp_tracks.get()[3][0]) + self.assertInResponse(u'OK') def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') @@ -77,83 +67,93 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"') - self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index') + + self.sendRequest(u'addid "dummy://foo" "6"') + self.assertEqualResponse(u'ACK [2@0] {addid} Bad song index') def test_addid_with_uri_not_found_in_library_should_ack(self): - result = self.dispatcher.handle_request(u'addid "dummy://foo"') - self.assertEqual(result[0], u'ACK [50@0] {addid} No such song') + self.sendRequest(u'addid "dummy://foo"') + self.assertEqualResponse(u'ACK [50@0] {addid} No such song') def test_clear(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'clear') + + self.sendRequest(u'clear') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0) self.assertEqual(self.backend.playback.current_track.get(), None) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_delete_songpos(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'delete "%d"' % + + self.sendRequest(u'delete "%d"' % self.backend.current_playlist.cp_tracks.get()[2][0]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_delete_songpos_out_of_bounds(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'delete "5"') + + self.sendRequest(u'delete "5"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') + self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'delete "1:"') + + self.sendRequest(u'delete "1:"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_delete_closed_range(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'delete "1:3"') + + self.sendRequest(u'delete "1:3"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_delete_range_out_of_bounds(self): self.backend.current_playlist.append( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - result = self.dispatcher.handle_request(u'delete "5:7"') + + self.sendRequest(u'delete "5:7"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5) - self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index') + self.assertEqualResponse(u'ACK [2@0] {delete} Bad song index') def test_deleteid(self): self.backend.current_playlist.append([Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2) - result = self.dispatcher.handle_request(u'deleteid "1"') + + self.sendRequest(u'deleteid "1"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_deleteid_does_not_exist(self): self.backend.current_playlist.append([Track(), Track()]) self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2) - result = self.dispatcher.handle_request(u'deleteid "12345"') + + self.sendRequest(u'deleteid "12345"') self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2) - self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song') + self.assertEqualResponse(u'ACK [50@0] {deleteid} No such song') def test_move_songpos(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'move "1" "0"') + + self.sendRequest(u'move "1" "0"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[1].name, 'a') @@ -161,14 +161,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[5].name, 'f') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_move_open_range(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'move "2:" "0"') + + self.sendRequest(u'move "2:" "0"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'c') self.assertEqual(tracks[1].name, 'd') @@ -176,14 +177,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'f') self.assertEqual(tracks[4].name, 'a') self.assertEqual(tracks[5].name, 'b') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_move_closed_range(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'move "1:3" "0"') + + self.sendRequest(u'move "1:3" "0"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[1].name, 'c') @@ -191,14 +193,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[5].name, 'f') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_moveid(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'moveid "4" "2"') + + self.sendRequest(u'moveid "4" "2"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'b') @@ -206,179 +209,182 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'c') self.assertEqual(tracks[4].name, 'd') self.assertEqual(tracks[5].name, 'f') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_playlist_returns_same_as_playlistinfo(self): - playlist_result = self.dispatcher.handle_request(u'playlist') - playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo') - self.assertEqual(playlist_result, playlistinfo_result) + playlist_response = self.sendRequest(u'playlist') + playlistinfo_response = self.sendRequest(u'playlistinfo') + self.assertEqual(playlist_response, playlistinfo_response) def test_playlistfind(self): - result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'playlistfind "tag" "needle"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') def test_playlistfind_by_filename_not_in_current_playlist(self): - result = self.dispatcher.handle_request( - u'playlistfind "filename" "file:///dev/null"') - self.assertEqual(len(result), 1) - self.assert_(u'OK' in result) + self.sendRequest(u'playlistfind "filename" "file:///dev/null"') + self.assertEqualResponse(u'OK') def test_playlistfind_by_filename_without_quotes(self): - result = self.dispatcher.handle_request( - u'playlistfind filename "file:///dev/null"') - self.assertEqual(len(result), 1) - self.assert_(u'OK' in result) + self.sendRequest(u'playlistfind filename "file:///dev/null"') + self.assertEqualResponse(u'OK') def test_playlistfind_by_filename_in_current_playlist(self): self.backend.current_playlist.append([ Track(uri='file:///exists')]) - result = self.dispatcher.handle_request( - u'playlistfind filename "file:///exists"') - self.assert_(u'file: file:///exists' in result) - self.assert_(u'Id: 0' in result) - self.assert_(u'Pos: 0' in result) - self.assert_(u'OK' in result) + + self.sendRequest( u'playlistfind filename "file:///exists"') + self.assertInResponse(u'file: file:///exists') + self.assertInResponse(u'Id: 0') + self.assertInResponse(u'Pos: 0') + self.assertInResponse(u'OK') def test_playlistid_without_songid(self): self.backend.current_playlist.append([Track(name='a'), Track(name='b')]) - result = self.dispatcher.handle_request(u'playlistid') - self.assert_(u'Title: a' in result) - self.assert_(u'Title: b' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistid') + self.assertInResponse(u'Title: a') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'OK') def test_playlistid_with_songid(self): self.backend.current_playlist.append([Track(name='a'), Track(name='b')]) - result = self.dispatcher.handle_request(u'playlistid "1"') - self.assert_(u'Title: a' not in result) - self.assert_(u'Id: 0' not in result) - self.assert_(u'Title: b' in result) - self.assert_(u'Id: 1' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistid "1"') + self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Id: 0') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'Id: 1') + self.assertInResponse(u'OK') def test_playlistid_with_not_existing_songid_fails(self): self.backend.current_playlist.append([Track(name='a'), Track(name='b')]) - result = self.dispatcher.handle_request(u'playlistid "25"') - self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song') + + self.sendRequest(u'playlistid "25"') + self.assertEqualResponse(u'ACK [50@0] {playlistid} No such song') def test_playlistinfo_without_songpos_or_range(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'playlistinfo') - self.assert_(u'Title: a' in result) - self.assert_(u'Title: b' in result) - self.assert_(u'Title: c' in result) - self.assert_(u'Title: d' in result) - self.assert_(u'Title: e' in result) - self.assert_(u'Title: f' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistinfo') + self.assertInResponse(u'Title: a') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'Title: d') + self.assertInResponse(u'Title: e') + self.assertInResponse(u'Title: f') + self.assertInResponse(u'OK') def test_playlistinfo_with_songpos(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'playlistinfo "4"') - self.assert_(u'Title: a' not in result) - self.assert_(u'Title: b' not in result) - self.assert_(u'Title: c' not in result) - self.assert_(u'Title: d' not in result) - self.assert_(u'Title: e' in result) - self.assert_(u'Title: f' not in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistinfo "4"') + self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Title: b') + self.assertNotInResponse(u'Title: c') + self.assertNotInResponse(u'Title: d') + self.assertInResponse(u'Title: e') + self.assertNotInResponse(u'Title: f') + self.assertInResponse(u'OK') def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self): - result1 = self.dispatcher.handle_request(u'playlistinfo "-1"') - result2 = self.dispatcher.handle_request(u'playlistinfo') - self.assertEqual(result1, result2) + response1 = self.sendRequest(u'playlistinfo "-1"') + response2 = self.sendRequest(u'playlistinfo') + self.assertEqual(response1, response2) def test_playlistinfo_with_open_range(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'playlistinfo "2:"') - self.assert_(u'Title: a' not in result) - self.assert_(u'Title: b' not in result) - self.assert_(u'Title: c' in result) - self.assert_(u'Title: d' in result) - self.assert_(u'Title: e' in result) - self.assert_(u'Title: f' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistinfo "2:"') + self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'Title: d') + self.assertInResponse(u'Title: e') + self.assertInResponse(u'Title: f') + self.assertInResponse(u'OK') def test_playlistinfo_with_closed_range(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'playlistinfo "2:4"') - self.assert_(u'Title: a' not in result) - self.assert_(u'Title: b' not in result) - self.assert_(u'Title: c' in result) - self.assert_(u'Title: d' in result) - self.assert_(u'Title: e' not in result) - self.assert_(u'Title: f' not in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'playlistinfo "2:4"') + self.assertNotInResponse(u'Title: a') + self.assertNotInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'Title: d') + self.assertNotInResponse(u'Title: e') + self.assertNotInResponse(u'Title: f') + self.assertInResponse(u'OK') def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self): - result = self.dispatcher.handle_request(u'playlistinfo "10:20"') - self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result) + self.sendRequest(u'playlistinfo "10:20"') + self.assertEqualResponse(u'ACK [2@0] {playlistinfo} Bad song index') def test_playlistinfo_with_too_high_end_of_range_returns_ok(self): - result = self.dispatcher.handle_request(u'playlistinfo "0:20"') - self.assert_(u'OK' in result) + self.sendRequest(u'playlistinfo "0:20"') + self.assertInResponse(u'OK') def test_playlistsearch(self): - result = self.dispatcher.handle_request( - u'playlistsearch "any" "needle"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest( u'playlistsearch "any" "needle"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') def test_playlistsearch_without_quotes(self): - result = self.dispatcher.handle_request(u'playlistsearch any "needle"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) + self.sendRequest(u'playlistsearch any "needle"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') def test_plchanges(self): self.backend.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) - result = self.dispatcher.handle_request(u'plchanges "0"') - self.assert_(u'Title: a' in result) - self.assert_(u'Title: b' in result) - self.assert_(u'Title: c' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'plchanges "0"') + self.assertInResponse(u'Title: a') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'OK') def test_plchanges_with_minus_one_returns_entire_playlist(self): self.backend.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) - result = self.dispatcher.handle_request(u'plchanges "-1"') - self.assert_(u'Title: a' in result) - self.assert_(u'Title: b' in result) - self.assert_(u'Title: c' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'plchanges "-1"') + self.assertInResponse(u'Title: a') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'OK') def test_plchanges_without_quotes_works(self): self.backend.current_playlist.append( [Track(name='a'), Track(name='b'), Track(name='c')]) - result = self.dispatcher.handle_request(u'plchanges 0') - self.assert_(u'Title: a' in result) - self.assert_(u'Title: b' in result) - self.assert_(u'Title: c' in result) - self.assert_(u'OK' in result) + + self.sendRequest(u'plchanges 0') + self.assertInResponse(u'Title: a') + self.assertInResponse(u'Title: b') + self.assertInResponse(u'Title: c') + self.assertInResponse(u'OK') def test_plchangesposid(self): self.backend.current_playlist.append([Track(), Track(), Track()]) - result = self.dispatcher.handle_request(u'plchangesposid "0"') + + self.sendRequest(u'plchangesposid "0"') cp_tracks = self.backend.current_playlist.cp_tracks.get() - self.assert_(u'cpos: 0' in result) - self.assert_(u'Id: %d' % cp_tracks[0][0] - in result) - self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % cp_tracks[1][0] - in result) - self.assert_(u'cpos: 2' in result) - self.assert_(u'Id: %d' % cp_tracks[2][0] - in result) - self.assert_(u'OK' in result) + self.assertInResponse(u'cpos: 0') + self.assertInResponse(u'Id: %d' % cp_tracks[0][0]) + self.assertInResponse(u'cpos: 2') + self.assertInResponse(u'Id: %d' % cp_tracks[1][0]) + self.assertInResponse(u'cpos: 2') + self.assertInResponse(u'Id: %d' % cp_tracks[2][0]) + self.assertInResponse(u'OK') def test_shuffle_without_range(self): self.backend.current_playlist.append([ @@ -386,9 +392,10 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.backend.current_playlist.version.get() - result = self.dispatcher.handle_request(u'shuffle') + + self.sendRequest(u'shuffle') self.assert_(version < self.backend.current_playlist.version.get()) - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_shuffle_with_open_range(self): self.backend.current_playlist.append([ @@ -396,14 +403,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.backend.current_playlist.version.get() - result = self.dispatcher.handle_request(u'shuffle "4:"') + + self.sendRequest(u'shuffle "4:"') self.assert_(version < self.backend.current_playlist.version.get()) tracks = self.backend.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) + self.assertInResponse(u'OK') def test_shuffle_with_closed_range(self): self.backend.current_playlist.append([ @@ -411,21 +419,23 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.backend.current_playlist.version.get() - result = self.dispatcher.handle_request(u'shuffle "1:3"') + + self.sendRequest(u'shuffle "1:3"') self.assert_(version < self.backend.current_playlist.version.get()) tracks = self.backend.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) + self.assertInResponse(u'OK') def test_swap(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'swap "1" "4"') + + self.sendRequest(u'swap "1" "4"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'e') @@ -433,14 +443,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'b') self.assertEqual(tracks[5].name, 'f') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') def test_swapid(self): self.backend.current_playlist.append([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) - result = self.dispatcher.handle_request(u'swapid "1" "4"') + + self.sendRequest(u'swapid "1" "4"') tracks = self.backend.current_playlist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'e') @@ -448,4 +459,4 @@ class CurrentPlaylistHandlerTest(unittest.TestCase): self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'b') self.assertEqual(tracks[5].name, 'f') - self.assert_(u'OK' in result) + self.assertInResponse(u'OK') From 93d2aa824074fd9b4c5be5fd11f2b795ae9fbb87 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 17:53:24 +0200 Subject: [PATCH 176/350] Migrate music_db_test.py --- tests/frontends/mpd/music_db_test.py | 412 ------------------ tests/frontends/mpd/protocol/music_db_test.py | 343 +++++++++++++++ 2 files changed, 343 insertions(+), 412 deletions(-) delete mode 100644 tests/frontends/mpd/music_db_test.py create mode 100644 tests/frontends/mpd/protocol/music_db_test.py diff --git a/tests/frontends/mpd/music_db_test.py b/tests/frontends/mpd/music_db_test.py deleted file mode 100644 index 3793db9e..00000000 --- a/tests/frontends/mpd/music_db_test.py +++ /dev/null @@ -1,412 +0,0 @@ -import unittest - -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer - -class MusicDatabaseHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_count(self): - result = self.dispatcher.handle_request(u'count "tag" "needle"') - self.assert_(u'songs: 0' in result) - self.assert_(u'playtime: 0' in result) - self.assert_(u'OK' in result) - - def test_findadd(self): - result = self.dispatcher.handle_request(u'findadd "album" "what"') - self.assert_(u'OK' in result) - - def test_listall(self): - result = self.dispatcher.handle_request( - u'listall "file:///dev/urandom"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_listallinfo(self): - result = self.dispatcher.handle_request( - u'listallinfo "file:///dev/urandom"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_lsinfo_without_path_returns_same_as_listplaylists(self): - lsinfo_result = self.dispatcher.handle_request(u'lsinfo') - listplaylists_result = self.dispatcher.handle_request(u'listplaylists') - self.assertEqual(lsinfo_result, listplaylists_result) - - def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): - lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""') - listplaylists_result = self.dispatcher.handle_request(u'listplaylists') - self.assertEqual(lsinfo_result, listplaylists_result) - - def test_lsinfo_for_root_returns_same_as_listplaylists(self): - lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"') - listplaylists_result = self.dispatcher.handle_request(u'listplaylists') - self.assertEqual(lsinfo_result, listplaylists_result) - - def test_update_without_uri(self): - result = self.dispatcher.handle_request(u'update') - self.assert_(u'OK' in result) - self.assert_(u'updating_db: 0' in result) - - def test_update_with_uri(self): - result = self.dispatcher.handle_request(u'update "file:///dev/urandom"') - self.assert_(u'OK' in result) - self.assert_(u'updating_db: 0' in result) - - def test_rescan_without_uri(self): - result = self.dispatcher.handle_request(u'rescan') - self.assert_(u'OK' in result) - self.assert_(u'updating_db: 0' in result) - - def test_rescan_with_uri(self): - result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"') - self.assert_(u'OK' in result) - self.assert_(u'updating_db: 0' in result) - - -class MusicDatabaseFindTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_find_album(self): - result = self.dispatcher.handle_request(u'find "album" "what"') - self.assert_(u'OK' in result) - - def test_find_album_without_quotes(self): - result = self.dispatcher.handle_request(u'find album "what"') - self.assert_(u'OK' in result) - - def test_find_artist(self): - result = self.dispatcher.handle_request(u'find "artist" "what"') - self.assert_(u'OK' in result) - - def test_find_artist_without_quotes(self): - result = self.dispatcher.handle_request(u'find artist "what"') - self.assert_(u'OK' in result) - - def test_find_title(self): - result = self.dispatcher.handle_request(u'find "title" "what"') - self.assert_(u'OK' in result) - - def test_find_title_without_quotes(self): - result = self.dispatcher.handle_request(u'find title "what"') - self.assert_(u'OK' in result) - - def test_find_date(self): - result = self.dispatcher.handle_request(u'find "date" "2002-01-01"') - self.assert_(u'OK' in result) - - def test_find_date_without_quotes(self): - result = self.dispatcher.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.dispatcher.handle_request(u'find Date "2005"') - self.assert_(u'OK' in result) - - def test_find_else_should_fail(self): - - result = self.dispatcher.handle_request(u'find "somethingelse" "what"') - self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments') - - def test_find_album_and_artist(self): - result = self.dispatcher.handle_request( - u'find album "album_what" artist "artist_what"') - self.assert_(u'OK' in result) - - -class MusicDatabaseListTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_list_foo_returns_ack(self): - result = self.dispatcher.handle_request(u'list "foo"') - self.assertEqual(result[0], - u'ACK [2@0] {list} incorrect arguments') - - ### Artist - - def test_list_artist_with_quotes(self): - result = self.dispatcher.handle_request(u'list "artist"') - self.assert_(u'OK' in result) - - def test_list_artist_without_quotes(self): - result = self.dispatcher.handle_request(u'list artist') - self.assert_(u'OK' in result) - - def test_list_artist_without_quotes_and_capitalized(self): - result = self.dispatcher.handle_request(u'list Artist') - self.assert_(u'OK' in result) - - def test_list_artist_with_query_of_one_token(self): - result = self.dispatcher.handle_request(u'list "artist" "anartist"') - self.assertEqual(result[0], - u'ACK [2@0] {list} should be "Album" for 3 arguments') - - def test_list_artist_with_unknown_field_in_query_returns_ack(self): - result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"') - self.assertEqual(result[0], - u'ACK [2@0] {list} not able to parse args') - - def test_list_artist_by_artist(self): - result = self.dispatcher.handle_request( - u'list "artist" "artist" "anartist"') - self.assert_(u'OK' in result) - - def test_list_artist_by_album(self): - result = self.dispatcher.handle_request( - u'list "artist" "album" "analbum"') - self.assert_(u'OK' in result) - - def test_list_artist_by_full_date(self): - result = self.dispatcher.handle_request( - u'list "artist" "date" "2001-01-01"') - self.assert_(u'OK' in result) - - def test_list_artist_by_year(self): - result = self.dispatcher.handle_request( - u'list "artist" "date" "2001"') - self.assert_(u'OK' in result) - - def test_list_artist_by_genre(self): - result = self.dispatcher.handle_request( - u'list "artist" "genre" "agenre"') - self.assert_(u'OK' in result) - - def test_list_artist_by_artist_and_album(self): - result = self.dispatcher.handle_request( - u'list "artist" "artist" "anartist" "album" "analbum"') - self.assert_(u'OK' in result) - - ### Album - - def test_list_album_with_quotes(self): - result = self.dispatcher.handle_request(u'list "album"') - self.assert_(u'OK' in result) - - def test_list_album_without_quotes(self): - result = self.dispatcher.handle_request(u'list album') - self.assert_(u'OK' in result) - - def test_list_album_without_quotes_and_capitalized(self): - result = self.dispatcher.handle_request(u'list Album') - self.assert_(u'OK' in result) - - def test_list_album_with_artist_name(self): - result = self.dispatcher.handle_request(u'list "album" "anartist"') - self.assert_(u'OK' in result) - - def test_list_album_by_artist(self): - result = self.dispatcher.handle_request( - u'list "album" "artist" "anartist"') - self.assert_(u'OK' in result) - - def test_list_album_by_album(self): - result = self.dispatcher.handle_request( - u'list "album" "album" "analbum"') - self.assert_(u'OK' in result) - - def test_list_album_by_full_date(self): - result = self.dispatcher.handle_request( - u'list "album" "date" "2001-01-01"') - self.assert_(u'OK' in result) - - def test_list_album_by_year(self): - result = self.dispatcher.handle_request( - u'list "album" "date" "2001"') - self.assert_(u'OK' in result) - - def test_list_album_by_genre(self): - result = self.dispatcher.handle_request( - u'list "album" "genre" "agenre"') - self.assert_(u'OK' in result) - - def test_list_album_by_artist_and_album(self): - result = self.dispatcher.handle_request( - u'list "album" "artist" "anartist" "album" "analbum"') - self.assert_(u'OK' in result) - - ### Date - - def test_list_date_with_quotes(self): - result = self.dispatcher.handle_request(u'list "date"') - self.assert_(u'OK' in result) - - def test_list_date_without_quotes(self): - result = self.dispatcher.handle_request(u'list date') - self.assert_(u'OK' in result) - - def test_list_date_without_quotes_and_capitalized(self): - result = self.dispatcher.handle_request(u'list Date') - self.assert_(u'OK' in result) - - def test_list_date_with_query_of_one_token(self): - result = self.dispatcher.handle_request(u'list "date" "anartist"') - self.assertEqual(result[0], - u'ACK [2@0] {list} should be "Album" for 3 arguments') - - def test_list_date_by_artist(self): - result = self.dispatcher.handle_request( - u'list "date" "artist" "anartist"') - self.assert_(u'OK' in result) - - def test_list_date_by_album(self): - result = self.dispatcher.handle_request( - u'list "date" "album" "analbum"') - self.assert_(u'OK' in result) - - def test_list_date_by_full_date(self): - result = self.dispatcher.handle_request( - u'list "date" "date" "2001-01-01"') - self.assert_(u'OK' in result) - - def test_list_date_by_year(self): - result = self.dispatcher.handle_request(u'list "date" "date" "2001"') - self.assert_(u'OK' in result) - - def test_list_date_by_genre(self): - result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"') - self.assert_(u'OK' in result) - - def test_list_date_by_artist_and_album(self): - result = self.dispatcher.handle_request( - u'list "date" "artist" "anartist" "album" "analbum"') - self.assert_(u'OK' in result) - - ### Genre - - def test_list_genre_with_quotes(self): - result = self.dispatcher.handle_request(u'list "genre"') - self.assert_(u'OK' in result) - - def test_list_genre_without_quotes(self): - result = self.dispatcher.handle_request(u'list genre') - self.assert_(u'OK' in result) - - def test_list_genre_without_quotes_and_capitalized(self): - result = self.dispatcher.handle_request(u'list Genre') - self.assert_(u'OK' in result) - - def test_list_genre_with_query_of_one_token(self): - result = self.dispatcher.handle_request(u'list "genre" "anartist"') - self.assertEqual(result[0], - u'ACK [2@0] {list} should be "Album" for 3 arguments') - - def test_list_genre_by_artist(self): - result = self.dispatcher.handle_request( - u'list "genre" "artist" "anartist"') - self.assert_(u'OK' in result) - - def test_list_genre_by_album(self): - result = self.dispatcher.handle_request( - u'list "genre" "album" "analbum"') - self.assert_(u'OK' in result) - - def test_list_genre_by_full_date(self): - result = self.dispatcher.handle_request( - u'list "genre" "date" "2001-01-01"') - self.assert_(u'OK' in result) - - def test_list_genre_by_year(self): - result = self.dispatcher.handle_request( - u'list "genre" "date" "2001"') - self.assert_(u'OK' in result) - - def test_list_genre_by_genre(self): - result = self.dispatcher.handle_request( - u'list "genre" "genre" "agenre"') - self.assert_(u'OK' in result) - - def test_list_genre_by_artist_and_album(self): - result = self.dispatcher.handle_request( - u'list "genre" "artist" "anartist" "album" "analbum"') - self.assert_(u'OK' in result) - - -class MusicDatabaseSearchTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_search_album(self): - result = self.dispatcher.handle_request(u'search "album" "analbum"') - self.assert_(u'OK' in result) - - def test_search_album_without_quotes(self): - result = self.dispatcher.handle_request(u'search album "analbum"') - self.assert_(u'OK' in result) - - def test_search_artist(self): - result = self.dispatcher.handle_request(u'search "artist" "anartist"') - self.assert_(u'OK' in result) - - def test_search_artist_without_quotes(self): - result = self.dispatcher.handle_request(u'search artist "anartist"') - self.assert_(u'OK' in result) - - def test_search_filename(self): - result = self.dispatcher.handle_request( - u'search "filename" "afilename"') - self.assert_(u'OK' in result) - - def test_search_filename_without_quotes(self): - result = self.dispatcher.handle_request(u'search filename "afilename"') - self.assert_(u'OK' in result) - - def test_search_title(self): - result = self.dispatcher.handle_request(u'search "title" "atitle"') - self.assert_(u'OK' in result) - - def test_search_title_without_quotes(self): - result = self.dispatcher.handle_request(u'search title "atitle"') - self.assert_(u'OK' in result) - - def test_search_any(self): - result = self.dispatcher.handle_request(u'search "any" "anything"') - self.assert_(u'OK' in result) - - def test_search_any_without_quotes(self): - result = self.dispatcher.handle_request(u'search any "anything"') - self.assert_(u'OK' in result) - - def test_search_date(self): - result = self.dispatcher.handle_request(u'search "date" "2002-01-01"') - self.assert_(u'OK' in result) - - def test_search_date_without_quotes(self): - result = self.dispatcher.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.dispatcher.handle_request(u'search Date "2005"') - self.assert_(u'OK' in result) - - def test_search_else_should_fail(self): - result = self.dispatcher.handle_request( - u'search "sometype" "something"') - self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments') - - diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py new file mode 100644 index 00000000..dc0789c9 --- /dev/null +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -0,0 +1,343 @@ +from tests.frontends.mpd import protocol + +class MusicDatabaseHandlerTest(protocol.BaseTestCase): + def test_count(self): + self.sendRequest(u'count "tag" "needle"') + self.assertInResponse(u'songs: 0') + self.assertInResponse(u'playtime: 0') + self.assertInResponse(u'OK') + + def test_findadd(self): + self.sendRequest(u'findadd "album" "what"') + self.assertInResponse(u'OK') + + def test_listall(self): + self.sendRequest(u'listall "file:///dev/urandom"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_listallinfo(self): + self.sendRequest(u'listallinfo "file:///dev/urandom"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_lsinfo_without_path_returns_same_as_listplaylists(self): + lsinfo_response = self.sendRequest(u'lsinfo') + listplaylists_response = self.sendRequest(u'listplaylists') + self.assertEqual(lsinfo_response, listplaylists_response) + + def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): + lsinfo_response = self.sendRequest(u'lsinfo ""') + listplaylists_response = self.sendRequest(u'listplaylists') + self.assertEqual(lsinfo_response, listplaylists_response) + + def test_lsinfo_for_root_returns_same_as_listplaylists(self): + lsinfo_response = self.sendRequest(u'lsinfo "/"') + listplaylists_response = self.sendRequest(u'listplaylists') + self.assertEqual(lsinfo_response, listplaylists_response) + + def test_update_without_uri(self): + self.sendRequest(u'update') + self.assertInResponse(u'updating_db: 0') + self.assertInResponse(u'OK') + + def test_update_with_uri(self): + self.sendRequest(u'update "file:///dev/urandom"') + self.assertInResponse(u'updating_db: 0') + self.assertInResponse(u'OK') + + def test_rescan_without_uri(self): + self.sendRequest(u'rescan') + self.assertInResponse(u'updating_db: 0') + self.assertInResponse(u'OK') + + def test_rescan_with_uri(self): + self.sendRequest(u'rescan "file:///dev/urandom"') + self.assertInResponse(u'updating_db: 0') + self.assertInResponse(u'OK') + + +class MusicDatabaseFindTest(protocol.BaseTestCase): + def test_find_album(self): + self.sendRequest(u'find "album" "what"') + self.assertInResponse(u'OK') + + def test_find_album_without_quotes(self): + self.sendRequest(u'find album "what"') + self.assertInResponse(u'OK') + + def test_find_artist(self): + self.sendRequest(u'find "artist" "what"') + self.assertInResponse(u'OK') + + def test_find_artist_without_quotes(self): + self.sendRequest(u'find artist "what"') + self.assertInResponse(u'OK') + + def test_find_title(self): + self.sendRequest(u'find "title" "what"') + self.assertInResponse(u'OK') + + def test_find_title_without_quotes(self): + self.sendRequest(u'find title "what"') + self.assertInResponse(u'OK') + + def test_find_date(self): + self.sendRequest(u'find "date" "2002-01-01"') + self.assertInResponse(u'OK') + + def test_find_date_without_quotes(self): + self.sendRequest(u'find date "2002-01-01"') + self.assertInResponse(u'OK') + + def test_find_date_with_capital_d_and_incomplete_date(self): + self.sendRequest(u'find Date "2005"') + self.assertInResponse(u'OK') + + def test_find_else_should_fail(self): + self.sendRequest(u'find "somethingelse" "what"') + self.assertEqualResponse(u'ACK [2@0] {find} incorrect arguments') + + def test_find_album_and_artist(self): + self.sendRequest(u'find album "album_what" artist "artist_what"') + self.assertInResponse(u'OK') + + +class MusicDatabaseListTest(protocol.BaseTestCase): + def test_list_foo_returns_ack(self): + self.sendRequest(u'list "foo"') + self.assertEqualResponse(u'ACK [2@0] {list} incorrect arguments') + + ### Artist + + def test_list_artist_with_quotes(self): + self.sendRequest(u'list "artist"') + self.assertInResponse(u'OK') + + def test_list_artist_without_quotes(self): + self.sendRequest(u'list artist') + self.assertInResponse(u'OK') + + def test_list_artist_without_quotes_and_capitalized(self): + self.sendRequest(u'list Artist') + self.assertInResponse(u'OK') + + def test_list_artist_with_query_of_one_token(self): + self.sendRequest(u'list "artist" "anartist"') + self.assertEqualResponse( + u'ACK [2@0] {list} should be "Album" for 3 arguments') + + def test_list_artist_with_unknown_field_in_query_returns_ack(self): + self.sendRequest(u'list "artist" "foo" "bar"') + self.assertEqualResponse(u'ACK [2@0] {list} not able to parse args') + + def test_list_artist_by_artist(self): + self.sendRequest(u'list "artist" "artist" "anartist"') + self.assertInResponse(u'OK') + + def test_list_artist_by_album(self): + self.sendRequest(u'list "artist" "album" "analbum"') + self.assertInResponse(u'OK') + + def test_list_artist_by_full_date(self): + self.sendRequest(u'list "artist" "date" "2001-01-01"') + self.assertInResponse(u'OK') + + def test_list_artist_by_year(self): + self.sendRequest(u'list "artist" "date" "2001"') + self.assertInResponse(u'OK') + + def test_list_artist_by_genre(self): + self.sendRequest(u'list "artist" "genre" "agenre"') + self.assertInResponse(u'OK') + + def test_list_artist_by_artist_and_album(self): + self.sendRequest( + u'list "artist" "artist" "anartist" "album" "analbum"') + self.assertInResponse(u'OK') + + ### Album + + def test_list_album_with_quotes(self): + self.sendRequest(u'list "album"') + self.assertInResponse(u'OK') + + def test_list_album_without_quotes(self): + self.sendRequest(u'list album') + self.assertInResponse(u'OK') + + def test_list_album_without_quotes_and_capitalized(self): + self.sendRequest(u'list Album') + self.assertInResponse(u'OK') + + def test_list_album_with_artist_name(self): + self.sendRequest(u'list "album" "anartist"') + self.assertInResponse(u'OK') + + def test_list_album_by_artist(self): + self.sendRequest(u'list "album" "artist" "anartist"') + self.assertInResponse(u'OK') + + def test_list_album_by_album(self): + self.sendRequest(u'list "album" "album" "analbum"') + self.assertInResponse(u'OK') + + def test_list_album_by_full_date(self): + self.sendRequest(u'list "album" "date" "2001-01-01"') + self.assertInResponse(u'OK') + + def test_list_album_by_year(self): + self.sendRequest(u'list "album" "date" "2001"') + self.assertInResponse(u'OK') + + def test_list_album_by_genre(self): + self.sendRequest(u'list "album" "genre" "agenre"') + self.assertInResponse(u'OK') + + def test_list_album_by_artist_and_album(self): + self.sendRequest( + u'list "album" "artist" "anartist" "album" "analbum"') + self.assertInResponse(u'OK') + + ### Date + + def test_list_date_with_quotes(self): + self.sendRequest(u'list "date"') + self.assertInResponse(u'OK') + + def test_list_date_without_quotes(self): + self.sendRequest(u'list date') + self.assertInResponse(u'OK') + + def test_list_date_without_quotes_and_capitalized(self): + self.sendRequest(u'list Date') + self.assertInResponse(u'OK') + + def test_list_date_with_query_of_one_token(self): + self.sendRequest(u'list "date" "anartist"') + self.assertEqualResponse( + u'ACK [2@0] {list} should be "Album" for 3 arguments') + + def test_list_date_by_artist(self): + self.sendRequest(u'list "date" "artist" "anartist"') + self.assertInResponse(u'OK') + + def test_list_date_by_album(self): + self.sendRequest(u'list "date" "album" "analbum"') + self.assertInResponse(u'OK') + + def test_list_date_by_full_date(self): + self.sendRequest(u'list "date" "date" "2001-01-01"') + self.assertInResponse(u'OK') + + def test_list_date_by_year(self): + self.sendRequest(u'list "date" "date" "2001"') + self.assertInResponse(u'OK') + + def test_list_date_by_genre(self): + self.sendRequest(u'list "date" "genre" "agenre"') + self.assertInResponse(u'OK') + + def test_list_date_by_artist_and_album(self): + self.sendRequest(u'list "date" "artist" "anartist" "album" "analbum"') + self.assertInResponse(u'OK') + + ### Genre + + def test_list_genre_with_quotes(self): + self.sendRequest(u'list "genre"') + self.assertInResponse(u'OK') + + def test_list_genre_without_quotes(self): + self.sendRequest(u'list genre') + self.assertInResponse(u'OK') + + def test_list_genre_without_quotes_and_capitalized(self): + self.sendRequest(u'list Genre') + self.assertInResponse(u'OK') + + def test_list_genre_with_query_of_one_token(self): + self.sendRequest(u'list "genre" "anartist"') + self.assertEqualResponse( + u'ACK [2@0] {list} should be "Album" for 3 arguments') + + def test_list_genre_by_artist(self): + self.sendRequest(u'list "genre" "artist" "anartist"') + self.assertInResponse(u'OK') + + def test_list_genre_by_album(self): + self.sendRequest(u'list "genre" "album" "analbum"') + self.assertInResponse(u'OK') + + def test_list_genre_by_full_date(self): + self.sendRequest(u'list "genre" "date" "2001-01-01"') + self.assertInResponse(u'OK') + + def test_list_genre_by_year(self): + self.sendRequest(u'list "genre" "date" "2001"') + self.assertInResponse(u'OK') + + def test_list_genre_by_genre(self): + self.sendRequest(u'list "genre" "genre" "agenre"') + self.assertInResponse(u'OK') + + def test_list_genre_by_artist_and_album(self): + self.sendRequest( + u'list "genre" "artist" "anartist" "album" "analbum"') + self.assertInResponse(u'OK') + + +class MusicDatabaseSearchTest(protocol.BaseTestCase): + def test_search_album(self): + self.sendRequest(u'search "album" "analbum"') + self.assertInResponse(u'OK') + + def test_search_album_without_quotes(self): + self.sendRequest(u'search album "analbum"') + self.assertInResponse(u'OK') + + def test_search_artist(self): + self.sendRequest(u'search "artist" "anartist"') + self.assertInResponse(u'OK') + + def test_search_artist_without_quotes(self): + self.sendRequest(u'search artist "anartist"') + self.assertInResponse(u'OK') + + def test_search_filename(self): + self.sendRequest(u'search "filename" "afilename"') + self.assertInResponse(u'OK') + + def test_search_filename_without_quotes(self): + self.sendRequest(u'search filename "afilename"') + self.assertInResponse(u'OK') + + def test_search_title(self): + self.sendRequest(u'search "title" "atitle"') + self.assertInResponse(u'OK') + + def test_search_title_without_quotes(self): + self.sendRequest(u'search title "atitle"') + self.assertInResponse(u'OK') + + def test_search_any(self): + self.sendRequest(u'search "any" "anything"') + self.assertInResponse(u'OK') + + def test_search_any_without_quotes(self): + self.sendRequest(u'search any "anything"') + self.assertInResponse(u'OK') + + def test_search_date(self): + self.sendRequest(u'search "date" "2002-01-01"') + self.assertInResponse(u'OK') + + def test_search_date_without_quotes(self): + self.sendRequest(u'search date "2002-01-01"') + self.assertInResponse(u'OK') + + def test_search_date_with_capital_d_and_incomplete_date(self): + self.sendRequest(u'search Date "2005"') + self.assertInResponse(u'OK') + + def test_search_else_should_fail(self): + self.sendRequest(u'search "sometype" "something"') + self.assertEqualResponse(u'ACK [2@0] {search} incorrect arguments') From c3a498e62bf138676a6d9c881ea5ff6bf92a06c3 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 18:00:59 +0200 Subject: [PATCH 177/350] Migrate stickers_test --- tests/frontends/mpd/protocol/stickers_test.py | 32 +++++++++++++ tests/frontends/mpd/stickers_test.py | 45 ------------------- 2 files changed, 32 insertions(+), 45 deletions(-) create mode 100644 tests/frontends/mpd/protocol/stickers_test.py delete mode 100644 tests/frontends/mpd/stickers_test.py diff --git a/tests/frontends/mpd/protocol/stickers_test.py b/tests/frontends/mpd/protocol/stickers_test.py new file mode 100644 index 00000000..1c48cfd3 --- /dev/null +++ b/tests/frontends/mpd/protocol/stickers_test.py @@ -0,0 +1,32 @@ +from tests.frontends.mpd import protocol + +class StickersHandlerTest(protocol.BaseTestCase): + def test_sticker_get(self): + self.sendRequest( + u'sticker get "song" "file:///dev/urandom" "a_name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_sticker_set(self): + self.sendRequest( + u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_sticker_delete_with_name(self): + self.sendRequest( + u'sticker delete "song" "file:///dev/urandom" "a_name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_sticker_delete_without_name(self): + self.sendRequest( + u'sticker delete "song" "file:///dev/urandom"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_sticker_list(self): + self.sendRequest( + u'sticker list "song" "file:///dev/urandom"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_sticker_find(self): + self.sendRequest( + u'sticker find "song" "file:///dev/urandom" "a_name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') diff --git a/tests/frontends/mpd/stickers_test.py b/tests/frontends/mpd/stickers_test.py deleted file mode 100644 index 86ac8aec..00000000 --- a/tests/frontends/mpd/stickers_test.py +++ /dev/null @@ -1,45 +0,0 @@ -import unittest - -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer - -class StickersHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_sticker_get(self): - result = self.dispatcher.handle_request( - u'sticker get "song" "file:///dev/urandom" "a_name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_sticker_set(self): - result = self.dispatcher.handle_request( - u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_sticker_delete_with_name(self): - result = self.dispatcher.handle_request( - u'sticker delete "song" "file:///dev/urandom" "a_name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_sticker_delete_without_name(self): - result = self.dispatcher.handle_request( - u'sticker delete "song" "file:///dev/urandom"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_sticker_list(self): - result = self.dispatcher.handle_request( - u'sticker list "song" "file:///dev/urandom"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_sticker_find(self): - result = self.dispatcher.handle_request( - u'sticker find "song" "file:///dev/urandom" "a_name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) From ec69ce204474ebd4fe9e26845b9c0ba36820d33e Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 19:36:03 +0200 Subject: [PATCH 178/350] Partially migrate status_test --- tests/frontends/mpd/protocol/status_test.py | 51 ++++++++++++++++ tests/frontends/mpd/status_test.py | 66 ++++----------------- 2 files changed, 63 insertions(+), 54 deletions(-) create mode 100644 tests/frontends/mpd/protocol/status_test.py diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py new file mode 100644 index 00000000..6762a4fb --- /dev/null +++ b/tests/frontends/mpd/protocol/status_test.py @@ -0,0 +1,51 @@ +from mopidy.models import Track + +from tests.frontends.mpd import protocol + +class StatusHandlerTest(protocol.BaseTestCase): + def test_clearerror(self): + self.sendRequest(u'clearerror') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_currentsong(self): + track = Track() + self.backend.current_playlist.append([track]) + self.backend.playback.play() + self.sendRequest(u'currentsong') + self.assertInResponse(u'file: ') + self.assertInResponse(u'Time: 0') + self.assertInResponse(u'Artist: ') + self.assertInResponse(u'Title: ') + self.assertInResponse(u'Album: ') + self.assertInResponse(u'Track: 0') + self.assertInResponse(u'Date: ') + self.assertInResponse(u'Pos: 0') + self.assertInResponse(u'Id: 0') + self.assertInResponse(u'OK') + + def test_currentsong_without_song(self): + self.sendRequest(u'currentsong') + self.assertInResponse(u'OK') + + def test_idle_without_subsystems(self): + # FIXME this is not the correct behaviour for idle... + self.sendRequest(u'idle') + self.assertInResponse(u'OK') + + def test_idle_with_subsystems(self): + # FIXME this is not the correct behaviour for idle... + self.sendRequest(u'idle database playlist') + self.assertInResponse(u'OK') + + def test_noidle(self): + # FIXME this is not the correct behaviour for idle... + self.sendRequest(u'noidle') + self.assertInResponse(u'OK') + + def test_stats_command(self): + self.sendRequest(u'stats') + self.assertInResponse(u'OK') + + def test_status_command(self): + self.sendRequest(u'status') + self.assertInResponse(u'OK') diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index a7ed921f..d277227a 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,67 +1,29 @@ import unittest -from mopidy.backends.base import PlaybackController -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher +from mopidy.backends import dummy as backend +from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status -from mopidy.mixers.dummy import DummyMixer +from mopidy.mixers import dummy as mixer from mopidy.models import Track -PAUSED = PlaybackController.PAUSED -PLAYING = PlaybackController.PLAYING -STOPPED = PlaybackController.STOPPED +PAUSED = backend.PlaybackController.PAUSED +PLAYING = backend.PlaybackController.PLAYING +STOPPED = backend.PlaybackController.STOPPED + +# FIXME migrate to using protocol.BaseTestCase instead of status.stats +# directly? class StatusHandlerTest(unittest.TestCase): def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() + self.backend = backend.DummyBackend.start().proxy() + self.mixer = mixer.DummyMixer.start().proxy() + self.dispatcher = dispatcher.MpdDispatcher() self.context = self.dispatcher.context def tearDown(self): self.backend.stop().get() self.mixer.stop().get() - def test_clearerror(self): - result = self.dispatcher.handle_request(u'clearerror') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_currentsong(self): - track = Track() - self.backend.current_playlist.append([track]) - self.backend.playback.play() - result = self.dispatcher.handle_request(u'currentsong') - self.assert_(u'file: ' in result) - self.assert_(u'Time: 0' in result) - self.assert_(u'Artist: ' in result) - self.assert_(u'Title: ' in result) - self.assert_(u'Album: ' in result) - self.assert_(u'Track: 0' in result) - self.assert_(u'Date: ' in result) - self.assert_(u'Pos: 0' in result) - self.assert_(u'Id: 0' in result) - self.assert_(u'OK' in result) - - def test_currentsong_without_song(self): - result = self.dispatcher.handle_request(u'currentsong') - self.assert_(u'OK' in result) - - def test_idle_without_subsystems(self): - result = self.dispatcher.handle_request(u'idle') - self.assert_(u'OK' in result) - - def test_idle_with_subsystems(self): - result = self.dispatcher.handle_request(u'idle database playlist') - self.assert_(u'OK' in result) - - def test_noidle(self): - result = self.dispatcher.handle_request(u'noidle') - self.assert_(u'OK' in result) - - def test_stats_command(self): - result = self.dispatcher.handle_request(u'stats') - self.assert_(u'OK' in result) - def test_stats_method(self): result = status.stats(self.context) self.assert_('artists' in result) @@ -79,10 +41,6 @@ class StatusHandlerTest(unittest.TestCase): self.assert_('playtime' in result) self.assert_(int(result['playtime']) >= 0) - def test_status_command(self): - result = self.dispatcher.handle_request(u'status') - self.assert_(u'OK' in result) - def test_status_method_contains_volume_which_defaults_to_0(self): result = dict(status.status(self.context)) self.assert_('volume' in result) From 5d18c64bf991f8fa568a7c8daaeb6f36fadc13c8 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 19:44:06 +0200 Subject: [PATCH 179/350] Migrate stored_playlists_test --- .../mpd/protocol/stored_playlists_test.py | 93 ++++++++++++++++ tests/frontends/mpd/stored_playlists_test.py | 102 ------------------ 2 files changed, 93 insertions(+), 102 deletions(-) create mode 100644 tests/frontends/mpd/protocol/stored_playlists_test.py delete mode 100644 tests/frontends/mpd/stored_playlists_test.py diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py new file mode 100644 index 00000000..6d9448a6 --- /dev/null +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -0,0 +1,93 @@ +import datetime as dt + +from mopidy.models import Track, Playlist + +from tests.frontends.mpd import protocol + +class StoredPlaylistsHandlerTest(protocol.BaseTestCase): + def test_listplaylist(self): + self.backend.stored_playlists.playlists = [ + Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + + self.sendRequest(u'listplaylist "name"') + self.assertInResponse(u'file: file:///dev/urandom') + self.assertInResponse(u'OK') + + def test_listplaylist_fails_if_no_playlist_is_found(self): + self.sendRequest(u'listplaylist "name"') + self.assertEqualResponse(u'ACK [50@0] {listplaylist} No such playlist') + + def test_listplaylistinfo(self): + self.backend.stored_playlists.playlists = [ + Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] + + self.sendRequest(u'listplaylistinfo "name"') + self.assertInResponse(u'file: file:///dev/urandom') + self.assertInResponse(u'Track: 0') + self.assertNotInResponse(u'Pos: 0') + self.assertInResponse(u'OK') + + def test_listplaylistinfo_fails_if_no_playlist_is_found(self): + self.sendRequest(u'listplaylistinfo "name"') + self.assertEqualResponse( + u'ACK [50@0] {listplaylistinfo} No such playlist') + + def test_listplaylists(self): + last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345) + self.backend.stored_playlists.playlists = [Playlist(name='a', + last_modified=last_modified)] + + self.sendRequest(u'listplaylists') + self.assertInResponse(u'playlist: a') + # Date without microseconds and with time zone information + self.assertInResponse(u'Last-Modified: 2001-03-17T13:41:17Z') + self.assertInResponse(u'OK') + + def test_load_known_playlist_appends_to_current_playlist(self): + self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) + self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2) + self.backend.stored_playlists.playlists = [Playlist(name='A-list', + tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] + + self.sendRequest(u'load "A-list"') + tracks = self.backend.current_playlist.tracks.get() + self.assertEqual(5, len(tracks)) + self.assertEqual('a', tracks[0].uri) + self.assertEqual('b', tracks[1].uri) + self.assertEqual('c', tracks[2].uri) + self.assertEqual('d', tracks[3].uri) + self.assertEqual('e', tracks[4].uri) + self.assertInResponse(u'OK') + + def test_load_unknown_playlist_acks(self): + self.sendRequest(u'load "unknown playlist"') + self.assertEqual(0, len(self.backend.current_playlist.tracks.get())) + self.assertEqualResponse(u'ACK [50@0] {load} No such playlist') + + def test_playlistadd(self): + self.sendRequest(u'playlistadd "name" "file:///dev/urandom"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_playlistclear(self): + self.sendRequest(u'playlistclear "name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_playlistdelete(self): + self.sendRequest(u'playlistdelete "name" "5"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_playlistmove(self): + self.sendRequest(u'playlistmove "name" "5" "10"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_rename(self): + self.sendRequest(u'rename "old_name" "new_name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_rm(self): + self.sendRequest(u'rm "name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') + + def test_save(self): + self.sendRequest(u'save "name"') + self.assertEqualResponse(u'ACK [0@0] {} Not implemented') diff --git a/tests/frontends/mpd/stored_playlists_test.py b/tests/frontends/mpd/stored_playlists_test.py deleted file mode 100644 index 04bab6f1..00000000 --- a/tests/frontends/mpd/stored_playlists_test.py +++ /dev/null @@ -1,102 +0,0 @@ -import datetime as dt -import unittest - -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer -from mopidy.models import Track, Playlist - -class StoredPlaylistsHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - - def test_listplaylist(self): - self.backend.stored_playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] - result = self.dispatcher.handle_request(u'listplaylist "name"') - self.assert_(u'file: file:///dev/urandom' in result) - self.assert_(u'OK' in result) - - def test_listplaylist_fails_if_no_playlist_is_found(self): - result = self.dispatcher.handle_request(u'listplaylist "name"') - self.assertEqual(result[0], - u'ACK [50@0] {listplaylist} No such playlist') - - def test_listplaylistinfo(self): - self.backend.stored_playlists.playlists = [ - Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])] - result = self.dispatcher.handle_request(u'listplaylistinfo "name"') - self.assert_(u'file: file:///dev/urandom' in result) - self.assert_(u'Track: 0' in result) - self.assert_(u'Pos: 0' not in result) - self.assert_(u'OK' in result) - - def test_listplaylistinfo_fails_if_no_playlist_is_found(self): - result = self.dispatcher.handle_request(u'listplaylistinfo "name"') - self.assertEqual(result[0], - u'ACK [50@0] {listplaylistinfo} No such playlist') - - def test_listplaylists(self): - last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345) - self.backend.stored_playlists.playlists = [Playlist(name='a', - last_modified=last_modified)] - result = self.dispatcher.handle_request(u'listplaylists') - self.assert_(u'playlist: a' in result) - # Date without microseconds and with time zone information - self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result) - self.assert_(u'OK' in result) - - def test_load_known_playlist_appends_to_current_playlist(self): - self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) - self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2) - self.backend.stored_playlists.playlists = [Playlist(name='A-list', - tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])] - result = self.dispatcher.handle_request(u'load "A-list"') - self.assert_(u'OK' in result) - tracks = self.backend.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.dispatcher.handle_request(u'load "unknown playlist"') - self.assert_(u'ACK [50@0] {load} No such playlist' in result) - self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0) - - def test_playlistadd(self): - result = self.dispatcher.handle_request( - u'playlistadd "name" "file:///dev/urandom"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_playlistclear(self): - result = self.dispatcher.handle_request(u'playlistclear "name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_playlistdelete(self): - result = self.dispatcher.handle_request(u'playlistdelete "name" "5"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_playlistmove(self): - result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_rename(self): - result = self.dispatcher.handle_request(u'rename "old_name" "new_name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_rm(self): - result = self.dispatcher.handle_request(u'rm "name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_save(self): - result = self.dispatcher.handle_request(u'save "name"') - self.assert_(u'ACK [0@0] {} Not implemented' in result) From 730368dbeb2d2db3958681adae6882b9911cc428 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:00:25 +0200 Subject: [PATCH 180/350] Migrate regression_test --- .../mpd/{ => protocol}/regression_test.py | 120 ++++++------------ 1 file changed, 42 insertions(+), 78 deletions(-) rename tests/frontends/mpd/{ => protocol}/regression_test.py (50%) diff --git a/tests/frontends/mpd/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py similarity index 50% rename from tests/frontends/mpd/regression_test.py rename to tests/frontends/mpd/protocol/regression_test.py index f786cf0a..1b257d8b 100644 --- a/tests/frontends/mpd/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -1,12 +1,10 @@ import random -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): +from tests.frontends.mpd import protocol + +class IssueGH17RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues#issue/17 @@ -16,36 +14,27 @@ class IssueGH17RegressionTest(unittest.TestCase): - Turn on random mode - Press next until you get to the unplayable track """ - - def setUp(self): - self.backend = DummyBackend.start().proxy() + def test(self): self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), None, Track(uri='d'), Track(uri='e'), Track(uri='f')]) - 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.sendRequest(u'play') self.assertEquals('a', self.backend.playback.current_track.get().uri) - self.mpd.handle_request(u'random "1"') - self.mpd.handle_request(u'next') + self.sendRequest(u'random "1"') + self.sendRequest(u'next') self.assertEquals('b', self.backend.playback.current_track.get().uri) - self.mpd.handle_request(u'next') + self.sendRequest(u'next') # Should now be at track 'c', but playback fails and it skips ahead self.assertEquals('f', self.backend.playback.current_track.get().uri) - self.mpd.handle_request(u'next') + self.sendRequest(u'next') self.assertEquals('d', self.backend.playback.current_track.get().uri) - self.mpd.handle_request(u'next') + self.sendRequest(u'next') self.assertEquals('e', self.backend.playback.current_track.get().uri) -class IssueGH18RegressionTest(unittest.TestCase): +class IssueGH18RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues#issue/18 @@ -56,38 +45,30 @@ class IssueGH18RegressionTest(unittest.TestCase): At this point it gives the same song over and over. """ - def setUp(self): - self.backend = DummyBackend.start().proxy() + def test(self): self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - 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) - self.mpd.handle_request(u'play') - self.mpd.handle_request(u'random "1"') - self.mpd.handle_request(u'next') - self.mpd.handle_request(u'random "0"') - self.mpd.handle_request(u'next') - self.mpd.handle_request(u'next') + self.sendRequest(u'play') + self.sendRequest(u'random "1"') + self.sendRequest(u'next') + self.sendRequest(u'random "0"') + self.sendRequest(u'next') + + self.sendRequest(u'next') cp_track_1 = self.backend.playback.current_cp_track.get() - self.mpd.handle_request(u'next') + self.sendRequest(u'next') cp_track_2 = self.backend.playback.current_cp_track.get() - self.mpd.handle_request(u'next') + self.sendRequest(u'next') 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) -class IssueGH22RegressionTest(unittest.TestCase): +class IssueGH22RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues/#issue/22 @@ -100,32 +81,24 @@ class IssueGH22RegressionTest(unittest.TestCase): playlist, press next until it crashes. """ - def setUp(self): - self.backend = DummyBackend.start().proxy() + def test(self): self.backend.current_playlist.append([ Track(uri='a'), Track(uri='b'), Track(uri='c'), Track(uri='d'), Track(uri='e'), Track(uri='f')]) - 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) - self.mpd.handle_request(u'play') - self.mpd.handle_request(u'random "1"') - self.mpd.handle_request(u'deleteid "1"') - self.mpd.handle_request(u'deleteid "2"') - self.mpd.handle_request(u'deleteid "3"') - self.mpd.handle_request(u'deleteid "4"') - self.mpd.handle_request(u'deleteid "5"') - self.mpd.handle_request(u'deleteid "6"') - self.mpd.handle_request(u'status') + + self.sendRequest(u'play') + self.sendRequest(u'random "1"') + self.sendRequest(u'deleteid "1"') + self.sendRequest(u'deleteid "2"') + self.sendRequest(u'deleteid "3"') + self.sendRequest(u'deleteid "4"') + self.sendRequest(u'deleteid "5"') + self.sendRequest(u'deleteid "6"') + self.sendRequest(u'status') -class IssueGH69RegressionTest(unittest.TestCase): +class IssueGH69RegressionTest(protocol.BaseTestCase): """ The issue: https://github.com/mopidy/mopidy/issues#issue/69 @@ -136,23 +109,14 @@ class IssueGH69RegressionTest(unittest.TestCase): The status response now contains "song: None". """ - def setUp(self): - self.backend = DummyBackend.start().proxy() + def test(self): + self.backend.stored_playlists.create('foo') 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) + self.sendRequest(u'play') + self.sendRequest(u'stop') + self.sendRequest(u'clear') + self.sendRequest(u'load "foo"') + self.assertNotInResponse('song: None') From b445af7cfb495f31396cca52cde96bb1d5c9aba5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:09:58 +0200 Subject: [PATCH 181/350] Migrate reflection_test --- tests/frontends/mpd/protocol/__init__.py | 2 + .../frontends/mpd/protocol/reflection_test.py | 66 ++++++++++++++++ tests/frontends/mpd/reflection_test.py | 79 ------------------- 3 files changed, 68 insertions(+), 79 deletions(-) create mode 100644 tests/frontends/mpd/protocol/reflection_test.py delete mode 100644 tests/frontends/mpd/reflection_test.py diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index fd56c32e..f26b26fd 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -1,6 +1,7 @@ import unittest import mock +from mopidy import settings from mopidy.backends import dummy as backend from mopidy.frontends import mpd from mopidy.frontends.mpd import dispatcher @@ -32,6 +33,7 @@ class BaseTestCase(unittest.TestCase): def tearDown(self): self.backend.stop().get() self.mixer.stop().get() + settings.runtime.clear() def sendRequest(self, request, clear=False): self.connection.response = [] diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py new file mode 100644 index 00000000..315e3051 --- /dev/null +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -0,0 +1,66 @@ +from mopidy import settings + +from tests.frontends.mpd import protocol + +class ReflectionHandlerTest(protocol.BaseTestCase): + def test_commands_returns_list_of_all_commands(self): + self.sendRequest(u'commands') + # Check if some random commands are included + self.assertInResponse(u'command: commands') + self.assertInResponse(u'command: play') + self.assertInResponse(u'command: status') + # Check if commands you do not have access to are not present + self.assertNotInResponse(u'command: kill') + # Check if the blacklisted commands are not present + self.assertNotInResponse(u'command: command_list_begin') + self.assertNotInResponse(u'command: command_list_ok_begin') + self.assertNotInResponse(u'command: command_list_end') + self.assertNotInResponse(u'command: idle') + self.assertNotInResponse(u'command: noidle') + self.assertNotInResponse(u'command: sticker') + self.assertInResponse(u'OK') + + def test_commands_show_less_if_auth_required_and_not_authed(self): + settings.MPD_SERVER_PASSWORD = u'secret' + self.sendRequest(u'commands') + # Not requiring auth + self.assertInResponse(u'command: close') + self.assertInResponse(u'command: commands') + self.assertInResponse(u'command: notcommands') + self.assertInResponse(u'command: password') + self.assertInResponse(u'command: ping') + # Requiring auth + self.assertNotInResponse(u'command: play') + self.assertNotInResponse(u'command: status') + + def test_decoders(self): + self.sendRequest(u'decoders') + self.assertInResponse(u'ACK [0@0] {} Not implemented') + + def test_notcommands_returns_only_kill_and_ok(self): + response = self.sendRequest(u'notcommands') + self.assertEqual(2, len(response)) + self.assertInResponse(u'command: kill') + self.assertInResponse(u'OK') + + def test_notcommands_returns_more_if_auth_required_and_not_authed(self): + settings.MPD_SERVER_PASSWORD = u'secret' + self.sendRequest(u'notcommands') + # Not requiring auth + self.assertNotInResponse(u'command: close') + self.assertNotInResponse(u'command: commands') + self.assertNotInResponse(u'command: notcommands') + self.assertNotInResponse(u'command: password') + self.assertNotInResponse(u'command: ping') + # Requiring auth + self.assertInResponse(u'command: play') + self.assertInResponse(u'command: status') + + def test_tagtypes(self): + self.sendRequest(u'tagtypes') + self.assertInResponse(u'OK') + + def test_urlhandlers(self): + self.sendRequest(u'urlhandlers') + self.assertInResponse(u'OK') + self.assertInResponse(u'handler: dummy') diff --git a/tests/frontends/mpd/reflection_test.py b/tests/frontends/mpd/reflection_test.py deleted file mode 100644 index c4fd632a..00000000 --- a/tests/frontends/mpd/reflection_test.py +++ /dev/null @@ -1,79 +0,0 @@ -import unittest - -from mopidy import settings -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer - -class ReflectionHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = MpdDispatcher() - - def tearDown(self): - settings.runtime.clear() - self.backend.stop().get() - self.mixer.stop().get() - - def test_commands_returns_list_of_all_commands(self): - result = self.dispatcher.handle_request(u'commands') - # Check if some random commands are included - self.assert_(u'command: commands' in result) - self.assert_(u'command: play' in result) - self.assert_(u'command: status' in result) - # Check if commands you do not have access to are not present - self.assert_(u'command: kill' not in result) - # Check if the blacklisted commands are not present - self.assert_(u'command: command_list_begin' not in result) - self.assert_(u'command: command_list_ok_begin' not in result) - self.assert_(u'command: command_list_end' not in result) - self.assert_(u'command: idle' not in result) - self.assert_(u'command: noidle' not in result) - self.assert_(u'command: sticker' not in result) - self.assert_(u'OK' in result) - - def test_commands_show_less_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' - result = self.dispatcher.handle_request(u'commands') - # Not requiring auth - self.assert_(u'command: close' in result, result) - self.assert_(u'command: commands' in result, result) - self.assert_(u'command: notcommands' in result, result) - self.assert_(u'command: password' in result, result) - self.assert_(u'command: ping' in result, result) - # Requiring auth - self.assert_(u'command: play' not in result, result) - self.assert_(u'command: status' not in result, result) - - def test_decoders(self): - result = self.dispatcher.handle_request(u'decoders') - self.assert_(u'ACK [0@0] {} Not implemented' in result) - - def test_notcommands_returns_only_kill_and_ok(self): - result = self.dispatcher.handle_request(u'notcommands') - self.assertEqual(2, len(result)) - self.assert_(u'command: kill' in result) - self.assert_(u'OK' in result) - - def test_notcommands_returns_more_if_auth_required_and_not_authed(self): - settings.MPD_SERVER_PASSWORD = u'secret' - result = self.dispatcher.handle_request(u'notcommands') - # Not requiring auth - self.assert_(u'command: close' not in result, result) - self.assert_(u'command: commands' not in result, result) - self.assert_(u'command: notcommands' not in result, result) - self.assert_(u'command: password' not in result, result) - self.assert_(u'command: ping' not in result, result) - # Requiring auth - self.assert_(u'command: play' in result, result) - self.assert_(u'command: status' in result, result) - - def test_tagtypes(self): - result = self.dispatcher.handle_request(u'tagtypes') - self.assert_(u'OK' in result) - - def test_urlhandlers(self): - result = self.dispatcher.handle_request(u'urlhandlers') - self.assert_(u'OK' in result) - self.assert_(u'handler: dummy' in result) From f88a0a00908e7419daf5a7f975d179b4f72e205f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:23:14 +0200 Subject: [PATCH 182/350] Migrate connection_test and support any number of blanks as empty command --- mopidy/frontends/mpd/protocol/empty.py | 2 +- tests/frontends/mpd/connection_test.py | 53 ------------------- .../frontends/mpd/protocol/connection_test.py | 43 +++++++++++++++ 3 files changed, 44 insertions(+), 54 deletions(-) delete mode 100644 tests/frontends/mpd/connection_test.py create mode 100644 tests/frontends/mpd/protocol/connection_test.py diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py index 0e418551..33b3bd9f 100644 --- a/mopidy/frontends/mpd/protocol/empty.py +++ b/mopidy/frontends/mpd/protocol/empty.py @@ -1,6 +1,6 @@ from mopidy.frontends.mpd.protocol import handle_request -@handle_request(r'^$') +@handle_request(r'^\s*$') def empty(context): """The original MPD server returns ``OK`` on an empty request.""" pass diff --git a/tests/frontends/mpd/connection_test.py b/tests/frontends/mpd/connection_test.py deleted file mode 100644 index 82debabb..00000000 --- a/tests/frontends/mpd/connection_test.py +++ /dev/null @@ -1,53 +0,0 @@ -import mock -import unittest - -from mopidy import settings -from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpd import MpdSession -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -from mopidy.mixers.dummy import DummyMixer - -class ConnectionHandlerTest(unittest.TestCase): - def setUp(self): - self.backend = DummyBackend.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.session = mock.Mock(spec=MpdSession) - self.dispatcher = MpdDispatcher(session=self.session) - - def tearDown(self): - self.backend.stop().get() - self.mixer.stop().get() - settings.runtime.clear() - - def test_close_closes_the_client_connection(self): - result = self.dispatcher.handle_request(u'close') - self.assert_(self.session.close.called, - u'Should call close() on MpdSession') - self.assert_(u'OK' in result) - - def test_empty_request(self): - result = self.dispatcher.handle_request(u'') - self.assert_(u'OK' in result) - - def test_kill(self): - result = self.dispatcher.handle_request(u'kill') - self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result) - - def test_valid_password_is_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - result = self.dispatcher.handle_request(u'password "topsecret"') - self.assert_(u'OK' in result) - - def test_invalid_password_is_not_accepted(self): - settings.MPD_SERVER_PASSWORD = u'topsecret' - result = self.dispatcher.handle_request(u'password "secret"') - self.assert_(u'ACK [3@0] {password} incorrect password' in result) - - def test_any_password_is_not_accepted_when_password_check_turned_off(self): - settings.MPD_SERVER_PASSWORD = None - result = self.dispatcher.handle_request(u'password "secret"') - self.assert_(u'ACK [3@0] {password} incorrect password' in result) - - def test_ping(self): - result = self.dispatcher.handle_request(u'ping') - self.assert_(u'OK' in result) diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py new file mode 100644 index 00000000..33b5a1a2 --- /dev/null +++ b/tests/frontends/mpd/protocol/connection_test.py @@ -0,0 +1,43 @@ +from mock import patch + +from mopidy import settings + +from tests.frontends.mpd import protocol + +class ConnectionHandlerTest(protocol.BaseTestCase): + def test_close_closes_the_client_connection(self): + with patch.object(self.session, 'close') as close_mock: + response = self.sendRequest(u'close') + close_mock.assertEqualResponsecalled_once_with() + self.assertEqualResponse(u'OK') + + def test_empty_request(self): + self.sendRequest(u'') + self.assertEqualResponse(u'OK') + + self.sendRequest(u' ') + self.assertEqualResponse(u'OK') + + def test_kill(self): + self.sendRequest(u'kill') + self.assertEqualResponse( + u'ACK [4@0] {kill} you don\'t have permission for "kill"') + + def test_valid_password_is_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + self.sendRequest(u'password "topsecret"') + self.assertEqualResponse(u'OK') + + def test_invalid_password_is_not_accepted(self): + settings.MPD_SERVER_PASSWORD = u'topsecret' + self.sendRequest(u'password "secret"') + self.assertEqualResponse(u'ACK [3@0] {password} incorrect password') + + def test_any_password_is_not_accepted_when_password_check_turned_off(self): + settings.MPD_SERVER_PASSWORD = None + self.sendRequest(u'password "secret"') + self.assertEqualResponse(u'ACK [3@0] {password} incorrect password') + + def test_ping(self): + self.sendRequest(u'ping') + self.assertEqualResponse(u'OK') From 15e6f1a6ca18be540b8515c70a8e931e2bf831d1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:27:38 +0200 Subject: [PATCH 183/350] Cleanup imports and dispatcher assignment in protocol.BaseTestCase --- tests/frontends/mpd/protocol/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index f26b26fd..77825a6e 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -4,9 +4,7 @@ import mock from mopidy import settings from mopidy.backends import dummy as backend from mopidy.frontends import mpd -from mopidy.frontends.mpd import dispatcher from mopidy.mixers import dummy as mixer -from mopidy.utils import network class MockConnetion(mock.Mock): @@ -25,10 +23,10 @@ class BaseTestCase(unittest.TestCase): def setUp(self): self.backend = backend.DummyBackend.start().proxy() self.mixer = mixer.DummyMixer.start().proxy() - self.dispatcher = dispatcher.MpdDispatcher() self.connection = MockConnetion() self.session = mpd.MpdSession(self.connection) + self.dispatcher = self.session.dispatcher def tearDown(self): self.backend.stop().get() From df66a4234b58be56814df4c3722617f0f845576c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:30:50 +0200 Subject: [PATCH 184/350] Migrate command_list_test --- tests/frontends/mpd/command_list_test.py | 63 ------------------- .../mpd/protocol/command_list_test.py | 53 ++++++++++++++++ 2 files changed, 53 insertions(+), 63 deletions(-) delete mode 100644 tests/frontends/mpd/command_list_test.py create mode 100644 tests/frontends/mpd/protocol/command_list_test.py diff --git a/tests/frontends/mpd/command_list_test.py b/tests/frontends/mpd/command_list_test.py deleted file mode 100644 index 8fd4c828..00000000 --- a/tests/frontends/mpd/command_list_test.py +++ /dev/null @@ -1,63 +0,0 @@ -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.start().proxy() - self.mixer = DummyMixer.start().proxy() - self.dispatcher = dispatcher.MpdDispatcher() - - def tearDown(self): - self.b.stop().get() - self.mixer.stop().get() - - def test_command_list_begin(self): - result = self.dispatcher.handle_request(u'command_list_begin') - self.assertEquals(result, []) - - def test_command_list_end(self): - self.dispatcher.handle_request(u'command_list_begin') - result = self.dispatcher.handle_request(u'command_list_end') - self.assert_(u'OK' in result) - - def test_command_list_end_without_start_first_is_an_unknown_command(self): - result = self.dispatcher.handle_request(u'command_list_end') - self.assertEquals(result[0], - u'ACK [5@0] {} unknown command "command_list_end"') - - def test_command_list_with_ping(self): - self.dispatcher.handle_request(u'command_list_begin') - self.assertEqual([], self.dispatcher.command_list) - self.assertEqual(False, self.dispatcher.command_list_ok) - self.dispatcher.handle_request(u'ping') - self.assert_(u'ping' in self.dispatcher.command_list) - result = self.dispatcher.handle_request(u'command_list_end') - self.assert_(u'OK' in result) - self.assertEqual(False, self.dispatcher.command_list) - - def test_command_list_with_error_returns_ack_with_correct_index(self): - self.dispatcher.handle_request(u'command_list_begin') - self.dispatcher.handle_request(u'play') # Known command - self.dispatcher.handle_request(u'paly') # Unknown command - result = self.dispatcher.handle_request(u'command_list_end') - self.assertEqual(len(result), 1, result) - self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"') - - def test_command_list_ok_begin(self): - result = self.dispatcher.handle_request(u'command_list_ok_begin') - self.assertEquals(result, []) - - def test_command_list_ok_with_ping(self): - self.dispatcher.handle_request(u'command_list_ok_begin') - self.assertEqual([], self.dispatcher.command_list) - self.assertEqual(True, self.dispatcher.command_list_ok) - self.dispatcher.handle_request(u'ping') - self.assert_(u'ping' in self.dispatcher.command_list) - result = self.dispatcher.handle_request(u'command_list_end') - self.assert_(u'list_OK' in result) - self.assert_(u'OK' in result) - self.assertEqual(False, self.dispatcher.command_list) - self.assertEqual(False, self.dispatcher.command_list_ok) diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py new file mode 100644 index 00000000..9b5ef690 --- /dev/null +++ b/tests/frontends/mpd/protocol/command_list_test.py @@ -0,0 +1,53 @@ +from tests.frontends.mpd import protocol + +class CommandListsTest(protocol.BaseTestCase): + def test_command_list_begin(self): + response = self.sendRequest(u'command_list_begin') + self.assertEquals([], response) + + def test_command_list_end(self): + self.sendRequest(u'command_list_begin') + self.sendRequest(u'command_list_end') + self.assertInResponse(u'OK') + + def test_command_list_end_without_start_first_is_an_unknown_command(self): + self.sendRequest(u'command_list_end') + self.assertEqualResponse( + u'ACK [5@0] {} unknown command "command_list_end"') + + def test_command_list_with_ping(self): + self.sendRequest(u'command_list_begin') + self.assertEqual([], self.dispatcher.command_list) + self.assertEqual(False, self.dispatcher.command_list_ok) + self.sendRequest(u'ping') + self.assert_(u'ping' in self.dispatcher.command_list) + self.sendRequest(u'command_list_end') + self.assertInResponse(u'OK') + self.assertEqual(False, self.dispatcher.command_list) + + def test_command_list_with_error_returns_ack_with_correct_index(self): + self.sendRequest(u'command_list_begin') + self.sendRequest(u'play') # Known command + self.sendRequest(u'paly') # Unknown command + self.sendRequest(u'command_list_end') + self.assertEqualResponse(u'ACK [5@1] {} unknown command "paly"') + + def test_command_list_ok_begin(self): + response = self.sendRequest(u'command_list_ok_begin') + self.assertEquals([], response) + + def test_command_list_ok_with_ping(self): + self.sendRequest(u'command_list_ok_begin') + self.assertEqual([], self.dispatcher.command_list) + self.assertEqual(True, self.dispatcher.command_list_ok) + self.sendRequest(u'ping') + self.assert_(u'ping' in self.dispatcher.command_list) + self.sendRequest(u'command_list_end') + self.assertInResponse(u'list_OK') + self.assertInResponse(u'OK') + self.assertEqual(False, self.dispatcher.command_list) + self.assertEqual(False, self.dispatcher.command_list_ok) + + # FIXME this should also include the special handling of idle within a + # command list. That is that once a idle/noidle command is found inside a + # commad list, the rest of the list seems to be ignored. From b5a32a1dc3fda065a1bbc0da18378439a6436b1f Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 20 Jul 2011 20:35:27 +0200 Subject: [PATCH 185/350] Migrate authentication_test --- .../mpd/{ => protocol}/authentication_test.py | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) rename tests/frontends/mpd/{ => protocol}/authentication_test.py (53%) diff --git a/tests/frontends/mpd/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py similarity index 53% rename from tests/frontends/mpd/authentication_test.py rename to tests/frontends/mpd/protocol/authentication_test.py index fb32ea54..a1487cf9 100644 --- a/tests/frontends/mpd/authentication_test.py +++ b/tests/frontends/mpd/protocol/authentication_test.py @@ -1,63 +1,61 @@ -import mock -import unittest - from mopidy import settings -from mopidy.frontends.mpd import MpdSession -from mopidy.frontends.mpd.dispatcher import MpdDispatcher -class AuthenticationTest(unittest.TestCase): - def setUp(self): - self.session = mock.Mock(spec=MpdSession) - self.dispatcher = MpdDispatcher(session=self.session) - - def tearDown(self): - settings.runtime.clear() +from tests.frontends.mpd import protocol +class AuthenticationTest(protocol.BaseTestCase): def test_authentication_with_valid_password_is_accepted(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'password "topsecret"') + + self.sendRequest(u'password "topsecret"') self.assertTrue(self.dispatcher.authenticated) - self.assert_(u'OK' in response) + self.assertInResponse(u'OK') def test_authentication_with_invalid_password_is_not_accepted(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'password "secret"') + + self.sendRequest(u'password "secret"') self.assertFalse(self.dispatcher.authenticated) - self.assert_(u'ACK [3@0] {password} incorrect password' in response) + self.assertEqualResponse(u'ACK [3@0] {password} incorrect password') def test_authentication_with_anything_when_password_check_turned_off(self): settings.MPD_SERVER_PASSWORD = None - response = self.dispatcher.handle_request(u'any request at all') + + self.sendRequest(u'any request at all') self.assertTrue(self.dispatcher.authenticated) - self.assert_('ACK [5@0] {} unknown command "any"' in response) + self.assertEqualResponse('ACK [5@0] {} unknown command "any"') def test_anything_when_not_authenticated_should_fail(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'any request at all') + + self.sendRequest(u'any request at all') self.assertFalse(self.dispatcher.authenticated) - self.assert_( - u'ACK [4@0] {any} you don\'t have permission for "any"' in response) + self.assertEqualResponse( + u'ACK [4@0] {any} you don\'t have permission for "any"') def test_close_is_allowed_without_authentication(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'close') + + self.sendRequest(u'close') self.assertFalse(self.dispatcher.authenticated) - self.assert_(u'OK' in response) + self.assertInResponse(u'OK') def test_commands_is_allowed_without_authentication(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'commands') + + self.sendRequest(u'commands') self.assertFalse(self.dispatcher.authenticated) - self.assert_(u'OK' in response) + self.assertInResponse(u'OK') def test_notcommands_is_allowed_without_authentication(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'notcommands') + + self.sendRequest(u'notcommands') self.assertFalse(self.dispatcher.authenticated) - self.assert_(u'OK' in response) + self.assertInResponse(u'OK') def test_ping_is_allowed_without_authentication(self): settings.MPD_SERVER_PASSWORD = u'topsecret' - response = self.dispatcher.handle_request(u'ping') + + self.sendRequest(u'ping') self.assertFalse(self.dispatcher.authenticated) - self.assert_(u'OK' in response) + self.assertInResponse(u'OK') From 08d486785da5a3a90782278c6cabd74bae28ff0b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Fri, 22 Jul 2011 22:50:36 +0200 Subject: [PATCH 186/350] Basic working version of idle command --- mopidy/frontends/mpd/dispatcher.py | 66 +++++- mopidy/frontends/mpd/protocol/status.py | 6 +- tests/frontends/mpd/protocol/__init__.py | 4 + tests/frontends/mpd/protocol/idle_test.py | 210 ++++++++++++++++++ .../frontends/mpd/protocol/reflection_test.py | 4 +- tests/frontends/mpd/protocol/status_test.py | 15 -- 6 files changed, 283 insertions(+), 22 deletions(-) create mode 100644 tests/frontends/mpd/protocol/idle_test.py diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 6cc05bec..a36d38e3 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -20,6 +20,10 @@ from mopidy.utils import flatten logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') +#: Subsystems that can be registered with idle command. +SUBSYSTEMS = ['database', 'mixer', 'options', 'output', + 'player', 'playlist', 'stored_playlist', 'update', ] + class MpdDispatcher(object): """ The MPD session feeds the MPD dispatcher with requests. The dispatcher @@ -32,6 +36,8 @@ class MpdDispatcher(object): self.command_list = False self.command_list_ok = False self.command_list_index = None + self.subscriptions = set() + self.events = set() self.context = MpdContext(self, session=session) def handle_request(self, request, current_command_list_index=None): @@ -42,13 +48,26 @@ class MpdDispatcher(object): self._catch_mpd_ack_errors_filter, self._authenticate_filter, self._command_list_filter, + self._idle_filter, self._add_ok_filter, self._call_handler_filter, ] return self._call_next_filter(request, response, filter_chain) def handle_idle(self, subsystem): - logger.debug(u'Got idle event for %s', subsystem) + self.events.add(subsystem) + + subsystems = self.subscriptions.intersection(self.events) + if not subsystems: + return + + response = [] + for subsystem in subsystems: + response.append(u'changed: %s' % subsystem) + response.append(u'OK') + self.subscriptions = set() + self.events = set() + self.context.session.send_lines(response) def _call_next_filter(self, request, response, filter_chain): if filter_chain: @@ -111,17 +130,60 @@ class MpdDispatcher(object): and request != u'command_list_end') + ### Filter: idle + + def _idle_filter(self, request, response, filter_chain): + if re.match(r'^noidle$', request): + if not self.subscriptions: + return [] + self.subscriptions = set() + self.events = set() + self.context.session.connection.enable_timeout() + return [u'OK'] + + if self.subscriptions: + self.context.session.close() + return [] + + if re.match(r'^idle( .+)?$', request): + for subsystem in self._extract_subsystems(request): + self.subscriptions.add(subsystem) + + subsystems = self.subscriptions.intersection(self.events) + if subsystems: + for subsystem in subsystems: + response.append(u'changed: %s' % subsystem) + self.events = set() + self.subscriptions = set() + response.append(u'OK') + return response + else: + self.context.session.connection.disable_timeout() + return [] + + return self._call_next_filter(request, response, filter_chain) + + def _extract_subsystems(self, request): + match = re.match(r'^idle (?P.+)$', request) + if not match: + return SUBSYSTEMS + return match.groupdict()['subsystems'].split(' ') + + ### Filter: add OK def _add_ok_filter(self, request, response, filter_chain): response = self._call_next_filter(request, response, filter_chain) - if not self._has_error(response): + if not self._has_error(response) and not self._is_idle(request): response.append(u'OK') return response def _has_error(self, response): return response and response[-1].startswith(u'ACK') + def _is_idle(self, request): + return request.startswith('idle') + ### Filter: call handler diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index abbb8d7f..4a961e76 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -32,8 +32,8 @@ def currentsong(context): position=context.backend.playback.current_playlist_position.get(), cpid=current_cp_track.cpid) -@handle_request(r'^idle$') -@handle_request(r'^idle (?P.+)$') +#@handle_request(r'^idle$') +#@handle_request(r'^idle (?P.+)$') def idle(context, subsystems=None): """ *musicpd.org, status section:* @@ -69,7 +69,7 @@ def idle(context, subsystems=None): """ pass # TODO -@handle_request(r'^noidle$') +#@handle_request(r'^noidle$') def noidle(context): """See :meth:`_status_idle`.""" pass # TODO diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 77825a6e..68668bc2 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -27,6 +27,7 @@ class BaseTestCase(unittest.TestCase): self.connection = MockConnetion() self.session = mpd.MpdSession(self.connection) self.dispatcher = self.session.dispatcher + self.context = self.dispatcher.context def tearDown(self): self.backend.stop().get() @@ -38,6 +39,9 @@ class BaseTestCase(unittest.TestCase): self.session.on_line_received(request) return self.connection.response + def assertNoResponse(self): + self.assertEqual([], self.connection.response) + def assertInResponse(self, value): self.assert_(value in self.connection.response, u'Did not find %s ' 'in %s' % (repr(value), repr(self.connection.response))) diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py new file mode 100644 index 00000000..319789bf --- /dev/null +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -0,0 +1,210 @@ +from mock import patch + +from mopidy.frontends.mpd.dispatcher import SUBSYSTEMS +from mopidy.models import Track + +from tests.frontends.mpd import protocol + +class IdleHandlerTest(protocol.BaseTestCase): + def idleEvent(self, subsystem): + self.session.on_idle(subsystem) + + def assertEqualEvents(self, events): + self.assertEqual(set(events), self.dispatcher.events) + + def assertEqualSubscriptions(self, events): + self.assertEqual(set(events), self.dispatcher.subscriptions) + + def assertNoEvents(self): + self.assertEqualEvents([]) + + def assertNoSubscriptions(self): + self.assertEqualSubscriptions([]) + + def test_base_state(self): + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertNoResponse() + + def test_idle(self): + self.sendRequest(u'idle') + self.assertEqualSubscriptions(SUBSYSTEMS) + self.assertNoEvents() + self.assertNoResponse() + + def test_idle_disables_timeout(self): + self.sendRequest(u'idle') + self.connection.disable_timeout.assert_called_once_with() + + def test_noidle(self): + self.sendRequest(u'noidle') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertNoResponse() + + def test_noidle_does_not_call_enable_timeout(self): + self.sendRequest(u'noidle') + self.assertEqual(0, self.connection.enable_timeout.call_count) + + def test_idle_player(self): + self.sendRequest(u'idle player') + self.assertEqualSubscriptions(['player']) + self.assertNoEvents() + self.assertNoResponse() + + def test_idle_player_playlist(self): + self.sendRequest(u'idle player playlist') + self.assertEqualSubscriptions(['player', 'playlist']) + self.assertNoEvents() + self.assertNoResponse() + + def test_idle_then_noidle(self): + self.sendRequest(u'idle') + self.sendRequest(u'noidle') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertInResponse(u'OK') + + def test_idle_then_noidle_enables_timeout(self): + self.sendRequest(u'idle') + self.sendRequest(u'noidle') + self.connection.enable_timeout.assert_called_once_with() + + def test_idle_then_play(self): + with patch.object(self.session, 'stop') as stop_mock: + self.sendRequest(u'idle') + self.sendRequest(u'play') + stop_mock.assert_called_once_with() + + def test_idle_then_idle(self): + with patch.object(self.session, 'stop') as stop_mock: + self.sendRequest(u'idle') + self.sendRequest(u'idle') + stop_mock.assert_called_once_with() + + def test_idle_player_then_play(self): + with patch.object(self.session, 'stop') as stop_mock: + self.sendRequest(u'idle player') + self.sendRequest(u'play') + stop_mock.assert_called_once_with() + + def test_idle_then_player(self): + self.sendRequest(u'idle') + self.idleEvent(u'player') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertInResponse(u'changed: player') + self.assertInResponse(u'OK') + + def test_idle_player_then_event_player(self): + self.sendRequest(u'idle player') + self.idleEvent(u'player') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertInResponse(u'changed: player') + self.assertInResponse(u'OK') + + def test_idle_player_then_noidle(self): + self.sendRequest(u'idle player') + self.sendRequest(u'noidle') + self.assertNoSubscriptions() + self.assertNoEvents() + self.assertInResponse(u'OK') + + def test_idle_player_playlist_then_noidle(self): + self.sendRequest(u'idle player playlist') + self.sendRequest(u'noidle') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'OK') + + def test_idle_player_playlist_then_player(self): + self.sendRequest(u'idle player playlist') + self.idleEvent(u'player') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'changed: player') + self.assertNotInResponse(u'changed: playlist') + self.assertInResponse(u'OK') + + def test_idle_playlist_then_player(self): + self.sendRequest(u'idle playlist') + self.idleEvent(u'player') + self.assertEqualEvents(['player']) + self.assertEqualSubscriptions(['playlist']) + self.assertNoResponse() + + def test_idle_playlist_then_player_then_playlist(self): + self.sendRequest(u'idle playlist') + self.idleEvent(u'player') + self.idleEvent(u'playlist') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertNotInResponse(u'changed: player') + self.assertInResponse(u'changed: playlist') + self.assertInResponse(u'OK') + + def test_player(self): + self.idleEvent(u'player') + self.assertEqualEvents(['player']) + self.assertNoSubscriptions() + self.assertNoResponse() + + def test_player_then_idle_player(self): + self.idleEvent(u'player') + self.sendRequest(u'idle player') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'changed: player') + self.assertNotInResponse(u'changed: playlist') + self.assertInResponse(u'OK') + + def test_player_then_playlist(self): + self.idleEvent(u'player') + self.idleEvent(u'playlist') + self.assertEqualEvents(['player', 'playlist']) + self.assertNoSubscriptions() + self.assertNoResponse() + + def test_player_then_idle(self): + self.idleEvent(u'player') + self.sendRequest(u'idle') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'changed: player') + self.assertInResponse(u'OK') + + def test_player_then_playlist_then_idle(self): + self.idleEvent(u'player') + self.idleEvent(u'playlist') + self.sendRequest(u'idle') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'changed: player') + self.assertInResponse(u'changed: playlist') + self.assertInResponse(u'OK') + + def test_player_then_idle_playlist(self): + self.idleEvent(u'player') + self.sendRequest(u'idle playlist') + self.assertEqualEvents(['player']) + self.assertEqualSubscriptions(['playlist']) + self.assertNoResponse() + + def test_player_then_idle_playlist_then_noidle(self): + self.idleEvent(u'player') + self.sendRequest(u'idle playlist') + self.sendRequest(u'noidle') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertInResponse(u'OK') + + def test_player_then_playlist_then_idle_playlist(self): + self.idleEvent(u'player') + self.idleEvent(u'playlist') + self.sendRequest(u'idle playlist') + self.assertNoEvents() + self.assertNoSubscriptions() + self.assertNotInResponse(u'changed: player') + self.assertInResponse(u'changed: playlist') + self.assertInResponse(u'OK') diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index 315e3051..9cf1ef97 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -9,14 +9,14 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertInResponse(u'command: commands') self.assertInResponse(u'command: play') self.assertInResponse(u'command: status') + self.assertInResponse(u'command: idle') + self.assertInResponse(u'command: noidle') # Check if commands you do not have access to are not present self.assertNotInResponse(u'command: kill') # Check if the blacklisted commands are not present self.assertNotInResponse(u'command: command_list_begin') self.assertNotInResponse(u'command: command_list_ok_begin') self.assertNotInResponse(u'command: command_list_end') - self.assertNotInResponse(u'command: idle') - self.assertNotInResponse(u'command: noidle') self.assertNotInResponse(u'command: sticker') self.assertInResponse(u'OK') diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py index 6762a4fb..f50ecd24 100644 --- a/tests/frontends/mpd/protocol/status_test.py +++ b/tests/frontends/mpd/protocol/status_test.py @@ -27,21 +27,6 @@ class StatusHandlerTest(protocol.BaseTestCase): self.sendRequest(u'currentsong') self.assertInResponse(u'OK') - def test_idle_without_subsystems(self): - # FIXME this is not the correct behaviour for idle... - self.sendRequest(u'idle') - self.assertInResponse(u'OK') - - def test_idle_with_subsystems(self): - # FIXME this is not the correct behaviour for idle... - self.sendRequest(u'idle database playlist') - self.assertInResponse(u'OK') - - def test_noidle(self): - # FIXME this is not the correct behaviour for idle... - self.sendRequest(u'noidle') - self.assertInResponse(u'OK') - def test_stats_command(self): self.sendRequest(u'stats') self.assertInResponse(u'OK') From 171137504f3e53de5dc54ae4dfc7e29111bb8285 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 02:30:13 +0200 Subject: [PATCH 187/350] Move subscriptions and events into context object --- mopidy/frontends/mpd/dispatcher.py | 36 ++++++++++++++--------- tests/frontends/mpd/protocol/idle_test.py | 4 +-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index a36d38e3..ea2fc1e8 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -36,8 +36,6 @@ class MpdDispatcher(object): self.command_list = False self.command_list_ok = False self.command_list_index = None - self.subscriptions = set() - self.events = set() self.context = MpdContext(self, session=session) def handle_request(self, request, current_command_list_index=None): @@ -55,9 +53,10 @@ class MpdDispatcher(object): return self._call_next_filter(request, response, filter_chain) def handle_idle(self, subsystem): - self.events.add(subsystem) + self.context.events.add(subsystem) - subsystems = self.subscriptions.intersection(self.events) + subsystems = self.context.subscriptions.intersection( + self.context.events) if not subsystems: return @@ -65,8 +64,8 @@ class MpdDispatcher(object): for subsystem in subsystems: response.append(u'changed: %s' % subsystem) response.append(u'OK') - self.subscriptions = set() - self.events = set() + self.context.subscriptions = set() + self.context.events = set() self.context.session.send_lines(response) def _call_next_filter(self, request, response, filter_chain): @@ -134,27 +133,28 @@ class MpdDispatcher(object): def _idle_filter(self, request, response, filter_chain): if re.match(r'^noidle$', request): - if not self.subscriptions: + if not self.context.subscriptions: return [] - self.subscriptions = set() - self.events = set() + self.context.subscriptions = set() + self.context.events = set() self.context.session.connection.enable_timeout() return [u'OK'] - if self.subscriptions: + if self.context.subscriptions: self.context.session.close() return [] if re.match(r'^idle( .+)?$', request): for subsystem in self._extract_subsystems(request): - self.subscriptions.add(subsystem) + self.context.subscriptions.add(subsystem) - subsystems = self.subscriptions.intersection(self.events) + subsystems = self.context.subscriptions.intersection( + self.context.events) if subsystems: for subsystem in subsystems: response.append(u'changed: %s' % subsystem) - self.events = set() - self.subscriptions = set() + self.context.events = set() + self.context.subscriptions = set() response.append(u'OK') return response else: @@ -246,9 +246,17 @@ class MpdContext(object): #: The current :class:`mopidy.frontends.mpd.MpdSession`. session = None + #: The active subsystems that have pending events. + events = None + + #: The subsytems that we want to be notified about in idle mode. + subscriptions = None + def __init__(self, dispatcher, session=None): self.dispatcher = dispatcher self.session = session + self.events = set() + self.subscriptions = set() self._backend = None self._mixer = None diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index 319789bf..3efb0cb1 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -10,10 +10,10 @@ class IdleHandlerTest(protocol.BaseTestCase): self.session.on_idle(subsystem) def assertEqualEvents(self, events): - self.assertEqual(set(events), self.dispatcher.events) + self.assertEqual(set(events), self.context.events) def assertEqualSubscriptions(self, events): - self.assertEqual(set(events), self.dispatcher.subscriptions) + self.assertEqual(set(events), self.context.subscriptions) def assertNoEvents(self): self.assertEqualEvents([]) From e4ce31a438d5adbb13752c09fd8be5bd38776b37 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 03:28:29 +0200 Subject: [PATCH 188/350] Ensure that empty command does not get added to command list --- mopidy/frontends/mpd/protocol/empty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/empty.py b/mopidy/frontends/mpd/protocol/empty.py index 33b3bd9f..4cdafd87 100644 --- a/mopidy/frontends/mpd/protocol/empty.py +++ b/mopidy/frontends/mpd/protocol/empty.py @@ -1,6 +1,6 @@ from mopidy.frontends.mpd.protocol import handle_request -@handle_request(r'^\s*$') +@handle_request(r'^[ ]*$') def empty(context): """The original MPD server returns ``OK`` on an empty request.""" pass From 63dba5553fbc74a26420fa3c5c5557469672c3fa Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 03:32:45 +0200 Subject: [PATCH 189/350] Move idle code from dispatcher to protocol.status module --- mopidy/frontends/mpd/dispatcher.py | 55 ++++++--------------- mopidy/frontends/mpd/protocol/reflection.py | 4 -- mopidy/frontends/mpd/protocol/status.py | 40 +++++++++++++-- tests/frontends/mpd/protocol/idle_test.py | 2 +- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index ea2fc1e8..660f82ec 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -20,10 +20,6 @@ from mopidy.utils import flatten logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') -#: Subsystems that can be registered with idle command. -SUBSYSTEMS = ['database', 'mixer', 'options', 'output', - 'player', 'playlist', 'stored_playlist', 'update', ] - class MpdDispatcher(object): """ The MPD session feeds the MPD dispatcher with requests. The dispatcher @@ -132,59 +128,40 @@ class MpdDispatcher(object): ### Filter: idle def _idle_filter(self, request, response, filter_chain): - if re.match(r'^noidle$', request): - if not self.context.subscriptions: - return [] - self.context.subscriptions = set() - self.context.events = set() - self.context.session.connection.enable_timeout() - return [u'OK'] - - if self.context.subscriptions: + if self._is_currently_idle() and not self._is_noidle(request): + logger.debug(u'Client send us %s, only %s is allowed while in ' + 'the idle state', repr(request), repr('noidle')) self.context.session.close() return [] - if re.match(r'^idle( .+)?$', request): - for subsystem in self._extract_subsystems(request): - self.context.subscriptions.add(subsystem) + if not self._is_currently_idle() and self._is_noidle(request): + return [] - subsystems = self.context.subscriptions.intersection( - self.context.events) - if subsystems: - for subsystem in subsystems: - response.append(u'changed: %s' % subsystem) - self.context.events = set() - self.context.subscriptions = set() - response.append(u'OK') - return response - else: - self.context.session.connection.disable_timeout() - return [] + response = self._call_next_filter(request, response, filter_chain) - return self._call_next_filter(request, response, filter_chain) + if self._is_currently_idle(): + return [] + else: + return response - def _extract_subsystems(self, request): - match = re.match(r'^idle (?P.+)$', request) - if not match: - return SUBSYSTEMS - return match.groupdict()['subsystems'].split(' ') + def _is_currently_idle(self): + return bool(self.context.subscriptions) + + def _is_noidle(self, request): + return re.match(r'^noidle$', request) ### Filter: add OK def _add_ok_filter(self, request, response, filter_chain): response = self._call_next_filter(request, response, filter_chain) - if not self._has_error(response) and not self._is_idle(request): + if not self._has_error(response): response.append(u'OK') return response def _has_error(self, response): return response and response[-1].startswith(u'ACK') - def _is_idle(self, request): - return request.startswith('idle') - - ### Filter: call handler def _call_handler_filter(self, request, response, filter_chain): diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 3618f5e1..dbd76034 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -27,10 +27,6 @@ def commands(context): command_names.remove('command_list_ok_begin') if 'command_list_end' in command_names: command_names.remove('command_list_end') - if 'idle' in command_names: - command_names.remove('idle') - if 'noidle' in command_names: - command_names.remove('noidle') if 'sticker' in command_names: command_names.remove('sticker') diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 4a961e76..444ec0c2 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -4,6 +4,10 @@ from mopidy.backends.base import PlaybackController from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdNotImplemented +#: Subsystems that can be registered with idle command. +SUBSYSTEMS = ['database', 'mixer', 'options', 'output', + 'player', 'playlist', 'stored_playlist', 'update', ] + @handle_request(r'^clearerror$') def clearerror(context): """ @@ -32,8 +36,8 @@ def currentsong(context): position=context.backend.playback.current_playlist_position.get(), cpid=current_cp_track.cpid) -#@handle_request(r'^idle$') -#@handle_request(r'^idle (?P.+)$') +@handle_request(r'^idle$') +@handle_request(r'^idle (?P.+)$') def idle(context, subsystems=None): """ *musicpd.org, status section:* @@ -67,12 +71,38 @@ def idle(context, subsystems=None): notifications when something changed in one of the specified subsystems. """ - pass # TODO -#@handle_request(r'^noidle$') + if subsystems: + subsystems = subsystems.split() + else: + subsystems = SUBSYSTEMS + + for subsystem in subsystems: + context.subscriptions.add(subsystem) + + active = context.subscriptions.intersection(context.events) + if not active: + context.session.connection.disable_timeout() + return + + response = [] + context.events = set() + context.subscriptions = set() + + for subsystem in active: + response.append(u'changed: %s' % subsystem) + response.append(u'OK') + + return response + +@handle_request(r'^noidle$') def noidle(context): """See :meth:`_status_idle`.""" - pass # TODO + if not context.subscriptions: + return + context.subscriptions = set() + context.events = set() + context.session.connection.enable_timeout() @handle_request(r'^stats$') def stats(context): diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index 3efb0cb1..0f56cd61 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -1,6 +1,6 @@ from mock import patch -from mopidy.frontends.mpd.dispatcher import SUBSYSTEMS +from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS from mopidy.models import Track from tests.frontends.mpd import protocol From 0e58d771cdf332edef1f4a93fb131cd5f4ba434b Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 03:58:26 +0200 Subject: [PATCH 190/350] Make tests check that response only has values once, fixes double OK bug --- mopidy/frontends/mpd/protocol/status.py | 2 -- tests/frontends/mpd/protocol/__init__.py | 5 +++ tests/frontends/mpd/protocol/idle_test.py | 42 +++++++++++------------ 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 444ec0c2..5a319a5d 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -91,8 +91,6 @@ def idle(context, subsystems=None): for subsystem in active: response.append(u'changed: %s' % subsystem) - response.append(u'OK') - return response @handle_request(r'^noidle$') diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 68668bc2..00314946 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -46,6 +46,11 @@ class BaseTestCase(unittest.TestCase): self.assert_(value in self.connection.response, u'Did not find %s ' 'in %s' % (repr(value), repr(self.connection.response))) + def assertOnceInResponse(self, value): + matched = len([r for r in self.connection.response if r == value]) + self.assertEqual(1, matched, 'Expected to find %s once in %s' % + (repr(value), repr(self.connection.response))) + def assertNotInResponse(self, value): self.assert_(value not in self.connection.response, u'Found %s in %s' % (repr(value), repr(self.connection.response))) diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index 0f56cd61..0f5d4b06 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -63,7 +63,7 @@ class IdleHandlerTest(protocol.BaseTestCase): self.sendRequest(u'noidle') self.assertNoSubscriptions() self.assertNoEvents() - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_idle_then_noidle_enables_timeout(self): self.sendRequest(u'idle') @@ -93,39 +93,39 @@ class IdleHandlerTest(protocol.BaseTestCase): self.idleEvent(u'player') self.assertNoSubscriptions() self.assertNoEvents() - self.assertInResponse(u'changed: player') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: player') + self.assertOnceInResponse(u'OK') def test_idle_player_then_event_player(self): self.sendRequest(u'idle player') self.idleEvent(u'player') self.assertNoSubscriptions() self.assertNoEvents() - self.assertInResponse(u'changed: player') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: player') + self.assertOnceInResponse(u'OK') def test_idle_player_then_noidle(self): self.sendRequest(u'idle player') self.sendRequest(u'noidle') self.assertNoSubscriptions() self.assertNoEvents() - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_idle_player_playlist_then_noidle(self): self.sendRequest(u'idle player playlist') self.sendRequest(u'noidle') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_idle_player_playlist_then_player(self): self.sendRequest(u'idle player playlist') self.idleEvent(u'player') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'changed: player') + self.assertOnceInResponse(u'changed: player') self.assertNotInResponse(u'changed: playlist') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_idle_playlist_then_player(self): self.sendRequest(u'idle playlist') @@ -141,8 +141,8 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertNoEvents() self.assertNoSubscriptions() self.assertNotInResponse(u'changed: player') - self.assertInResponse(u'changed: playlist') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: playlist') + self.assertOnceInResponse(u'OK') def test_player(self): self.idleEvent(u'player') @@ -155,9 +155,9 @@ class IdleHandlerTest(protocol.BaseTestCase): self.sendRequest(u'idle player') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'changed: player') + self.assertOnceInResponse(u'changed: player') self.assertNotInResponse(u'changed: playlist') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_player_then_playlist(self): self.idleEvent(u'player') @@ -171,8 +171,8 @@ class IdleHandlerTest(protocol.BaseTestCase): self.sendRequest(u'idle') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'changed: player') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: player') + self.assertOnceInResponse(u'OK') def test_player_then_playlist_then_idle(self): self.idleEvent(u'player') @@ -180,9 +180,9 @@ class IdleHandlerTest(protocol.BaseTestCase): self.sendRequest(u'idle') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'changed: player') - self.assertInResponse(u'changed: playlist') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: player') + self.assertOnceInResponse(u'changed: playlist') + self.assertOnceInResponse(u'OK') def test_player_then_idle_playlist(self): self.idleEvent(u'player') @@ -197,7 +197,7 @@ class IdleHandlerTest(protocol.BaseTestCase): self.sendRequest(u'noidle') self.assertNoEvents() self.assertNoSubscriptions() - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'OK') def test_player_then_playlist_then_idle_playlist(self): self.idleEvent(u'player') @@ -206,5 +206,5 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertNoEvents() self.assertNoSubscriptions() self.assertNotInResponse(u'changed: player') - self.assertInResponse(u'changed: playlist') - self.assertInResponse(u'OK') + self.assertOnceInResponse(u'changed: playlist') + self.assertOnceInResponse(u'OK') From b33e66b0c807e532a20638415c05aaa322fd66c6 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 14:47:35 +0200 Subject: [PATCH 191/350] Add comment to idle filter --- mopidy/frontends/mpd/dispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 660f82ec..a638d6e9 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -135,7 +135,7 @@ class MpdDispatcher(object): return [] if not self._is_currently_idle() and self._is_noidle(request): - return [] + return [] # noidle was called before idle response = self._call_next_filter(request, response, filter_chain) From 9895f5197cfb1d910a130a2a4d692ea2a379919a Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sat, 23 Jul 2011 14:48:52 +0200 Subject: [PATCH 192/350] Test via on_receive instead of on_line_received to ensure timeout code is also tested --- tests/frontends/mpd/protocol/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 00314946..8cd91d60 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -34,9 +34,10 @@ class BaseTestCase(unittest.TestCase): self.mixer.stop().get() settings.runtime.clear() - def sendRequest(self, request, clear=False): + def sendRequest(self, request): self.connection.response = [] - self.session.on_line_received(request) + request = '%s\n' % request.encode('utf-8') + self.session.on_receive({'received': request}) return self.connection.response def assertNoResponse(self): From 451b52fde543e2edd0a05b3dd569e67aab66be99 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Jul 2011 01:59:32 +0200 Subject: [PATCH 193/350] Make sure we prevent timeouts when in idle mode --- mopidy/frontends/mpd/protocol/status.py | 4 ++-- mopidy/utils/network.py | 4 +++- tests/frontends/mpd/protocol/idle_test.py | 4 ---- tests/utils/network/lineprotocol_test.py | 12 ++++++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 5a319a5d..5ac99dfe 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -82,7 +82,7 @@ def idle(context, subsystems=None): active = context.subscriptions.intersection(context.events) if not active: - context.session.connection.disable_timeout() + context.session.prevent_timeout = True return response = [] @@ -100,7 +100,7 @@ def noidle(context): return context.subscriptions = set() context.events = set() - context.session.connection.enable_timeout() + context.session.prevent_timeout = False @handle_request(r'^stats$') def stats(context): diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index b7cc144d..719def69 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -286,6 +286,7 @@ class LineProtocol(ThreadingActor): def __init__(self, connection): self.connection = connection + self.prevent_timeout = False self.recv_buffer = '' @property @@ -316,7 +317,8 @@ class LineProtocol(ThreadingActor): line = self.decode(line) self.on_line_received(line) - self.connection.enable_timeout() + if not self.prevent_timeout: + self.connection.enable_timeout() def on_stop(self): """Ensure that cleanup when actor stops.""" diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index 0f5d4b06..da16bf33 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -42,10 +42,6 @@ class IdleHandlerTest(protocol.BaseTestCase): self.assertNoEvents() self.assertNoResponse() - def test_noidle_does_not_call_enable_timeout(self): - self.sendRequest(u'noidle') - self.assertEqual(0, self.connection.enable_timeout.call_count) - def test_idle_player(self): self.sendRequest(u'idle player') self.assertEqualSubscriptions(['player']) diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index a87f461c..41d3fbf2 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -11,11 +11,13 @@ class LineProtocolTest(unittest.TestCase): self.mock = Mock(spec=network.LineProtocol) self.mock.terminator = network.LineProtocol.terminator self.mock.encoding = network.LineProtocol.encoding + self.mock.prevent_timeout = False def test_init_stores_values_in_attributes(self): network.LineProtocol.__init__(self.mock, sentinel.connection) self.assertEqual(sentinel.connection, self.mock.connection) self.assertEqual('', self.mock.recv_buffer) + self.assertFalse(self.mock.prevent_timeout) def test_on_receive_no_new_lines_adds_to_recv_buffer(self): self.mock.connection = Mock(spec=network.Connection) @@ -36,6 +38,16 @@ class LineProtocolTest(unittest.TestCase): self.mock.connection.disable_timeout.assert_called_once_with() self.mock.connection.enable_timeout.assert_called_once_with() + def test_on_receive_toggles_unless_prevent_timeout_is_set(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [] + self.mock.prevent_timeout = True + + network.LineProtocol.on_receive(self.mock, {'received': 'data'}) + self.mock.connection.disable_timeout.assert_called_once_with() + self.assertEqual(0, self.mock.connection.enable_timeout.call_count) + def test_on_receive_no_new_lines_calls_parse_lines(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' From 6bd39187061432250a9fc78b11492e0fee1c9ef4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Jul 2011 12:03:30 +0200 Subject: [PATCH 194/350] Add idle addition to changelog --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 0a9ab925..19c65ee9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -25,6 +25,9 @@ v0.6.0 (in development) - The local client now tries to lookup where your music is via XDG, it will fall-back to ``~/music`` or use whatever setting you set manually. +- The idle command is now supported by mopidy for the following subsystems: + player, playlist, options and mixer (Fixes: :issue:`32`). + **Changes** - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with From 2c0f2ab82cea30434b3793a918d96ce340c02b89 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 24 Jul 2011 19:48:41 +0200 Subject: [PATCH 195/350] Typo fix --- mopidy/frontends/mpd/dispatcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index a638d6e9..b502ee5c 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -129,8 +129,8 @@ class MpdDispatcher(object): def _idle_filter(self, request, response, filter_chain): if self._is_currently_idle() and not self._is_noidle(request): - logger.debug(u'Client send us %s, only %s is allowed while in ' - 'the idle state', repr(request), repr('noidle')) + logger.debug(u'Client sent us %s, only %s is allowed while in ' + 'the idle state', repr(request), repr(u'noidle')) self.context.session.close() return [] From 5d025a5721216c4f9ffb7c99665374e4a332b258 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 00:30:21 +0200 Subject: [PATCH 196/350] Compile noidle regexp used for is_noidle check --- mopidy/frontends/mpd/dispatcher.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index b502ee5c..cab014a8 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -27,6 +27,8 @@ class MpdDispatcher(object): back to the MPD session. """ + _noidle = re.compile(r'^noidle$') + def __init__(self, session=None): self.authenticated = False self.command_list = False @@ -128,13 +130,13 @@ class MpdDispatcher(object): ### Filter: idle def _idle_filter(self, request, response, filter_chain): - if self._is_currently_idle() and not self._is_noidle(request): + if self._is_currently_idle() and not self._noidle.match(request): logger.debug(u'Client sent us %s, only %s is allowed while in ' 'the idle state', repr(request), repr(u'noidle')) self.context.session.close() return [] - if not self._is_currently_idle() and self._is_noidle(request): + if not self._is_currently_idle() and self._noidle.match(request): return [] # noidle was called before idle response = self._call_next_filter(request, response, filter_chain) @@ -147,9 +149,6 @@ class MpdDispatcher(object): def _is_currently_idle(self): return bool(self.context.subscriptions) - def _is_noidle(self, request): - return re.match(r'^noidle$', request) - ### Filter: add OK From 4049b23c3f8baeb191c6657f42229dae3cc4c500 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 00:50:41 +0200 Subject: [PATCH 197/350] Add concept of delimeter to complemend terminator in LineProtocol --- mopidy/utils/network.py | 13 +++++++++-- tests/utils/network/lineprotocol_test.py | 28 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index b7cc144d..9a02035b 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -278,9 +278,13 @@ class LineProtocol(ThreadingActor): then splitting data along line boundaries. """ - #: What terminator to use to split lines. + #: Line terinator to use for outputed lines. terminator = '\n' + #: Regex to use for splitings lines, will be set compiled version of its + #: own value, or to `terminator`s value if it is not set itself. + delimeter = None + #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' @@ -288,6 +292,11 @@ class LineProtocol(ThreadingActor): self.connection = connection self.recv_buffer = '' + if self.delimeter: + self.delimeter = re.compile(self.delimeter) + else: + self.delimeter = re.compile(self.terminator) + @property def host(self): return self.connection.host @@ -325,7 +334,7 @@ class LineProtocol(ThreadingActor): def parse_lines(self): """Consume new data and yield any lines found.""" while re.search(self.terminator, self.recv_buffer): - line, self.recv_buffer = re.split(self.terminator, + line, self.recv_buffer = self.delimeter.split( self.recv_buffer, 1) yield line diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index a87f461c..57b78417 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -1,5 +1,6 @@ #encoding: utf-8 +import re import unittest from mopidy.utils import network @@ -11,11 +12,21 @@ class LineProtocolTest(unittest.TestCase): self.mock = Mock(spec=network.LineProtocol) self.mock.terminator = network.LineProtocol.terminator self.mock.encoding = network.LineProtocol.encoding + self.mock.delimeter = network.LineProtocol.delimeter def test_init_stores_values_in_attributes(self): + delimeter = re.compile(network.LineProtocol.terminator) network.LineProtocol.__init__(self.mock, sentinel.connection) self.assertEqual(sentinel.connection, self.mock.connection) self.assertEqual('', self.mock.recv_buffer) + self.assertEqual(delimeter, self.mock.delimeter) + + def test_init_compiles_delimeter(self): + self.mock.delimeter = '\r?\n' + delimeter = re.compile('\r?\n') + + network.LineProtocol.__init__(self.mock, sentinel.connection) + self.assertEqual(delimeter, self.mock.delimeter) def test_on_receive_no_new_lines_adds_to_recv_buffer(self): self.mock.connection = Mock(spec=network.Connection) @@ -74,18 +85,21 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual(2, self.mock.on_line_received.call_count) def test_parse_lines_emtpy_buffer(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = '' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) def test_parse_lines_no_terminator(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = 'data' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) def test_parse_lines_termintor(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = 'data\n' lines = network.LineProtocol.parse_lines(self.mock) @@ -93,7 +107,17 @@ class LineProtocolTest(unittest.TestCase): self.assertRaises(StopIteration, lines.next) self.assertEqual('', self.mock.recv_buffer) + def test_parse_lines_termintor_with_carriage_return(self): + self.mock.delimeter = re.compile(r'\r?\n') + self.mock.recv_buffer = 'data\r\n' + + lines = network.LineProtocol.parse_lines(self.mock) + self.assertEqual('data', lines.next()) + self.assertRaises(StopIteration, lines.next) + self.assertEqual('', self.mock.recv_buffer) + def test_parse_lines_no_data_before_terminator(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = '\n' lines = network.LineProtocol.parse_lines(self.mock) @@ -102,6 +126,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_extra_data_after_terminator(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = 'data1\ndata2' lines = network.LineProtocol.parse_lines(self.mock) @@ -110,6 +135,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual('data2', self.mock.recv_buffer) def test_parse_lines_unicode(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = u'æøå\n'.encode('utf-8') lines = network.LineProtocol.parse_lines(self.mock) @@ -118,6 +144,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_multiple_lines(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = 'abc\ndef\nghi\njkl' lines = network.LineProtocol.parse_lines(self.mock) @@ -128,6 +155,7 @@ class LineProtocolTest(unittest.TestCase): self.assertEqual('jkl', self.mock.recv_buffer) def test_parse_lines_multiple_calls(self): + self.mock.delimeter = re.compile(r'\n') self.mock.recv_buffer = 'data1' lines = network.LineProtocol.parse_lines(self.mock) From 68c947ddf261c3db9632772c743bf9586d46f041 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 00:52:32 +0200 Subject: [PATCH 198/350] Allow clients to use carriage return in mpd sessions --- mopidy/frontends/mpd/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 8b6d3770..78742ed5 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -48,6 +48,7 @@ class MpdSession(network.LineProtocol): terminator = protocol.LINE_TERMINATOR encoding = protocol.ENCODING + delimeter = r'\r?\n' def __init__(self, connection): super(MpdSession, self).__init__(connection) From c724fcd7c9e12192e3c3f5afccadab6aede88f02 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 01:00:18 +0200 Subject: [PATCH 199/350] Turns out idle and noidle are not commands that should be listed --- mopidy/frontends/mpd/protocol/reflection.py | 4 ++++ tests/frontends/mpd/protocol/reflection_test.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index dbd76034..3618f5e1 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -27,6 +27,10 @@ def commands(context): command_names.remove('command_list_ok_begin') if 'command_list_end' in command_names: command_names.remove('command_list_end') + if 'idle' in command_names: + command_names.remove('idle') + if 'noidle' in command_names: + command_names.remove('noidle') if 'sticker' in command_names: command_names.remove('sticker') diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index 9cf1ef97..315e3051 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -9,14 +9,14 @@ class ReflectionHandlerTest(protocol.BaseTestCase): self.assertInResponse(u'command: commands') self.assertInResponse(u'command: play') self.assertInResponse(u'command: status') - self.assertInResponse(u'command: idle') - self.assertInResponse(u'command: noidle') # Check if commands you do not have access to are not present self.assertNotInResponse(u'command: kill') # Check if the blacklisted commands are not present self.assertNotInResponse(u'command: command_list_begin') self.assertNotInResponse(u'command: command_list_ok_begin') self.assertNotInResponse(u'command: command_list_end') + self.assertNotInResponse(u'command: idle') + self.assertNotInResponse(u'command: noidle') self.assertNotInResponse(u'command: sticker') self.assertInResponse(u'OK') From 01cfbead30b57803dd8dc58c9ec96bec13ba741c Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 01:03:58 +0200 Subject: [PATCH 200/350] Use sets to compute output --- mopidy/frontends/mpd/protocol/reflection.py | 28 ++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/reflection.py b/mopidy/frontends/mpd/protocol/reflection.py index 3618f5e1..df13b4b4 100644 --- a/mopidy/frontends/mpd/protocol/reflection.py +++ b/mopidy/frontends/mpd/protocol/reflection.py @@ -11,28 +11,16 @@ def commands(context): Shows which commands the current user has access to. """ if context.dispatcher.authenticated: - command_names = [command.name for command in mpd_commands] + command_names = set([command.name for command in mpd_commands]) else: - command_names = [command.name for command in mpd_commands - if not command.auth_required] + command_names = set([command.name for command in mpd_commands + if not command.auth_required]) - # No permission to use - if 'kill' in command_names: - command_names.remove('kill') - - # Not shown by MPD in its command list - if 'command_list_begin' in command_names: - command_names.remove('command_list_begin') - if 'command_list_ok_begin' in command_names: - command_names.remove('command_list_ok_begin') - if 'command_list_end' in command_names: - command_names.remove('command_list_end') - if 'idle' in command_names: - command_names.remove('idle') - if 'noidle' in command_names: - command_names.remove('noidle') - if 'sticker' in command_names: - command_names.remove('sticker') + # No one is permited to use kill, rest of commands are not listed by MPD, + # so we shouldn't either. + command_names = command_names - set(['kill', 'command_list_begin', + 'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end', + 'idle', 'noidle', 'sticker']) return [('command', command_name) for command_name in sorted(command_names)] From 9fe4674b367b7714f235660d6d31019354db9b43 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 01:24:19 +0200 Subject: [PATCH 201/350] Update on_received to handle that decode can fail --- mopidy/utils/network.py | 3 ++- tests/utils/network/lineprotocol_test.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 9a02035b..6b2f69e5 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -323,7 +323,8 @@ class LineProtocol(ThreadingActor): for line in self.parse_lines(): line = self.decode(line) - self.on_line_received(line) + if line is not None: + self.on_line_received(line) self.connection.enable_timeout() diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index 57b78417..f3877126 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -74,6 +74,15 @@ class LineProtocolTest(unittest.TestCase): network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) self.mock.on_line_received.assert_called_once_with(sentinel.decoded) + def test_on_receive_with_new_line_with_failed_decode(self): + self.mock.connection = Mock(spec=network.Connection) + self.mock.recv_buffer = '' + self.mock.parse_lines.return_value = [sentinel.line] + self.mock.decode.return_value = None + + network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) + self.assertEqual(0, self.mock.on_line_received.call_count) + def test_on_receive_with_new_lines_calls_on_recieve(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' From b2188f13cbc36bf6286400621dec4a12a0ef23c9 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Mon, 25 Jul 2011 17:41:02 +0200 Subject: [PATCH 202/350] Typo fixes --- mopidy/utils/network.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index a1ddeb82..9306ccd7 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -278,11 +278,11 @@ class LineProtocol(ThreadingActor): then splitting data along line boundaries. """ - #: Line terinator to use for outputed lines. + #: Line terminator to use for outputed lines. terminator = '\n' - #: Regex to use for splitings lines, will be set compiled version of its - #: own value, or to `terminator`s value if it is not set itself. + #: Regex to use for spliting lines, will be set compiled version of its + #: own value, or to ``terminator``s value if it is not set itself. delimeter = None #: What encoding to expect incomming data to be in, can be :class:`None`. From 02713d0b5c291708e909a4abc3b6b65d4ab6a1db Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Jul 2011 10:52:25 +0200 Subject: [PATCH 203/350] Work around high start time position in ncmpcpp and mpc --- mopidy/frontends/mpd/protocol/status.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 5ac99dfe..8fe83be5 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -246,7 +246,13 @@ def _status_time(futures): _status_time_total(futures) // 1000) def _status_time_elapsed(futures): - return futures['playback.time_position'].get() + time_position = futures['playback.time_position'].get() + if time_position < 1000: + # XXX ncmpcpp and mpc interpretes the elapsed time as seconds instead + # of milliseconds if the elapsed time is less than approx. 1000. + return 0 + else: + return time_position def _status_time_total(futures): current_cp_track = futures['playback.current_cp_track'].get() From 6e2bfcf3d54cbb32cf92ce9361ce23a5bb3ee10a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 26 Jul 2011 10:57:20 +0200 Subject: [PATCH 204/350] Add test for previous commit --- tests/frontends/mpd/status_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index d277227a..a7eeeb5e 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -165,6 +165,13 @@ class StatusHandlerTest(unittest.TestCase): self.assert_('elapsed' in result) self.assertEqual(int(result['elapsed']), 59123) + def test_status_method_when_starting_playing_contains_elapsed_zero(self): + self.backend.playback.state = PAUSED + self.backend.playback.play_time_accumulated = 123 # Less than 1000ms + result = dict(status.status(self.context)) + self.assert_('elapsed' in result) + self.assertEqual(int(result['elapsed']), 0) # Zero + def test_status_method_when_playing_contains_bitrate(self): self.backend.current_playlist.append([Track(bitrate=320)]) self.backend.playback.play() From fcaaa5e645f277f79a871cd960ec637121e11782 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Jul 2011 00:08:17 +0200 Subject: [PATCH 205/350] Remove workaround from last to commits in favor of fix of root cause. Problem was that 'elapsed' should be returned in seconds, not milliseconds --- mopidy/frontends/mpd/protocol/status.py | 15 +++++---------- tests/frontends/mpd/status_test.py | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 8fe83be5..0b5ff1df 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -137,7 +137,7 @@ def status(context): Reports the current status of the player and the volume level. - - ``volume``: 0-100 + - ``volume``: 0-100 (or -1 if no output is set). - ``repeat``: 0 or 1 - ``single``: 0 or 1 - ``consume``: 0 or 1 @@ -153,7 +153,8 @@ def status(context): - ``nextsongid``: playlist songid of the next song to be played - ``time``: total time elapsed (of current playing/paused song) - ``elapsed``: Total time elapsed within the current song, but with - higher resolution. + higher resolution (i.e. time in seconds with milliseconds in decimal + places). - ``bitrate``: instantaneous bitrate in kbps - ``xfade``: crossfade in seconds - ``audio``: sampleRate``:bits``:channels @@ -242,17 +243,11 @@ def _status_state(futures): return u'pause' def _status_time(futures): - return u'%s:%s' % (_status_time_elapsed(futures) // 1000, + return u'%d:%d' % (futures['playback.time_position'].get() // 1000, _status_time_total(futures) // 1000) def _status_time_elapsed(futures): - time_position = futures['playback.time_position'].get() - if time_position < 1000: - # XXX ncmpcpp and mpc interpretes the elapsed time as seconds instead - # of milliseconds if the elapsed time is less than approx. 1000. - return 0 - else: - return time_position + return u'%.3f' % (futures['playback.time_position'].get() / 1000.0) def _status_time_total(futures): current_cp_track = futures['playback.current_cp_track'].get() diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index a7eeeb5e..2f97a7d4 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -163,14 +163,14 @@ class StatusHandlerTest(unittest.TestCase): self.backend.playback.play_time_accumulated = 59123 result = dict(status.status(self.context)) self.assert_('elapsed' in result) - self.assertEqual(int(result['elapsed']), 59123) + self.assertEqual(result['elapsed'], '59.123') def test_status_method_when_starting_playing_contains_elapsed_zero(self): self.backend.playback.state = PAUSED self.backend.playback.play_time_accumulated = 123 # Less than 1000ms result = dict(status.status(self.context)) self.assert_('elapsed' in result) - self.assertEqual(int(result['elapsed']), 0) # Zero + self.assertEqual(result['elapsed'], '0.123') def test_status_method_when_playing_contains_bitrate(self): self.backend.current_playlist.append([Track(bitrate=320)]) From 47a8993b26bc5e3d6e3430e00c1238c0ecffde40 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 00:21:14 +0200 Subject: [PATCH 206/350] Formatting --- docs/changes.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 19c65ee9..8179bcbd 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -25,21 +25,24 @@ v0.6.0 (in development) - The local client now tries to lookup where your music is via XDG, it will fall-back to ``~/music`` or use whatever setting you set manually. -- The idle command is now supported by mopidy for the following subsystems: - player, playlist, options and mixer (Fixes: :issue:`32`). +- The MPD command ``idle`` is now supported by Mopidy for the following + subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`) **Changes** - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with :attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part up to the colon of an URI, and not any prefix. + - Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors wanting to receive events from the backend. This is a formalization of the ad hoc events the Last.fm scrobbler has already been using for some time. + - Replaced all of the MPD network code that was provided by asyncore with - custom stack. This change was made to facilitate the future support of the - ``idle`` command, and to reduce the number of event loops being used. -- Fix metadata update in Shoutcast streaming (Fixes: :issue:`122`) + custom stack. This change was made to facilitate support for the ``idle`` + command, and to reduce the number of event loops being used. + +- Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`) v0.5.0 (2011-06-15) From 709fc59e4336d48eb69b329d776a1d73cacd3cb4 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 27 Jul 2011 00:28:18 +0200 Subject: [PATCH 207/350] Extract our clearifications to the mpd doc --- mopidy/frontends/mpd/protocol/status.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mopidy/frontends/mpd/protocol/status.py b/mopidy/frontends/mpd/protocol/status.py index 0b5ff1df..20a66775 100644 --- a/mopidy/frontends/mpd/protocol/status.py +++ b/mopidy/frontends/mpd/protocol/status.py @@ -137,7 +137,7 @@ def status(context): Reports the current status of the player and the volume level. - - ``volume``: 0-100 (or -1 if no output is set). + - ``volume``: 0-100 - ``repeat``: 0 or 1 - ``single``: 0 or 1 - ``consume``: 0 or 1 @@ -153,13 +153,17 @@ def status(context): - ``nextsongid``: playlist songid of the next song to be played - ``time``: total time elapsed (of current playing/paused song) - ``elapsed``: Total time elapsed within the current song, but with - higher resolution (i.e. time in seconds with milliseconds in decimal - places). + higher resolution. - ``bitrate``: instantaneous bitrate in kbps - ``xfade``: crossfade in seconds - ``audio``: sampleRate``:bits``:channels - ``updatings_db``: job id - ``error``: if there is an error, returns message here + + *Clarifications based on experience implementing* + - ``volume``: can also be -1 if no output is set. + - ``elapsed``: Higher resolution means time in seconds with three + decimal places for millisecond precision. """ futures = { 'current_playlist.tracks': context.backend.current_playlist.tracks, From 1399bb61dcf4354d7e3c974cfdee9247ffb9f11d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 00:47:34 +0200 Subject: [PATCH 208/350] Update listener method names to match interface --- mopidy/frontends/mpris.py | 16 ++++++++-------- tests/frontends/mpris/events_test.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index bbc7230f..5d7b1950 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -107,8 +107,8 @@ class MprisFrontend(ThreadingActor, BackendListener): except ImportError as e: logger.debug(u'Startup notification was not sent (%s)', e) - def paused_playing(self, track, time_position): - logger.debug(u'Received paused playing event') + def track_playback_paused(self, track, time_position): + logger.debug(u'Received track playback paused event') if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { @@ -116,8 +116,8 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) - def resumed_playing(self, track, time_position): - logger.debug(u'Received resumed playing event') + def track_playback_resumed(self, track, time_position): + logger.debug(u'Received track playback resumed event') if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { @@ -125,8 +125,8 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) - def started_playing(self, track): - logger.debug(u'Received started playing event') + def track_playback_started(self, track): + logger.debug(u'Received track playback started event') if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { @@ -135,8 +135,8 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) - def stopped_playing(self, track, time_position): - logger.debug(u'Received stopped playing event') + def track_playback_ended(self, track, time_position): + logger.debug(u'Received track playback ended event') if self.mpris_object is None: return self.mpris_object.PropertiesChanged(PLAYER_IFACE, { diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index b9a6ba77..3a3c8ad5 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -10,27 +10,27 @@ class BackendEventsTest(unittest.TestCase): self.mpris_object = mock.Mock(spec=MprisObject) self.mpris_frontend.mpris_object = self.mpris_object - def test_paused_playing_event_changes_playback_status(self): + def test_track_playback_paused_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Paused' - self.mpris_frontend.paused_playing(Track(), 0) + self.mpris_frontend.track_playback_paused(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, []) - def test_resumed_playing_event_changes_playback_status(self): + def test_track_playback_resumed_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Playing' - self.mpris_frontend.resumed_playing(Track(), 0) + self.mpris_frontend.track_playback_resumed(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) - def test_started_playing_event_changes_playback_status_and_metadata(self): + def test_track_playback_started_event_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' - self.mpris_frontend.started_playing(Track()) + self.mpris_frontend.track_playback_started(Track()) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((PLAYER_IFACE, 'Metadata'), {}), ((PLAYER_IFACE, 'PlaybackStatus'), {}), @@ -38,9 +38,9 @@ class BackendEventsTest(unittest.TestCase): self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) - def test_stopped_playing_event_changes_playback_status_and_metadata(self): + def test_track_playback_ended_event_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' - self.mpris_frontend.stopped_playing(Track(), 0) + self.mpris_frontend.track_playback_ended(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((PLAYER_IFACE, 'Metadata'), {}), ((PLAYER_IFACE, 'PlaybackStatus'), {}), From db4f2d135f8259cd387c51836d50f27688452da1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 00:53:18 +0200 Subject: [PATCH 209/350] Add missing tests for default impl of listener methods --- mopidy/listeners.py | 4 ++-- tests/listeners_test.py | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/mopidy/listeners.py b/mopidy/listeners.py index cdf693de..bb855b4d 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -14,8 +14,8 @@ class BackendListener(object): @staticmethod def send(event, **kwargs): """Helper to allow calling of backend listener events""" - # FIXME this should be updated once pykka supports non-blocking calls - # on proxies or some similar solution + # FIXME this should be updated once Pykka supports non-blocking calls + # on proxies or some similar solution. registry.ActorRegistry.broadcast({ 'command': 'pykka_call', 'attr_path': (event,), diff --git a/tests/listeners_test.py b/tests/listeners_test.py index 72737a9d..f2156d05 100644 --- a/tests/listeners_test.py +++ b/tests/listeners_test.py @@ -7,14 +7,26 @@ class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() - def test_listener_has_default_impl_for_the_track_playback_paused_event(self): + def test_listener_has_default_impl_for_track_playback_paused(self): self.listener.track_playback_paused(Track(), 0) - def test_listener_has_default_impl_for_the_track_playback_resumed_event(self): + def test_listener_has_default_impl_for_track_playback_resumed(self): self.listener.track_playback_resumed(Track(), 0) - def test_listener_has_default_impl_for_the_track_playback_started(self): + def test_listener_has_default_impl_for_track_playback_started(self): self.listener.track_playback_started(Track()) - def test_listener_has_default_impl_for_the_track_playback_ended(self): + def test_listener_has_default_impl_for_track_playback_ended(self): self.listener.track_playback_ended(Track(), 0) + + def test_listener_has_default_impl_for_playback_state_changed(self): + self.listener.playback_state_changed() + + def test_listener_has_default_impl_for_playlist_changed(self): + self.listener.playlist_changed() + + def test_listener_has_default_impl_for_options_changed(self): + self.listener.options_changed() + + def test_listener_has_default_impl_for_volume_changed(self): + self.listener.volume_changed() From 3c2a944e8a7b5147c3940846e4535906c9395ceb Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 01:01:34 +0200 Subject: [PATCH 210/350] Emit signal on volume change --- mopidy/frontends/mpris.py | 8 ++++++++ tests/frontends/mpris/events_test.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 5d7b1950..0f682a31 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -145,6 +145,14 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), }, []) + def volume_changed(self): + logger.debug(u'Received volume changed event') + if self.mpris_object is None: + return + self.mpris_object.PropertiesChanged(PLAYER_IFACE, { + 'Volume': self.mpris_object.Get(PLAYER_IFACE, 'Volume'), + }, []) + class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.1/spec/""" diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 3a3c8ad5..3b87bc88 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -47,3 +47,13 @@ class BackendEventsTest(unittest.TestCase): ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) + + def test_volume_changed_event_changes_volume(self): + self.mpris_object.Get.return_value = 1.0 + self.mpris_frontend.volume_changed() + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'Volume'), {}), + ]) + self.mpris_object.PropertiesChanged.assert_called_with( + PLAYER_IFACE, {'Volume': 1.0}, []) + From 607cdc7871521bac12af0c4fdce40021ffb8bdd2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 01:20:00 +0200 Subject: [PATCH 211/350] Add seeked event --- mopidy/backends/base/playback.py | 9 ++++++++- mopidy/listeners.py | 9 +++++++++ tests/listeners_test.py | 3 +++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 3a548dda..57a7ad85 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -484,7 +484,10 @@ class PlaybackController(object): self.play_time_started = self._current_wall_time self.play_time_accumulated = time_position - return self.provider.seek(time_position) + success = self.provider.seek(time_position) + if success: + self._trigger_seeked() + return success def stop(self, clear_current_track=False): """ @@ -540,6 +543,10 @@ class PlaybackController(object): logger.debug(u'Triggering options changed event') BackendListener.send('options_changed') + def _trigger_seeked(self): + logger.debug(u'Triggering seeked event') + BackendListener.send('seeked') + class BasePlaybackProvider(object): """ diff --git a/mopidy/listeners.py b/mopidy/listeners.py index bb855b4d..ee360bf3 100644 --- a/mopidy/listeners.py +++ b/mopidy/listeners.py @@ -105,3 +105,12 @@ class BackendListener(object): *MAY* be implemented by actor. """ pass + + def seeked(self): + """ + Called whenever the time position changes by an unexpected amount, e.g. + at seek to a new time position. + + *MAY* be implemented by actor. + """ + pass diff --git a/tests/listeners_test.py b/tests/listeners_test.py index f2156d05..d67da692 100644 --- a/tests/listeners_test.py +++ b/tests/listeners_test.py @@ -30,3 +30,6 @@ class BackendListenerTest(unittest.TestCase): def test_listener_has_default_impl_for_volume_changed(self): self.listener.volume_changed() + + def test_listener_has_default_impl_for_seeked(self): + self.listener.seeked() From 2ad54204eec9560eb92ae2a97afbc523f4ec1627 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 01:23:39 +0200 Subject: [PATCH 212/350] Emit mpris.Seeked signal on seek --- mopidy/frontends/mpris.py | 6 ++++++ tests/frontends/mpris/events_test.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 0f682a31..b2726138 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -153,6 +153,12 @@ class MprisFrontend(ThreadingActor, BackendListener): 'Volume': self.mpris_object.Get(PLAYER_IFACE, 'Volume'), }, []) + def seeked(self): + logger.debug(u'Received seeked event') + if self.mpris_object is None: + return + self.mpris_object.Seeked(PLAYER_IFACE, self.mpris_object.Get(PLAYER_IFACE, 'Position')) + class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.1/spec/""" diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 3b87bc88..3e2f0560 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -57,3 +57,10 @@ class BackendEventsTest(unittest.TestCase): self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'Volume': 1.0}, []) + def test_seeked_event_causes_mpris_seeked_event(self): + self.mpris_object.Get.return_value = 31000000 + self.mpris_frontend.seeked() + self.assertListEqual(self.mpris_object.Get.call_args_list, [ + ((PLAYER_IFACE, 'Position'), {}), + ]) + self.mpris_object.Seeked.assert_called_with( PLAYER_IFACE, 31000000) From a73303ee84f9957b0d5a3b553b695e735408050c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 01:31:07 +0200 Subject: [PATCH 213/350] Remove irrelevant warning now when the glib loop is Mopidy's main loop --- mopidy/frontends/mpris.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index b2726138..f76902e1 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -68,9 +68,6 @@ class MprisFrontend(ThreadingActor, BackendListener): player.Quit(dbus_interface='org.mpris.MediaPlayer2') """ - # This thread requires :class:`mopidy.utils.process.GObjectEventThread` to - # be running too. This is not enforced in any way by the code. - def __init__(self): self.indicate_server = None self.mpris_object = None From 13c47a763a8cdc793b999d5be50446e6ad0d18e1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 20:56:49 +0200 Subject: [PATCH 214/350] Document how to manually make the Ubuntu Sound Menu work --- docs/settings.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/settings.rst b/docs/settings.rst index 68adfd55..24d9b0bd 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -120,6 +120,31 @@ file:: LASTFM_PASSWORD = u'mysecret' +Controlling Mopidy through the Ubuntu Sound Menu +================================================ + +If you are running Ubuntu and installed Mopidy using the Debian package from +APT you should be able to control Mopidy through the `Ubuntu Sound Menu +`_ without any changes. + +If you installed Mopidy in any other way and want to control Mopidy through the +Ubuntu Sound Menu, you must install the ``mopidy.desktop`` file which can be +found in the ``data/`` dir of the Mopidy source into the +``/usr/share/applications`` dir by hand:: + + cd /path/to/mopidy/source + sudo cp data/mopidy.desktop /usr/share/applications/ + +After you have installed the file, start Mopidy in any way, and Mopidy should +appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed +in the Ubuntu Sound Menu, and may be restarted by selecting it there. + +The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend. +The MPRIS frontend supports the minimum requirements of the `MPRIS +specification `_. The ``TrackList`` and the +``Playlists`` interfaces of the spec are not supported. + + Streaming audio through a SHOUTcast/Icecast server ================================================== From 7d1d1fb6e38d0c13c6f3e59560c5756b3708c0c3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 00:04:53 +0200 Subject: [PATCH 215/350] Remove redundant gobject.threads_init() --- mopidy/frontends/mpris.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index f76902e1..97d065f8 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -4,7 +4,6 @@ try: import dbus import dbus.mainloop.glib import dbus.service - import gobject except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) @@ -21,7 +20,6 @@ from mopidy.utils.process import exit_process logger = logging.getLogger('mopidy.frontends.mpris') # Must be done before dbus.SessionBus() is called -gobject.threads_init() dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) From d0c8f89ffd06c1693e26b9e1038ad26cbad19d30 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 00:08:44 +0200 Subject: [PATCH 216/350] Move libindicate import to module level --- mopidy/frontends/mpris.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 97d065f8..2503e930 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -1,5 +1,7 @@ import logging +logger = logging.getLogger('mopidy.frontends.mpris') + try: import dbus import dbus.mainloop.glib @@ -8,6 +10,12 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) +try: + import indicate +except ImportError as import_error: + indicate = None + logger.debug(u'Startup notification will not be sent (%s)', import_error) + from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry @@ -17,8 +25,6 @@ from mopidy.listeners import BackendListener from mopidy.mixers.base import BaseMixer from mopidy.utils.process import exit_process -logger = logging.getLogger('mopidy.frontends.mpris') - # Must be done before dbus.SessionBus() is called dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) @@ -89,18 +95,16 @@ class MprisFrontend(ThreadingActor, BackendListener): running. When Mopidy exits, the server will be unreferenced and Mopidy will automatically be unregistered from e.g. the sound menu. """ - try: - import indicate - logger.debug(u'Sending startup notification...') - self.indicate_server = indicate.Server() - self.indicate_server.set_type('music.mopidy') - # FIXME Location of .desktop file shouldn't be hardcoded - self.indicate_server.set_desktop_file( - '/usr/share/applications/mopidy.desktop') - self.indicate_server.show() - logger.debug(u'Startup notification sent') - except ImportError as e: - logger.debug(u'Startup notification was not sent (%s)', e) + if not indicate: + return + logger.debug(u'Sending startup notification...') + self.indicate_server = indicate.Server() + self.indicate_server.set_type('music.mopidy') + # FIXME Location of .desktop file shouldn't be hardcoded + self.indicate_server.set_desktop_file( + '/usr/share/applications/mopidy.desktop') + self.indicate_server.show() + logger.debug(u'Startup notification sent') def track_playback_paused(self, track, time_position): logger.debug(u'Received track playback paused event') From 3c1ba51580da1fa6b57a8724479f2ce3dcbb9072 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 00:34:44 +0200 Subject: [PATCH 217/350] Add util method for emitting PropertiesChanged --- mopidy/frontends/mpris.py | 44 ++++++++-------------------- tests/frontends/mpris/events_test.py | 4 +-- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris.py index 2503e930..d932b853 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris.py @@ -106,51 +106,33 @@ class MprisFrontend(ThreadingActor, BackendListener): self.indicate_server.show() logger.debug(u'Startup notification sent') - def track_playback_paused(self, track, time_position): - logger.debug(u'Received track playback paused event') + def _emit_properties_changed(self, *changed_properties): if self.mpris_object is None: return - self.mpris_object.PropertiesChanged(PLAYER_IFACE, { - 'PlaybackStatus': - self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), - }, []) + props_with_new_values = [(p, self.mpris_object.Get(PLAYER_IFACE, p)) + for p in changed_properties] + self.mpris_object.PropertiesChanged(PLAYER_IFACE, + dict(props_with_new_values), []) + + def track_playback_paused(self, track, time_position): + logger.debug(u'Received track playback paused event') + self._emit_properties_changed('PlaybackStatus') def track_playback_resumed(self, track, time_position): logger.debug(u'Received track playback resumed event') - if self.mpris_object is None: - return - self.mpris_object.PropertiesChanged(PLAYER_IFACE, { - 'PlaybackStatus': - self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), - }, []) + self._emit_properties_changed('PlaybackStatus') def track_playback_started(self, track): logger.debug(u'Received track playback started event') - if self.mpris_object is None: - return - self.mpris_object.PropertiesChanged(PLAYER_IFACE, { - 'Metadata': self.mpris_object.Get(PLAYER_IFACE, 'Metadata'), - 'PlaybackStatus': - self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), - }, []) + self._emit_properties_changed('PlaybackStatus', 'Metadata') def track_playback_ended(self, track, time_position): logger.debug(u'Received track playback ended event') - if self.mpris_object is None: - return - self.mpris_object.PropertiesChanged(PLAYER_IFACE, { - 'Metadata': self.mpris_object.Get(PLAYER_IFACE, 'Metadata'), - 'PlaybackStatus': - self.mpris_object.Get(PLAYER_IFACE, 'PlaybackStatus'), - }, []) + self._emit_properties_changed('PlaybackStatus', 'Metadata') def volume_changed(self): logger.debug(u'Received volume changed event') - if self.mpris_object is None: - return - self.mpris_object.PropertiesChanged(PLAYER_IFACE, { - 'Volume': self.mpris_object.Get(PLAYER_IFACE, 'Volume'), - }, []) + self._emit_properties_changed('Volume') def seeked(self): logger.debug(u'Received seeked event') diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 3e2f0560..803094a8 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -32,8 +32,8 @@ class BackendEventsTest(unittest.TestCase): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_started(Track()) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'Metadata'), {}), ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ((PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) @@ -42,8 +42,8 @@ class BackendEventsTest(unittest.TestCase): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_ended(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'Metadata'), {}), ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ((PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) From d14dbc558789ce96e88429b363866c7f85a7a26f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 01:03:51 +0200 Subject: [PATCH 218/350] Split MPRIS frontend into multiple files. Fix some pylint warnings. --- mopidy/frontends/mpris/__init__.py | 122 ++++++++++++++++++ .../frontends/{mpris.py => mpris/objects.py} | 121 +---------------- tests/frontends/mpris/events_test.py | 34 ++--- .../frontends/mpris/player_interface_test.py | 116 ++++++++--------- tests/frontends/mpris/root_interface_test.py | 24 ++-- 5 files changed, 214 insertions(+), 203 deletions(-) create mode 100644 mopidy/frontends/mpris/__init__.py rename mopidy/frontends/{mpris.py => mpris/objects.py} (78%) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py new file mode 100644 index 00000000..7e979152 --- /dev/null +++ b/mopidy/frontends/mpris/__init__.py @@ -0,0 +1,122 @@ +import logging + +logger = logging.getLogger('mopidy.frontends.mpris') + +try: + import indicate +except ImportError as import_error: + indicate = None + logger.debug(u'Startup notification will not be sent (%s)', import_error) + +from pykka.actor import ThreadingActor + +from mopidy.frontends.mpris import objects +from mopidy.listeners import BackendListener + + +class MprisFrontend(ThreadingActor, BackendListener): + """ + Frontend which lets you control Mopidy through the Media Player Remote + Interfacing Specification (MPRIS) D-Bus interface. + + An example of an MPRIS client is `Ubuntu's sound menu + `_. + + **Dependencies:** + + - ``dbus`` Python bindings. The package is named ``python-dbus`` in + Ubuntu/Debian. + - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the + Ubuntu Sound Menu. The package is named ``python-indicate`` in + Ubuntu/Debian. + + **Testing the frontend** + + To test, start Mopidy, and then run the following in a Python shell:: + + import dbus + bus = dbus.SessionBus() + player = bus.get_object('org.mpris.MediaPlayer2.mopidy', + '/org/mpris/MediaPlayer2') + + Now you can control Mopidy through the player object. Examples: + + - To get some properties from Mopidy, run:: + + props = player.GetAll('org.mpris.MediaPlayer2', + dbus_interface='org.freedesktop.DBus.Properties') + + - To quit Mopidy through D-Bus, run:: + + player.Quit(dbus_interface='org.mpris.MediaPlayer2') + """ + + def __init__(self): + self.indicate_server = None + self.mpris_object = None + + def on_start(self): + self.mpris_object = objects.MprisObject() + self.send_startup_notification() + + def on_stop(self): + logger.debug(u'Removing MPRIS object from D-Bus connection...') + self.mpris_object.remove_from_connection() + self.mpris_object = None + logger.debug(u'Removed MPRIS object from D-Bus connection') + + def send_startup_notification(self): + """ + Send startup notification using libindicate to make Mopidy appear in + e.g. `Ubuntu's sound menu `_. + + A reference to the libindicate server is kept for as long as Mopidy is + running. When Mopidy exits, the server will be unreferenced and Mopidy + will automatically be unregistered from e.g. the sound menu. + """ + if not indicate: + return + logger.debug(u'Sending startup notification...') + self.indicate_server = indicate.Server() + self.indicate_server.set_type('music.mopidy') + # FIXME Location of .desktop file shouldn't be hardcoded + self.indicate_server.set_desktop_file( + '/usr/share/applications/mopidy.desktop') + self.indicate_server.show() + logger.debug(u'Startup notification sent') + + def _emit_properties_changed(self, *changed_properties): + if self.mpris_object is None: + return + props_with_new_values = [ + (p, self.mpris_object.Get(objects.PLAYER_IFACE, p)) + for p in changed_properties] + self.mpris_object.PropertiesChanged(objects.PLAYER_IFACE, + dict(props_with_new_values), []) + + def track_playback_paused(self, track, time_position): + logger.debug(u'Received track playback paused event') + self._emit_properties_changed('PlaybackStatus') + + def track_playback_resumed(self, track, time_position): + logger.debug(u'Received track playback resumed event') + self._emit_properties_changed('PlaybackStatus') + + def track_playback_started(self, track): + logger.debug(u'Received track playback started event') + self._emit_properties_changed('PlaybackStatus', 'Metadata') + + def track_playback_ended(self, track, time_position): + logger.debug(u'Received track playback ended event') + self._emit_properties_changed('PlaybackStatus', 'Metadata') + + def volume_changed(self): + logger.debug(u'Received volume changed event') + self._emit_properties_changed('Volume') + + def seeked(self): + logger.debug(u'Received seeked event') + if self.mpris_object is None: + return + self.mpris_object.Seeked( + self.mpris_object.Get(objects.PLAYER_IFACE, 'Position')) diff --git a/mopidy/frontends/mpris.py b/mopidy/frontends/mpris/objects.py similarity index 78% rename from mopidy/frontends/mpris.py rename to mopidy/frontends/mpris/objects.py index d932b853..3721e119 100644 --- a/mopidy/frontends/mpris.py +++ b/mopidy/frontends/mpris/objects.py @@ -10,18 +10,10 @@ except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) -try: - import indicate -except ImportError as import_error: - indicate = None - logger.debug(u'Startup notification will not be sent (%s)', import_error) - -from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController -from mopidy.listeners import BackendListener from mopidy.mixers.base import BaseMixer from mopidy.utils.process import exit_process @@ -35,112 +27,6 @@ ROOT_IFACE = 'org.mpris.MediaPlayer2' PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' -class MprisFrontend(ThreadingActor, BackendListener): - """ - Frontend which lets you control Mopidy through the Media Player Remote - Interfacing Specification (MPRIS) D-Bus interface. - - An example of an MPRIS client is `Ubuntu's sound menu - `_. - - **Dependencies:** - - - ``dbus`` Python bindings. The package is named ``python-dbus`` in - Ubuntu/Debian. - - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the - Ubuntu Sound Menu. The package is named ``python-indicate`` in - Ubuntu/Debian. - - **Testing the frontend** - - To test, start Mopidy, and then run the following in a Python shell:: - - import dbus - bus = dbus.SessionBus() - player = bus.get_object('org.mpris.MediaPlayer2.mopidy', - '/org/mpris/MediaPlayer2') - - Now you can control Mopidy through the player object. Examples: - - - To get some properties from Mopidy, run:: - - props = player.GetAll('org.mpris.MediaPlayer2', - dbus_interface='org.freedesktop.DBus.Properties') - - - To quit Mopidy through D-Bus, run:: - - player.Quit(dbus_interface='org.mpris.MediaPlayer2') - """ - - def __init__(self): - self.indicate_server = None - self.mpris_object = None - - def on_start(self): - self.mpris_object = MprisObject() - self.send_startup_notification() - - def on_stop(self): - logger.debug(u'Removing MPRIS object from D-Bus connection...') - self.mpris_object.remove_from_connection() - self.mpris_object = None - logger.debug(u'Removed MPRIS object from D-Bus connection') - - def send_startup_notification(self): - """ - Send startup notification using libindicate to make Mopidy appear in - e.g. `Ubuntu's sound menu `_. - - A reference to the libindicate server is kept for as long as Mopidy is - running. When Mopidy exits, the server will be unreferenced and Mopidy - will automatically be unregistered from e.g. the sound menu. - """ - if not indicate: - return - logger.debug(u'Sending startup notification...') - self.indicate_server = indicate.Server() - self.indicate_server.set_type('music.mopidy') - # FIXME Location of .desktop file shouldn't be hardcoded - self.indicate_server.set_desktop_file( - '/usr/share/applications/mopidy.desktop') - self.indicate_server.show() - logger.debug(u'Startup notification sent') - - def _emit_properties_changed(self, *changed_properties): - if self.mpris_object is None: - return - props_with_new_values = [(p, self.mpris_object.Get(PLAYER_IFACE, p)) - for p in changed_properties] - self.mpris_object.PropertiesChanged(PLAYER_IFACE, - dict(props_with_new_values), []) - - def track_playback_paused(self, track, time_position): - logger.debug(u'Received track playback paused event') - self._emit_properties_changed('PlaybackStatus') - - def track_playback_resumed(self, track, time_position): - logger.debug(u'Received track playback resumed event') - self._emit_properties_changed('PlaybackStatus') - - def track_playback_started(self, track): - logger.debug(u'Received track playback started event') - self._emit_properties_changed('PlaybackStatus', 'Metadata') - - def track_playback_ended(self, track, time_position): - logger.debug(u'Received track playback ended event') - self._emit_properties_changed('PlaybackStatus', 'Metadata') - - def volume_changed(self): - logger.debug(u'Received volume changed event') - self._emit_properties_changed('Volume') - - def seeked(self): - logger.debug(u'Received seeked event') - if self.mpris_object is None: - return - self.mpris_object.Seeked(PLAYER_IFACE, self.mpris_object.Get(PLAYER_IFACE, 'Position')) - - class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.1/spec/""" @@ -198,7 +84,8 @@ class MprisObject(dbus.service.Object): def backend(self): if self._backend is None: backend_refs = ActorRegistry.get_by_class(Backend) - assert len(backend_refs) == 1, 'Expected exactly one running backend.' + assert len(backend_refs) == 1, \ + 'Expected exactly one running backend.' self._backend = backend_refs[0].proxy() return self._backend @@ -262,7 +149,7 @@ class MprisObject(dbus.service.Object): @dbus.service.method(dbus_interface=ROOT_IFACE) def Raise(self): logger.debug(u'%s.Raise called', ROOT_IFACE) - pass # We do not have a GUI + # Do nothing, as we do not have a GUI @dbus.service.method(dbus_interface=ROOT_IFACE) def Quit(self): @@ -390,7 +277,7 @@ class MprisObject(dbus.service.Object): @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') def Seeked(self, position): logger.debug(u'%s.Seeked signaled', PLAYER_IFACE) - pass + # Do nothing, as just calling the method is enough to emit the signal. ### Player interface properties diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 803094a8..2f737744 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -1,66 +1,68 @@ import mock import unittest -from mopidy.frontends.mpris import MprisFrontend, MprisObject, PLAYER_IFACE +from mopidy.frontends.mpris import MprisFrontend, objects from mopidy.models import Track class BackendEventsTest(unittest.TestCase): def setUp(self): self.mpris_frontend = MprisFrontend() # As a plain class, not an actor - self.mpris_object = mock.Mock(spec=MprisObject) + self.mpris_object = mock.Mock(spec=objects.MprisObject) self.mpris_frontend.mpris_object = self.mpris_object def test_track_playback_paused_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Paused' self.mpris_frontend.track_playback_paused(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, []) + objects.PLAYER_IFACE, {'PlaybackStatus': 'Paused'}, []) def test_track_playback_resumed_event_changes_playback_status(self): self.mpris_object.Get.return_value = 'Playing' self.mpris_frontend.track_playback_resumed(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'PlaybackStatus'), {}), + ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) + objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) def test_track_playback_started_event_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_started(Track()) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'PlaybackStatus'), {}), - ((PLAYER_IFACE, 'Metadata'), {}), + ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), + ((objects.PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) + objects.PLAYER_IFACE, + {'Metadata': '...', 'PlaybackStatus': '...'}, []) def test_track_playback_ended_event_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_ended(Track(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'PlaybackStatus'), {}), - ((PLAYER_IFACE, 'Metadata'), {}), + ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), + ((objects.PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) + objects.PLAYER_IFACE, + {'Metadata': '...', 'PlaybackStatus': '...'}, []) def test_volume_changed_event_changes_volume(self): self.mpris_object.Get.return_value = 1.0 self.mpris_frontend.volume_changed() self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'Volume'), {}), + ((objects.PLAYER_IFACE, 'Volume'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( - PLAYER_IFACE, {'Volume': 1.0}, []) + objects.PLAYER_IFACE, {'Volume': 1.0}, []) def test_seeked_event_causes_mpris_seeked_event(self): self.mpris_object.Get.return_value = 31000000 self.mpris_frontend.seeked() self.assertListEqual(self.mpris_object.Get.call_args_list, [ - ((PLAYER_IFACE, 'Position'), {}), + ((objects.PLAYER_IFACE, 'Position'), {}), ]) - self.mpris_object.Seeked.assert_called_with( PLAYER_IFACE, 31000000) + self.mpris_object.Seeked.assert_called_with(31000000) diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index 1ddd23fe..ee668a33 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -3,7 +3,7 @@ import unittest from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController -from mopidy.frontends import mpris +from mopidy.frontends.mpris import objects from mopidy.mixers.dummy import DummyMixer from mopidy.models import Album, Artist, Track @@ -13,10 +13,10 @@ STOPPED = PlaybackController.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): - mpris.MprisObject._connect_to_dbus = mock.Mock() + objects.MprisObject._connect_to_dbus = mock.Mock() self.mixer = DummyMixer.start().proxy() self.backend = DummyBackend.start().proxy() - self.mpris = mpris.MprisObject() + self.mpris = objects.MprisObject() self.mpris._backend = self.backend def tearDown(self): @@ -25,68 +25,68 @@ class PlayerInterfaceTest(unittest.TestCase): def test_get_playback_status_is_playing_when_playing(self): self.backend.playback.state = PLAYING - result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Playing', result) def test_get_playback_status_is_paused_when_paused(self): self.backend.playback.state = PAUSED - result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Paused', result) def test_get_playback_status_is_stopped_when_stopped(self): self.backend.playback.state = STOPPED - result = self.mpris.Get(mpris.PLAYER_IFACE, 'PlaybackStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Stopped', result) def test_get_loop_status_is_none_when_not_looping(self): self.backend.playback.repeat = False self.backend.playback.single = False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus') self.assertEqual('None', result) def test_get_loop_status_is_track_when_looping_a_single_track(self): self.backend.playback.repeat = True self.backend.playback.single = True - result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Track', result) def test_get_loop_status_is_playlist_when_looping_the_current_playlist(self): self.backend.playback.repeat = True self.backend.playback.single = False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'LoopStatus') + result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Playlist', result) def test_set_loop_status_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.backend.playback.repeat = True self.backend.playback.single = True - self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') + self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), True) def test_set_loop_status_to_none_unsets_repeat_and_single(self): - self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'None') + self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEquals(self.backend.playback.repeat.get(), False) self.assertEquals(self.backend.playback.single.get(), False) def test_set_loop_status_to_track_sets_repeat_and_single(self): - self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Track') + self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track') self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), True) def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self): - self.mpris.Set(mpris.PLAYER_IFACE, 'LoopStatus', 'Playlist') + self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist') self.assertEquals(self.backend.playback.repeat.get(), True) self.assertEquals(self.backend.playback.single.get(), False) def test_get_rate_is_greater_or_equal_than_minimum_rate(self): - rate = self.mpris.Get(mpris.PLAYER_IFACE, 'Rate') - minimum_rate = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') + rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate') + minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate') self.assert_(rate >= minimum_rate) def test_get_rate_is_less_or_equal_than_maximum_rate(self): - rate = self.mpris.Get(mpris.PLAYER_IFACE, 'Rate') - maximum_rate = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') + rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate') + maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate') self.assert_(rate >= maximum_rate) def test_set_rate_is_ignored_if_can_control_is_false(self): @@ -94,46 +94,46 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) + self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) self.assertEquals(self.backend.playback.state.get(), PLAYING) def test_set_rate_to_zero_pauses_playback(self): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.assertEquals(self.backend.playback.state.get(), PLAYING) - self.mpris.Set(mpris.PLAYER_IFACE, 'Rate', 0) + self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) self.assertEquals(self.backend.playback.state.get(), PAUSED) def test_get_shuffle_returns_true_if_random_is_active(self): self.backend.playback.random = True - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle') self.assertTrue(result) def test_get_shuffle_returns_false_if_random_is_inactive(self): self.backend.playback.random = False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Shuffle') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle') self.assertFalse(result) def test_set_shuffle_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.backend.playback.random = False - result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) + result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True) self.assertFalse(self.backend.playback.random.get()) def test_set_shuffle_to_true_activates_random_mode(self): self.backend.playback.random = False self.assertFalse(self.backend.playback.random.get()) - result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', True) + result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True) self.assertTrue(self.backend.playback.random.get()) def test_set_shuffle_to_false_deactivates_random_mode(self): self.backend.playback.random = True self.assertTrue(self.backend.playback.random.get()) - result = self.mpris.Set(mpris.PLAYER_IFACE, 'Shuffle', False) + result = self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False) self.assertFalse(self.backend.playback.random.get()) def test_get_metadata_has_trackid_even_when_no_current_track(self): - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assert_('mpris:trackid' in result.keys()) self.assertEquals(result['mpris:trackid'], '') @@ -141,7 +141,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a')]) self.backend.playback.play() (cpid, track) = self.backend.playback.current_cp_track.get() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:trackid', result.keys()) self.assertEquals(result['mpris:trackid'], '/com/mopidy/track/%d' % cpid) @@ -149,21 +149,21 @@ class PlayerInterfaceTest(unittest.TestCase): def test_get_metadata_has_track_length(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:length', result.keys()) self.assertEquals(result['mpris:length'], 40000000) def test_get_metadata_has_track_uri(self): self.backend.current_playlist.append([Track(uri='a')]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:url', result.keys()) self.assertEquals(result['xesam:url'], 'a') def test_get_metadata_has_track_title(self): self.backend.current_playlist.append([Track(name='a')]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:title', result.keys()) self.assertEquals(result['xesam:title'], 'a') @@ -171,14 +171,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)])]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:artist', result.keys()) self.assertEquals(result['xesam:artist'], ['a', 'b']) def test_get_metadata_has_track_album(self): self.backend.current_playlist.append([Track(album=Album(name='a'))]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:album', result.keys()) self.assertEquals(result['xesam:album'], 'a') @@ -186,75 +186,75 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(album=Album(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)]))]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:albumArtist', result.keys()) self.assertEquals(result['xesam:albumArtist'], ['a', 'b']) def test_get_metadata_has_track_number_in_album(self): self.backend.current_playlist.append([Track(track_no=7)]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Metadata') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:trackNumber', result.keys()) self.assertEquals(result['xesam:trackNumber'], 7) def test_get_volume_should_return_volume_between_zero_and_one(self): self.mixer.volume = 0 - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 0) self.mixer.volume = 50 - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 0.5) self.mixer.volume = 100 - result = self.mpris.Get(mpris.PLAYER_IFACE, 'Volume') + result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEquals(result, 1) def test_set_volume_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.mixer.volume = 0 - self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 1.0) + self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) self.assertEquals(self.mixer.volume.get(), 0) def test_set_volume_to_one_should_set_mixer_volume_to_100(self): - self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 1.0) + self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) self.assertEquals(self.mixer.volume.get(), 100) def test_set_volume_to_anything_above_one_should_set_mixer_volume_to_100(self): - self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', 2.0) + self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0) self.assertEquals(self.mixer.volume.get(), 100) def test_set_volume_to_anything_not_a_number_does_not_change_volume(self): self.mixer.volume = 10 - self.mpris.Set(mpris.PLAYER_IFACE, 'Volume', None) + self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None) self.assertEquals(self.mixer.volume.get(), 10) def test_get_position_returns_time_position_in_microseconds(self): self.backend.current_playlist.append([Track(uri='a', length=40000)]) self.backend.playback.play() self.backend.playback.seek(10000) - result_in_microseconds = self.mpris.Get(mpris.PLAYER_IFACE, 'Position') + result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position') result_in_milliseconds = result_in_microseconds // 1000 self.assert_(result_in_milliseconds >= 10000) def test_get_position_when_no_current_track_should_be_zero(self): - result_in_microseconds = self.mpris.Get(mpris.PLAYER_IFACE, 'Position') + result_in_microseconds = self.mpris.Get(objects.PLAYER_IFACE, 'Position') result_in_milliseconds = result_in_microseconds // 1000 self.assertEquals(result_in_milliseconds, 0) def test_get_minimum_rate_is_one_or_less(self): - result = self.mpris.Get(mpris.PLAYER_IFACE, 'MinimumRate') + result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate') self.assert_(result <= 1.0) def test_get_maximum_rate_is_one_or_more(self): - result = self.mpris.Get(mpris.PLAYER_IFACE, 'MaximumRate') + result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate') self.assert_(result >= 1.0) def test_can_go_next_is_true_if_can_control_and_other_next_track(self): self.mpris.get_CanControl = lambda *_: True self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertTrue(result) def test_can_go_next_is_false_if_next_track_is_the_same(self): @@ -262,14 +262,14 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a')]) self.backend.playback.repeat = True self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertFalse(result) def test_can_go_next_is_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoNext') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertFalse(result) def test_can_go_previous_is_true_if_can_control_and_other_previous_track(self): @@ -277,7 +277,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.backend.playback.next() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') self.assertTrue(result) def test_can_go_previous_is_false_if_previous_track_is_the_same(self): @@ -285,7 +285,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a')]) self.backend.playback.repeat = True self.backend.playback.play() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') self.assertFalse(result) def test_can_go_previous_is_false_if_can_control_is_false(self): @@ -293,7 +293,7 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')]) self.backend.playback.play() self.backend.playback.next() - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanGoPrevious') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoPrevious') self.assertFalse(result) def test_can_play_is_true_if_can_control_and_current_track(self): @@ -301,42 +301,42 @@ class PlayerInterfaceTest(unittest.TestCase): self.backend.current_playlist.append([Track(uri='a')]) self.backend.playback.play() self.assertTrue(self.backend.playback.current_track.get()) - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay') self.assertTrue(result) def test_can_play_is_false_if_no_current_track(self): self.mpris.get_CanControl = lambda *_: True self.assertFalse(self.backend.playback.current_track.get()) - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay') self.assertFalse(result) def test_can_play_if_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPlay') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPlay') self.assertFalse(result) def test_can_pause_is_true_if_can_control_and_track_can_be_paused(self): self.mpris.get_CanControl = lambda *_: True - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPause') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause') self.assertTrue(result) def test_can_pause_if_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanPause') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanPause') self.assertFalse(result) def test_can_seek_is_true_if_can_control_is_true(self): self.mpris.get_CanControl = lambda *_: True - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanSeek') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek') self.assertTrue(result) def test_can_seek_is_false_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanSeek') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanSeek') self.assertFalse(result) def test_can_control_is_true(self): - result = self.mpris.Get(mpris.PLAYER_IFACE, 'CanControl') + result = self.mpris.Get(objects.PLAYER_IFACE, 'CanControl') self.assertTrue(result) def test_next_is_ignored_if_can_go_next_is_false(self): diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index f781d261..72800f64 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -2,14 +2,14 @@ import mock import unittest from mopidy.backends.dummy import DummyBackend -from mopidy.frontends import mpris +from mopidy.frontends.mpris import objects class RootInterfaceTest(unittest.TestCase): def setUp(self): - mpris.exit_process = mock.Mock() - mpris.MprisObject._connect_to_dbus = mock.Mock() + objects.exit_process = mock.Mock() + objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = DummyBackend.start().proxy() - self.mpris = mpris.MprisObject() + self.mpris = objects.MprisObject() def tearDown(self): self.backend.stop() @@ -18,37 +18,37 @@ class RootInterfaceTest(unittest.TestCase): self.assert_(self.mpris._connect_to_dbus.called) def test_can_raise_returns_false(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'CanRaise') + result = self.mpris.Get(objects.ROOT_IFACE, 'CanRaise') self.assertFalse(result) def test_raise_does_nothing(self): self.mpris.Raise() def test_can_quit_returns_true(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'CanQuit') + result = self.mpris.Get(objects.ROOT_IFACE, 'CanQuit') self.assertTrue(result) def test_quit_should_stop_all_actors(self): self.mpris.Quit() - self.assert_(mpris.exit_process.called) + self.assert_(objects.exit_process.called) def test_has_track_list_returns_false(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'HasTrackList') + result = self.mpris.Get(objects.ROOT_IFACE, 'HasTrackList') self.assertFalse(result) def test_identify_is_mopidy(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'Identity') + result = self.mpris.Get(objects.ROOT_IFACE, 'Identity') self.assertEquals(result, 'Mopidy') def test_desktop_entry_is_mopidy(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'DesktopEntry') + result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') self.assertEquals(result, 'mopidy') def test_supported_uri_schemes_is_empty(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedUriSchemes') + result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes') self.assertEquals(len(result), 1) self.assertEquals(result[0], 'dummy') def test_supported_mime_types_is_empty(self): - result = self.mpris.Get(mpris.ROOT_IFACE, 'SupportedMimeTypes') + result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedMimeTypes') self.assertEquals(len(result), 0) From 264d08142070eb94ba4916fda30a219a5c8475d5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 01:09:13 +0200 Subject: [PATCH 219/350] Update documented default value of the FRONTENDS setting --- mopidy/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mopidy/settings.py b/mopidy/settings.py index 70d71832..a5137543 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -56,6 +56,7 @@ DEBUG_LOG_FILENAME = u'mopidy.log' #: FRONTENDS = ( #: u'mopidy.frontends.mpd.MpdFrontend', #: u'mopidy.frontends.lastfm.LastfmFrontend', +#: u'mopidy.frontends.mpris.MprisFrontend', #: ) FRONTENDS = ( u'mopidy.frontends.mpd.MpdFrontend', From 596d29ebf66a6e610dcc4fa5945f77a6b027b053 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 01:17:39 +0200 Subject: [PATCH 220/350] Test that seek() emits seeked event --- tests/backends/events_test.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index f33c7be7..0bc0b8b9 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -15,6 +15,7 @@ class BackendEventsTest(unittest.TestCase): 'track_playback_resumed': threading.Event(), 'track_playback_started': threading.Event(), 'track_playback_ended': threading.Event(), + 'seeked': threading.Event(), } self.backend = DummyBackend.start().proxy() self.listener = DummyBackendListener.start(self.events).proxy() @@ -23,14 +24,14 @@ class BackendEventsTest(unittest.TestCase): ActorRegistry.stop_all() def test_pause_sends_track_playback_paused_event(self): - self.backend.current_playlist.add([Track(uri='a')]) + self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() self.backend.playback.pause() self.events['track_playback_paused'].wait(timeout=1) self.assertTrue(self.events['track_playback_paused'].is_set()) def test_resume_sends_track_playback_resumed(self): - self.backend.current_playlist.add([Track(uri='a')]) + self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() self.backend.playback.pause() self.backend.playback.resume() @@ -38,18 +39,25 @@ class BackendEventsTest(unittest.TestCase): self.assertTrue(self.events['track_playback_resumed'].is_set()) def test_play_sends_track_playback_started_event(self): - self.backend.current_playlist.add([Track(uri='a')]) + self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() self.events['track_playback_started'].wait(timeout=1) self.assertTrue(self.events['track_playback_started'].is_set()) def test_stop_sends_track_playback_ended_event(self): - self.backend.current_playlist.add([Track(uri='a')]) + self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() self.backend.playback.stop() self.events['track_playback_ended'].wait(timeout=1) self.assertTrue(self.events['track_playback_ended'].is_set()) + def test_seek_sends_seeked_event(self): + self.backend.current_playlist.add(Track(uri='a', length=40000)) + self.backend.playback.play() + self.backend.playback.seek(1000) + self.events['seeked'].wait(timeout=1) + self.assertTrue(self.events['seeked'].is_set()) + class DummyBackendListener(ThreadingActor, BackendListener): def __init__(self, events): @@ -66,3 +74,6 @@ class DummyBackendListener(ThreadingActor, BackendListener): def track_playback_ended(self, track, time_position): self.events['track_playback_ended'].set() + + def seeked(self): + self.events['seeked'].set() From bd8471b353166f90c328b39baca3925dfa30544a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 28 Jul 2011 01:54:05 +0200 Subject: [PATCH 221/350] Convert from listener impl to mock for testing actually emitting of events --- tests/backends/events_test.py | 77 ++++++++++++----------------------- 1 file changed, 27 insertions(+), 50 deletions(-) diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index 0bc0b8b9..0a2ef382 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -1,79 +1,56 @@ -import threading +import mock import unittest -from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry +from mopidy import listeners from mopidy.backends.dummy import DummyBackend -from mopidy.listeners import BackendListener from mopidy.models import Track class BackendEventsTest(unittest.TestCase): def setUp(self): - self.events = { - 'track_playback_paused': threading.Event(), - 'track_playback_resumed': threading.Event(), - 'track_playback_started': threading.Event(), - 'track_playback_ended': threading.Event(), - 'seeked': threading.Event(), - } + self.listener_send = mock.Mock() + listeners.BackendListener.send = self.listener_send self.backend = DummyBackend.start().proxy() - self.listener = DummyBackendListener.start(self.events).proxy() def tearDown(self): ActorRegistry.stop_all() def test_pause_sends_track_playback_paused_event(self): self.backend.current_playlist.add(Track(uri='a')) - self.backend.playback.play() - self.backend.playback.pause() - self.events['track_playback_paused'].wait(timeout=1) - self.assertTrue(self.events['track_playback_paused'].is_set()) + self.backend.playback.play().get() + self.listener_send.reset_mock() + self.backend.playback.pause().get() + self.assertEqual(self.listener_send.call_args[0][0], + 'track_playback_paused') def test_resume_sends_track_playback_resumed(self): self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() - self.backend.playback.pause() - self.backend.playback.resume() - self.events['track_playback_resumed'].wait(timeout=1) - self.assertTrue(self.events['track_playback_resumed'].is_set()) + self.backend.playback.pause().get() + self.listener_send.reset_mock() + self.backend.playback.resume().get() + self.assertEqual(self.listener_send.call_args[0][0], + 'track_playback_resumed') def test_play_sends_track_playback_started_event(self): self.backend.current_playlist.add(Track(uri='a')) - self.backend.playback.play() - self.events['track_playback_started'].wait(timeout=1) - self.assertTrue(self.events['track_playback_started'].is_set()) + self.listener_send.reset_mock() + self.backend.playback.play().get() + self.assertEqual(self.listener_send.call_args[0][0], + 'track_playback_started') def test_stop_sends_track_playback_ended_event(self): self.backend.current_playlist.add(Track(uri='a')) - self.backend.playback.play() - self.backend.playback.stop() - self.events['track_playback_ended'].wait(timeout=1) - self.assertTrue(self.events['track_playback_ended'].is_set()) + self.backend.playback.play().get() + self.listener_send.reset_mock() + self.backend.playback.stop().get() + self.assertEqual(self.listener_send.call_args_list[0][0][0], + 'track_playback_ended') def test_seek_sends_seeked_event(self): self.backend.current_playlist.add(Track(uri='a', length=40000)) - self.backend.playback.play() - self.backend.playback.seek(1000) - self.events['seeked'].wait(timeout=1) - self.assertTrue(self.events['seeked'].is_set()) - - -class DummyBackendListener(ThreadingActor, BackendListener): - def __init__(self, events): - self.events = events - - def track_playback_paused(self, track, time_position): - self.events['track_playback_paused'].set() - - def track_playback_resumed(self, track, time_position): - self.events['track_playback_resumed'].set() - - def track_playback_started(self, track): - self.events['track_playback_started'].set() - - def track_playback_ended(self, track, time_position): - self.events['track_playback_ended'].set() - - def seeked(self): - self.events['seeked'].set() + self.backend.playback.play().get() + self.listener_send.reset_mock() + self.backend.playback.seek(1000).get() + self.assertEqual(self.listener_send.call_args[0][0], 'seeked') From 13d4510e12be3486336f4098a1ee356ffc9cdc45 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:28:17 +0200 Subject: [PATCH 222/350] Rename send to send_queue in network.Connection --- mopidy/utils/network.py | 4 ++-- tests/frontends/mpd/protocol/__init__.py | 2 +- tests/utils/network/connection_test.py | 10 +++++----- tests/utils/network/lineprotocol_test.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 9306ccd7..50b7d239 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -161,7 +161,7 @@ class Connection(object): except socket.error: pass - def send(self, data): + def queue_send(self, data): """Send data to client exactly as is.""" self.send_lock.acquire(True) self.send_buffer += data @@ -383,4 +383,4 @@ class LineProtocol(ThreadingActor): return data = self.join_lines(lines) - self.connection.send(self.encode(data)) + self.connection.queue_send(self.encode(data)) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 8cd91d60..078153b5 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -14,7 +14,7 @@ class MockConnetion(mock.Mock): self.port = mock.sentinel.port self.response = [] - def send(self, data): + def queue_send(self, data): lines = (line for line in data.split('\n') if line) self.response.extend(lines) diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 6e68f250..7e7ff05a 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -318,7 +318,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_lock = Mock() self.mock.send_buffer = '' - network.Connection.send(self.mock, 'data') + network.Connection.queue_send(self.mock, 'data') self.mock.send_lock.acquire.assert_called_once_with(True) self.mock.send_lock.release.assert_called_once_with() @@ -326,20 +326,20 @@ class ConnectionTest(unittest.TestCase): self.mock.send_lock = Mock() self.mock.send_buffer = '' - network.Connection.send(self.mock, 'abc') + network.Connection.queue_send(self.mock, 'abc') self.assertEqual('abc', self.mock.send_buffer) - network.Connection.send(self.mock, 'def') + network.Connection.queue_send(self.mock, 'def') self.assertEqual('abcdef', self.mock.send_buffer) - network.Connection.send(self.mock, '') + network.Connection.queue_send(self.mock, '') self.assertEqual('abcdef', self.mock.send_buffer) def test_send_calls_enable_send(self): self.mock.send_lock = Mock() self.mock.send_buffer = '' - network.Connection.send(self.mock, 'data') + network.Connection.queue_send(self.mock, 'data') self.mock.enable_send.assert_called_once_with() def test_recv_callback_respects_io_err(self): diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index 3d16f81c..4e7df132 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -196,7 +196,7 @@ class LineProtocolTest(unittest.TestCase): network.LineProtocol.send_lines(self.mock, []) self.assertEqual(0, self.mock.encode.call_count) - self.assertEqual(0, self.mock.connection.send.call_count) + self.assertEqual(0, self.mock.connection.queue_send.call_count) def test_send_lines_calls_join_lines(self): self.mock.connection = Mock(spec=network.Connection) @@ -218,7 +218,7 @@ class LineProtocolTest(unittest.TestCase): self.mock.encode.return_value = sentinel.data network.LineProtocol.send_lines(self.mock, sentinel.lines) - self.mock.connection.send.assert_called_once_with(sentinel.data) + self.mock.connection.queue_send.assert_called_once_with(sentinel.data) def test_join_lines_returns_empty_string_for_no_lines(self): self.assertEqual(u'', network.LineProtocol.join_lines(self.mock, [])) From 43f4f1537ef2b07346d46c351c4850c7c2279c95 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:34:47 +0200 Subject: [PATCH 223/350] Extract send to seperate method --- mopidy/utils/network.py | 15 ++++++++++----- tests/utils/network/connection_test.py | 16 +++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 50b7d239..7da31f84 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -253,18 +253,23 @@ class Connection(object): return True try: - sent = self.sock.send(self.send_buffer) - self.send_buffer = self.send_buffer[sent:] + self.send_buffer = self.send(self.send_buffer) if not self.send_buffer: self.disable_send() - except socket.error as e: - if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): - self.stop(u'Unexpected client error: %s' % e) finally: self.send_lock.release() return True + def send(self, data): + try: + sent = self.sock.send(data) + return data[sent:] + except socket.error as e: + if e.errno in (errno.EWOULDBLOCK, errno.EINTR): + return data + self.stop(u'Unexpected client error: %s' % e) + def timeout_callback(self): self.stop(u'Client timeout out after %s seconds' % self.timeout) return False diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 7e7ff05a..6f22aeb3 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -8,7 +8,7 @@ import unittest from mopidy.utils import network from mock import patch, sentinel, Mock -from tests import any_int, any_unicode +from tests import any_int, any_unicode, SkipTest class ConnectionTest(unittest.TestCase): def setUp(self): @@ -473,32 +473,30 @@ class ConnectionTest(unittest.TestCase): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' - self.mock.sock = Mock(spec=socket.SocketType) - self.mock.sock.send.return_value = 4 + self.mock.send.return_value = '' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.disable_send.assert_called_once_with() - self.mock.sock.send.assert_called_once_with('data') + self.mock.send.assert_called_once_with('data') self.assertEqual('', self.mock.send_buffer) def test_send_callback_sends_partial_data(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' - self.mock.sock = Mock(spec=socket.SocketType) - self.mock.sock.send.return_value = 2 + self.mock.send.return_value = 'ta' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) - self.mock.sock.send.assert_called_once_with('data') + self.mock.send.assert_called_once_with('data') self.assertEqual('ta', self.mock.send_buffer) + @SkipTest def test_send_callback_recoverable_error(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' - self.mock.sock = Mock(spec=socket.SocketType) for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.send.side_effect = socket.error(error, '') @@ -506,11 +504,11 @@ class ConnectionTest(unittest.TestCase): self.mock, sentinel.fd, gobject.IO_IN)) self.assertEqual(0, self.mock.stop.call_count) + @SkipTest def test_send_callback_unrecoverable_error(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' - self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.side_effect = socket.error self.assertTrue(network.Connection.send_callback( From 4f6ddd3532876c43b23b54029271d1b4851bd7a5 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:41:53 +0200 Subject: [PATCH 224/350] Add error handling tests for new send method --- mopidy/utils/network.py | 1 + tests/utils/network/connection_test.py | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 7da31f84..5267afd5 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -269,6 +269,7 @@ class Connection(object): if e.errno in (errno.EWOULDBLOCK, errno.EINTR): return data self.stop(u'Unexpected client error: %s' % e) + return '' def timeout_callback(self): self.stop(u'Client timeout out after %s seconds' % self.timeout) diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 6f22aeb3..7241dc5a 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -492,27 +492,20 @@ class ConnectionTest(unittest.TestCase): self.mock.send.assert_called_once_with('data') self.assertEqual('ta', self.mock.send_buffer) - @SkipTest - def test_send_callback_recoverable_error(self): - self.mock.send_lock = Mock() - self.mock.send_lock.acquire.return_value = True - self.mock.send_buffer = 'data' + def test_send_recoverable_error(self): + self.mock.sock = Mock(spec=socket.SocketType) for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.send.side_effect = socket.error(error, '') - self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + + network.Connection.send(self.mock, 'data') self.assertEqual(0, self.mock.stop.call_count) - @SkipTest - def test_send_callback_unrecoverable_error(self): - self.mock.send_lock = Mock() - self.mock.send_lock.acquire.return_value = True - self.mock.send_buffer = 'data' - + def test_send_unrecoverable_error(self): + self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.side_effect = socket.error - self.assertTrue(network.Connection.send_callback( - self.mock, sentinel.fd, gobject.IO_IN)) + + self.assertEqual('', network.Connection.send(self.mock, 'data')) self.mock.stop.assert_called_once_with(any_unicode) def test_timeout_callback(self): From 93c16cc2cdc723344e1792828a42c3db78a17780 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:43:54 +0200 Subject: [PATCH 225/350] Add tests for socket sending --- tests/utils/network/connection_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 7241dc5a..16ec3979 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -501,6 +501,20 @@ class ConnectionTest(unittest.TestCase): network.Connection.send(self.mock, 'data') self.assertEqual(0, self.mock.stop.call_count) + def test_send_calls_socket_send(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 4 + + self.assertEqual('', network.Connection.send(self.mock, 'data')) + self.mock.sock.send.assert_called_once_with('data') + + def test_send_calls_socket_send_partial_send(self): + self.mock.sock = Mock(spec=socket.SocketType) + self.mock.sock.send.return_value = 2 + + self.assertEqual('ta', network.Connection.send(self.mock, 'data')) + self.mock.sock.send.assert_called_once_with('data') + def test_send_unrecoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.side_effect = socket.error From 3195476421b031a6b8516404a56a1a75dc91acbb Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:44:43 +0200 Subject: [PATCH 226/350] Rename old send tests to queue_send --- tests/utils/network/connection_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 16ec3979..e96d1852 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -314,7 +314,7 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.timeout_id) - def test_send_acquires_and_releases_lock(self): + def test_queue_send_acquires_and_releases_lock(self): self.mock.send_lock = Mock() self.mock.send_buffer = '' @@ -322,7 +322,7 @@ class ConnectionTest(unittest.TestCase): self.mock.send_lock.acquire.assert_called_once_with(True) self.mock.send_lock.release.assert_called_once_with() - def test_send_appends_to_send_buffer(self): + def test_queue_send_appends_to_send_buffer(self): self.mock.send_lock = Mock() self.mock.send_buffer = '' @@ -335,7 +335,7 @@ class ConnectionTest(unittest.TestCase): network.Connection.queue_send(self.mock, '') self.assertEqual('abcdef', self.mock.send_buffer) - def test_send_calls_enable_send(self): + def test_queue_send_calls_enable_send(self): self.mock.send_lock = Mock() self.mock.send_buffer = '' From cb4f32cb5846d55d9db76140b90646837f0e5edd Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:51:41 +0200 Subject: [PATCH 227/350] Try to send directly in quene_send when we can to prevent uneeded context switches --- mopidy/utils/network.py | 5 ++-- tests/utils/network/connection_test.py | 38 ++++++++++++++++---------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 5267afd5..84fe49bd 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -164,9 +164,10 @@ class Connection(object): def queue_send(self, data): """Send data to client exactly as is.""" self.send_lock.acquire(True) - self.send_buffer += data + self.send_buffer = self.send(self.send_buffer + data) self.send_lock.release() - self.enable_send() + if self.send_buffer: + self.enable_send() def enable_timeout(self): """Reactivate timeout mechanism.""" diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index e96d1852..1bda0af5 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -322,25 +322,35 @@ class ConnectionTest(unittest.TestCase): self.mock.send_lock.acquire.assert_called_once_with(True) self.mock.send_lock.release.assert_called_once_with() - def test_queue_send_appends_to_send_buffer(self): - self.mock.send_lock = Mock() + def test_queue_send_calls_send(self): self.mock.send_buffer = '' - - network.Connection.queue_send(self.mock, 'abc') - self.assertEqual('abc', self.mock.send_buffer) - - network.Connection.queue_send(self.mock, 'def') - self.assertEqual('abcdef', self.mock.send_buffer) - - network.Connection.queue_send(self.mock, '') - self.assertEqual('abcdef', self.mock.send_buffer) - - def test_queue_send_calls_enable_send(self): self.mock.send_lock = Mock() - self.mock.send_buffer = '' + self.mock.send.return_value = '' network.Connection.queue_send(self.mock, 'data') + self.mock.send.assert_called_once_with('data') + self.assertEqual(0, self.mock.enable_send.call_count) + self.assertEqual('', self.mock.send_buffer) + + def test_queue_send_calls_enable_send_for_partial_send(self): + self.mock.send_buffer = '' + self.mock.send_lock = Mock() + self.mock.send.return_value = 'ta' + + network.Connection.queue_send(self.mock, 'data') + self.mock.send.assert_called_once_with('data') self.mock.enable_send.assert_called_once_with() + self.assertEqual('ta', self.mock.send_buffer) + + def test_queue_send_calls_send_with_existing_buffer(self): + self.mock.send_buffer = 'foo' + self.mock.send_lock = Mock() + self.mock.send.return_value = '' + + network.Connection.queue_send(self.mock, 'bar') + self.mock.send.assert_called_once_with('foobar') + self.assertEqual(0, self.mock.enable_send.call_count) + self.assertEqual('', self.mock.send_buffer) def test_recv_callback_respects_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) From 34594e40e83ec7b0065c9b3e0ea4f47789cf3fa1 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Thu, 28 Jul 2011 22:55:54 +0200 Subject: [PATCH 228/350] Move method and cleanup docstrings --- mopidy/utils/network.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 84fe49bd..5079fe7c 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -162,13 +162,24 @@ class Connection(object): pass def queue_send(self, data): - """Send data to client exactly as is.""" + """Try to send data to client exactly as is and queue rest.""" self.send_lock.acquire(True) self.send_buffer = self.send(self.send_buffer + data) self.send_lock.release() if self.send_buffer: self.enable_send() + def send(self, data): + """Send data to client, return any unsent data.""" + try: + sent = self.sock.send(data) + return data[sent:] + except socket.error as e: + if e.errno in (errno.EWOULDBLOCK, errno.EINTR): + return data + self.stop(u'Unexpected client error: %s' % e) + return '' + def enable_timeout(self): """Reactivate timeout mechanism.""" if self.timeout <= 0: @@ -262,16 +273,6 @@ class Connection(object): return True - def send(self, data): - try: - sent = self.sock.send(data) - return data[sent:] - except socket.error as e: - if e.errno in (errno.EWOULDBLOCK, errno.EINTR): - return data - self.stop(u'Unexpected client error: %s' % e) - return '' - def timeout_callback(self): self.stop(u'Client timeout out after %s seconds' % self.timeout) return False From ae4cd6a7de7c1792847affe632653e15a0fbfbdf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 22:23:04 +0200 Subject: [PATCH 229/350] Extract .desktop file path to new setting DESKTOP_FILE --- mopidy/frontends/mpris/__init__.py | 5 ++--- mopidy/frontends/mpris/objects.py | 7 ++++++- mopidy/settings.py | 9 +++++++++ tests/frontends/mpris/root_interface_test.py | 7 +++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 7e979152..6656347f 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -10,6 +10,7 @@ except ImportError as import_error: from pykka.actor import ThreadingActor +from mopidy import settings from mopidy.frontends.mpris import objects from mopidy.listeners import BackendListener @@ -79,9 +80,7 @@ class MprisFrontend(ThreadingActor, BackendListener): logger.debug(u'Sending startup notification...') self.indicate_server = indicate.Server() self.indicate_server.set_type('music.mopidy') - # FIXME Location of .desktop file shouldn't be hardcoded - self.indicate_server.set_desktop_file( - '/usr/share/applications/mopidy.desktop') + self.indicate_server.set_desktop_file(settings.DESKTOP_FILE) self.indicate_server.show() logger.debug(u'Startup notification sent') diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 3721e119..fa85116f 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -1,4 +1,5 @@ import logging +import os logger = logging.getLogger('mopidy.frontends.mpris') @@ -12,6 +13,7 @@ except ImportError as import_error: from pykka.registry import ActorRegistry +from mopidy import settings from mopidy.backends.base import Backend from mopidy.backends.base.playback import PlaybackController from mopidy.mixers.base import BaseMixer @@ -49,7 +51,7 @@ class MprisObject(dbus.service.Object): # NOTE Change if adding optional track list support 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), - 'DesktopEntry': ('mopidy', None), + 'DesktopEntry': (self.get_DesktopEntry, None), 'SupportedUriSchemes': (self.get_SupportedUriSchemes, None), # TODO Return MIME types supported by local backend if active 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), @@ -159,6 +161,9 @@ class MprisObject(dbus.service.Object): ### Root interface properties + def get_DesktopEntry(self): + return os.path.splitext(os.path.basename(settings.DESKTOP_FILE))[0] + def get_SupportedUriSchemes(self): return dbus.Array(self.backend.uri_schemes.get(), signature='s') diff --git a/mopidy/settings.py b/mopidy/settings.py index a5137543..b1e0c791 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -49,6 +49,15 @@ DEBUG_LOG_FORMAT = u'%(levelname)-8s %(asctime)s' + \ #: DEBUG_LOG_FILENAME = u'mopidy.log' DEBUG_LOG_FILENAME = u'mopidy.log' +#: Location of the Mopidy .desktop file. +#: +#: Used by :mod:`mopidy.frontends.mpris`. +#: +#: Default:: +#: +#: DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop' +DESKTOP_FILE = u'/usr/share/applications/mopidy.desktop' + #: List of server frontends to use. #: #: Default:: diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 72800f64..52a1c66e 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,6 +1,7 @@ import mock import unittest +from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpris import objects @@ -44,6 +45,12 @@ class RootInterfaceTest(unittest.TestCase): result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') self.assertEquals(result, 'mopidy') + def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self): + settings.runtime['DESKTOP_FILE'] = '/tmp/foo.desktop' + result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') + self.assertEquals(result, 'foo') + settings.runtime.clear() + def test_supported_uri_schemes_is_empty(self): result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedUriSchemes') self.assertEquals(len(result), 1) From 16a60ada8ad0bc733d34499a33fd60687c98dfd1 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 22:54:09 +0200 Subject: [PATCH 230/350] Convert MIME type related TODOs to NOTEs --- mopidy/frontends/mpris/objects.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index fa85116f..2156d37c 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -53,7 +53,8 @@ class MprisObject(dbus.service.Object): 'Identity': ('Mopidy', None), 'DesktopEntry': (self.get_DesktopEntry, None), 'SupportedUriSchemes': (self.get_SupportedUriSchemes, None), - # TODO Return MIME types supported by local backend if active + # NOTE Return MIME types supported by local backend if support for + # reporting supported MIME types is added 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), } @@ -265,7 +266,8 @@ class MprisObject(dbus.service.Object): # the other methods doesn't help much if OpenUri is open for use. logger.debug(u'%s.Play not allowed', PLAYER_IFACE) return - # TODO Check if URI has known MIME type. + # NOTE Check if URI has MIME type known to the backend, if MIME support + # is added to the backend. uri_schemes = self.backend.uri_schemes.get() if not any([uri.startswith(uri_scheme) for uri_scheme in uri_schemes]): return From 0a7b438d76c9e1e52dfdfd8ced37d566ed8e4067 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:04:43 +0200 Subject: [PATCH 231/350] Handle errors when connecting to D-Bus and libindicate by stopping the MPRIS frontend --- mopidy/frontends/mpris/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 6656347f..4e52fd79 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -57,13 +57,18 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object = None def on_start(self): - self.mpris_object = objects.MprisObject() - self.send_startup_notification() + try: + self.mpris_object = objects.MprisObject() + self.send_startup_notification() + except Exception as e: + logger.error(u'MPRIS frontend setup failed (%s)', e) + self.stop() def on_stop(self): logger.debug(u'Removing MPRIS object from D-Bus connection...') - self.mpris_object.remove_from_connection() - self.mpris_object = None + if self.mpris_object: + self.mpris_object.remove_from_connection() + self.mpris_object = None logger.debug(u'Removed MPRIS object from D-Bus connection') def send_startup_notification(self): From 52dded9a709e9c7925554e2716910d6dd5b25cb0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:08:36 +0200 Subject: [PATCH 232/350] Remove 'Raise error' TODOs I don't know what kind of errors I should throw, and the MPRIS spec always use 'may' and 'should' regarding the throwing of errors. --- mopidy/frontends/mpris/objects.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 2156d37c..ec7f3eb6 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -200,7 +200,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.PlayPause called', PLAYER_IFACE) if not self.get_CanPause(): logger.debug(u'%s.PlayPause not allowed', PLAYER_IFACE) - return # TODO Raise error + return state = self.backend.playback.state.get() if state == PlaybackController.PLAYING: self.backend.playback.pause().get() @@ -214,7 +214,7 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Stop called', PLAYER_IFACE) if not self.get_CanControl(): logger.debug(u'%s.Stop not allowed', PLAYER_IFACE) - return # TODO Raise error + return self.backend.playback.stop().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) @@ -312,7 +312,7 @@ class MprisObject(dbus.service.Object): def set_LoopStatus(self, value): if not self.get_CanControl(): logger.debug(u'Setting %s.LoopStatus not allowed', PLAYER_IFACE) - return # TODO Raise error + return if value == 'None': self.backend.playback.repeat = False self.backend.playback.single = False @@ -328,7 +328,7 @@ class MprisObject(dbus.service.Object): # NOTE The spec does not explictly require this check, but it was # added to be consistent with all the other property setters. logger.debug(u'Setting %s.Rate not allowed', PLAYER_IFACE) - return # TODO Raise error + return if value == 0: self.Pause() @@ -338,7 +338,7 @@ class MprisObject(dbus.service.Object): def set_Shuffle(self, value): if not self.get_CanControl(): logger.debug(u'Setting %s.Shuffle not allowed', PLAYER_IFACE) - return # TODO Raise error + return if value: self.backend.playback.random = True else: @@ -381,7 +381,7 @@ class MprisObject(dbus.service.Object): def set_Volume(self, value): if not self.get_CanControl(): logger.debug(u'Setting %s.Volume not allowed', PLAYER_IFACE) - return # TODO Raise error + return if value is None: return elif value < 0: From 8795933803022347c28138557658ea75708eb574 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:29:39 +0200 Subject: [PATCH 233/350] Update MPD docs --- docs/modules/frontends/mpd.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/modules/frontends/mpd.rst b/docs/modules/frontends/mpd.rst index 6f69b2a9..b0c7e3c5 100644 --- a/docs/modules/frontends/mpd.rst +++ b/docs/modules/frontends/mpd.rst @@ -9,26 +9,6 @@ :members: -MPD server -========== - -.. inheritance-diagram:: mopidy.frontends.mpd.server - -.. automodule:: mopidy.frontends.mpd.server - :synopsis: MPD server - :members: - - -MPD session -=========== - -.. inheritance-diagram:: mopidy.frontends.mpd.session - -.. automodule:: mopidy.frontends.mpd.session - :synopsis: MPD client session - :members: - - MPD dispatcher ============== From 61ca0669e766de34d7c526a6fe0b94c4db2f2e5c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:45:41 +0200 Subject: [PATCH 234/350] Add docs for MPRIS frontend, update changelog. --- docs/api/frontends.rst | 1 + docs/changes.rst | 5 +++++ docs/modules/frontends/mpris.rst | 7 +++++++ docs/settings.rst | 11 ++++++----- mopidy/frontends/mpris/__init__.py | 10 +++++++--- 5 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 docs/modules/frontends/mpris.rst diff --git a/docs/api/frontends.rst b/docs/api/frontends.rst index 792e4bc9..dc53cca2 100644 --- a/docs/api/frontends.rst +++ b/docs/api/frontends.rst @@ -28,3 +28,4 @@ Frontend implementations * :mod:`mopidy.frontends.lastfm` * :mod:`mopidy.frontends.mpd` +* :mod:`mopidy.frontends.mpris` diff --git a/docs/changes.rst b/docs/changes.rst index f0a546af..f2211dab 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -28,6 +28,11 @@ v0.6.0 (in development) - The MPD command ``idle`` is now supported by Mopidy for the following subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`) +- A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes + Mopidy through the `MPRIS interface `_ over D-Bus. In + practice, this makes it possible to control Mopidy thorugh the `Ubuntu Sound + Menu `_. + **Changes** - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with diff --git a/docs/modules/frontends/mpris.rst b/docs/modules/frontends/mpris.rst new file mode 100644 index 00000000..05a6e287 --- /dev/null +++ b/docs/modules/frontends/mpris.rst @@ -0,0 +1,7 @@ +*********************************************** +:mod:`mopidy.frontends.mpris` -- MPRIS frontend +*********************************************** + +.. automodule:: mopidy.frontends.mpris + :synopsis: MPRIS frontend + :members: diff --git a/docs/settings.rst b/docs/settings.rst index 24d9b0bd..76eb6315 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -92,7 +92,6 @@ To make a ``tag_cache`` of your local music available for Mopidy: .. _use_mpd_on_a_network: - Connecting from other machines on the network ============================================= @@ -120,6 +119,8 @@ file:: LASTFM_PASSWORD = u'mysecret' +.. _install_desktop_file: + Controlling Mopidy through the Ubuntu Sound Menu ================================================ @@ -139,10 +140,10 @@ After you have installed the file, start Mopidy in any way, and Mopidy should appear in the Ubuntu Sound Menu. When you quit Mopidy, it will still be listed in the Ubuntu Sound Menu, and may be restarted by selecting it there. -The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend. -The MPRIS frontend supports the minimum requirements of the `MPRIS -specification `_. The ``TrackList`` and the -``Playlists`` interfaces of the spec are not supported. +The Ubuntu Sound Menu interacts with Mopidy's MPRIS frontend, +:mod:`mopidy.frontends.mpris`. The MPRIS frontend supports the minimum +requirements of the `MPRIS specification `_. The +``TrackList`` and the ``Playlists`` interfaces of the spec are not supported. Streaming audio through a SHOUTcast/Icecast server diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 4e52fd79..c68d5139 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -18,18 +18,22 @@ from mopidy.listeners import BackendListener class MprisFrontend(ThreadingActor, BackendListener): """ Frontend which lets you control Mopidy through the Media Player Remote - Interfacing Specification (MPRIS) D-Bus interface. + Interfacing Specification (`MPRIS `_) D-Bus + interface. - An example of an MPRIS client is `Ubuntu's sound menu + An example of an MPRIS client is the `Ubuntu Sound Menu `_. **Dependencies:** - - ``dbus`` Python bindings. The package is named ``python-dbus`` in + - D-Bus Python bindings. The package is named ``python-dbus`` in Ubuntu/Debian. - ``libindicate`` Python bindings is needed to expose Mopidy in e.g. the Ubuntu Sound Menu. The package is named ``python-indicate`` in Ubuntu/Debian. + - An ``.desktop`` file for Mopidy installed at the path set in + :attr:`mopidy.settings.DESKTOP_FILE`. See :ref:`install_desktop_file` for + details. **Testing the frontend** From d56d4ea62800285154c0b38f1e74c87d214264a3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:45:54 +0200 Subject: [PATCH 235/350] Make method private --- mopidy/frontends/mpris/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index c68d5139..579038ca 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -63,7 +63,7 @@ class MprisFrontend(ThreadingActor, BackendListener): def on_start(self): try: self.mpris_object = objects.MprisObject() - self.send_startup_notification() + self._send_startup_notification() except Exception as e: logger.error(u'MPRIS frontend setup failed (%s)', e) self.stop() @@ -75,7 +75,7 @@ class MprisFrontend(ThreadingActor, BackendListener): self.mpris_object = None logger.debug(u'Removed MPRIS object from D-Bus connection') - def send_startup_notification(self): + def _send_startup_notification(self): """ Send startup notification using libindicate to make Mopidy appear in e.g. `Ubuntu's sound menu `_. From a290a89b1a8543f1f6b04409ae0bca4376d55ca9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 23:46:43 +0200 Subject: [PATCH 236/350] Readd gobjects.threads_init() in MPRIS frontend so that it may be imported before mopidy.core (e.g. by Sphinx autodoc) --- mopidy/frontends/mpris/objects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index ec7f3eb6..5d802525 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -7,6 +7,7 @@ try: import dbus import dbus.mainloop.glib import dbus.service + import gobject except ImportError as import_error: from mopidy import OptionalDependencyError raise OptionalDependencyError(import_error) @@ -20,6 +21,7 @@ from mopidy.mixers.base import BaseMixer from mopidy.utils.process import exit_process # Must be done before dbus.SessionBus() is called +gobject.threads_init() dbus.mainloop.glib.threads_init() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) From da3d1772d2888d81b4d5f038e4d230067447e643 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Jul 2011 01:30:42 +0200 Subject: [PATCH 237/350] Unroll onne-line if-else --- mopidy/frontends/mpris/objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index 5d802525..77278778 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -117,7 +117,10 @@ class MprisObject(dbus.service.Object): logger.debug(u'%s.Get(%s, %s) called', dbus.PROPERTIES_IFACE, repr(interface), repr(prop)) (getter, setter) = self.properties[interface][prop] - return getter() if callable(getter) else getter + if callable(getter): + return getter() + else: + return getter @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') From 662a17e7ba1d23423f6301fc2a8ce759d2a71f78 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 31 Jul 2011 17:05:18 +0200 Subject: [PATCH 238/350] Use mock.patch instead of assigning a mock to the imported module --- tests/backends/events_test.py | 39 +++++++++++++++-------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index 0a2ef382..88429166 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -3,54 +3,49 @@ import unittest from pykka.registry import ActorRegistry -from mopidy import listeners from mopidy.backends.dummy import DummyBackend +from mopidy.listeners import BackendListener from mopidy.models import Track +@mock.patch.object(BackendListener, 'send') class BackendEventsTest(unittest.TestCase): def setUp(self): - self.listener_send = mock.Mock() - listeners.BackendListener.send = self.listener_send self.backend = DummyBackend.start().proxy() def tearDown(self): ActorRegistry.stop_all() - def test_pause_sends_track_playback_paused_event(self): + def test_pause_sends_track_playback_paused_event(self, send): self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play().get() - self.listener_send.reset_mock() + send.reset_mock() self.backend.playback.pause().get() - self.assertEqual(self.listener_send.call_args[0][0], - 'track_playback_paused') + self.assertEqual(send.call_args[0][0], 'track_playback_paused') - def test_resume_sends_track_playback_resumed(self): + def test_resume_sends_track_playback_resumed(self, send): self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play() self.backend.playback.pause().get() - self.listener_send.reset_mock() + send.reset_mock() self.backend.playback.resume().get() - self.assertEqual(self.listener_send.call_args[0][0], - 'track_playback_resumed') + self.assertEqual(send.call_args[0][0], 'track_playback_resumed') - def test_play_sends_track_playback_started_event(self): + def test_play_sends_track_playback_started_event(self, send): self.backend.current_playlist.add(Track(uri='a')) - self.listener_send.reset_mock() + send.reset_mock() self.backend.playback.play().get() - self.assertEqual(self.listener_send.call_args[0][0], - 'track_playback_started') + self.assertEqual(send.call_args[0][0], 'track_playback_started') - def test_stop_sends_track_playback_ended_event(self): + def test_stop_sends_track_playback_ended_event(self, send): self.backend.current_playlist.add(Track(uri='a')) self.backend.playback.play().get() - self.listener_send.reset_mock() + send.reset_mock() self.backend.playback.stop().get() - self.assertEqual(self.listener_send.call_args_list[0][0][0], - 'track_playback_ended') + self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended') - def test_seek_sends_seeked_event(self): + def test_seek_sends_seeked_event(self, send): self.backend.current_playlist.add(Track(uri='a', length=40000)) self.backend.playback.play().get() - self.listener_send.reset_mock() + send.reset_mock() self.backend.playback.seek(1000).get() - self.assertEqual(self.listener_send.call_args[0][0], 'seeked') + self.assertEqual(send.call_args[0][0], 'seeked') From 1985b4af76ba1921f20ab1661c1f2af7730dfe94 Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Jul 2011 22:21:34 +0200 Subject: [PATCH 239/350] Switch all tests over to from tests import unittest, which will be unittest2 on < 2.7 and plain unittest otherwise --- mopidy/utils/path.py | 1 + tests/__init__.py | 18 +++++----- tests/backends/base/current_playlist.py | 2 +- tests/backends/base/library.py | 9 +++-- tests/backends/base/playback.py | 16 ++++----- tests/backends/base/stored_playlists.py | 12 ++++--- tests/backends/events_test.py | 4 ++- tests/backends/local/current_playlist_test.py | 10 +++--- tests/backends/local/library_test.py | 11 +++---- tests/backends/local/playback_test.py | 11 +++---- tests/backends/local/stored_playlists_test.py | 28 ++++++++-------- tests/backends/local/translator_test.py | 9 +++-- tests/frontends/mpd/dispatcher_test.py | 5 +-- tests/frontends/mpd/exception_test.py | 5 +-- tests/frontends/mpd/protocol/__init__.py | 3 +- .../mpd/protocol/audio_output_test.py | 1 + .../mpd/protocol/authentication_test.py | 1 + .../mpd/protocol/command_list_test.py | 1 + .../frontends/mpd/protocol/connection_test.py | 1 + .../mpd/protocol/current_playlist_test.py | 1 + tests/frontends/mpd/protocol/idle_test.py | 1 + tests/frontends/mpd/protocol/music_db_test.py | 1 + tests/frontends/mpd/protocol/playback_test.py | 11 ++++--- .../frontends/mpd/protocol/reflection_test.py | 1 + .../frontends/mpd/protocol/regression_test.py | 1 + tests/frontends/mpd/protocol/status_test.py | 1 + tests/frontends/mpd/protocol/stickers_test.py | 1 + .../mpd/protocol/stored_playlists_test.py | 5 +-- tests/frontends/mpd/serializer_test.py | 10 +++--- tests/frontends/mpd/status_test.py | 5 +-- tests/frontends/mpris/events_test.py | 4 ++- .../frontends/mpris/player_interface_test.py | 4 ++- tests/frontends/mpris/root_interface_test.py | 4 ++- tests/gstreamer_test.py | 21 +++++------- tests/help_test.py | 4 ++- tests/listeners_test.py | 5 +-- tests/mixers/denon_test.py | 6 ++-- tests/mixers/dummy_test.py | 5 +-- tests/models_test.py | 33 ++++++++++--------- tests/scanner_test.py | 8 +++-- tests/utils/init_test.py | 5 +-- tests/utils/network/connection_test.py | 6 ++-- tests/utils/network/lineprotocol_test.py | 5 +-- tests/utils/network/server_test.py | 6 ++-- tests/utils/network/utils_test.py | 8 ++--- tests/utils/path_test.py | 4 +-- tests/utils/settings_test.py | 4 ++- tests/version_test.py | 4 ++- 48 files changed, 181 insertions(+), 141 deletions(-) diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 540cb4fa..8bd39f06 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -60,6 +60,7 @@ def find_files(path): yield filename # pylint: enable = W0612 +# FIXME replace with mock usage in tests. class Mtime(object): def __init__(self): self.fake = None diff --git a/tests/__init__.py b/tests/__init__.py index 663b89ec..833ff239 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,27 +1,24 @@ import os +import sys -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 - except ImportError: # Failsafe - class SkipTest(Exception): - pass +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest from mopidy import settings # Nuke any local settings to ensure same test env all over settings.local.clear() + 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) + class IsA(object): def __init__(self, klass): self.klass = klass @@ -38,6 +35,7 @@ class IsA(object): def __repr__(self): return str(self.klass) + any_int = IsA(int) any_str = IsA(str) any_unicode = IsA(unicode) diff --git a/tests/backends/base/current_playlist.py b/tests/backends/base/current_playlist.py index b84391af..c81f4a0d 100644 --- a/tests/backends/base/current_playlist.py +++ b/tests/backends/base/current_playlist.py @@ -1,5 +1,4 @@ import mock -import multiprocessing import random from mopidy.models import Playlist, Track @@ -7,6 +6,7 @@ from mopidy.gstreamer import GStreamer from tests.backends.base import populate_playlist + class CurrentPlaylistControllerTest(object): tracks = [] diff --git a/tests/backends/base/library.py b/tests/backends/base/library.py index 2a3de730..4b3ef5c0 100644 --- a/tests/backends/base/library.py +++ b/tests/backends/base/library.py @@ -1,6 +1,7 @@ from mopidy.models import Playlist, Track, Album, Artist -from tests import SkipTest, path_to_data_dir +from tests import unittest, path_to_data_dir + class LibraryControllerTest(object): artists = [Artist(name='artist1'), Artist(name='artist2'), Artist()] @@ -20,11 +21,13 @@ class LibraryControllerTest(object): def test_refresh(self): self.library.refresh() + @unittest.SkipTest def test_refresh_uri(self): - raise SkipTest + pass + @unittest.SkipTest def test_refresh_missing_uri(self): - raise SkipTest + pass def test_lookup(self): track = self.library.lookup(self.tracks[0].uri) diff --git a/tests/backends/base/playback.py b/tests/backends/base/playback.py index 47a14e3c..40c49709 100644 --- a/tests/backends/base/playback.py +++ b/tests/backends/base/playback.py @@ -1,16 +1,16 @@ import mock -import multiprocessing import random import time from mopidy.models import Track from mopidy.gstreamer import GStreamer -from tests import SkipTest +from tests import unittest from tests.backends.base import populate_playlist # TODO Test 'playlist repeat', e.g. repeat=1,single=0 + class PlaybackControllerTest(object): tracks = [] @@ -520,7 +520,7 @@ class PlaybackControllerTest(object): self.assert_(wrapper.called) - @SkipTest # Blocks for 10ms + @unittest.SkipTest # Blocks for 10ms @populate_playlist def test_end_of_track_callback_gets_called(self): self.playback.play() @@ -599,7 +599,7 @@ class PlaybackControllerTest(object): self.playback.pause() self.assertEqual(self.playback.resume(), None) - @SkipTest # Uses sleep and might not work with LocalBackend + @unittest.SkipTest # Uses sleep and might not work with LocalBackend @populate_playlist def test_resume_continues_from_right_position(self): self.playback.play() @@ -668,7 +668,7 @@ class PlaybackControllerTest(object): self.playback.seek(0) self.assertEqual(self.playback.state, self.playback.PLAYING) - @SkipTest + @unittest.SkipTest @populate_playlist def test_seek_beyond_end_of_song(self): # FIXME need to decide return value @@ -688,7 +688,7 @@ class PlaybackControllerTest(object): self.playback.seek(self.current_playlist.tracks[-1].length * 100) self.assertEqual(self.playback.state, self.playback.STOPPED) - @SkipTest + @unittest.SkipTest @populate_playlist def test_seek_beyond_start_of_song(self): # FIXME need to decide return value @@ -741,7 +741,7 @@ class PlaybackControllerTest(object): self.assertEqual(self.playback.time_position, 0) - @SkipTest # Uses sleep and does might not work with LocalBackend + @unittest.SkipTest # Uses sleep and does might not work with LocalBackend @populate_playlist def test_time_position_when_playing(self): self.playback.play() @@ -750,7 +750,7 @@ class PlaybackControllerTest(object): second = self.playback.time_position self.assert_(second > first, '%s - %s' % (first, second)) - @SkipTest # Uses sleep + @unittest.SkipTest # Uses sleep @populate_playlist def test_time_position_when_paused(self): self.playback.play() diff --git a/tests/backends/base/stored_playlists.py b/tests/backends/base/stored_playlists.py index 839d5bed..54315e62 100644 --- a/tests/backends/base/stored_playlists.py +++ b/tests/backends/base/stored_playlists.py @@ -5,7 +5,8 @@ import tempfile from mopidy import settings from mopidy.models import Playlist -from tests import SkipTest, path_to_data_dir +from tests import unittest, path_to_data_dir + class StoredPlaylistsControllerTest(object): def setUp(self): @@ -78,11 +79,13 @@ class StoredPlaylistsControllerTest(object): except LookupError as e: self.assertEqual(u'"name=c" match no playlists', e[0]) + @unittest.SkipTest def test_lookup(self): - raise SkipTest + pass + @unittest.SkipTest def test_refresh(self): - raise SkipTest + pass def test_rename(self): playlist = self.stored.create('test') @@ -100,5 +103,6 @@ class StoredPlaylistsControllerTest(object): self.stored.save(playlist) self.assert_(playlist in self.stored.playlists) + @unittest.SkipTest def test_playlist_with_unknown_track(self): - raise SkipTest + pass diff --git a/tests/backends/events_test.py b/tests/backends/events_test.py index 88429166..d761676d 100644 --- a/tests/backends/events_test.py +++ b/tests/backends/events_test.py @@ -1,5 +1,4 @@ import mock -import unittest from pykka.registry import ActorRegistry @@ -7,6 +6,9 @@ from mopidy.backends.dummy import DummyBackend from mopidy.listeners import BackendListener from mopidy.models import Track +from tests import unittest + + @mock.patch.object(BackendListener, 'send') class BackendEventsTest(unittest.TestCase): def setUp(self): diff --git a/tests/backends/local/current_playlist_test.py b/tests/backends/local/current_playlist_test.py index 6f72d7d5..a475a6fd 100644 --- a/tests/backends/local/current_playlist_test.py +++ b/tests/backends/local/current_playlist_test.py @@ -1,18 +1,16 @@ -import unittest - -# FIXME Our Windows build server does not support GStreamer yet import sys -if sys.platform == 'win32': - from tests import SkipTest - raise SkipTest from mopidy import settings from mopidy.backends.local import LocalBackend from mopidy.models import Track +from tests import unittest from tests.backends.base.current_playlist import CurrentPlaylistControllerTest from tests.backends.local import generate_song + +@unittest.skipIf(sys.platform == 'win32', + 'Our Windows build server does not support GStreamer yet') class LocalCurrentPlaylistControllerTest(CurrentPlaylistControllerTest, unittest.TestCase): diff --git a/tests/backends/local/library_test.py b/tests/backends/local/library_test.py index 68ab22e9..046e747a 100644 --- a/tests/backends/local/library_test.py +++ b/tests/backends/local/library_test.py @@ -1,17 +1,14 @@ -import unittest - -# FIXME Our Windows build server does not support GStreamer yet import sys -if sys.platform == 'win32': - from tests import SkipTest - raise SkipTest from mopidy import settings from mopidy.backends.local import LocalBackend -from tests import path_to_data_dir +from tests import unittest, path_to_data_dir from tests.backends.base.library import LibraryControllerTest + +@unittest.skipIf(sys.platform == 'win32', + 'Our Windows build server does not support GStreamer yet') class LocalLibraryControllerTest(LibraryControllerTest, unittest.TestCase): backend_class = LocalBackend diff --git a/tests/backends/local/playback_test.py b/tests/backends/local/playback_test.py index 6aec680f..788fe33c 100644 --- a/tests/backends/local/playback_test.py +++ b/tests/backends/local/playback_test.py @@ -1,20 +1,17 @@ -import unittest - -# FIXME Our Windows build server does not support GStreamer yet import sys -if sys.platform == 'win32': - from tests import SkipTest - raise SkipTest from mopidy import settings from mopidy.backends.local import LocalBackend from mopidy.models import Track from mopidy.utils.path import path_to_uri -from tests import path_to_data_dir +from tests import unittest, path_to_data_dir from tests.backends.base.playback import PlaybackControllerTest from tests.backends.local import generate_song + +@unittest.skipIf(sys.platform == 'win32', + 'Our Windows build server does not support GStreamer yet') class LocalPlaybackControllerTest(PlaybackControllerTest, unittest.TestCase): backend_class = LocalBackend tracks = [Track(uri=generate_song(i), length=4464) diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index b426e9ce..2bb80d09 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -1,12 +1,5 @@ -import unittest import os - -from tests import SkipTest - -# FIXME Our Windows build server does not support GStreamer yet import sys -if sys.platform == 'win32': - raise SkipTest from mopidy import settings from mopidy.backends.local import LocalBackend @@ -14,11 +7,14 @@ from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track from mopidy.utils.path import path_to_uri -from tests import path_to_data_dir -from tests.backends.base.stored_playlists import \ - StoredPlaylistsControllerTest +from tests import unittest, path_to_data_dir +from tests.backends.base.stored_playlists import ( + StoredPlaylistsControllerTest) from tests.backends.local import generate_song + +@unittest.skipIf(sys.platform == 'win32', + 'Our Windows build server does not support GStreamer yet') class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, unittest.TestCase): @@ -77,14 +73,18 @@ class LocalStoredPlaylistsControllerTest(StoredPlaylistsControllerTest, self.assertEqual('test', self.stored.playlists[0].name) self.assertEqual(track.uri, self.stored.playlists[0].tracks[0].uri) + @unittest.SkipTest def test_santitising_of_playlist_filenames(self): - raise SkipTest + pass + @unittest.SkipTest def test_playlist_folder_is_createad(self): - raise SkipTest + pass + @unittest.SkipTest def test_create_sets_playlist_uri(self): - raise SkipTest + pass + @unittest.SkipTest def test_save_sets_playlist_uri(self): - raise SkipTest + pass diff --git a/tests/backends/local/translator_test.py b/tests/backends/local/translator_test.py index a4e9f317..1dceb737 100644 --- a/tests/backends/local/translator_test.py +++ b/tests/backends/local/translator_test.py @@ -2,13 +2,12 @@ import os import tempfile -import unittest 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, path_to_data_dir +from tests import unittest, path_to_data_dir song1_path = path_to_data_dir('song1.mp3') song2_path = path_to_data_dir('song2.mp3') @@ -17,6 +16,9 @@ song1_uri = path_to_uri(song1_path) song2_uri = path_to_uri(song2_path) encoded_uri = path_to_uri(encoded_path) +# FIXME use mock instead of tempfile.NamedTemporaryFile + + class M3UToUriTest(unittest.TestCase): def test_empty_file(self): uris = parse_m3u(path_to_data_dir('empty.m3u')) @@ -127,9 +129,10 @@ class MPDTagCacheToTracksTest(unittest.TestCase): self.assertEqual(track, list(tracks)[0]) + @unittest.SkipTest def test_misencoded_cache(self): # FIXME not sure if this can happen - raise SkipTest + pass def test_cache_with_blank_track_info(self): tracks = parse_mpd_tag_cache(path_to_data_dir('blank_tag_cache'), diff --git a/tests/frontends/mpd/dispatcher_test.py b/tests/frontends/mpd/dispatcher_test.py index 7708ce31..bfa7c548 100644 --- a/tests/frontends/mpd/dispatcher_test.py +++ b/tests/frontends/mpd/dispatcher_test.py @@ -1,11 +1,12 @@ -import unittest - from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpd.dispatcher import MpdDispatcher from mopidy.frontends.mpd.exceptions import MpdAckError from mopidy.frontends.mpd.protocol import request_handlers, handle_request from mopidy.mixers.dummy import DummyMixer +from tests import unittest + + class MpdDispatcherTest(unittest.TestCase): def setUp(self): self.backend = DummyBackend.start().proxy() diff --git a/tests/frontends/mpd/exception_test.py b/tests/frontends/mpd/exception_test.py index df2cd65e..2ea3fe62 100644 --- a/tests/frontends/mpd/exception_test.py +++ b/tests/frontends/mpd/exception_test.py @@ -1,8 +1,9 @@ -import unittest - from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError, MpdUnknownCommand, MpdSystemError, MpdNotImplemented) +from tests import unittest + + class MpdExceptionsTest(unittest.TestCase): def test_key_error_wrapped_in_mpd_ack_error(self): try: diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 078153b5..591ef5ce 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -1,4 +1,3 @@ -import unittest import mock from mopidy import settings @@ -6,6 +5,8 @@ from mopidy.backends import dummy as backend from mopidy.frontends import mpd from mopidy.mixers import dummy as mixer +from tests import unittest + class MockConnetion(mock.Mock): def __init__(self, *args, **kwargs): diff --git a/tests/frontends/mpd/protocol/audio_output_test.py b/tests/frontends/mpd/protocol/audio_output_test.py index f9374159..3bb8dce8 100644 --- a/tests/frontends/mpd/protocol/audio_output_test.py +++ b/tests/frontends/mpd/protocol/audio_output_test.py @@ -1,5 +1,6 @@ from tests.frontends.mpd import protocol + class AudioOutputHandlerTest(protocol.BaseTestCase): def test_enableoutput(self): self.sendRequest(u'enableoutput "0"') diff --git a/tests/frontends/mpd/protocol/authentication_test.py b/tests/frontends/mpd/protocol/authentication_test.py index a1487cf9..20422f5b 100644 --- a/tests/frontends/mpd/protocol/authentication_test.py +++ b/tests/frontends/mpd/protocol/authentication_test.py @@ -2,6 +2,7 @@ from mopidy import settings from tests.frontends.mpd import protocol + class AuthenticationTest(protocol.BaseTestCase): def test_authentication_with_valid_password_is_accepted(self): settings.MPD_SERVER_PASSWORD = u'topsecret' diff --git a/tests/frontends/mpd/protocol/command_list_test.py b/tests/frontends/mpd/protocol/command_list_test.py index 9b5ef690..a81725ad 100644 --- a/tests/frontends/mpd/protocol/command_list_test.py +++ b/tests/frontends/mpd/protocol/command_list_test.py @@ -1,5 +1,6 @@ from tests.frontends.mpd import protocol + class CommandListsTest(protocol.BaseTestCase): def test_command_list_begin(self): response = self.sendRequest(u'command_list_begin') diff --git a/tests/frontends/mpd/protocol/connection_test.py b/tests/frontends/mpd/protocol/connection_test.py index 33b5a1a2..cd08313f 100644 --- a/tests/frontends/mpd/protocol/connection_test.py +++ b/tests/frontends/mpd/protocol/connection_test.py @@ -4,6 +4,7 @@ from mopidy import settings from tests.frontends.mpd import protocol + class ConnectionHandlerTest(protocol.BaseTestCase): def test_close_closes_the_client_connection(self): with patch.object(self.session, 'close') as close_mock: diff --git a/tests/frontends/mpd/protocol/current_playlist_test.py b/tests/frontends/mpd/protocol/current_playlist_test.py index 1b0ae404..343b230b 100644 --- a/tests/frontends/mpd/protocol/current_playlist_test.py +++ b/tests/frontends/mpd/protocol/current_playlist_test.py @@ -2,6 +2,7 @@ from mopidy.models import Track from tests.frontends.mpd import protocol + class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add(self): needle = Track(uri='dummy://foo') diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index da16bf33..ab78b0b9 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -5,6 +5,7 @@ from mopidy.models import Track from tests.frontends.mpd import protocol + class IdleHandlerTest(protocol.BaseTestCase): def idleEvent(self, subsystem): self.session.on_idle(subsystem) diff --git a/tests/frontends/mpd/protocol/music_db_test.py b/tests/frontends/mpd/protocol/music_db_test.py index dc0789c9..088502c4 100644 --- a/tests/frontends/mpd/protocol/music_db_test.py +++ b/tests/frontends/mpd/protocol/music_db_test.py @@ -1,5 +1,6 @@ from tests.frontends.mpd import protocol + class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_count(self): self.sendRequest(u'count "tag" "needle"') diff --git a/tests/frontends/mpd/protocol/playback_test.py b/tests/frontends/mpd/protocol/playback_test.py index 6f93dc72..01658f6d 100644 --- a/tests/frontends/mpd/protocol/playback_test.py +++ b/tests/frontends/mpd/protocol/playback_test.py @@ -1,7 +1,7 @@ from mopidy.backends import base as backend from mopidy.models import Track -from tests import SkipTest +from tests import unittest from tests.frontends.mpd import protocol PAUSED = backend.PlaybackController.PAUSED @@ -146,14 +146,17 @@ class PlaybackOptionsHandlerTest(protocol.BaseTestCase): self.assertInResponse(u'OK') self.assertInResponse(u'off') + @unittest.SkipTest def test_replay_gain_status_off(self): - raise SkipTest # TODO + pass + @unittest.SkipTest def test_replay_gain_status_track(self): - raise SkipTest # TODO + pass + @unittest.SkipTest def test_replay_gain_status_album(self): - raise SkipTest # TODO + pass class PlaybackControlHandlerTest(protocol.BaseTestCase): diff --git a/tests/frontends/mpd/protocol/reflection_test.py b/tests/frontends/mpd/protocol/reflection_test.py index 315e3051..8bd9b7e0 100644 --- a/tests/frontends/mpd/protocol/reflection_test.py +++ b/tests/frontends/mpd/protocol/reflection_test.py @@ -2,6 +2,7 @@ from mopidy import settings from tests.frontends.mpd import protocol + class ReflectionHandlerTest(protocol.BaseTestCase): def test_commands_returns_list_of_all_commands(self): self.sendRequest(u'commands') diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index 1b257d8b..0ac66f69 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -4,6 +4,7 @@ from mopidy.models import Track from tests.frontends.mpd import protocol + class IssueGH17RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues#issue/17 diff --git a/tests/frontends/mpd/protocol/status_test.py b/tests/frontends/mpd/protocol/status_test.py index f50ecd24..e6572eab 100644 --- a/tests/frontends/mpd/protocol/status_test.py +++ b/tests/frontends/mpd/protocol/status_test.py @@ -2,6 +2,7 @@ from mopidy.models import Track from tests.frontends.mpd import protocol + class StatusHandlerTest(protocol.BaseTestCase): def test_clearerror(self): self.sendRequest(u'clearerror') diff --git a/tests/frontends/mpd/protocol/stickers_test.py b/tests/frontends/mpd/protocol/stickers_test.py index 1c48cfd3..3e8b687f 100644 --- a/tests/frontends/mpd/protocol/stickers_test.py +++ b/tests/frontends/mpd/protocol/stickers_test.py @@ -1,5 +1,6 @@ from tests.frontends.mpd import protocol + class StickersHandlerTest(protocol.BaseTestCase): def test_sticker_get(self): self.sendRequest( diff --git a/tests/frontends/mpd/protocol/stored_playlists_test.py b/tests/frontends/mpd/protocol/stored_playlists_test.py index 6d9448a6..45d6a09a 100644 --- a/tests/frontends/mpd/protocol/stored_playlists_test.py +++ b/tests/frontends/mpd/protocol/stored_playlists_test.py @@ -1,9 +1,10 @@ -import datetime as dt +import datetime from mopidy.models import Track, Playlist from tests.frontends.mpd import protocol + class StoredPlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylist(self): self.backend.stored_playlists.playlists = [ @@ -33,7 +34,7 @@ class StoredPlaylistsHandlerTest(protocol.BaseTestCase): u'ACK [50@0] {listplaylistinfo} No such playlist') def test_listplaylists(self): - last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345) + last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) self.backend.stored_playlists.playlists = [Playlist(name='a', last_modified=last_modified)] diff --git a/tests/frontends/mpd/serializer_test.py b/tests/frontends/mpd/serializer_test.py index b0c57588..681ab20f 100644 --- a/tests/frontends/mpd/serializer_test.py +++ b/tests/frontends/mpd/serializer_test.py @@ -1,12 +1,14 @@ -import datetime as dt +import datetime import os -import unittest from mopidy import settings 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 unittest + + class TrackMpdFormatTest(unittest.TestCase): track = Track( uri=u'a uri', @@ -15,7 +17,7 @@ class TrackMpdFormatTest(unittest.TestCase): album=Album(name=u'an album', num_tracks=13, artists=[Artist(name=u'an other artist')]), track_no=7, - date=dt.date(1977, 1, 1), + date=datetime.date(1977, 1, 1), length=137000, ) @@ -61,7 +63,7 @@ class TrackMpdFormatTest(unittest.TestCase): self.assert_(('Album', 'an album') in result) self.assert_(('AlbumArtist', 'an other artist') in result) self.assert_(('Track', '7/13') in result) - self.assert_(('Date', dt.date(1977, 1, 1)) in result) + self.assert_(('Date', datetime.date(1977, 1, 1)) in result) self.assert_(('Pos', 9) in result) self.assert_(('Id', 122) in result) self.assertEqual(len(result), 10) diff --git a/tests/frontends/mpd/status_test.py b/tests/frontends/mpd/status_test.py index 2f97a7d4..bdd2dab8 100644 --- a/tests/frontends/mpd/status_test.py +++ b/tests/frontends/mpd/status_test.py @@ -1,11 +1,11 @@ -import unittest - from mopidy.backends import dummy as backend from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status from mopidy.mixers import dummy as mixer from mopidy.models import Track +from tests import unittest + PAUSED = backend.PlaybackController.PAUSED PLAYING = backend.PlaybackController.PLAYING STOPPED = backend.PlaybackController.STOPPED @@ -13,6 +13,7 @@ STOPPED = backend.PlaybackController.STOPPED # FIXME migrate to using protocol.BaseTestCase instead of status.stats # directly? + class StatusHandlerTest(unittest.TestCase): def setUp(self): self.backend = backend.DummyBackend.start().proxy() diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 2f737744..90cdab6a 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -1,9 +1,11 @@ import mock -import unittest from mopidy.frontends.mpris import MprisFrontend, objects from mopidy.models import Track +from tests import unittest + + class BackendEventsTest(unittest.TestCase): def setUp(self): self.mpris_frontend = MprisFrontend() # As a plain class, not an actor diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index ee668a33..a966403e 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -1,5 +1,4 @@ import mock -import unittest from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController @@ -7,10 +6,13 @@ from mopidy.frontends.mpris import objects from mopidy.mixers.dummy import DummyMixer from mopidy.models import Album, Artist, Track +from tests import unittest + PLAYING = PlaybackController.PLAYING PAUSED = PlaybackController.PAUSED STOPPED = PlaybackController.STOPPED + class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 52a1c66e..443efdd3 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,10 +1,12 @@ import mock -import unittest from mopidy import settings from mopidy.backends.dummy import DummyBackend from mopidy.frontends.mpris import objects +from tests import unittest + + class RootInterfaceTest(unittest.TestCase): def setUp(self): objects.exit_process = mock.Mock() diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 0b9a559e..66e0995e 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -1,21 +1,16 @@ -import multiprocessing -import unittest - -from tests import SkipTest - -# FIXME Our Windows build server does not support GStreamer yet import sys -if sys.platform == 'win32': - raise SkipTest from mopidy import settings from mopidy.gstreamer import GStreamer from mopidy.utils.path import path_to_uri -from tests import path_to_data_dir +from tests import unittest, path_to_data_dir # TODO BaseOutputTest? + +@unittest.skipIf(sys.platform == 'win32', + 'Our Windows build server does not support GStreamer yet') class GStreamerTest(unittest.TestCase): def setUp(self): settings.BACKENDS = ('mopidy.backends.local.LocalBackend',) @@ -48,11 +43,11 @@ class GStreamerTest(unittest.TestCase): self.gstreamer.start_playback() self.assertTrue(self.gstreamer.stop_playback()) - @SkipTest + @unittest.SkipTest def test_deliver_data(self): pass # TODO - @SkipTest + @unittest.SkipTest def test_end_of_data_stream(self): pass # TODO @@ -71,10 +66,10 @@ class GStreamerTest(unittest.TestCase): self.assertTrue(self.gstreamer.set_volume(100)) self.assertEqual(100, self.gstreamer.get_volume()) - @SkipTest + @unittest.SkipTest def test_set_state_encapsulation(self): pass # TODO - @SkipTest + @unittest.SkipTest def test_set_position(self): pass # TODO diff --git a/tests/help_test.py b/tests/help_test.py index 25f534c2..1fa22c2f 100644 --- a/tests/help_test.py +++ b/tests/help_test.py @@ -1,10 +1,12 @@ import os import subprocess import sys -import unittest import mopidy +from tests import unittest + + class HelpTest(unittest.TestCase): def test_help_has_mopidy_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) diff --git a/tests/listeners_test.py b/tests/listeners_test.py index d67da692..486dcf9c 100644 --- a/tests/listeners_test.py +++ b/tests/listeners_test.py @@ -1,8 +1,9 @@ -import unittest - from mopidy.listeners import BackendListener from mopidy.models import Track +from tests import unittest + + class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py index 5370f155..7fec3c82 100644 --- a/tests/mixers/denon_test.py +++ b/tests/mixers/denon_test.py @@ -1,8 +1,9 @@ -import unittest - from mopidy.mixers.denon import DenonMixer from tests.mixers.base_test import BaseMixerTest +from tests import unittest + + class DenonMixerDeviceMock(object): def __init__(self): self._open = True @@ -24,6 +25,7 @@ class DenonMixerDeviceMock(object): def open(self): self._open = True + class DenonMixerTest(BaseMixerTest, unittest.TestCase): ACTUAL_MAX = 99 INITIAL = 1 diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py index 334dc8a1..8ae8623c 100644 --- a/tests/mixers/dummy_test.py +++ b/tests/mixers/dummy_test.py @@ -1,8 +1,9 @@ -import unittest - from mopidy.mixers.dummy import DummyMixer + +from tests import unittest from tests.mixers.base_test import BaseMixerTest + class DenonMixerTest(BaseMixerTest, unittest.TestCase): mixer_class = DummyMixer diff --git a/tests/models_test.py b/tests/models_test.py index 637a8209..978f35b6 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,9 +1,9 @@ -import datetime as dt -import unittest +import datetime from mopidy.models import Artist, Album, CpTrack, Track, Playlist -from tests import SkipTest +from tests import unittest + class GenericCopyTets(unittest.TestCase): def compare(self, orig, other): @@ -49,6 +49,7 @@ class GenericCopyTets(unittest.TestCase): test = lambda: Track().copy(invalid_key=True) self.assertRaises(TypeError, test) + class ArtistTest(unittest.TestCase): def test_uri(self): uri = u'an_uri' @@ -321,7 +322,7 @@ class TrackTest(unittest.TestCase): self.assertRaises(AttributeError, setattr, track, 'track_no', None) def test_date(self): - date = dt.date(1977, 1, 1) + date = datetime.date(1977, 1, 1) track = Track(date=date) self.assertEqual(track.date, date) self.assertRaises(AttributeError, setattr, track, 'date', None) @@ -400,7 +401,7 @@ class TrackTest(unittest.TestCase): self.assertEqual(hash(track1), hash(track2)) def test_eq_date(self): - date = dt.date.today() + date = datetime.date.today() track1 = Track(date=date) track2 = Track(date=date) self.assertEqual(track1, track2) @@ -425,7 +426,7 @@ class TrackTest(unittest.TestCase): self.assertEqual(hash(track1), hash(track2)) def test_eq(self): - date = dt.date.today() + date = datetime.date.today() artists = [Artist()] album = Album() track1 = Track(uri=u'uri', name=u'name', artists=artists, album=album, @@ -474,8 +475,8 @@ class TrackTest(unittest.TestCase): self.assertNotEqual(hash(track1), hash(track2)) def test_ne_date(self): - track1 = Track(date=dt.date.today()) - track2 = Track(date=dt.date.today()-dt.timedelta(days=1)) + track1 = Track(date=datetime.date.today()) + track2 = Track(date=datetime.date.today()-datetime.timedelta(days=1)) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) @@ -500,11 +501,11 @@ class TrackTest(unittest.TestCase): def test_ne(self): track1 = Track(uri=u'uri1', name=u'name1', artists=[Artist(name=u'name1')], album=Album(name=u'name1'), - track_no=1, date=dt.date.today(), length=100, bitrate=100, + track_no=1, date=datetime.date.today(), length=100, bitrate=100, musicbrainz_id='id1') track2 = Track(uri=u'uri2', name=u'name2', artists=[Artist(name=u'name2')], album=Album(name=u'name2'), - track_no=2, date=dt.date.today()-dt.timedelta(days=1), + track_no=2, date=datetime.date.today()-datetime.timedelta(days=1), length=200, bitrate=200, musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) @@ -535,7 +536,7 @@ class PlaylistTest(unittest.TestCase): self.assertEqual(playlist.length, 3) def test_last_modified(self): - last_modified = dt.datetime.now() + last_modified = datetime.datetime.now() playlist = Playlist(last_modified=last_modified) self.assertEqual(playlist.last_modified, last_modified) self.assertRaises(AttributeError, setattr, playlist, 'last_modified', @@ -543,7 +544,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_uri(self): tracks = [Track()] - last_modified = dt.datetime.now() + last_modified = datetime.datetime.now() playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(uri=u'another uri') @@ -554,7 +555,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_name(self): tracks = [Track()] - last_modified = dt.datetime.now() + last_modified = datetime.datetime.now() playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(name=u'another name') @@ -565,7 +566,7 @@ class PlaylistTest(unittest.TestCase): def test_with_new_tracks(self): tracks = [Track()] - last_modified = dt.datetime.now() + last_modified = datetime.datetime.now() playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) new_tracks = [Track(), Track()] @@ -577,8 +578,8 @@ class PlaylistTest(unittest.TestCase): def test_with_new_last_modified(self): tracks = [Track()] - last_modified = dt.datetime.now() - new_last_modified = last_modified + dt.timedelta(1) + last_modified = datetime.datetime.now() + new_last_modified = last_modified + datetime.timedelta(1) playlist = Playlist(uri=u'an uri', name=u'a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(last_modified=new_last_modified) diff --git a/tests/scanner_test.py b/tests/scanner_test.py index f403a221..91e67e11 100644 --- a/tests/scanner_test.py +++ b/tests/scanner_test.py @@ -1,10 +1,10 @@ -import unittest from datetime import date from mopidy.scanner import Scanner, translator from mopidy.models import Track, Artist, Album -from tests import path_to_data_dir, SkipTest +from tests import unittest, path_to_data_dir + class FakeGstDate(object): def __init__(self, year, month, day): @@ -12,6 +12,7 @@ class FakeGstDate(object): self.month = month self.day = day + class TranslatorTest(unittest.TestCase): def setUp(self): self.data = { @@ -126,6 +127,7 @@ class TranslatorTest(unittest.TestCase): del self.track['date'] self.check() + class ScannerTest(unittest.TestCase): def setUp(self): self.errors = {} @@ -185,6 +187,6 @@ class ScannerTest(unittest.TestCase): self.scan('scanner/image') self.assert_(self.errors) - @SkipTest + @unittest.SkipTest def test_song_without_time_is_handeled(self): pass diff --git a/tests/utils/init_test.py b/tests/utils/init_test.py index 70dd7e36..2097e3e6 100644 --- a/tests/utils/init_test.py +++ b/tests/utils/init_test.py @@ -1,7 +1,8 @@ -import unittest - from mopidy.utils import get_class +from tests import unittest + + class GetClassTest(unittest.TestCase): def test_loading_module_that_does_not_exist(self): self.assertRaises(ImportError, get_class, 'foo.bar.Baz') diff --git a/tests/utils/network/connection_test.py b/tests/utils/network/connection_test.py index 1bda0af5..aa1be2b6 100644 --- a/tests/utils/network/connection_test.py +++ b/tests/utils/network/connection_test.py @@ -3,12 +3,12 @@ import gobject import logging import pykka import socket -import unittest +from mock import patch, sentinel, Mock from mopidy.utils import network -from mock import patch, sentinel, Mock -from tests import any_int, any_unicode, SkipTest +from tests import unittest, any_int, any_unicode + class ConnectionTest(unittest.TestCase): def setUp(self): diff --git a/tests/utils/network/lineprotocol_test.py b/tests/utils/network/lineprotocol_test.py index 4e7df132..b323de09 100644 --- a/tests/utils/network/lineprotocol_test.py +++ b/tests/utils/network/lineprotocol_test.py @@ -1,11 +1,12 @@ #encoding: utf-8 import re -import unittest +from mock import sentinel, Mock from mopidy.utils import network -from mock import sentinel, Mock +from tests import unittest + class LineProtocolTest(unittest.TestCase): def setUp(self): diff --git a/tests/utils/network/server_test.py b/tests/utils/network/server_test.py index 75b33d61..e0399525 100644 --- a/tests/utils/network/server_test.py +++ b/tests/utils/network/server_test.py @@ -1,12 +1,12 @@ import errno import gobject import socket -import unittest +from mock import patch, sentinel, Mock from mopidy.utils import network -from mock import patch, sentinel, Mock -from tests import any_int +from tests import unittest, any_int + class ServerTest(unittest.TestCase): def setUp(self): diff --git a/tests/utils/network/utils_test.py b/tests/utils/network/utils_test.py index ada1de01..1e11673e 100644 --- a/tests/utils/network/utils_test.py +++ b/tests/utils/network/utils_test.py @@ -1,10 +1,10 @@ import socket -import unittest +from mock import patch, Mock from mopidy.utils import network -from mock import patch, Mock -from tests import SkipTest +from tests import unittest + class FormatHostnameTest(unittest.TestCase): @patch('mopidy.utils.network.has_ipv6', True) @@ -52,6 +52,6 @@ class CreateSocketTest(unittest.TestCase): self.assertEqual(socket_mock.call_args[0], (socket.AF_INET6, socket.SOCK_STREAM)) - @SkipTest + @unittest.SkipTest def test_ipv6_only_is_set(self): pass diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index 088a7049..ba1fcf97 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -4,12 +4,12 @@ import os import shutil import sys import tempfile -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 path_to_data_dir +from tests import unittest, path_to_data_dir + class GetOrCreateFolderTest(unittest.TestCase): def setUp(self): diff --git a/tests/utils/settings_test.py b/tests/utils/settings_test.py index ec470ea9..55e1156b 100644 --- a/tests/utils/settings_test.py +++ b/tests/utils/settings_test.py @@ -1,10 +1,12 @@ import os -import unittest from mopidy import settings as default_settings_module, SettingsError from mopidy.utils.settings import (format_settings_list, mask_value_if_secret, SettingsProxy, validate_settings) +from tests import unittest + + class ValidateSettingsTest(unittest.TestCase): def setUp(self): self.defaults = { diff --git a/tests/version_test.py b/tests/version_test.py index 9b53c63f..f85c1e30 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,9 +1,11 @@ from distutils.version import StrictVersion as SV -import unittest import platform from mopidy import get_version, get_plain_version, get_platform, get_python +from tests import unittest + + class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): SV(get_plain_version()) From 92a2408acedd316ea3d4f587d460bd6fce11faff Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Sun, 31 Jul 2011 22:27:10 +0200 Subject: [PATCH 240/350] Remove some unused imports --- tests/backends/local/stored_playlists_test.py | 1 - tests/frontends/mpd/protocol/idle_test.py | 1 - tests/version_test.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/backends/local/stored_playlists_test.py b/tests/backends/local/stored_playlists_test.py index 2bb80d09..56be92c4 100644 --- a/tests/backends/local/stored_playlists_test.py +++ b/tests/backends/local/stored_playlists_test.py @@ -3,7 +3,6 @@ import sys from mopidy import settings from mopidy.backends.local import LocalBackend -from mopidy.mixers.dummy import DummyMixer from mopidy.models import Playlist, Track from mopidy.utils.path import path_to_uri diff --git a/tests/frontends/mpd/protocol/idle_test.py b/tests/frontends/mpd/protocol/idle_test.py index ab78b0b9..ae23c88e 100644 --- a/tests/frontends/mpd/protocol/idle_test.py +++ b/tests/frontends/mpd/protocol/idle_test.py @@ -1,7 +1,6 @@ from mock import patch from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS -from mopidy.models import Track from tests.frontends.mpd import protocol diff --git a/tests/version_test.py b/tests/version_test.py index f85c1e30..4544349d 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,7 +1,7 @@ from distutils.version import StrictVersion as SV import platform -from mopidy import get_version, get_plain_version, get_platform, get_python +from mopidy import get_plain_version, get_platform, get_python from tests import unittest From b93926bec68a6f5c4a2da7ae8c664af2e2043478 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Aug 2011 14:56:35 +0200 Subject: [PATCH 241/350] This doesn't match the Spotify Linux client after we moved it again. --- docs/changes.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index f2211dab..1bd5748b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -17,8 +17,7 @@ v0.6.0 (in development) - This means that your settings file will need to be moved from ``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``. - Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of - ``~/.mopidy/spotify_cache``, this matches Spotify's own behaviour for their - Linux client. + ``~/.mopidy/spotify_cache``. - The local backend's ``tag_cache`` should now be in ``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in ``~/.local/share/mopidy/playlists``. From d03c184f0eb87d1abc6d1f76c82e2d0637bf9d7b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 1 Aug 2011 16:52:39 +0200 Subject: [PATCH 242/350] Fix wrong dates in changelog --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1bd5748b..41a98fcf 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -273,7 +273,7 @@ loading from Mopidy 0.3.0 is still present. the debug log, to ease debugging of issues with attached debug logs. -v0.3.1 (2010-01-22) +v0.3.1 (2011-01-22) =================== A couple of fixes to the 0.3.0 release is needed to get a smooth installation. @@ -287,7 +287,7 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation. installed if the installation is executed as the root user. -v0.3.0 (2010-01-22) +v0.3.0 (2011-01-22) =================== Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large From 611efebd6f7269e77c5662fbc378601cdd6962a9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Aug 2011 01:53:20 +0200 Subject: [PATCH 243/350] Update GitHub Issues URLs --- tests/frontends/mpd/protocol/regression_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index 0ac66f69..8f2d6588 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -7,7 +7,7 @@ from tests.frontends.mpd import protocol class IssueGH17RegressionTest(protocol.BaseTestCase): """ - The issue: http://github.com/mopidy/mopidy/issues#issue/17 + The issue: http://github.com/mopidy/mopidy/issues/17 How to reproduce: @@ -37,7 +37,7 @@ class IssueGH17RegressionTest(protocol.BaseTestCase): class IssueGH18RegressionTest(protocol.BaseTestCase): """ - The issue: http://github.com/mopidy/mopidy/issues#issue/18 + The issue: http://github.com/mopidy/mopidy/issues/18 How to reproduce: @@ -71,7 +71,7 @@ class IssueGH18RegressionTest(protocol.BaseTestCase): class IssueGH22RegressionTest(protocol.BaseTestCase): """ - The issue: http://github.com/mopidy/mopidy/issues/#issue/22 + The issue: http://github.com/mopidy/mopidy/issues/22 How to reproduce: @@ -101,7 +101,7 @@ class IssueGH22RegressionTest(protocol.BaseTestCase): class IssueGH69RegressionTest(protocol.BaseTestCase): """ - The issue: https://github.com/mopidy/mopidy/issues#issue/69 + The issue: https://github.com/mopidy/mopidy/issues/69 How to reproduce: From 2d4ed8572af03a4f1847711459f85f9645811701 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Aug 2011 22:30:48 +0200 Subject: [PATCH 244/350] Update MPD client review since Mopidy 0.6 got 'idle' support --- docs/clients/mpd.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index f5066210..0d187416 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -20,9 +20,8 @@ A command line client. Version 0.14 had some issues with Mopidy (see ncmpc ----- -A console client. Uses the ``idle`` command heavily, which Mopidy doesn't -support yet (see :issue:`32`). If you want a console client, use ncmpcpp -instead. +A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD +command, but in resource inefficient way. ncmpcpp @@ -48,15 +47,15 @@ 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:: +In newer versions of ncmpcpp, like ncmpcpp 0.5.5 shipped with Ubuntu 11.04, +ncmcpp defaults to "notifications" mode for MPD communications, which Mopidy +did not support before Mopidy 0.6. To workaround this limitation in earlier +versions of 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`. +If you use Mopidy 0.6 or newer, you don't need to change anything. Graphical clients From d8959341e8a1131e7633539669a1a460b1802a6a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Aug 2011 02:49:34 +0200 Subject: [PATCH 245/350] Unescapes all incoming MPD requests (fixes #113) --- mopidy/frontends/mpd/__init__.py | 8 ++++++ .../frontends/mpd/protocol/regression_test.py | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 57b41fc0..a2faedc2 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -96,5 +96,13 @@ class MpdSession(network.LineProtocol): def on_idle(self, subsystem): self.dispatcher.handle_idle(subsystem) + def decode(self, line): + try: + return super(MpdSession, self).decode(line.decode('string_escape')) + except ValueError: + logger.warning(u'Stopping actor due to unescaping error, data ' + 'supplied by client was not valid.') + self.stop() + def close(self): self.stop() diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index 8f2d6588..d4e4b2aa 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -121,3 +121,28 @@ class IssueGH69RegressionTest(protocol.BaseTestCase): self.sendRequest(u'clear') self.sendRequest(u'load "foo"') self.assertNotInResponse('song: None') + + +class IssueGH113RegressionTest(protocol.BaseTestCase): + """ + The issue: https://github.com/mopidy/mopidy/issues/113 + + How to reproduce: + + - Have a playlist with a name contining backslashes, like + "all lart spotify:track:\w\{22\} pastes". + - Try to load the playlist with the backslashes in the playlist name + escaped. + """ + + def test(self): + self.backend.stored_playlists.create( + u'all lart spotify:track:\w\{22\} pastes') + + self.sendRequest(u'lsinfo "/"') + self.assertInResponse( + u'playlist: all lart spotify:track:\w\{22\} pastes') + + self.sendRequest( + r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"') + self.assertInResponse('OK') From e14c2aee043f298143404137e74b1f2e00767a55 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 2 Aug 2011 22:42:09 +0200 Subject: [PATCH 246/350] Update changelog with fix for #113 --- docs/changes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 41a98fcf..dab00c08 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -48,6 +48,8 @@ v0.6.0 (in development) - Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`) +- Unescape all incoming MPD requests. (Fixes: :issue:`113`) + v0.5.0 (2011-06-15) =================== From ef29a8262637a7109176b4819da1816ba16858e0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Aug 2011 23:43:31 +0200 Subject: [PATCH 247/350] Add missing word --- docs/clients/mpd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/clients/mpd.rst b/docs/clients/mpd.rst index 0d187416..4c789eba 100644 --- a/docs/clients/mpd.rst +++ b/docs/clients/mpd.rst @@ -21,7 +21,7 @@ ncmpc ----- A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD -command, but in resource inefficient way. +command, but in a resource inefficient way. ncmpcpp From c6d258fae2383bf483cfc0b0ab95eddd3b228294 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 3 Aug 2011 12:15:52 +0200 Subject: [PATCH 248/350] Remove unused destroy() methods from backend controllers and providers --- docs/changes.rst | 6 ++++++ mopidy/backends/base/current_playlist.py | 4 ---- mopidy/backends/base/library.py | 12 ------------ mopidy/backends/base/playback.py | 14 -------------- mopidy/backends/base/stored_playlists.py | 12 ------------ 5 files changed, 6 insertions(+), 42 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index dab00c08..32115a95 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,12 @@ v0.6.0 (in development) - Unescape all incoming MPD requests. (Fixes: :issue:`113`) +**multi-backend changes** + +- Remove `destroy()` methods from backend controller and provider APIs, as it + was not in use and actually not called by any code. Will reintroduce when + needed. + v0.5.0 (2011-06-15) =================== diff --git a/mopidy/backends/base/current_playlist.py b/mopidy/backends/base/current_playlist.py index e89c23d5..17125ac0 100644 --- a/mopidy/backends/base/current_playlist.py +++ b/mopidy/backends/base/current_playlist.py @@ -21,10 +21,6 @@ class CurrentPlaylistController(object): self._cp_tracks = [] self._version = 0 - def destroy(self): - """Cleanup after component.""" - pass - @property def cp_tracks(self): """ diff --git a/mopidy/backends/base/library.py b/mopidy/backends/base/library.py index a30ed412..9e3afe9a 100644 --- a/mopidy/backends/base/library.py +++ b/mopidy/backends/base/library.py @@ -16,10 +16,6 @@ class LibraryController(object): self.backend = backend self.provider = provider - def destroy(self): - """Cleanup after component.""" - self.provider.destroy() - def find_exact(self, **query): """ Search the library for tracks where ``field`` is ``values``. @@ -89,14 +85,6 @@ class BaseLibraryProvider(object): def __init__(self, backend): self.backend = backend - def destroy(self): - """ - Cleanup after component. - - *MAY be implemented by subclasses.* - """ - pass - def find_exact(self, **query): """ See :meth:`mopidy.backends.base.LibraryController.find_exact`. diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 57a7ad85..51fe0d3b 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -82,12 +82,6 @@ class PlaybackController(object): self.play_time_accumulated = 0 self.play_time_started = None - def destroy(self): - """ - Cleanup after component. - """ - self.provider.destroy() - def _get_cpid(self, cp_track): if cp_track is None: return None @@ -559,14 +553,6 @@ class BasePlaybackProvider(object): def __init__(self, backend): self.backend = backend - def destroy(self): - """ - Cleanup after component. - - *MAY be implemented by subclasses.* - """ - pass - def pause(self): """ Pause playback. diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index aca78a8c..c742cd23 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -17,10 +17,6 @@ class StoredPlaylistsController(object): self.backend = backend self.provider = provider - def destroy(self): - """Cleanup after component.""" - self.provider.destroy() - @property def playlists(self): """ @@ -133,14 +129,6 @@ class BaseStoredPlaylistsProvider(object): self.backend = backend self._playlists = [] - def destroy(self): - """ - Cleanup after component. - - *MAY be implemented by subclass.* - """ - pass - @property def playlists(self): """ From 1817ca2804f37fb92cf6e76abf387c9b36c9d992 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Aug 2011 00:41:26 +0200 Subject: [PATCH 249/350] Formatting --- mopidy/backends/base/stored_playlists.py | 1 - mopidy/backends/local/__init__.py | 3 ++- mopidy/backends/spotify/__init__.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mopidy/backends/base/stored_playlists.py b/mopidy/backends/base/stored_playlists.py index c742cd23..0ce2e196 100644 --- a/mopidy/backends/base/stored_playlists.py +++ b/mopidy/backends/base/stored_playlists.py @@ -189,4 +189,3 @@ class BaseStoredPlaylistsProvider(object): *MUST be implemented by subclass.* """ raise NotImplementedError - diff --git a/mopidy/backends/local/__init__.py b/mopidy/backends/local/__init__.py index e689f666..e1d11bcb 100644 --- a/mopidy/backends/local/__init__.py +++ b/mopidy/backends/local/__init__.py @@ -67,7 +67,8 @@ class LocalBackend(ThreadingActor, Backend): def on_start(self): gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.' + assert len(gstreamer_refs) == 1, \ + 'Expected exactly one running GStreamer.' self.gstreamer = gstreamer_refs[0].proxy() diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 02ccd802..08c3ed49 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -78,7 +78,8 @@ class SpotifyBackend(ThreadingActor, Backend): def on_start(self): gstreamer_refs = ActorRegistry.get_by_class(GStreamer) - assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.' + assert len(gstreamer_refs) == 1, \ + 'Expected exactly one running GStreamer.' self.gstreamer = gstreamer_refs[0].proxy() logger.info(u'Mopidy uses SPOTIFY(R) CORE') From 6a470f96940c281f309acd46a358be36de42a817 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 6 Aug 2011 01:19:45 +0200 Subject: [PATCH 250/350] Fix typo --- tests/frontends/mpd/protocol/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/frontends/mpd/protocol/__init__.py b/tests/frontends/mpd/protocol/__init__.py index 591ef5ce..b54906be 100644 --- a/tests/frontends/mpd/protocol/__init__.py +++ b/tests/frontends/mpd/protocol/__init__.py @@ -8,9 +8,9 @@ from mopidy.mixers import dummy as mixer from tests import unittest -class MockConnetion(mock.Mock): +class MockConnection(mock.Mock): def __init__(self, *args, **kwargs): - super(MockConnetion, self).__init__(*args, **kwargs) + super(MockConnection, self).__init__(*args, **kwargs) self.host = mock.sentinel.host self.port = mock.sentinel.port self.response = [] @@ -25,7 +25,7 @@ class BaseTestCase(unittest.TestCase): self.backend = backend.DummyBackend.start().proxy() self.mixer = mixer.DummyMixer.start().proxy() - self.connection = MockConnetion() + self.connection = MockConnection() self.session = mpd.MpdSession(self.connection) self.dispatcher = self.session.dispatcher self.context = self.dispatcher.context From 28257306a4f59d2824a7a8efdde71258cbe78153 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 13 Aug 2011 17:39:14 +0200 Subject: [PATCH 251/350] Increase max number of Spotify search results --- docs/changes.rst | 3 +++ mopidy/backends/spotify/session_manager.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 32115a95..968ef255 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -50,6 +50,9 @@ v0.6.0 (in development) - Unescape all incoming MPD requests. (Fixes: :issue:`113`) +- Increase the maximum number of results returned by Spotify searches from 32 + to 100. + **multi-backend changes** - Remove `destroy()` methods from backend controller and provider APIs, as it diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 9c8853e6..9a883d23 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -151,9 +151,12 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): """Search method used by Mopidy backend""" def callback(results, userdata=None): # TODO Include results from results.albums(), etc. too + # TODO Consider launching a second search if results.total_tracks() + # is larger than len(results.tracks()) playlist = Playlist(tracks=[ SpotifyTranslator.to_mopidy_track(t) for t in results.tracks()]) queue.put(playlist) self.connected.wait() - self.session.search(query, callback) + self.session.search(query, callback, track_count=100, + album_count=0, artist_count=0) From 83bf9af8c14dc59637fa94755551f5ea8f0eb1b1 Mon Sep 17 00:00:00 2001 From: sandos Date: Wed, 21 Sep 2011 21:25:11 +0200 Subject: [PATCH 252/350] Log out from spotify when shutting down --- mopidy/backends/spotify/__init__.py | 3 +++ mopidy/backends/spotify/session_manager.py | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index 08c3ed49..a50f1724 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -85,6 +85,9 @@ class SpotifyBackend(ThreadingActor, Backend): logger.info(u'Mopidy uses SPOTIFY(R) CORE') self.spotify = self._connect() + def on_stop(self): + self.spotify.logout() + def _connect(self): from .session_manager import SpotifySessionManager diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 9a883d23..5261f0cf 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -160,3 +160,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.connected.wait() self.session.search(query, callback, track_count=100, album_count=0, artist_count=0) + + def logout(self): + """Log out from spotify""" + logger.debug(u'Logging out from spotify') + self.session.logout() From 808b9e026a272a37d2d6de39be54aa80653c5480 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 24 Sep 2011 19:09:05 +0200 Subject: [PATCH 253/350] Add yappi profiling to tests/__main__.py --- requirements/tests.txt | 1 + tests/__main__.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements/tests.txt b/requirements/tests.txt index 0bc8380f..922ef6dc 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -2,3 +2,4 @@ coverage mock >= 0.7 nose tox +yappi diff --git a/tests/__main__.py b/tests/__main__.py index e2bb3e72..69113580 100644 --- a/tests/__main__.py +++ b/tests/__main__.py @@ -1,4 +1,8 @@ import nose +import yappi -if __name__ == '__main__': +try: + yappi.start() nose.main() +finally: + yappi.print_stats() From 6b7e7d288944635bbbd01c15a115a78128abac74 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 24 Sep 2011 20:04:55 +0200 Subject: [PATCH 254/350] Use unicode for Spotify search queries, as required by pyspotify 1.4 (fixes #129) --- docs/changes.rst | 5 +++++ mopidy/backends/spotify/library.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 968ef255..1d85821c 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,8 @@ v0.6.0 (in development) - Pykka 0.12.3 or greater is required. +- pyspotify 1.4 or greater is required. + - All config, data, and cache locations are now based on the XDG spec. - This means that your settings file will need to be moved from @@ -53,6 +55,9 @@ v0.6.0 (in development) - Increase the maximum number of results returned by Spotify searches from 32 to 100. +- Send Spotify search queries to pyspotify as unicode objects, as required by + pyspotify 1.4. (Fixes: :issue:`129`) + **multi-backend changes** - Remove `destroy()` methods from backend controller and provider APIs, as it diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 40d4a099..59aa9a2c 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -55,7 +55,7 @@ class SpotifyLibraryProvider(BaseLibraryProvider): spotify_query = u' '.join(spotify_query) logger.debug(u'Spotify search query: %s' % spotify_query) queue = Queue.Queue() - self.backend.spotify.search(spotify_query.encode(ENCODING), queue) + self.backend.spotify.search(spotify_query, queue) try: return queue.get(timeout=3) # XXX What is an reasonable timeout? except Queue.Empty: From eb4bf2e8bf351851e5b56d020bdc821fb13d95fc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 25 Sep 2011 11:30:42 +0200 Subject: [PATCH 255/350] Formatting --- mopidy/frontends/mpd/dispatcher.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index cab014a8..5ee70a5b 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -90,7 +90,7 @@ class MpdDispatcher(object): def _authenticate_filter(self, request, response, filter_chain): if self.authenticated: return self._call_next_filter(request, response, filter_chain) - elif settings.MPD_SERVER_PASSWORD is None: + elif settings.MPD_SERVER_PASSWORD is None: self.authenticated = True return self._call_next_filter(request, response, filter_chain) else: @@ -161,6 +161,7 @@ class MpdDispatcher(object): def _has_error(self, response): return response and response[-1].startswith(u'ACK') + ### Filter: call handler def _call_handler_filter(self, request, response, filter_chain): @@ -241,11 +242,10 @@ class MpdContext(object): """ The backend. An instance of :class:`mopidy.backends.base.Backend`. """ - if self._backend is not None: - return self._backend - backend_refs = ActorRegistry.get_by_class(Backend) - assert len(backend_refs) == 1, 'Expected exactly one running backend.' - self._backend = backend_refs[0].proxy() + if self._backend is None: + backend_refs = ActorRegistry.get_by_class(Backend) + assert len(backend_refs) == 1, 'Expected exactly one running backend.' + self._backend = backend_refs[0].proxy() return self._backend @property @@ -253,9 +253,8 @@ class MpdContext(object): """ The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`. """ - if self._mixer is not None: - return self._mixer - mixer_refs = ActorRegistry.get_by_class(BaseMixer) - assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' - self._mixer = mixer_refs[0].proxy() + if self._mixer is None: + mixer_refs = ActorRegistry.get_by_class(BaseMixer) + assert len(mixer_refs) == 1, 'Expected exactly one running mixer.' + self._mixer = mixer_refs[0].proxy() return self._mixer From e0bddfa10984877e9cd32c18ff29f77d8679811c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 25 Sep 2011 11:49:31 +0200 Subject: [PATCH 256/350] Add MPD_SERVER_MAX_CONNECTIONS setting (fixes: #134) --- docs/changes.rst | 3 +++ mopidy/frontends/mpd/__init__.py | 3 ++- mopidy/settings.py | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1d85821c..1a447179 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -58,6 +58,9 @@ v0.6.0 (in development) - Send Spotify search queries to pyspotify as unicode objects, as required by pyspotify 1.4. (Fixes: :issue:`129`) +- Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes: + :issue:`134`) + **multi-backend changes** - Remove `destroy()` methods from backend controller and provider APIs, as it diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index a2faedc2..b6adc09d 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -29,7 +29,8 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): port = settings.MPD_SERVER_PORT try: - network.Server(hostname, port, protocol=MpdSession) + network.Server(hostname, port, protocol=MpdSession, + max_connections=settings.MPD_SERVER_MAX_CONNECTIONS) except IOError, e: logger.error(u'MPD server startup failed: %s', e) sys.exit(1) diff --git a/mopidy/settings.py b/mopidy/settings.py index b1e0c791..ccbf8457 100644 --- a/mopidy/settings.py +++ b/mopidy/settings.py @@ -180,6 +180,11 @@ MPD_SERVER_PORT = 6600 #: Default: :class:`None`, which means no password required. MPD_SERVER_PASSWORD = None +#: The maximum number of concurrent connections the MPD server will accept. +#: +#: Default: 20 +MPD_SERVER_MAX_CONNECTIONS = 20 + #: List of outputs to use. See :mod:`mopidy.outputs` for all available #: backends #: From 65ddaa1583bf724c61958b915a404b855b695b3f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Oct 2011 00:03:03 +0200 Subject: [PATCH 257/350] Update changelog for v0.6.0 release --- docs/changes.rst | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1a447179..445e7984 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,8 +5,19 @@ Changes This change log is used to track all major changes to Mopidy. -v0.6.0 (in development) -======================= +v0.6.0 (2011-10-09) +=================== + +The development of Mopidy have been quite slow for the last couple of months, +but we do have some goodies to release which have been idling in the +develop branch since the warmer days of the summer. This release brings support +for the MPD ``idle`` command, which makes it possible for a client wait for +updates from the server instead of polling every second. Also, we've added +support for the MPRIS standard, so that Mopidy can be controlled over D-Bus +from e.g. the Ubuntu Sound Menu. + +Please note that 0.6.0 requires some updated dependencies, as listed under +*Important changes* below. **Important changes** @@ -31,7 +42,7 @@ v0.6.0 (in development) - A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes Mopidy through the `MPRIS interface `_ over D-Bus. In - practice, this makes it possible to control Mopidy thorugh the `Ubuntu Sound + practice, this makes it possible to control Mopidy through the `Ubuntu Sound Menu `_. **Changes** @@ -61,8 +72,6 @@ v0.6.0 (in development) - Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes: :issue:`134`) -**multi-backend changes** - - Remove `destroy()` methods from backend controller and provider APIs, as it was not in use and actually not called by any code. Will reintroduce when needed. From b0319d1f70e9064c311699308950fbc1fd020fff Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Oct 2011 00:43:20 +0200 Subject: [PATCH 258/350] Prepare for v0.7 development --- docs/changes.rst | 12 ++++++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 445e7984..cc3deacc 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,18 @@ Changes This change log is used to track all major changes to Mopidy. +v0.7.0 (in development) +======================= + +**Important changes** + +- Nothing so far + +**Changes** + +- Nothing so far + + v0.6.0 (2011-10-09) =================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 1d820fd0..ced47e07 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ import os from subprocess import PIPE, Popen -VERSION = (0, 6, 0) +VERSION = (0, 7, 0) DATA_PATH = os.path.join(glib.get_user_data_dir(), 'mopidy') CACHE_PATH = os.path.join(glib.get_user_cache_dir(), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index 4544349d..bd9ba7b9 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -22,8 +22,9 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.3.1') < SV('0.4.0')) self.assert_(SV('0.4.0') < SV('0.4.1')) self.assert_(SV('0.4.1') < SV('0.5.0')) - self.assert_(SV('0.5.0') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.6.1')) + self.assert_(SV('0.5.0') < SV('0.6.0')) + self.assert_(SV('0.6.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.7.1')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From 041fd27990f21e53750b3b1b1cd76a8464a67d9a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Oct 2011 19:30:57 +0200 Subject: [PATCH 259/350] docs: Add link from requirements to the MPRIS frontend --- docs/installation/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 198ac9e8..8fd3e840 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -46,6 +46,9 @@ Otherwise, make sure you got the required dependencies installed. - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for additional requirements. + - To use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see + :mod:`mopidy.frontends.mpris` for additional requirements. + Install latest stable release ============================= From a95f960fdb98f8a164ffd6649fb1b433ee678e56 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Oct 2011 19:34:28 +0200 Subject: [PATCH 260/350] docs: Update location of settings file after XDG-ification --- docs/settings.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 76eb6315..a6ad3693 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -10,10 +10,10 @@ changes you may want to do, and a complete listing of available settings. Changing settings ================= -Mopidy reads settings from the file ``~/.mopidy/settings.py``, where ``~`` -means your *home directory*. If your username is ``alice`` and you are running -Linux, the settings file should probably be at -``/home/alice/.mopidy/settings.py``. +Mopidy reads settings from the file ``~/.config/mopidy/settings.py``, where +``~`` means your *home directory*. If your username is ``alice`` and you are +running Linux, the settings file should probably be at +``/home/alice/.config/mopidy/settings.py``. You can either create the settings file yourself, or run the ``mopidy`` command, and it will create an empty settings file for you. @@ -22,7 +22,7 @@ When you have created the settings file, open it in a text editor, and add settings you want to change. If you want to keep the default value for setting, you should *not* redefine it in your own settings file. -A complete ``~/.mopidy/settings.py`` may look as simple as this:: +A complete ``~/.config/mopidy/settings.py`` may look as simple as this:: MPD_SERVER_HOSTNAME = u'::' SPOTIFY_USERNAME = u'alice' @@ -77,7 +77,7 @@ To make a ``tag_cache`` of your local music available for Mopidy: mopidy --list-settings -#. Scan your music library. Currently the command outputs the ``tag_cache`` to +#. Scan your music library. The command outputs the ``tag_cache`` to ``stdout``, which means that you will need to redirect the output to a file yourself:: From 139ecb6ccf540287cb67f82ce8a4675f85b11675 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 9 Oct 2011 19:45:10 +0200 Subject: [PATCH 261/350] docs: Note on stopping Mopidy using kill/SIGTERM --- docs/running.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/running.rst b/docs/running.rst index 4912512f..6c8d0ede 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -10,4 +10,11 @@ When Mopidy says ``MPD server running at [127.0.0.1]:6600`` it's ready to accept connections by any MPD client. Check out our non-exhaustive :doc:`/clients/mpd` list to find recommended clients. -To stop Mopidy, press ``CTRL+C``. +To stop Mopidy, press ``CTRL+C`` in the terminal where you started Mopidy. + +Mopidy will also shut down properly if you send it the TERM signal, e.g. by +using ``kill``:: + + kill `ps ax | grep mopidy | grep -v grep | cut -d' ' -f1` + +This can be useful e.g. if you create init script for managing Mopidy. From 20cd11eb30d92635199fffe944277dbf954b3a23 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Oct 2011 19:12:03 +0200 Subject: [PATCH 262/350] Update to async version of Google Analytics tracking --- docs/_templates/layout.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index d6cb00e9..14113da6 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -3,13 +3,13 @@ {% block footer %} {{ super() }} - {% endblock %} From e5bbda739ce47304c415e74c5e7a9a905d9fd980 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Oct 2011 19:12:03 +0200 Subject: [PATCH 263/350] Update to async version of Google Analytics tracking --- docs/_templates/layout.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index d6cb00e9..14113da6 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -3,13 +3,13 @@ {% block footer %} {{ super() }} - {% endblock %} From ceb5753c81ce4966146f5d5d54cf113987afb916 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 19 Oct 2011 20:10:24 +0200 Subject: [PATCH 264/350] Move Google Analytics code to --- docs/_templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 14113da6..485debc5 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -1,6 +1,6 @@ {% extends "!layout.html" %} -{% block footer %} +{% block extrahead %} {{ super() }} -{% endblock %} diff --git a/docs/_themes/nature/static/nature.css_t b/docs/_themes/nature/static/nature.css_t deleted file mode 100644 index b6c0f22e..00000000 --- a/docs/_themes/nature/static/nature.css_t +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Sphinx stylesheet -- default theme - * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: Arial, sans-serif; - font-size: 100%; - background-color: #111111; - color: #555555; - margin: 0; - padding: 0; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 300px; -} - -hr{ - border: 1px solid #B1B4B6; -} - -div.document { - background-color: #eeeeee; -} - -div.body { - background-color: #ffffff; - color: #3E4349; - padding: 1em 30px 30px 30px; - font-size: 0.9em; -} - -div.footer { - color: #555; - width: 100%; - padding: 13px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #444444; -} - -div.related { - background-color: #6BA81E; - line-height: 36px; - color: #ffffff; - text-shadow: 0px 1px 0 #444444; - font-size: 1.1em; -} - -div.related a { - color: #E2F3CC; -} - -div.related .right { - font-size: 0.9em; -} - -div.sphinxsidebar { - font-size: 0.9em; - line-height: 1.5em; - width: 300px -} - -div.sphinxsidebarwrapper{ - padding: 20px 0; -} - -div.sphinxsidebar h3, -div.sphinxsidebar h4 { - font-family: Arial, sans-serif; - color: #222222; - font-size: 1.2em; - font-weight: bold; - margin: 0; - padding: 5px 10px; - text-shadow: 1px 1px 0 white -} - -div.sphinxsidebar h3 a { - color: #444444; -} - -div.sphinxsidebar p { - color: #888888; - padding: 5px 20px; - margin: 0.5em 0px; -} - -div.sphinxsidebar p.topless { -} - -div.sphinxsidebar ul { - margin: 10px 10px 10px 20px; - padding: 0; - color: #000000; -} - -div.sphinxsidebar a { - color: #444444; -} - -div.sphinxsidebar a:hover { - color: #E32E00; -} - -div.sphinxsidebar input { - border: 1px solid #cccccc; - font-family: sans-serif; - font-size: 1.1em; - padding: 0.15em 0.3em; -} - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} - -/* -- body styles ----------------------------------------------------------- */ - -a { - color: #005B81; - text-decoration: none; -} - -a:hover { - color: #E32E00; -} - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: Arial, sans-serif; - font-weight: normal; - color: #212224; - margin: 30px 0px 10px 0px; - padding: 5px 0 5px 0px; - text-shadow: 0px 1px 0 white; - border-bottom: 1px solid #C8D5E3; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; } -div.body h3 { font-size: 120%; } -div.body h4 { font-size: 110%; } -div.body h5 { font-size: 100%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - line-height: 1.8em; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.highlight{ - background-color: white; -} - -div.note { - background-color: #eeeeee; - border: 1px solid #cccccc; -} - -div.seealso { - background-color: #ffffcc; - border: 1px solid #ffff66; -} - -div.topic { - background-color: #fafafa; - border-width: 0; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #ff6666; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 10px; - background-color: #eeeeee; - color: #222222; - line-height: 1.5em; - font-size: 1.1em; - margin: 1.5em 0 1.5em 0; - -webkit-box-shadow: 0px 0px 4px #d8d8d8; - -moz-box-shadow: 0px 0px 4px #d8d8d8; - box-shadow: 0px 0px 4px #d8d8d8; -} - -tt { - color: #222222; - padding: 1px 2px; - font-size: 1.2em; - font-family: monospace; -} - -#table-of-contents ul { - padding-left: 2em; -} diff --git a/docs/_themes/nature/static/pygments.css b/docs/_themes/nature/static/pygments.css deleted file mode 100644 index 652b7612..00000000 --- a/docs/_themes/nature/static/pygments.css +++ /dev/null @@ -1,54 +0,0 @@ -.c { color: #999988; font-style: italic } /* Comment */ -.k { font-weight: bold } /* Keyword */ -.o { font-weight: bold } /* Operator */ -.cm { color: #999988; font-style: italic } /* Comment.Multiline */ -.cp { color: #999999; font-weight: bold } /* Comment.preproc */ -.c1 { color: #999988; font-style: italic } /* Comment.Single */ -.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ -.ge { font-style: italic } /* Generic.Emph */ -.gr { color: #aa0000 } /* Generic.Error */ -.gh { color: #999999 } /* Generic.Heading */ -.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ -.go { color: #111 } /* Generic.Output */ -.gp { color: #555555 } /* Generic.Prompt */ -.gs { font-weight: bold } /* Generic.Strong */ -.gu { color: #aaaaaa } /* Generic.Subheading */ -.gt { color: #aa0000 } /* Generic.Traceback */ -.kc { font-weight: bold } /* Keyword.Constant */ -.kd { font-weight: bold } /* Keyword.Declaration */ -.kp { font-weight: bold } /* Keyword.Pseudo */ -.kr { font-weight: bold } /* Keyword.Reserved */ -.kt { color: #445588; font-weight: bold } /* Keyword.Type */ -.m { color: #009999 } /* Literal.Number */ -.s { color: #bb8844 } /* Literal.String */ -.na { color: #008080 } /* Name.Attribute */ -.nb { color: #999999 } /* Name.Builtin */ -.nc { color: #445588; font-weight: bold } /* Name.Class */ -.no { color: #ff99ff } /* Name.Constant */ -.ni { color: #800080 } /* Name.Entity */ -.ne { color: #990000; font-weight: bold } /* Name.Exception */ -.nf { color: #990000; font-weight: bold } /* Name.Function */ -.nn { color: #555555 } /* Name.Namespace */ -.nt { color: #000080 } /* Name.Tag */ -.nv { color: purple } /* Name.Variable */ -.ow { font-weight: bold } /* Operator.Word */ -.mf { color: #009999 } /* Literal.Number.Float */ -.mh { color: #009999 } /* Literal.Number.Hex */ -.mi { color: #009999 } /* Literal.Number.Integer */ -.mo { color: #009999 } /* Literal.Number.Oct */ -.sb { color: #bb8844 } /* Literal.String.Backtick */ -.sc { color: #bb8844 } /* Literal.String.Char */ -.sd { color: #bb8844 } /* Literal.String.Doc */ -.s2 { color: #bb8844 } /* Literal.String.Double */ -.se { color: #bb8844 } /* Literal.String.Escape */ -.sh { color: #bb8844 } /* Literal.String.Heredoc */ -.si { color: #bb8844 } /* Literal.String.Interpol */ -.sx { color: #bb8844 } /* Literal.String.Other */ -.sr { color: #808000 } /* Literal.String.Regex */ -.s1 { color: #bb8844 } /* Literal.String.Single */ -.ss { color: #bb8844 } /* Literal.String.Symbol */ -.bp { color: #999999 } /* Name.Builtin.Pseudo */ -.vc { color: #ff99ff } /* Name.Variable.Class */ -.vg { color: #ff99ff } /* Name.Variable.Global */ -.vi { color: #ff99ff } /* Name.Variable.Instance */ -.il { color: #009999 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_themes/nature/theme.conf b/docs/_themes/nature/theme.conf deleted file mode 100644 index 1cc40044..00000000 --- a/docs/_themes/nature/theme.conf +++ /dev/null @@ -1,4 +0,0 @@ -[theme] -inherit = basic -stylesheet = nature.css -pygments_style = tango diff --git a/docs/conf.py b/docs/conf.py index d14075d0..b4dbc35b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -135,10 +135,7 @@ modindex_common_prefix = ['mopidy.'] # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -if on_rtd: - html_theme = 'default' -else: - html_theme = 'nature' +html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -157,7 +154,8 @@ html_theme_path = ['_themes'] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/mopidy.png' +if on_rtd: + html_logo = '_static/mopidy.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 From 9bcd0fe3a718d28a4d57a02c0e7490f5a19d6840 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 6 Feb 2012 13:22:42 +0100 Subject: [PATCH 305/350] Replace aptitude with apt-get --- docs/development/contributing.rst | 4 ++-- docs/installation/index.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/development/contributing.rst b/docs/development/contributing.rst index 0303bdc7..782d2f20 100644 --- a/docs/development/contributing.rst +++ b/docs/development/contributing.rst @@ -74,7 +74,7 @@ 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-mock python-nose + sudo apt-get install python-coverage python-mock python-nose Or, they can be installed using ``pip``:: @@ -126,7 +126,7 @@ from the documentation files, you need some additional dependencies. You can install them through Debian/Ubuntu package management:: - sudo aptitude install python-sphinx python-pygraphviz graphviz + sudo apt-get install python-sphinx python-pygraphviz graphviz Then, to generate docs:: diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 8dd47ada..b9d76e60 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -100,7 +100,7 @@ install Mopidy from PyPI using Pip. #. Then, you need to install Pip:: - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian sudo easy_install pip # On OS X #. To install the currently latest stable release of Mopidy:: @@ -134,7 +134,7 @@ Mopidy's ``develop`` branch. #. Then, you need to install Pip:: - sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian + sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian sudo easy_install pip # On OS X #. To install the latest snapshot of Mopidy, run:: @@ -157,7 +157,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git. #. Then install Git, if haven't already:: - sudo aptitude install git-core # On Ubuntu/Debian + sudo apt-get install git-core # On Ubuntu/Debian sudo brew install git # On OS X using Homebrew #. Clone the official Mopidy repository, or your own fork of it:: From 95ab34dd7095196724a3bd952510e6451ef20684 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:11:55 +0100 Subject: [PATCH 306/350] Simplify requirements listing --- docs/installation/index.rst | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index b9d76e60..59e51500 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -18,36 +18,36 @@ Requirements gstreamer libspotify -If you install Mopidy from the APT archive, as described below, you can skip -the dependency installation part. +If you install Mopidy from the APT archive, as described below, APT will take +care of all the dependencies for you. Otherwise, make sure you got the required +dependencies installed. -Otherwise, make sure you got the required dependencies installed. +- Hard dependencies: -- Python >= 2.6, < 3 + - Python >= 2.6, < 3 -- `Pykka `_ >= 0.12.3 + - Pykka >= 0.12.3:: -- GStreamer >= 0.10, with Python bindings. See :doc:`gstreamer`. + sudo pip install -U pykka -- Mixer dependencies: The default mixer does not require any additional - dependencies. If you use another mixer, see the mixer's docs for any - additional requirements. - -- Dependencies for at least one Mopidy backend: - - - The default backend, :mod:`mopidy.backends.spotify`, requires libspotify - and pyspotify. See :doc:`libspotify`. - - - The local backend, :mod:`mopidy.backends.local`, requires no additional - dependencies. + - GStreamer 0.10.x, with Python bindings. See :doc:`gstreamer`. - Optional dependencies: - - To use the Last.FM scrobbler, see :mod:`mopidy.frontends.lastfm` for - additional requirements. + - For Spotify support, you need libspotify and pyspotify. See + :doc:`libspotify`. - - To use the MPRIS frontend, e.g. using the Ubuntu Sound Menu, see - :mod:`mopidy.frontends.mpris` for additional requirements. + - To scrobble your played tracks to Last.FM, you need pylast:: + + sudo pip install -U pylast + + - To use MPRIS, e.g. for controlling Mopidy from the Ubuntu Sound Menu, you + need some additional requirements:: + + sudo apt-get install python-dbus python-indicate + + - Some custom mixers (but not the default one) require additional + dependencies. See the docs for each mixer. Install latest stable release From 0d16bb0048a973d376a7b7b87628a3360ee6fb13 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:27:14 +0100 Subject: [PATCH 307/350] Simplify libspotify/pyspotify installation instructions --- docs/installation/libspotify.rst | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 2728be94..0b0535d7 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -12,12 +12,6 @@ install libspotify and `pyspotify `_. This backend requires a paid `Spotify premium account `_. -.. note:: - - This product uses SPOTIFY CORE but is not endorsed, certified or otherwise - approved in any way by Spotify. Spotify is the registered trade mark of the - Spotify Group. - Installing libspotify ===================== @@ -26,23 +20,20 @@ Installing libspotify On Linux from APT archive ------------------------- -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 libspotify8 - -When libspotify has been installed, continue with -:ref:`pyspotify_installation`. +If you install from APT, jump directly to :ref:`pyspotify_installation` below. On Linux from source -------------------- -Download and install libspotify 0.0.8 for your OS and CPU architecture from -https://developer.spotify.com/en/libspotify/. +First, check pyspotify's changelog to see what's the latest version of +libspotify which is supported. The versions of libspotify and pyspotify are +tightly coupled. -For 64-bit Linux the process is as follows:: +Download and install the appropriate version of libspotify for your OS and CPU +architecture from https://developer.spotify.com/en/libspotify/. + +For libspotify 0.0.8 for 64-bit Linux the process is as follows:: wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz @@ -50,6 +41,9 @@ For 64-bit Linux the process is as follows:: sudo make install prefix=/usr/local sudo ldconfig +Remember to adjust for the latest libspotify version supported by pyspotify, +your OS and your CPU architecture. + When libspotify has been installed, continue with :ref:`pyspotify_installation`. @@ -84,29 +78,35 @@ by installing pyspotify. On Linux from APT archive ------------------------- -Assuming that you've already set up http://apt.mopidy.com/ as a software -source, run:: +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 python-spotify -If you haven't already installed libspotify, this command will install both -libspotify and pyspotify for you. +This command will install both libspotify and pyspotify for you. -On Linux/OS X from source +On Linux from source ------------------------- +If you have have already installed libspotify, you can continue with installing +the libspotify Python bindings, called pyspotify. + On Linux, you need to get the Python development files installed. On Debian/Ubuntu systems run:: sudo apt-get install python-dev -On OS X no additional dependencies are needed. - Then get, build, and install the latest releast of pyspotify using ``pip``:: sudo pip install -U pyspotify -Or using the older ``easy_install``:: - sudo easy_install pyspotify +On OS X from source +------------------- + +If you have already installed libspotify, you can get, build, and install the +latest releast of pyspotify using ``pip``:: + + sudo pip install -U pyspotify From a679d0c2eee75bffccb4d334044f9c0d5f3d8a6c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:46:38 +0100 Subject: [PATCH 308/350] Add detailed GStreamer installation instructions for OS X --- docs/installation/gstreamer.rst | 70 ++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 08e16378..7daac9cf 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -2,7 +2,8 @@ GStreamer installation ********************** -To use the Mopidy, you first need to install GStreamer and its Python bindings. +To use the Mopidy, you first need to install GStreamer and the GStreamer Python +bindings. Installing GStreamer @@ -15,6 +16,10 @@ GStreamer is packaged for most popular Linux distributions. Search for GStreamer in your package manager, and make sure to install the Python bindings, and the "good" and "ugly" plugin sets. + +On Debian/Ubuntu +^^^^^^^^^^^^^^^^ + If you use Debian/Ubuntu you can install GStreamer like this:: sudo apt-get install python-gst0.10 gstreamer0.10-plugins-good \ @@ -24,30 +29,67 @@ If you install Mopidy from our APT archive, you don't need to install GStreamer yourself. The Mopidy Debian package will handle it for you. +On Arch Linux +^^^^^^^^^^^^^ + +If you use Arch Linux, install the following packages from the official +repository:: + + sudo pacman -S gstreamer0.10-python gstreamer0.10-good-plugins \ + gstreamer0.10-ugly-plugins + + On OS X from Homebrew --------------------- .. note:: - We have created GStreamer formulas for Homebrew to make the GStreamer - installation easy for you, but not all our formulas have been merged into - Homebrew's master branch yet. You should either fetch the formula files - from `Homebrew's issue #1612 - `_ yourself, or fall - back to using MacPorts. + We have been working with `Homebrew `_ to + make all the GStreamer packages easily installable on OS X using Homebrew. + We've gotten most of our packages included, but the Homebrew guys aren't + very happy to include Python specific packages into Homebrew, even though + they are not installable by pip. If you're interested, see the discussion + in `Homebrew's issue #1612 + `_ for details. -To install GStreamer on OS X using Homebrew:: +The following is currently the shortest path to installing GStreamer with +Python bindings on OS X using Homebrew. - brew install gst-python gst-plugins-good gst-plugins-ugly +#. Install `Homebrew `_. +#. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and + `gst-python`:: -On OS X from MacPorts ---------------------- + wget -O/usr/local/Library/Formula/pycairo.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb + wget -O/usr/local/Library/Formula/pygobject.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb + wget -O/usr/local/Library/Formula/pygtk.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb + wget -O/usr/local/Library/Formula/gst-python.rb \ + https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb -To install GStreamer on OS X using MacPorts:: +#. Install the required packages:: - sudo port install py26-gst-python gstreamer-plugins-good \ - gstreamer-plugins-ugly + brew install gst-python gst-plugins-good gst-plugins-ugly + +#. Make sure to include Homebrew's Python ``site-packages`` directory in your + ``PYTHONPATH``. If you don't include this, Mopidy will not find GStreamer + and crash. + + You can either amend your ``PYTHONPATH`` permanently, by adding the + following statement to your shell's init file, e.g. ``~/.bashrc``:: + + export PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages:$PYTHONPATH + + Or, you can prefix the Mopidy command every time you run it:: + + PYTHONPATH=$(brew --prefix)/lib/python2.6/site-packages mopidy + + Note that you need to replace ``python2.6`` with ``python2.7`` if that's + the Python version you are using. To find your Python version, run:: + + python --version Testing the installation From 3f86d3fd321d778f2e8f54eab2ad8f5a20d6d9b4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:51:34 +0100 Subject: [PATCH 309/350] A bit less easy_install in the world --- docs/installation/index.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 59e51500..8e62421c 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -101,7 +101,7 @@ install Mopidy from PyPI using Pip. #. Then, you need to install Pip:: sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian - sudo easy_install pip # On OS X + sudo easy_install pip # On OS X #. To install the currently latest stable release of Mopidy:: @@ -112,8 +112,6 @@ install Mopidy from PyPI using Pip. #. Next, you need to set a couple of :doc:`settings `, and then you're ready to :doc:`run Mopidy `. -If you for some reason can't use Pip, try ``easy_install`` instead. - Install development version =========================== @@ -135,7 +133,7 @@ Mopidy's ``develop`` branch. #. Then, you need to install Pip:: sudo apt-get install python-setuptools python-pip # On Ubuntu/Debian - sudo easy_install pip # On OS X + sudo easy_install pip # On OS X #. To install the latest snapshot of Mopidy, run:: From cfea4be681e010884510effad3266eeff2c8e5dd Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:55:19 +0100 Subject: [PATCH 310/350] "brew upgrade" has replaced " brew install `brew outdated`" --- docs/installation/libspotify.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 0b0535d7..5543f38e 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -60,7 +60,7 @@ libspotify:: To update your existing libspotify installation using Homebrew:: brew update - brew install `brew outdated` + brew upgrade When libspotify has been installed, continue with :ref:`pyspotify_installation`. From 5a15964910994720e63ab38fb7e46bcd69dd994f Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 00:56:58 +0100 Subject: [PATCH 311/350] Add missing word --- docs/installation/libspotify.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/libspotify.rst b/docs/installation/libspotify.rst index 5543f38e..223e4ed7 100644 --- a/docs/installation/libspotify.rst +++ b/docs/installation/libspotify.rst @@ -79,8 +79,8 @@ On Linux from APT archive ------------------------- 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:: +http://apt.mopidy.com/ for how to use the Mopidy APT archive as a software +source on your system. Then, simply run:: sudo apt-get install python-spotify From 05b0d20fa5d14dca65dfd9e3c201f5f84c42016b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:01:52 +0100 Subject: [PATCH 312/350] Remove use of /usr/local, as Homebrew can be installed in other locations --- docs/installation/gstreamer.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 7daac9cf..72b2feb8 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -60,13 +60,13 @@ Python bindings on OS X using Homebrew. #. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and `gst-python`:: - wget -O/usr/local/Library/Formula/pycairo.rb \ + wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb - wget -O/usr/local/Library/Formula/pygobject.rb \ + wget -O $(brew --prefix)/Library/Formula/pygobject.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb - wget -O/usr/local/Library/Formula/pygtk.rb \ + wget -O $(brew --prefix)/Library/Formula/pygtk.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb - wget -O/usr/local/Library/Formula/gst-python.rb \ + wget -O $(brew --prefix)/Library/Formula/gst-python.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb #. Install the required packages:: From 48db56cc887e7242948a0ea785a0bd6a21e4104d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:10:46 +0100 Subject: [PATCH 313/350] GitHub is on HTTPS now --- docs/installation/gstreamer.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 72b2feb8..82fbab57 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -50,7 +50,7 @@ On OS X from Homebrew very happy to include Python specific packages into Homebrew, even though they are not installable by pip. If you're interested, see the discussion in `Homebrew's issue #1612 - `_ for details. + `_ for details. The following is currently the shortest path to installing GStreamer with Python bindings on OS X using Homebrew. From 5e6fd8d7bca987c8b4c335590d4d6ffb0ffd282d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:13:17 +0100 Subject: [PATCH 314/350] Formatting fix --- docs/installation/gstreamer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 82fbab57..37cc9a6f 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -57,8 +57,8 @@ Python bindings on OS X using Homebrew. #. Install `Homebrew `_. -#. Download our Homebrew formulas for `pycairo`, `pygobject`, `pygtk`, and - `gst-python`:: +#. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, + and ``gst-python``:: wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb From bb2c05b215c0b3ae6470cda247e35fea48e3413c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:14:11 +0100 Subject: [PATCH 315/350] Pull a couple of section up a level --- docs/installation/gstreamer.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index 37cc9a6f..d70dd156 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -6,19 +6,16 @@ To use the Mopidy, you first need to install GStreamer and the GStreamer Python bindings. -Installing GStreamer -==================== - -On Linux --------- +Installing GStreamer on Linux +============================= GStreamer is packaged for most popular Linux distributions. Search for GStreamer in your package manager, and make sure to install the Python bindings, and the "good" and "ugly" plugin sets. -On Debian/Ubuntu -^^^^^^^^^^^^^^^^ +Debian/Ubuntu +------------- If you use Debian/Ubuntu you can install GStreamer like this:: @@ -29,8 +26,8 @@ If you install Mopidy from our APT archive, you don't need to install GStreamer yourself. The Mopidy Debian package will handle it for you. -On Arch Linux -^^^^^^^^^^^^^ +Arch Linux +---------- If you use Arch Linux, install the following packages from the official repository:: @@ -39,8 +36,8 @@ repository:: gstreamer0.10-ugly-plugins -On OS X from Homebrew ---------------------- +Installing GStreamer on OS X from Homebrew +------------------------------------------ .. note:: From f84cd6833a101b00b065fb250c0ea650392989da Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 01:23:48 +0100 Subject: [PATCH 316/350] Fix header level --- docs/installation/gstreamer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index d70dd156..fb8df33d 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -36,8 +36,8 @@ repository:: gstreamer0.10-ugly-plugins -Installing GStreamer on OS X from Homebrew ------------------------------------------- +Installing GStreamer on OS X +============================ .. note:: From ca0d2935f8f67ebce30a3ca2a75fd109c764802b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 7 Feb 2012 21:40:26 +0100 Subject: [PATCH 317/350] Fix URLs and use curl instead of wget, as wget isn't installed by default on OS X --- docs/installation/gstreamer.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/installation/gstreamer.rst b/docs/installation/gstreamer.rst index fb8df33d..d0dc0461 100644 --- a/docs/installation/gstreamer.rst +++ b/docs/installation/gstreamer.rst @@ -57,14 +57,14 @@ Python bindings on OS X using Homebrew. #. Download our Homebrew formulas for ``pycairo``, ``pygobject``, ``pygtk``, and ``gst-python``:: - wget -O $(brew --prefix)/Library/Formula/pycairo.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pycairo.rb - wget -O $(brew --prefix)/Library/Formula/pygobject.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygobject.rb - wget -O $(brew --prefix)/Library/Formula/pygtk.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/pygtk.rb - wget -O $(brew --prefix)/Library/Formula/gst-python.rb \ - https://github.com/jodal/homebrew/raw/gst-python/Library/Formula/gst-python.rb + curl -o $(brew --prefix)/Library/Formula/pycairo.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pycairo.rb + curl -o $(brew --prefix)/Library/Formula/pygobject.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygobject.rb + curl -o $(brew --prefix)/Library/Formula/pygtk.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/pygtk.rb + curl -o $(brew --prefix)/Library/Formula/gst-python.rb \ + https://raw.github.com/jodal/homebrew/gst-python/Library/Formula/gst-python.rb #. Install the required packages:: From f1ba8af0dec1089e5199b0d87f7ca474fcb9c801 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 8 Feb 2012 13:24:28 +0100 Subject: [PATCH 318/350] Ignore MPRIS tests on OS X --- tests/frontends/mpris/events_test.py | 10 +++++++++- tests/frontends/mpris/player_interface_test.py | 10 +++++++++- tests/frontends/mpris/root_interface_test.py | 11 +++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/frontends/mpris/events_test.py b/tests/frontends/mpris/events_test.py index 90cdab6a..49e56226 100644 --- a/tests/frontends/mpris/events_test.py +++ b/tests/frontends/mpris/events_test.py @@ -1,11 +1,19 @@ +import sys + import mock -from mopidy.frontends.mpris import MprisFrontend, objects +from mopidy import OptionalDependencyError from mopidy.models import Track +try: + from mopidy.frontends.mpris import MprisFrontend, objects +except OptionalDependencyError: + pass + from tests import unittest +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class BackendEventsTest(unittest.TestCase): def setUp(self): self.mpris_frontend = MprisFrontend() # As a plain class, not an actor diff --git a/tests/frontends/mpris/player_interface_test.py b/tests/frontends/mpris/player_interface_test.py index a966403e..24c426fb 100644 --- a/tests/frontends/mpris/player_interface_test.py +++ b/tests/frontends/mpris/player_interface_test.py @@ -1,11 +1,18 @@ +import sys + import mock +from mopidy import OptionalDependencyError from mopidy.backends.dummy import DummyBackend from mopidy.backends.base.playback import PlaybackController -from mopidy.frontends.mpris import objects from mopidy.mixers.dummy import DummyMixer from mopidy.models import Album, Artist, Track +try: + from mopidy.frontends.mpris import objects +except OptionalDependencyError: + pass + from tests import unittest PLAYING = PlaybackController.PLAYING @@ -13,6 +20,7 @@ PAUSED = PlaybackController.PAUSED STOPPED = PlaybackController.STOPPED +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() diff --git a/tests/frontends/mpris/root_interface_test.py b/tests/frontends/mpris/root_interface_test.py index 443efdd3..1e54fc15 100644 --- a/tests/frontends/mpris/root_interface_test.py +++ b/tests/frontends/mpris/root_interface_test.py @@ -1,12 +1,19 @@ +import sys + import mock -from mopidy import settings +from mopidy import OptionalDependencyError, settings from mopidy.backends.dummy import DummyBackend -from mopidy.frontends.mpris import objects + +try: + from mopidy.frontends.mpris import objects +except OptionalDependencyError: + pass from tests import unittest +@unittest.skipUnless(sys.platform.startswith('linux'), 'requires Linux') class RootInterfaceTest(unittest.TestCase): def setUp(self): objects.exit_process = mock.Mock() From 8c2a333938293f6e6286412648c22bb8af954218 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 9 Feb 2012 23:12:13 +0100 Subject: [PATCH 319/350] get_or_create_folder should also create intermediate folders --- docs/changes.rst | 4 ++++ mopidy/utils/path.py | 7 +++++-- tests/utils/path_test.py | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 8235e95d..528edaa8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -22,6 +22,10 @@ v0.7.0 (in development) need for copying the entire current playlist from one thread to another. Thanks to John Bäckstrand for pinpointing the issue. +- Fix crash on creation of config and cache directories if intermediate + directories does not exist. This was especially the case on OS X, where + ``~/.config`` doesn't exist for most users. + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/utils/path.py b/mopidy/utils/path.py index 8bd39f06..5d99ac12 100644 --- a/mopidy/utils/path.py +++ b/mopidy/utils/path.py @@ -8,9 +8,12 @@ logger = logging.getLogger('mopidy.utils.path') def get_or_create_folder(folder): folder = os.path.expanduser(folder) - if not os.path.isdir(folder): + if os.path.isfile(folder): + raise OSError('A file with the same name as the desired ' \ + 'dir, "%s", already exists.' % folder) + elif not os.path.isdir(folder): logger.info(u'Creating dir %s', folder) - os.mkdir(folder, 0755) + os.makedirs(folder, 0755) return folder def get_or_create_file(filename): diff --git a/tests/utils/path_test.py b/tests/utils/path_test.py index ba1fcf97..19bae375 100644 --- a/tests/utils/path_test.py +++ b/tests/utils/path_test.py @@ -28,12 +28,32 @@ class GetOrCreateFolderTest(unittest.TestCase): self.assert_(os.path.isdir(folder)) self.assertEqual(created, folder) + def test_creating_nested_folders(self): + level2_folder = os.path.join(self.parent, 'test') + level3_folder = os.path.join(self.parent, 'test', 'test') + self.assert_(not os.path.exists(level2_folder)) + self.assert_(not os.path.isdir(level2_folder)) + self.assert_(not os.path.exists(level3_folder)) + self.assert_(not os.path.isdir(level3_folder)) + created = get_or_create_folder(level3_folder) + self.assert_(os.path.exists(level2_folder)) + self.assert_(os.path.isdir(level2_folder)) + self.assert_(os.path.exists(level3_folder)) + self.assert_(os.path.isdir(level3_folder)) + self.assertEqual(created, level3_folder) + def test_creating_existing_folder(self): created = get_or_create_folder(self.parent) self.assert_(os.path.exists(self.parent)) self.assert_(os.path.isdir(self.parent)) self.assertEqual(created, self.parent) + def test_create_folder_with_name_of_existing_file_throws_oserror(self): + conflicting_file = os.path.join(self.parent, 'test') + open(conflicting_file, 'w').close() + folder = os.path.join(self.parent, 'test') + self.assertRaises(OSError, get_or_create_folder, folder) + class PathToFileURITest(unittest.TestCase): def test_simple_path(self): From 643f71363fdc9075568461531d4bb738c33d10e9 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:02:33 +0100 Subject: [PATCH 320/350] Make pylint ignore TODO/XXX/FIXME in the code, so we can see more important errors through the noise --- pylintrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylintrc b/pylintrc index d2f84b77..98e10416 100644 --- a/pylintrc +++ b/pylintrc @@ -18,6 +18,7 @@ # R0921 - Abstract class not referenced # W0141 - Used builtin function '%s' # W0142 - Used * or ** magic +# W0511 - TODO, FIXME and XXX in the code # W0613 - Unused argument %r # -disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0613 +disable = C0103,C0111,E0102,E0202,E1101,R0201,R0801,R0903,R0904,R0921,W0141,W0142,W0511,W0613 From 413603f9817846c74aca844a0c61e15d29fbd596 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:02:54 +0100 Subject: [PATCH 321/350] Remove old redundant comment --- tests/gstreamer_test.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/gstreamer_test.py b/tests/gstreamer_test.py index 66e0995e..012c9002 100644 --- a/tests/gstreamer_test.py +++ b/tests/gstreamer_test.py @@ -6,8 +6,6 @@ from mopidy.utils.path import path_to_uri from tests import unittest, path_to_data_dir -# TODO BaseOutputTest? - @unittest.skipIf(sys.platform == 'win32', 'Our Windows build server does not support GStreamer yet') From 24d9f8f200a48d93c0e64995cc0eb8d37932d1a5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:17:21 +0100 Subject: [PATCH 322/350] Fix a bunch of pylint warnings --- mopidy/backends/base/playback.py | 2 -- mopidy/backends/spotify/__init__.py | 1 - mopidy/backends/spotify/library.py | 1 - mopidy/backends/spotify/session_manager.py | 1 - mopidy/backends/spotify/translator.py | 1 - mopidy/core.py | 6 ++---- mopidy/frontends/mpd/dispatcher.py | 3 ++- mopidy/frontends/mpris/objects.py | 3 ++- mopidy/gstreamer.py | 3 ++- mopidy/utils/settings.py | 1 - 10 files changed, 8 insertions(+), 14 deletions(-) diff --git a/mopidy/backends/base/playback.py b/mopidy/backends/base/playback.py index 51fe0d3b..16ac75d1 100644 --- a/mopidy/backends/base/playback.py +++ b/mopidy/backends/base/playback.py @@ -2,8 +2,6 @@ import logging import random import time -from pykka.registry import ActorRegistry - from mopidy.listeners import BackendListener logger = logging.getLogger('mopidy.backends.base') diff --git a/mopidy/backends/spotify/__init__.py b/mopidy/backends/spotify/__init__.py index ad45014e..56775926 100644 --- a/mopidy/backends/spotify/__init__.py +++ b/mopidy/backends/spotify/__init__.py @@ -10,7 +10,6 @@ from mopidy.gstreamer import GStreamer logger = logging.getLogger('mopidy.backends.spotify') -ENCODING = 'utf-8' BITRATES = {96: 2, 160: 0, 320: 1} class SpotifyBackend(ThreadingActor, Backend): diff --git a/mopidy/backends/spotify/library.py b/mopidy/backends/spotify/library.py index 59aa9a2c..a080c7bd 100644 --- a/mopidy/backends/spotify/library.py +++ b/mopidy/backends/spotify/library.py @@ -4,7 +4,6 @@ import Queue from spotify import Link, SpotifyError from mopidy.backends.base import BaseLibraryProvider -from mopidy.backends.spotify import ENCODING from mopidy.backends.spotify.translator import SpotifyTranslator from mopidy.models import Playlist diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 4b81db1f..af731e1b 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -1,4 +1,3 @@ -import glib import logging import os import threading diff --git a/mopidy/backends/spotify/translator.py b/mopidy/backends/spotify/translator.py index 27f4719b..2f47a42b 100644 --- a/mopidy/backends/spotify/translator.py +++ b/mopidy/backends/spotify/translator.py @@ -4,7 +4,6 @@ import logging from spotify import Link, SpotifyError from mopidy import settings -from mopidy.backends.spotify import ENCODING from mopidy.models import Artist, Album, Track, Playlist logger = logging.getLogger('mopidy.backends.spotify.translator') diff --git a/mopidy/core.py b/mopidy/core.py index 08c5e0d7..596e0fe5 100644 --- a/mopidy/core.py +++ b/mopidy/core.py @@ -12,14 +12,12 @@ gobject.threads_init() # so that GStreamer doesn't hijack e.g. ``--help``. # NOTE This naive fix does not support values like ``bar`` in # ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``. -def is_gst_arg(arg): - return arg.startswith('--gst') or arg == '--help-gst' +def is_gst_arg(argument): + return argument.startswith('--gst') or argument == '--help-gst' gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)] mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)] sys.argv[1:] = gstreamer_args -from pykka.registry import ActorRegistry - from mopidy import (get_version, settings, OptionalDependencyError, SettingsError, DATA_PATH, SETTINGS_PATH, SETTINGS_FILE) from mopidy.gstreamer import GStreamer diff --git a/mopidy/frontends/mpd/dispatcher.py b/mopidy/frontends/mpd/dispatcher.py index 5ee70a5b..2b012c7c 100644 --- a/mopidy/frontends/mpd/dispatcher.py +++ b/mopidy/frontends/mpd/dispatcher.py @@ -244,7 +244,8 @@ class MpdContext(object): """ if self._backend is None: backend_refs = ActorRegistry.get_by_class(Backend) - assert len(backend_refs) == 1, 'Expected exactly one running backend.' + assert len(backend_refs) == 1, \ + 'Expected exactly one running backend.' self._backend = backend_refs[0].proxy() return self._backend diff --git a/mopidy/frontends/mpris/objects.py b/mopidy/frontends/mpris/objects.py index e0a83e03..9ed1fe2c 100644 --- a/mopidy/frontends/mpris/objects.py +++ b/mopidy/frontends/mpris/objects.py @@ -81,7 +81,8 @@ class MprisObject(dbus.service.Object): def _connect_to_dbus(self): logger.debug(u'Connecting to D-Bus...') mainloop = dbus.mainloop.glib.DBusGMainLoop() - bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus(mainloop=mainloop)) + bus_name = dbus.service.BusName(BUS_NAME, + dbus.SessionBus(mainloop=mainloop)) logger.info(u'Connected to D-Bus') return bus_name diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index ffb8c4f1..b6a8ab2b 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -332,7 +332,8 @@ class GStreamer(ThreadingActor): self._tee.send_event(event) def _handle_event_probe(self, teesrc, event): - if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'): + if (event.type == gst.EVENT_CUSTOM_DOWNSTREAM + and event.has_name('mopidy-unlink-tee')): data = self._get_structure_data(event.get_structure()) output = teesrc.get_peer().get_parent() diff --git a/mopidy/utils/settings.py b/mopidy/utils/settings.py index fca4f337..ff449a61 100644 --- a/mopidy/utils/settings.py +++ b/mopidy/utils/settings.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from copy import copy import getpass -import glib import logging import os from pprint import pformat From 2eae7aaae139be889b60f2806a48b837f0be5529 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:30:18 +0100 Subject: [PATCH 323/350] Call ThreadingActor/etc constructors to please pylint --- mopidy/frontends/lastfm.py | 1 + mopidy/frontends/mpd/__init__.py | 1 + mopidy/frontends/mpris/__init__.py | 1 + mopidy/gstreamer.py | 1 + mopidy/mixers/alsa.py | 1 + mopidy/mixers/denon.py | 1 + mopidy/mixers/dummy.py | 1 + mopidy/mixers/gstreamer_software.py | 1 + mopidy/mixers/nad.py | 2 ++ mopidy/utils/network.py | 1 + 10 files changed, 11 insertions(+) diff --git a/mopidy/frontends/lastfm.py b/mopidy/frontends/lastfm.py index 125457cd..0e79024b 100644 --- a/mopidy/frontends/lastfm.py +++ b/mopidy/frontends/lastfm.py @@ -37,6 +37,7 @@ class LastfmFrontend(ThreadingActor, BackendListener): """ def __init__(self): + super(LastfmFrontend, self).__init__() self.lastfm = None self.last_start_time = None diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index b6adc09d..99134012 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -25,6 +25,7 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): """ def __init__(self): + super(MpdFrontend, self).__init__() hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME) port = settings.MPD_SERVER_PORT diff --git a/mopidy/frontends/mpris/__init__.py b/mopidy/frontends/mpris/__init__.py index 579038ca..0f5d35c5 100644 --- a/mopidy/frontends/mpris/__init__.py +++ b/mopidy/frontends/mpris/__init__.py @@ -57,6 +57,7 @@ class MprisFrontend(ThreadingActor, BackendListener): """ def __init__(self): + super(MprisFrontend, self).__init__() self.indicate_server = None self.mpris_object = None diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index b6a8ab2b..282acd4e 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -25,6 +25,7 @@ class GStreamer(ThreadingActor): """ def __init__(self): + super(GStreamer, self).__init__() self._default_caps = gst.Caps(""" audio/x-raw-int, endianness=(int)1234, diff --git a/mopidy/mixers/alsa.py b/mopidy/mixers/alsa.py index ae4bd031..acb12e66 100644 --- a/mopidy/mixers/alsa.py +++ b/mopidy/mixers/alsa.py @@ -23,6 +23,7 @@ class AlsaMixer(ThreadingActor, BaseMixer): """ def __init__(self): + super(AlsaMixer, self).__init__() self._mixer = None def on_start(self): diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index d0dc5f54..e869dae8 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -26,6 +26,7 @@ class DenonMixer(ThreadingActor, BaseMixer): """ def __init__(self, *args, **kwargs): + super(DenonMixer, self).__init__(*arg, **kwargs) self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 diff --git a/mopidy/mixers/dummy.py b/mopidy/mixers/dummy.py index 23f96c4c..7262e83c 100644 --- a/mopidy/mixers/dummy.py +++ b/mopidy/mixers/dummy.py @@ -6,6 +6,7 @@ class DummyMixer(ThreadingActor, BaseMixer): """Mixer which just stores and reports the chosen volume.""" def __init__(self): + super(DummyMixer, self).__init__() self._volume = None def get_volume(self): diff --git a/mopidy/mixers/gstreamer_software.py b/mopidy/mixers/gstreamer_software.py index 523c3387..a38692db 100644 --- a/mopidy/mixers/gstreamer_software.py +++ b/mopidy/mixers/gstreamer_software.py @@ -8,6 +8,7 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer): """Mixer which uses GStreamer to control volume in software.""" def __init__(self): + super(GStreamerSoftwareMixer, self).__init__() self.output = None def on_start(self): diff --git a/mopidy/mixers/nad.py b/mopidy/mixers/nad.py index 4dbf27be..78473308 100644 --- a/mopidy/mixers/nad.py +++ b/mopidy/mixers/nad.py @@ -37,6 +37,7 @@ class NadMixer(ThreadingActor, BaseMixer): """ def __init__(self): + super(NadMixer, self).__init__() self._volume_cache = None self._nad_talker = NadTalker.start().proxy() @@ -71,6 +72,7 @@ class NadTalker(ThreadingActor): _nad_volume = None def __init__(self): + super(NadTalker, self).__init__() self._device = None def on_start(self): diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 5079fe7c..0a0928ce 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -297,6 +297,7 @@ class LineProtocol(ThreadingActor): encoding = 'utf-8' def __init__(self, connection): + super(LineProtocol, self).__init__() self.connection = connection self.prevent_timeout = False self.recv_buffer = '' From 9e8e02295dd468b72295ad71258258b7544fd5c7 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:31:42 +0100 Subject: [PATCH 324/350] Fix typo --- mopidy/mixers/denon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index e869dae8..02b86c38 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -26,7 +26,7 @@ class DenonMixer(ThreadingActor, BaseMixer): """ def __init__(self, *args, **kwargs): - super(DenonMixer, self).__init__(*arg, **kwargs) + super(DenonMixer, self).__init__(*args, **kwargs) self._device = kwargs.get('device', None) self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 From fc5f6df740c4f4444a96d6b06a157a2d862c3984 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:34:33 +0100 Subject: [PATCH 325/350] Simplify DenonMixer constructor args --- mopidy/mixers/denon.py | 6 +++--- tests/mixers/denon_test.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mopidy/mixers/denon.py b/mopidy/mixers/denon.py index 02b86c38..b0abbdb9 100644 --- a/mopidy/mixers/denon.py +++ b/mopidy/mixers/denon.py @@ -25,9 +25,9 @@ class DenonMixer(ThreadingActor, BaseMixer): - :attr:`mopidy.settings.MIXER_EXT_PORT` -- Example: ``/dev/ttyUSB0`` """ - def __init__(self, *args, **kwargs): - super(DenonMixer, self).__init__(*args, **kwargs) - self._device = kwargs.get('device', None) + def __init__(self, device=None): + super(DenonMixer, self).__init__() + self._device = device self._levels = ['99'] + ["%(#)02d" % {'#': v} for v in range(0, 99)] self._volume = 0 diff --git a/tests/mixers/denon_test.py b/tests/mixers/denon_test.py index 7fec3c82..cdfe0772 100644 --- a/tests/mixers/denon_test.py +++ b/tests/mixers/denon_test.py @@ -34,7 +34,7 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase): def setUp(self): self.device = DenonMixerDeviceMock() - self.mixer = DenonMixer(None, device=self.device) + self.mixer = DenonMixer(device=self.device) def test_reopen_device(self): self.device._open = False From 844b219565ed709d8535af552c14c9c281014379 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Feb 2012 00:42:39 +0100 Subject: [PATCH 326/350] Potential fix for gst LinkError (#144) suggested by adamcik --- mopidy/gstreamer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 282acd4e..5d393b66 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -83,6 +83,8 @@ class GStreamer(ThreadingActor): def _on_new_pad(self, source, pad, target_pad): if not pad.is_linked(): + if target_pad.is_linked(): + target_pad.unlink(target_pad.get_peer()) pad.link(target_pad) def _on_message(self, bus, message): From eb8ecc33a2320a9f6aa489180c16defaa6b8b722 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 09:37:21 +0100 Subject: [PATCH 327/350] Switch arguments so that unlink() is passed the sinkpad --- mopidy/gstreamer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/gstreamer.py b/mopidy/gstreamer.py index 5d393b66..c33dbe03 100644 --- a/mopidy/gstreamer.py +++ b/mopidy/gstreamer.py @@ -84,7 +84,7 @@ class GStreamer(ThreadingActor): def _on_new_pad(self, source, pad, target_pad): if not pad.is_linked(): if target_pad.is_linked(): - target_pad.unlink(target_pad.get_peer()) + target_pad.get_peer().unlink(target_pad) pad.link(target_pad) def _on_message(self, bus, message): From 904adc938ee4930f2b410c4c5d52689a56d4f079 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 09:52:09 +0100 Subject: [PATCH 328/350] Update changelog --- docs/changes.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 528edaa8..b389b7be 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,6 +26,11 @@ v0.7.0 (in development) directories does not exist. This was especially the case on OS X, where ``~/.config`` doesn't exist for most users. +**Bug fixes** + +- Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, + e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) + v0.6.1 (2011-12-28) =================== @@ -220,7 +225,7 @@ This is a bug fix release fixing audio problems on older GStreamer and some minor bugs. -**Bugfixes** +**Bug fixes** - Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10. The GStreamer `appsrc` bin wasn't being linked due to lack of default caps. @@ -347,7 +352,7 @@ v0.3.1 (2011-01-22) A couple of fixes to the 0.3.0 release is needed to get a smooth installation. -**Bugfixes** +**Bug fixes** - The Spotify application key was missing from the Python package. @@ -516,7 +521,7 @@ v0.2.1 (2011-01-07) This is a maintenance release without any new features. -**Bugfixes** +**Bug fixes** - Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if either :mod:`pylast` was not installed or the Last.fm scrobbling was not @@ -846,7 +851,7 @@ As always, report problems at our IRC channel or our issue tracker. Thanks! - Merged the ``gstreamer`` branch from Thomas Adamcik: - - More than 200 new tests, and thus several bugfixes to existing code. + - More than 200 new tests, and thus several bug fixes to existing code. - Several new generic features, like shuffle, consume, and playlist repeat. (Fixes: :issue:`3`) - **[Work in Progress]** A new backend for playing music from a local music From 219e723974e98d9c6da4e1f94cb6ff4be84c5dab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:30:46 +0100 Subject: [PATCH 329/350] Fix crash on mismatching quotation (fixes #137) --- docs/changes.rst | 3 +++ mopidy/frontends/mpd/protocol/music_db.py | 10 ++++++++-- tests/frontends/mpd/protocol/regression_test.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index b389b7be..69de8558 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -31,6 +31,9 @@ v0.7.0 (in development) - Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) +- Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes: + :issue:`137`) + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/frontends/mpd/protocol/music_db.py b/mopidy/frontends/mpd/protocol/music_db.py index 299fce97..cde2754a 100644 --- a/mopidy/frontends/mpd/protocol/music_db.py +++ b/mopidy/frontends/mpd/protocol/music_db.py @@ -189,8 +189,14 @@ def _list_build_query(field, mpd_query): """Converts a ``list`` query to a Mopidy query.""" if mpd_query is None: return {} - # shlex does not seem to be friends with unicode objects - tokens = shlex.split(mpd_query.encode('utf-8')) + try: + # shlex does not seem to be friends with unicode objects + tokens = shlex.split(mpd_query.encode('utf-8')) + except ValueError as error: + if error.message == 'No closing quotation': + raise MpdArgError(u'Invalid unquoted character', command=u'list') + else: + raise error tokens = [t.decode('utf-8') for t in tokens] if len(tokens) == 1: if field == u'album': diff --git a/tests/frontends/mpd/protocol/regression_test.py b/tests/frontends/mpd/protocol/regression_test.py index d4e4b2aa..7f214efa 100644 --- a/tests/frontends/mpd/protocol/regression_test.py +++ b/tests/frontends/mpd/protocol/regression_test.py @@ -146,3 +146,19 @@ class IssueGH113RegressionTest(protocol.BaseTestCase): self.sendRequest( r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"') self.assertInResponse('OK') + + +class IssueGH137RegressionTest(protocol.BaseTestCase): + """ + The issue: https://github.com/mopidy/mopidy/issues/137 + + How to reproduce: + + - Send "list" query with mismatching quotes + """ + + def test(self): + self.sendRequest(u'list Date Artist "Anita Ward" ' + u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"') + + self.assertInResponse('ACK [2@0] {list} Invalid unquoted character') From 86a4d6c36edf2619e45009d995ecf82f7dae908a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:33:17 +0100 Subject: [PATCH 330/350] Avoid HTTP to HTTPS redirect --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index b4dbc35b..f8b4ffc3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -241,4 +241,4 @@ latex_documents = [ needs_sphinx = '1.0' -extlinks = {'issue': ('http://github.com/mopidy/mopidy/issues/%s', 'GH-')} +extlinks = {'issue': ('https://github.com/mopidy/mopidy/issues/%s', 'GH-')} From 029192876c2d0d2438b15928dc0685f61f490241 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 13 Feb 2012 10:34:17 +0100 Subject: [PATCH 331/350] Remove 'Bug fixes' header only used for bugfix releases --- docs/changes.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 69de8558..20ad3169 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -26,8 +26,6 @@ v0.7.0 (in development) directories does not exist. This was especially the case on OS X, where ``~/.config`` doesn't exist for most users. -**Bug fixes** - - Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) From 0269686453a5035518106a5e1aa5243684b3d20c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 24 Feb 2012 23:50:45 +0100 Subject: [PATCH 332/350] Fix volume response when capping volume --- docs/changes.rst | 6 ++++++ mopidy/mixers/base.py | 19 ++++++++++++++++--- tests/mixers/dummy_test.py | 7 ++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 20ad3169..d955978d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -32,6 +32,12 @@ v0.7.0 (in development) - Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes: :issue:`137`) +- Volume is now reported to be the same as the volume was set to, also when + internal rounding have been done due to + :attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This + should make it possible to manage capped volume from clients that only + increase volume with one step at a time, like ncmpcpp does. + v0.6.1 (2011-12-28) =================== diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 8798076a..48df5325 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -21,19 +21,32 @@ class BaseMixer(object): Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is equal to 0. Values above 100 is equal to 100. """ + if not hasattr(self, '_user_volume'): + self._user_volume = 0 volume = self.get_volume() if volume is None: return None - return int(volume / self.amplification_factor) + elif not self.amplification_factor < 1: + return volume + else: + user_volume = int(volume / self.amplification_factor) + if (user_volume - 1) <= self._user_volume <= (user_volume + 1): + return self._user_volume + else: + return user_volume @volume.setter def volume(self, volume): - volume = int(int(volume) * self.amplification_factor) + if not hasattr(self, '_user_volume'): + self._user_volume = 0 + volume = int(volume) if volume < 0: volume = 0 elif volume > 100: volume = 100 - self.set_volume(volume) + self._user_volume = volume + real_volume = int(volume * self.amplification_factor) + self.set_volume(real_volume) self._trigger_volume_changed() def get_volume(self): diff --git a/tests/mixers/dummy_test.py b/tests/mixers/dummy_test.py index 8ae8623c..f9418d7a 100644 --- a/tests/mixers/dummy_test.py +++ b/tests/mixers/dummy_test.py @@ -4,7 +4,7 @@ from tests import unittest from tests.mixers.base_test import BaseMixerTest -class DenonMixerTest(BaseMixerTest, unittest.TestCase): +class DummyMixerTest(BaseMixerTest, unittest.TestCase): mixer_class = DummyMixer def test_set_volume_is_capped(self): @@ -16,3 +16,8 @@ class DenonMixerTest(BaseMixerTest, unittest.TestCase): self.mixer.amplification_factor = 0.5 self.mixer._volume = 50 self.assertEquals(self.mixer.volume, 100) + + def test_get_volume_get_the_same_number_as_was_set(self): + self.mixer.amplification_factor = 0.5 + self.mixer.volume = 13 + self.assertEquals(self.mixer.volume, 13) From d0b8d1942f702c544d2de0248314eb54db7abdee Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:24:41 +0100 Subject: [PATCH 333/350] Fix error in logger name --- mopidy/mixers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 48df5325..4690be61 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -2,7 +2,7 @@ import logging from mopidy import listeners, settings -logger = logging.getLogger('mopdy.mixers') +logger = logging.getLogger('mopidy.mixers') class BaseMixer(object): """ From b82b4d87cecc8eec6411e83e9c3b8b96981ab005 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:25:12 +0100 Subject: [PATCH 334/350] Simplify if statement --- mopidy/mixers/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mopidy/mixers/base.py b/mopidy/mixers/base.py index 4690be61..82783be1 100644 --- a/mopidy/mixers/base.py +++ b/mopidy/mixers/base.py @@ -24,9 +24,7 @@ class BaseMixer(object): if not hasattr(self, '_user_volume'): self._user_volume = 0 volume = self.get_volume() - if volume is None: - return None - elif not self.amplification_factor < 1: + if volume is None or not self.amplification_factor < 1: return volume else: user_volume = int(volume / self.amplification_factor) From 0f6e6ab4256ae36a5c3d51367a48e1018a325495 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 01:02:41 +0100 Subject: [PATCH 335/350] Update changelog for v0.7.0 release --- docs/changes.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d955978d..65bd6b4b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,12 +4,13 @@ Changes This change log is used to track all major changes to Mopidy. -v0.7.0 (in development) -======================= +v0.7.0 (2012-02-25) +=================== -**Important changes** - -- Nothing yet. +Not a big release with regard to features, but this release got some +performance improvements over v0.6, especially for slower Atom systems. It also +fixes a couple of other bugs, including one which made Mopidy crash when using +GStreamer from the prereleases of Ubuntu 12.04. **Changes** From 42d41d6fe71caa81aca8c7293d2c136622068c60 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 01:06:52 +0100 Subject: [PATCH 336/350] Prepare for v0.8 development --- docs/changes.rst | 12 ++++++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 65bd6b4b..4e5f5cb4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,18 @@ Changes This change log is used to track all major changes to Mopidy. +v0.8.0 (in development) +======================= + +**Important changes** + +- Nothing so far + +**Changes** + +- Nothing so far + + v0.7.0 (2012-02-25) =================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index b94378b2..f4167e3f 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ import os from subprocess import PIPE, Popen -VERSION = (0, 7, 0) +VERSION = (0, 8, 0) DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index dbaed0a2..a7fd82a4 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -24,8 +24,9 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.4.1') < SV('0.5.0')) self.assert_(SV('0.5.0') < SV('0.6.0')) self.assert_(SV('0.6.0') < SV('0.6.1')) - self.assert_(SV('0.6.1') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.7.1')) + self.assert_(SV('0.6.1') < SV('0.7.0')) + self.assert_(SV('0.7.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.8.1')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From ee0c7c1af564465d441af583a1f627a258f4c00d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 4 Mar 2012 23:26:24 +0100 Subject: [PATCH 337/350] Add __version__ to mopidy module --- docs/changes.rst | 7 ++++--- docs/conf.py | 12 +++++++++--- mopidy/__init__.py | 11 ++++------- setup.py | 6 +++++- tests/version_test.py | 8 ++++---- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 4e5f5cb4..a6b7e361 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,8 +4,8 @@ Changes This change log is used to track all major changes to Mopidy. -v0.8.0 (in development) -======================= +v0.8 (in development) +===================== **Important changes** @@ -13,7 +13,8 @@ v0.8.0 (in development) **Changes** -- Nothing so far +- Change from version tuple at :attr:`mopidy.VERSION` to :pep:`386` compliant + version string at :attr:`mopidy.__version__` to conform to :pep:`396`. v0.7.0 (2012-02-25) diff --git a/docs/conf.py b/docs/conf.py index f8b4ffc3..a33a8f2d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import re +import sys class Mock(object): def __init__(self, *args, **kwargs): @@ -49,6 +51,11 @@ MOCK_MODULES = [ for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() +def get_version(): + init_py = open('../mopidy/__init__.py').read() + metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py)) + return metadata['version'] + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -87,8 +94,7 @@ copyright = u'2010-2012, Stein Magnus Jodal and contributors' # built documents. # # The full version, including alpha/beta/rc tags. -import mopidy -release = mopidy.get_version() +release = get_version() # The short X.Y version. version = '.'.join(release.split('.')[:2]) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index f4167e3f..b2d9afa0 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -1,14 +1,14 @@ -import platform import sys if not (2, 6) <= sys.version_info < (3,): sys.exit(u'Mopidy requires Python >= 2.6, < 3') -import glib import os - +import platform from subprocess import PIPE, Popen -VERSION = (0, 8, 0) +import glib + +__version__ = '0.8' DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') @@ -30,9 +30,6 @@ def get_git_version(): version = version[1:] return version -def get_plain_version(): - return '.'.join(map(str, VERSION)) - def get_platform(): return platform.platform() diff --git a/setup.py b/setup.py index a8cf8ed1..ae6cc699 100644 --- a/setup.py +++ b/setup.py @@ -6,9 +6,13 @@ from distutils.core import setup from distutils.command.install_data import install_data from distutils.command.install import INSTALL_SCHEMES import os +import re import sys -from mopidy import get_version +def get_version(): + init_py = open('mopidy/__init__.py').read() + metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py)) + return metadata['version'] class osx_install_data(install_data): # On MacOS, the platform-specific lib dir is diff --git a/tests/version_test.py b/tests/version_test.py index a7fd82a4..86060693 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -1,14 +1,14 @@ from distutils.version import StrictVersion as SV import platform -from mopidy import get_plain_version, get_platform, get_python +from mopidy import __version__, get_platform, get_python from tests import unittest class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): - SV(get_plain_version()) + SV(__version__) def test_versions_can_be_strictly_ordered(self): self.assert_(SV('0.1.0a0') < SV('0.1.0a1')) @@ -25,8 +25,8 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.5.0') < SV('0.6.0')) self.assert_(SV('0.6.0') < SV('0.6.1')) self.assert_(SV('0.6.1') < SV('0.7.0')) - self.assert_(SV('0.7.0') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.8.1')) + self.assert_(SV('0.7.0') < SV(__version__)) + self.assert_(SV(__version__) < SV('0.8.1')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From 121ae0c2205ddc19ea0182b6e9d8fb04e8a6cb7c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 6 Mar 2012 11:18:10 +0100 Subject: [PATCH 338/350] Fix use of nonexistant function --- mopidy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index b2d9afa0..e0bce88c 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -19,7 +19,7 @@ def get_version(): try: return get_git_version() except EnvironmentError: - return get_plain_version() + return __version__ def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) From a987f7e5c1a3f8983c0346fa80d6fb39cbe0d4bc Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Thu, 8 Mar 2012 09:52:55 +0100 Subject: [PATCH 339/350] It's 'Last.fm', not 'Last.FM' --- docs/installation/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/index.rst b/docs/installation/index.rst index 8e62421c..fae50a1b 100644 --- a/docs/installation/index.rst +++ b/docs/installation/index.rst @@ -37,7 +37,7 @@ dependencies installed. - For Spotify support, you need libspotify and pyspotify. See :doc:`libspotify`. - - To scrobble your played tracks to Last.FM, you need pylast:: + - To scrobble your played tracks to Last.fm, you need pylast:: sudo pip install -U pylast From 4ea3bb11674b78480e09ee4e49872dcd7afcf39b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 22 Apr 2012 01:26:27 +0200 Subject: [PATCH 340/350] Don't override notify_main_thread, which has a sensible default implementation --- mopidy/backends/spotify/session_manager.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index af731e1b..2ae4ed2d 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -96,10 +96,6 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): """Callback used by pyspotify""" logger.debug(u'User message: %s', message.strip()) - def notify_main_thread(self, session): - """Callback used by pyspotify""" - logger.debug(u'notify_main_thread() called') - def music_delivery(self, session, frames, frame_size, num_frames, sample_type, sample_rate, channels): """Callback used by pyspotify""" From 1dae3442e0373a2cc6b4d62e99e395a06998ccb4 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 22 Apr 2012 01:30:20 +0200 Subject: [PATCH 341/350] Release v0.7.1 --- docs/changes.rst | 12 ++++++++++++ mopidy/__init__.py | 2 +- tests/version_test.py | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 65bd6b4b..5fd5212a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,18 @@ Changes This change log is used to track all major changes to Mopidy. + +v0.7.1 (2012-04-22) +=================== + +This is a maintenance release to make Mopidy 0.6 work with pyspotify >= 1.7. + +**Changes** + +- Don't override pyspotify's ``notify_main_thread`` callback. The default + implementation is sensible, while our override did nothing. + + v0.7.0 (2012-02-25) =================== diff --git a/mopidy/__init__.py b/mopidy/__init__.py index b94378b2..c433d47f 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ import os from subprocess import PIPE, Popen -VERSION = (0, 7, 0) +VERSION = (0, 7, 1) DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index dbaed0a2..58965589 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -24,8 +24,9 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.4.1') < SV('0.5.0')) self.assert_(SV('0.5.0') < SV('0.6.0')) self.assert_(SV('0.6.0') < SV('0.6.1')) - self.assert_(SV('0.6.1') < SV(get_plain_version())) - self.assert_(SV(get_plain_version()) < SV('0.7.1')) + self.assert_(SV('0.6.1') < SV('0.7.0')) + self.assert_(SV('0.7.0') < SV(get_plain_version())) + self.assert_(SV(get_plain_version()) < SV('0.7.2')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From aac7f738689400c5d1a30dfdebd9688f0eeeb4b2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 22 Apr 2012 01:41:12 +0200 Subject: [PATCH 342/350] Fix typo --- docs/changes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes.rst b/docs/changes.rst index 93c0fdbc..fa316c4d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,7 +21,7 @@ v0.8 (in development) v0.7.1 (2012-04-22) =================== -This is a maintenance release to make Mopidy 0.6 work with pyspotify >= 1.7. +This is a maintenance release to make Mopidy 0.7 work with pyspotify >= 1.7. **Changes** From 1f12951fa260049bc17e30edd0ab4967d085fb4b Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 7 May 2012 22:56:09 +0200 Subject: [PATCH 343/350] Prepare for maintenance release --- docs/changes.rst | 9 ++++----- mopidy/__init__.py | 2 +- tests/version_test.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index fa316c4d..84f5ffca 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,12 +5,11 @@ Changes This change log is used to track all major changes to Mopidy. -v0.8 (in development) -===================== +v0.7.2 (2012-05-07) +=================== -**Important changes** - -- Nothing so far +This is a maintenance release to make Mopidy 0.7 build on systems without all +of Mopidy's runtime dependencies, like Launchpad PPAs. **Changes** diff --git a/mopidy/__init__.py b/mopidy/__init__.py index e0bce88c..8a2b469e 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ from subprocess import PIPE, Popen import glib -__version__ = '0.8' +__version__ = '0.7.2' DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index f3ae3e0b..b1c0b90e 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -27,7 +27,7 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.6.1') < SV('0.7.0')) self.assert_(SV('0.7.0') < SV('0.7.1')) self.assert_(SV('0.7.1') < SV(__version__)) - self.assert_(SV(__version__) < SV('0.8.1')) + self.assert_(SV(__version__) < SV('0.8.0')) def test_get_platform_contains_platform(self): self.assert_(platform.platform() in get_platform()) From 0e66ffe6a5a7f227fd08ac94a98097939ddef4ab Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Aug 2012 23:12:04 +0200 Subject: [PATCH 344/350] Add locale_decode util function that decodes bytestrings using the current locale's encoding --- mopidy/utils/__init__.py | 7 +++++++ tests/utils/decode_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/utils/decode_test.py diff --git a/mopidy/utils/__init__.py b/mopidy/utils/__init__.py index 9d7532a0..00129cdd 100644 --- a/mopidy/utils/__init__.py +++ b/mopidy/utils/__init__.py @@ -1,3 +1,4 @@ +import locale import logging import os import sys @@ -29,3 +30,9 @@ def get_class(name): except (ImportError, AttributeError): raise ImportError("Couldn't load: %s" % name) return class_object + +def locale_decode(bytestr): + try: + return unicode(bytestr) + except UnicodeError: + return str(bytestr).decode(locale.getpreferredencoding()) diff --git a/tests/utils/decode_test.py b/tests/utils/decode_test.py new file mode 100644 index 00000000..edbfe651 --- /dev/null +++ b/tests/utils/decode_test.py @@ -0,0 +1,38 @@ +import mock + +from mopidy.utils import locale_decode + +from tests import unittest + + +@mock.patch('mopidy.utils.locale.getpreferredencoding') +class LocaleDecodeTest(unittest.TestCase): + def test_can_decode_utf8_strings_with_french_content(self, mock): + mock.return_value = 'UTF-8' + + result = locale_decode( + '[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') + + self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + + def test_can_decode_an_ioerror_with_french_content(self, mock): + mock.return_value = 'UTF-8' + + error = IOError(98, 'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') + result = locale_decode(error) + + self.assertEquals(u'[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) + + def test_does_not_use_locale_to_decode_unicode_strings(self, mock): + mock.return_value = 'UTF-8' + + locale_decode(u'abc') + + self.assertFalse(mock.called) + + def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock): + mock.return_value = 'UTF-8' + + locale_decode('abc') + + self.assertFalse(mock.called) From d6f17b4cf00868ceaac59d1867ba9ccfabcc8ede Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Aug 2012 23:17:41 +0200 Subject: [PATCH 345/350] Decode messages from IOError before logging them IOError messages are bytestrings, often in the language of the system, so they may include non-ASCII characters. Thus, we must decode them using the locale's preferred encoding to get Unicode objects we safely can pass on for logging the IOError. --- mopidy/backends/local/translator.py | 9 +++++---- mopidy/frontends/mpd/__init__.py | 6 +++--- mopidy/utils/network.py | 6 ++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mopidy/backends/local/translator.py b/mopidy/backends/local/translator.py index be7ab8a8..3b610a94 100644 --- a/mopidy/backends/local/translator.py +++ b/mopidy/backends/local/translator.py @@ -4,6 +4,7 @@ import os logger = logging.getLogger('mopidy.backends.local.translator') from mopidy.models import Track, Artist, Album +from mopidy.utils import locale_decode from mopidy.utils.path import path_to_uri def parse_m3u(file_path): @@ -33,8 +34,8 @@ def parse_m3u(file_path): try: with open(file_path) as m3u: contents = m3u.readlines() - except IOError, e: - logger.error('Couldn\'t open m3u: %s', e) + except IOError as error: + logger.error('Couldn\'t open m3u: %s', locale_decode(error)) return uris for line in contents: @@ -61,8 +62,8 @@ def parse_mpd_tag_cache(tag_cache, music_dir=''): try: with open(tag_cache) as library: contents = library.read() - except IOError, e: - logger.error('Could not open tag cache: %s', e) + except IOError as error: + logger.error('Could not open tag cache: %s', locale_decode(error)) return tracks current = {} diff --git a/mopidy/frontends/mpd/__init__.py b/mopidy/frontends/mpd/__init__.py index 99134012..e8b2aabe 100644 --- a/mopidy/frontends/mpd/__init__.py +++ b/mopidy/frontends/mpd/__init__.py @@ -5,7 +5,7 @@ from pykka import registry, actor from mopidy import listeners, settings from mopidy.frontends.mpd import dispatcher, protocol -from mopidy.utils import network, process, log +from mopidy.utils import locale_decode, log, network, process logger = logging.getLogger('mopidy.frontends.mpd') @@ -32,8 +32,8 @@ class MpdFrontend(actor.ThreadingActor, listeners.BackendListener): try: network.Server(hostname, port, protocol=MpdSession, max_connections=settings.MPD_SERVER_MAX_CONNECTIONS) - except IOError, e: - logger.error(u'MPD server startup failed: %s', e) + except IOError as error: + logger.error(u'MPD server startup failed: %s', locale_decode(error)) sys.exit(1) logger.info(u'MPD server running at [%s]:%s', hostname, port) diff --git a/mopidy/utils/network.py b/mopidy/utils/network.py index 0a0928ce..4b8a9ac9 100644 --- a/mopidy/utils/network.py +++ b/mopidy/utils/network.py @@ -9,6 +9,8 @@ from pykka import ActorDeadError from pykka.actor import ThreadingActor from pykka.registry import ActorRegistry +from mopidy.utils import locale_decode + logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): @@ -21,9 +23,9 @@ def try_ipv6_socket(): try: socket.socket(socket.AF_INET6).close() return True - except IOError, e: + except IOError as error: logger.debug(u'Platform supports IPv6, but socket ' - 'creation failed, disabling: %s', e) + 'creation failed, disabling: %s', locale_decode(error)) return False #: Boolean value that indicates if creating an IPv6 socket will succeed. From f392a7cccb485ce16f64d5b2109e710838351a5a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 00:29:04 +0200 Subject: [PATCH 346/350] Update changelog --- docs/changes.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 84f5ffca..1e0900d4 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,14 @@ Changes This change log is used to track all major changes to Mopidy. +v0.7.3 (in development) +======================= + +**Changes** + +- Fixed crash when logging :exc:`IOError` exceptions on systems using languages + with non-ASCII characters, like French. + v0.7.2 (2012-05-07) =================== From 436fd7815d6f967aa0fc53dbe10f7f516e7cd64a Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 00:31:58 +0200 Subject: [PATCH 347/350] Move the Spotify cache to a subdir of the Mopidy cache --- docs/changes.rst | 4 ++++ mopidy/backends/spotify/session_manager.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1e0900d4..a93369ea 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -12,6 +12,10 @@ v0.7.3 (in development) - Fixed crash when logging :exc:`IOError` exceptions on systems using languages with non-ASCII characters, like French. +- Move the default location of the Spotify cache from `~/.cache/mopidy` to + `~/.cache/mopidy/spotify`. You can change this by setting + :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`. + v0.7.2 (2012-05-07) =================== diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 2ae4ed2d..3794513c 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -23,8 +23,9 @@ logger = logging.getLogger('mopidy.backends.spotify.session_manager') class SpotifySessionManager(BaseThread, PyspotifySessionManager): - cache_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH - settings_location = settings.SPOTIFY_CACHE_PATH or CACHE_PATH + cache_location = (settings.SPOTIFY_CACHE_PATH + or os.path.join(CACHE_PATH, 'spotify')) + settings_location = cache_location appkey_file = os.path.join(os.path.dirname(__file__), 'spotify_appkey.key') user_agent = 'Mopidy %s' % get_version() From d78d62c68ce4c88cd3ac7e6fa89f9936e4ef7521 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 25 Feb 2012 00:52:44 +0100 Subject: [PATCH 348/350] Hack to speed up Spotify backend startup with clean cache from 35s to 12s The time Improvement is probably a magnitude or two larger on outdated caches. --- docs/changes.rst | 4 ++++ mopidy/backends/spotify/session_manager.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index a93369ea..7b923e1a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -16,6 +16,10 @@ v0.7.3 (in development) `~/.cache/mopidy/spotify`. You can change this by setting :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`. +- Reduce time required to update the Spotify cache on startup. One one + system/Spotify account, the time from clean cache to ready for use was + reduced from 35s to 12s. + v0.7.2 (2012-05-07) =================== diff --git a/mopidy/backends/spotify/session_manager.py b/mopidy/backends/spotify/session_manager.py index 3794513c..481f7a94 100644 --- a/mopidy/backends/spotify/session_manager.py +++ b/mopidy/backends/spotify/session_manager.py @@ -43,6 +43,8 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): self.container_manager = None self.playlist_manager = None + self._initial_data_receive_completed = False + def run_inside_try(self): self.setup() self.connect() @@ -126,6 +128,17 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def log_message(self, session, data): """Callback used by pyspotify""" logger.debug(u'System message: %s' % data.strip()) + if 'offline-mgr' in data and 'files unlocked' in data: + # XXX This is a very very fragile and ugly hack, but we get no + # proper event when libspotify is done with initial data loading. + # We delay the expensive refresh of Mopidy's stored playlists until + # this message arrives. This way, we avoid doing the refresh once + # for every playlist or other change. This reduces the time from + # startup until the Spotify backend is ready from 35s to 12s in one + # test with clean Spotify cache. In cases with an outdated cache + # the time improvements should be a lot better. + self._initial_data_receive_completed = True + self.refresh_stored_playlists() def end_of_track(self, session): """Callback used by pyspotify""" @@ -135,6 +148,9 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager): def refresh_stored_playlists(self): """Refresh the stored playlists in the backend with fresh meta data from Spotify""" + if not self._initial_data_receive_completed: + logger.debug(u'Still getting data; skipped refresh of playlists') + return playlists = map(SpotifyTranslator.to_mopidy_playlist, self.session.playlist_container()) playlists = filter(None, playlists) From 52e242cbe1055ce65a760d37472b99c21c30de28 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 03:37:55 +0200 Subject: [PATCH 349/350] Update version number to 0.7.3 --- mopidy/__init__.py | 2 +- tests/version_test.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mopidy/__init__.py b/mopidy/__init__.py index 8a2b469e..11293446 100644 --- a/mopidy/__init__.py +++ b/mopidy/__init__.py @@ -8,7 +8,7 @@ from subprocess import PIPE, Popen import glib -__version__ = '0.7.2' +__version__ = '0.7.3' DATA_PATH = os.path.join(str(glib.get_user_data_dir()), 'mopidy') CACHE_PATH = os.path.join(str(glib.get_user_cache_dir()), 'mopidy') diff --git a/tests/version_test.py b/tests/version_test.py index b1c0b90e..26045ac1 100644 --- a/tests/version_test.py +++ b/tests/version_test.py @@ -26,7 +26,8 @@ class VersionTest(unittest.TestCase): self.assert_(SV('0.6.0') < SV('0.6.1')) self.assert_(SV('0.6.1') < SV('0.7.0')) self.assert_(SV('0.7.0') < SV('0.7.1')) - self.assert_(SV('0.7.1') < SV(__version__)) + self.assert_(SV('0.7.1') < SV('0.7.2')) + self.assert_(SV('0.7.2') < SV(__version__)) self.assert_(SV(__version__) < SV('0.8.0')) def test_get_platform_contains_platform(self): From 90490375575b7406fb117237b166e46812f16377 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sat, 11 Aug 2012 03:38:07 +0200 Subject: [PATCH 350/350] Update changelog for v0.7.3 --- docs/changes.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 7b923e1a..a4aae058 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,8 +4,11 @@ Changes This change log is used to track all major changes to Mopidy. -v0.7.3 (in development) -======================= +v0.7.3 (2012-08-11) +=================== + +A small maintenance release to fix a crash affecting a few users, and a couple +of small adjustments to the Spotify backend. **Changes**