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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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/105] 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 1a6d577ed5b5b65475645627aab68b97b5d11576 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 10 Jun 2011 19:39:36 +0200 Subject: [PATCH 039/105] 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 040/105] 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 041/105] 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 042/105] 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 043/105] 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 044/105] 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 045/105] 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 43e4952e5fd7b1208ce932cd77fed4dfca8b54f0 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 19 Jun 2011 23:40:24 +0300 Subject: [PATCH 046/105] 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 047/105] 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 048/105] 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 049/105] 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 050/105] 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 051/105] 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 052/105] 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 053/105] 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 054/105] 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 055/105] 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 056/105] 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 057/105] 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 058/105] 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 059/105] 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 060/105] 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 061/105] 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 062/105] 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 063/105] 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 064/105] 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 065/105] 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 066/105] 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 067/105] 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 068/105] 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 069/105] 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 070/105] '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 071/105] 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 072/105] 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 073/105] 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 8092ffaa349aa0a8062364f7ba2cfd45aff6395c Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 24 Jun 2011 21:08:13 +0300 Subject: [PATCH 074/105] 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 075/105] 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 076/105] 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 077/105] 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 078/105] 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 079/105] 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 080/105] 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 081/105] 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 1399bb61dcf4354d7e3c974cfdee9247ffb9f11d Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Wed, 27 Jul 2011 00:47:34 +0200 Subject: [PATCH 082/105] 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 083/105] 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 084/105] 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 085/105] 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 086/105] 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 087/105] 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 088/105] 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 089/105] 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 090/105] 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 091/105] 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 092/105] 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 093/105] 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 094/105] 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 095/105] 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 ae4cd6a7de7c1792847affe632653e15a0fbfbdf Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Fri, 29 Jul 2011 22:23:04 +0200 Subject: [PATCH 096/105] 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 097/105] 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 098/105] 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 099/105] 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 100/105] 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 101/105] 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 102/105] 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 103/105] 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 104/105] 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 105/105] 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')