pax_global_header00006660000000000000000000000064122432226030014506gustar00rootroot0000000000000052 comment=990ec0cbb00e73c9b2f669280d80ef5608634703 mopidy-mpris-1.0.1/000077500000000000000000000000001224322260300141365ustar00rootroot00000000000000mopidy-mpris-1.0.1/.coveragerc000066400000000000000000000001151224322260300162540ustar00rootroot00000000000000[report] omit = */pyshared/* */python?.?/* */site-packages/nose/*mopidy-mpris-1.0.1/.gitignore000066400000000000000000000000661224322260300161300ustar00rootroot00000000000000*.egg-info *.pyc *.swp .coverage MANIFEST build/ dist/mopidy-mpris-1.0.1/.travis.yml000066400000000000000000000014101224322260300162430ustar00rootroot00000000000000language: python install: - "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" - "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list" - "sudo apt-get update || true" - "sudo apt-get install $(apt-cache depends mopidy-mpris | awk '$2 !~ /mopidy-mpris|python:any/ {print $2}')" - "pip install coveralls flake8 mopidy==dev" before_script: - "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt" script: - "flake8 $(find . -iname '*.py')" - "nosetests --with-coverage --cover-package=mopidy_mpris" after_success: - "coveralls" notifications: irc: channels: - "irc.freenode.org#mopidy" on_success: change on_failure: change use_notice: true skip_join: true mopidy-mpris-1.0.1/LICENSE000066400000000000000000000261351224322260300151520ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.mopidy-mpris-1.0.1/MANIFEST.in000066400000000000000000000001241224322260300156710ustar00rootroot00000000000000include LICENSE include MANIFEST.in include README.rst include mopidy_mpris/ext.confmopidy-mpris-1.0.1/README.rst000066400000000000000000000104261224322260300156300ustar00rootroot00000000000000************ Mopidy-MPRIS ************ .. image:: https://pypip.in/v/Mopidy-MPRIS/badge.png :target: https://crate.io/packages/Mopidy-MPRIS/ :alt: Latest PyPI version .. image:: https://pypip.in/d/Mopidy-MPRIS/badge.png :target: https://crate.io/packages/Mopidy-MPRIS/ :alt: Number of PyPI downloads .. image:: https://travis-ci.org/mopidy/mopidy-mpris.png?branch=master :target: https://travis-ci.org/mopidy/mopidy-mpris :alt: Travis CI build status .. image:: https://coveralls.io/repos/mopidy/mopidy-mpris/badge.png?branch=master :target: https://coveralls.io/r/mopidy/mopidy-mpris?branch=master :alt: Test coverage `Mopidy `_ extension for controlling Mopidy through the `MPRIS D-Bus interface `_. An example of an MPRIS client is the Ubuntu Sound Menu. Dependencies ============ - 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 the ``mpris/desktop_file`` config value. Mopidy installs this by default. See usage section below for details. Installation ============ Install by running:: pip install Mopidy-MPRIS Or, if available, install the Debian/Ubuntu package from `apt.mopidy.com `_. Configuration ============= There's no configuration needed for the MPRIS extension to work. The following configuration values are available: - ``mpris/enabled``: If the MPRIS extension should be enabled or not. - ``mpris/desktop_file``: Path to Mopidy's ``.desktop`` file. Usage ===== The extension is enabled by default if all dependencies are available. 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 repo into the ``/usr/share/applications`` dir by hand:: cd /path/to/mopidy/source sudo cp data/mopidy.desktop /usr/share/applications/ If the correct path to the installed ``mopidy.desktop`` file on your system isn't ``/usr/share/applications/mopidy.conf``, you'll need to set the ``mpris/desktop_file`` config value. 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`` interface of the spec is not supported. Testing the MPRIS API directly ------------------------------ To use the MPRIS API directly, 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') For details on the API, please refer to the `MPRIS specification `__. Project resources ================= - `Source code `_ - `Issue tracker `_ - `Download development snapshot `_ Changelog ========= v1.0.1 (2013-11-20) ------------------- - Update to work with Mopidy 0.16 which changed some APIs. - Remove redundant event loop setup already done by the ``mopidy`` process. v1.0.0 (2013-10-08) ------------------- - Moved extension out of the main Mopidy project. mopidy-mpris-1.0.1/mopidy_mpris/000077500000000000000000000000001224322260300166515ustar00rootroot00000000000000mopidy-mpris-1.0.1/mopidy_mpris/__init__.py000066400000000000000000000017321224322260300207650ustar00rootroot00000000000000from __future__ import unicode_literals import os from mopidy import config, exceptions, ext __version__ = '1.0.1' class Extension(ext.Extension): dist_name = 'Mopidy-MPRIS' ext_name = 'mpris' version = __version__ def get_default_config(self): conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') return config.read(conf_file) def get_config_schema(self): schema = super(Extension, self).get_config_schema() schema['desktop_file'] = config.Path() return schema def validate_environment(self): if 'DISPLAY' not in os.environ: raise exceptions.ExtensionError( 'An X11 $DISPLAY is needed to use D-Bus') try: import dbus # noqa except ImportError as e: raise exceptions.ExtensionError('dbus library not found', e) def get_frontend_classes(self): from .frontend import MprisFrontend return [MprisFrontend] mopidy-mpris-1.0.1/mopidy_mpris/ext.conf000066400000000000000000000001151224322260300203150ustar00rootroot00000000000000[mpris] enabled = true desktop_file = /usr/share/applications/mopidy.desktop mopidy-mpris-1.0.1/mopidy_mpris/frontend.py000066400000000000000000000076771224322260300210630ustar00rootroot00000000000000from __future__ import unicode_literals import logging import os import pykka from mopidy.core import CoreListener from mopidy_mpris import objects logger = logging.getLogger('mopidy_mpris') try: indicate = None if 'DISPLAY' in os.environ: import indicate except ImportError: pass if indicate is None: logger.debug('Startup notification will not be sent') class MprisFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MprisFrontend, self).__init__() self.config = config self.core = core self.indicate_server = None self.mpris_object = None def on_start(self): try: self.mpris_object = objects.MprisObject(self.config, self.core) self._send_startup_notification() except Exception as e: logger.warning('MPRIS frontend setup failed (%s)', e) self.stop() def on_stop(self): logger.debug('Removing MPRIS object from D-Bus connection...') if self.mpris_object: self.mpris_object.remove_from_connection() self.mpris_object = None logger.debug('Removed MPRIS object from D-Bus connection') def _send_startup_notification(self): """ Send startup notification using libindicate to make Mopidy appear in e.g. `Ubunt'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('Sending startup notification...') self.indicate_server = indicate.Server() self.indicate_server.set_type('music.mopidy') self.indicate_server.set_desktop_file( self.config['mpris']['desktop_file']) self.indicate_server.show() logger.debug('Startup notification sent') def _emit_properties_changed(self, interface, changed_properties): if self.mpris_object is None: return props_with_new_values = [ (p, self.mpris_object.Get(interface, p)) for p in changed_properties] self.mpris_object.PropertiesChanged( interface, dict(props_with_new_values), []) def track_playback_paused(self, tl_track, time_position): logger.debug('Received track_playback_paused event') self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus']) def track_playback_resumed(self, tl_track, time_position): logger.debug('Received track_playback_resumed event') self._emit_properties_changed(objects.PLAYER_IFACE, ['PlaybackStatus']) def track_playback_started(self, tl_track): logger.debug('Received track_playback_started event') self._emit_properties_changed( objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata']) def track_playback_ended(self, tl_track, time_position): logger.debug('Received track_playback_ended event') self._emit_properties_changed( objects.PLAYER_IFACE, ['PlaybackStatus', 'Metadata']) def volume_changed(self, volume): logger.debug('Received volume_changed event') self._emit_properties_changed(objects.PLAYER_IFACE, ['Volume']) def seeked(self, time_position_in_ms): logger.debug('Received seeked event') self.mpris_object.Seeked(time_position_in_ms * 1000) def playlists_loaded(self): logger.debug('Received playlists_loaded event') self._emit_properties_changed( objects.PLAYLISTS_IFACE, ['PlaylistCount']) def playlist_changed(self, playlist): logger.debug('Received playlist_changed event') playlist_id = self.mpris_object.get_playlist_id(playlist.uri) playlist = (playlist_id, playlist.name, '') self.mpris_object.PlaylistChanged(playlist) mopidy-mpris-1.0.1/mopidy_mpris/objects.py000066400000000000000000000444741224322260300206710ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import logging import os import dbus import dbus.service from mopidy.core import PlaybackState from mopidy.utils.process import exit_process logger = logging.getLogger('mopidy_mpris') BUS_NAME = 'org.mpris.MediaPlayer2.mopidy' OBJECT_PATH = '/org/mpris/MediaPlayer2' ROOT_IFACE = 'org.mpris.MediaPlayer2' PLAYER_IFACE = 'org.mpris.MediaPlayer2.Player' PLAYLISTS_IFACE = 'org.mpris.MediaPlayer2.Playlists' class MprisObject(dbus.service.Object): """Implements http://www.mpris.org/2.2/spec/""" properties = None def __init__(self, config, core): self.config = config self.core = core self.properties = { ROOT_IFACE: self._get_root_iface_properties(), PLAYER_IFACE: self._get_player_iface_properties(), PLAYLISTS_IFACE: self._get_playlists_iface_properties(), } bus_name = self._connect_to_dbus() dbus.service.Object.__init__(self, bus_name, OBJECT_PATH) def _get_root_iface_properties(self): return { 'CanQuit': (True, None), 'Fullscreen': (False, None), 'CanSetFullscreen': (False, None), 'CanRaise': (False, None), # NOTE Change if adding optional track list support 'HasTrackList': (False, None), 'Identity': ('Mopidy', None), 'DesktopEntry': (self.get_DesktopEntry, None), 'SupportedUriSchemes': (self.get_SupportedUriSchemes, None), # NOTE Return MIME types supported by local backend if support for # reporting supported MIME types is added 'SupportedMimeTypes': (dbus.Array([], signature='s'), None), } def _get_player_iface_properties(self): return { 'PlaybackStatus': (self.get_PlaybackStatus, None), 'LoopStatus': (self.get_LoopStatus, self.set_LoopStatus), 'Rate': (1.0, self.set_Rate), 'Shuffle': (self.get_Shuffle, self.set_Shuffle), 'Metadata': (self.get_Metadata, None), 'Volume': (self.get_Volume, self.set_Volume), 'Position': (self.get_Position, None), 'MinimumRate': (1.0, None), 'MaximumRate': (1.0, None), 'CanGoNext': (self.get_CanGoNext, None), 'CanGoPrevious': (self.get_CanGoPrevious, None), 'CanPlay': (self.get_CanPlay, None), 'CanPause': (self.get_CanPause, None), 'CanSeek': (self.get_CanSeek, None), 'CanControl': (self.get_CanControl, None), } def _get_playlists_iface_properties(self): return { 'PlaylistCount': (self.get_PlaylistCount, None), 'Orderings': (self.get_Orderings, None), 'ActivePlaylist': (self.get_ActivePlaylist, None), } def _connect_to_dbus(self): logger.debug('Connecting to D-Bus...') bus_name = dbus.service.BusName(BUS_NAME, dbus.SessionBus()) logger.info('MPRIS server connected to D-Bus') return bus_name def get_playlist_id(self, playlist_uri): # Only A-Za-z0-9_ is allowed, which is 63 chars, so we can't use # base64. Luckily, D-Bus does not limit the length of object paths. # Since base32 pads trailing bytes with "=" chars, we need to replace # them with an allowed character such as "_". encoded_uri = base64.b32encode(playlist_uri).replace('=', '_') return '/com/mopidy/playlist/%s' % encoded_uri def get_playlist_uri(self, playlist_id): encoded_uri = playlist_id.split('/')[-1].replace('_', '=') return base64.b32decode(encoded_uri) def get_track_id(self, tl_track): return '/com/mopidy/track/%d' % tl_track.tlid def get_track_tlid(self, track_id): assert track_id.startswith('/com/mopidy/track/') return track_id.split('/')[-1] ### Properties interface @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ss', out_signature='v') def Get(self, interface, prop): logger.debug( '%s.Get(%s, %s) called', dbus.PROPERTIES_IFACE, repr(interface), repr(prop)) (getter, _) = self.properties[interface][prop] if callable(getter): return getter() else: return getter @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='s', out_signature='a{sv}') def GetAll(self, interface): logger.debug( '%s.GetAll(%s) called', dbus.PROPERTIES_IFACE, repr(interface)) getters = {} for key, (getter, _) in self.properties[interface].iteritems(): getters[key] = getter() if callable(getter) else getter return getters @dbus.service.method(dbus_interface=dbus.PROPERTIES_IFACE, in_signature='ssv', out_signature='') def Set(self, interface, prop, value): logger.debug( '%s.Set(%s, %s, %s) called', dbus.PROPERTIES_IFACE, repr(interface), repr(prop), repr(value)) _, 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=dbus.PROPERTIES_IFACE, signature='sa{sv}as') def PropertiesChanged(self, interface, changed_properties, invalidated_properties): logger.debug( '%s.PropertiesChanged(%s, %s, %s) signaled', dbus.PROPERTIES_IFACE, interface, changed_properties, invalidated_properties) ### Root interface methods @dbus.service.method(dbus_interface=ROOT_IFACE) def Raise(self): logger.debug('%s.Raise called', ROOT_IFACE) # Do nothing, as we do not have a GUI @dbus.service.method(dbus_interface=ROOT_IFACE) def Quit(self): logger.debug('%s.Quit called', ROOT_IFACE) exit_process() ### Root interface properties def get_DesktopEntry(self): return os.path.splitext(os.path.basename( self.config['mpris']['desktop_file']))[0] def get_SupportedUriSchemes(self): return dbus.Array(self.core.uri_schemes.get(), signature='s') ### Player interface methods @dbus.service.method(dbus_interface=PLAYER_IFACE) def Next(self): logger.debug('%s.Next called', PLAYER_IFACE) if not self.get_CanGoNext(): logger.debug('%s.Next not allowed', PLAYER_IFACE) return self.core.playback.next().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Previous(self): logger.debug('%s.Previous called', PLAYER_IFACE) if not self.get_CanGoPrevious(): logger.debug('%s.Previous not allowed', PLAYER_IFACE) return self.core.playback.previous().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Pause(self): logger.debug('%s.Pause called', PLAYER_IFACE) if not self.get_CanPause(): logger.debug('%s.Pause not allowed', PLAYER_IFACE) return self.core.playback.pause().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def PlayPause(self): logger.debug('%s.PlayPause called', PLAYER_IFACE) if not self.get_CanPause(): logger.debug('%s.PlayPause not allowed', PLAYER_IFACE) return state = self.core.playback.state.get() if state == PlaybackState.PLAYING: self.core.playback.pause().get() elif state == PlaybackState.PAUSED: self.core.playback.resume().get() elif state == PlaybackState.STOPPED: self.core.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Stop(self): logger.debug('%s.Stop called', PLAYER_IFACE) if not self.get_CanControl(): logger.debug('%s.Stop not allowed', PLAYER_IFACE) return self.core.playback.stop().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Play(self): logger.debug('%s.Play called', PLAYER_IFACE) if not self.get_CanPlay(): logger.debug('%s.Play not allowed', PLAYER_IFACE) return state = self.core.playback.state.get() if state == PlaybackState.PAUSED: self.core.playback.resume().get() else: self.core.playback.play().get() @dbus.service.method(dbus_interface=PLAYER_IFACE) def Seek(self, offset): logger.debug('%s.Seek called', PLAYER_IFACE) if not self.get_CanSeek(): logger.debug('%s.Seek not allowed', PLAYER_IFACE) return offset_in_milliseconds = offset // 1000 current_position = self.core.playback.time_position.get() new_position = current_position + offset_in_milliseconds self.core.playback.seek(new_position) @dbus.service.method(dbus_interface=PLAYER_IFACE) def SetPosition(self, track_id, position): logger.debug('%s.SetPosition called', PLAYER_IFACE) if not self.get_CanSeek(): logger.debug('%s.SetPosition not allowed', PLAYER_IFACE) return position = position // 1000 current_tl_track = self.core.playback.current_tl_track.get() if current_tl_track is None: return if track_id != self.get_track_id(current_tl_track): return if position < 0: return if current_tl_track.track.length < position: return self.core.playback.seek(position) @dbus.service.method(dbus_interface=PLAYER_IFACE) def OpenUri(self, uri): logger.debug('%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('%s.Play not allowed', PLAYER_IFACE) return # NOTE Check if URI has MIME type known to the backend, if MIME support # is added to the backend. tl_tracks = self.core.tracklist.add(uri=uri).get() if tl_tracks: self.core.playback.play(tl_tracks[0]) else: logger.debug('Track with URI "%s" not found in library.', uri) ### Player interface signals @dbus.service.signal(dbus_interface=PLAYER_IFACE, signature='x') def Seeked(self, position): logger.debug('%s.Seeked signaled', PLAYER_IFACE) # Do nothing, as just calling the method is enough to emit the signal. ### Player interface properties def get_PlaybackStatus(self): state = self.core.playback.state.get() if state == PlaybackState.PLAYING: return 'Playing' elif state == PlaybackState.PAUSED: return 'Paused' elif state == PlaybackState.STOPPED: return 'Stopped' def get_LoopStatus(self): repeat = self.core.tracklist.repeat.get() single = self.core.tracklist.single.get() if not repeat: return 'None' else: if single: return 'Track' else: return 'Playlist' def set_LoopStatus(self, value): if not self.get_CanControl(): logger.debug('Setting %s.LoopStatus not allowed', PLAYER_IFACE) return if value == 'None': self.core.tracklist.repeat = False self.core.tracklist.single = False elif value == 'Track': self.core.tracklist.repeat = True self.core.tracklist.single = True elif value == 'Playlist': self.core.tracklist.repeat = True self.core.tracklist.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('Setting %s.Rate not allowed', PLAYER_IFACE) return if value == 0: self.Pause() def get_Shuffle(self): return self.core.tracklist.random.get() def set_Shuffle(self, value): if not self.get_CanControl(): logger.debug('Setting %s.Shuffle not allowed', PLAYER_IFACE) return if value: self.core.tracklist.random = True else: self.core.tracklist.random = False def get_Metadata(self): current_tl_track = self.core.playback.current_tl_track.get() if current_tl_track is None: return {'mpris:trackid': ''} else: (_, track) = current_tl_track metadata = {'mpris:trackid': self.get_track_id(current_tl_track)} 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.album and track.album.images: url = list(track.album.images)[0] if url: metadata['mpris:artUrl'] = url if track.disc_no: metadata['xesam:discNumber'] = track.disc_no if track.track_no: metadata['xesam:trackNumber'] = track.track_no return dbus.Dictionary(metadata, signature='sv') def get_Volume(self): volume = self.core.playback.volume.get() if volume is None: return 0 return volume / 100.0 def set_Volume(self, value): if not self.get_CanControl(): logger.debug('Setting %s.Volume not allowed', PLAYER_IFACE) return if value is None: return elif value < 0: self.core.playback.volume = 0 elif value > 1: self.core.playback.volume = 100 elif 0 <= value <= 1: self.core.playback.volume = int(value * 100) def get_Position(self): return self.core.playback.time_position.get() * 1000 def get_CanGoNext(self): if not self.get_CanControl(): return False current_tl_track = self.core.playback.current_tl_track.get() next_tl_track = self.core.tracklist.next_track(current_tl_track).get() return next_tl_track != current_tl_track def get_CanGoPrevious(self): if not self.get_CanControl(): return False current_tl_track = self.core.playback.current_tl_track.get() previous_tl_track = ( self.core.tracklist.previous_track(current_tl_track).get()) return previous_tl_track != current_tl_track def get_CanPlay(self): if not self.get_CanControl(): return False current_tl_track = self.core.playback.current_tl_track.get() next_tl_track = self.core.tracklist.next_track(current_tl_track).get() return current_tl_track is not None or next_tl_track is not None def get_CanPause(self): if not self.get_CanControl(): return False # 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 # 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): # NOTE This could be a setting for the end user to change. return True ### Playlists interface methods @dbus.service.method(dbus_interface=PLAYLISTS_IFACE) def ActivatePlaylist(self, playlist_id): logger.debug( '%s.ActivatePlaylist(%r) called', PLAYLISTS_IFACE, playlist_id) playlist_uri = self.get_playlist_uri(playlist_id) playlist = self.core.playlists.lookup(playlist_uri).get() if playlist and playlist.tracks: tl_tracks = self.core.tracklist.add(playlist.tracks).get() self.core.playback.play(tl_tracks[0]) @dbus.service.method(dbus_interface=PLAYLISTS_IFACE) def GetPlaylists(self, index, max_count, order, reverse): logger.debug( '%s.GetPlaylists(%r, %r, %r, %r) called', PLAYLISTS_IFACE, index, max_count, order, reverse) playlists = self.core.playlists.playlists.get() if order == 'Alphabetical': playlists.sort(key=lambda p: p.name, reverse=reverse) elif order == 'Modified': playlists.sort(key=lambda p: p.last_modified, reverse=reverse) elif order == 'User' and reverse: playlists.reverse() slice_end = index + max_count playlists = playlists[index:slice_end] results = [ (self.get_playlist_id(p.uri), p.name, '') for p in playlists] return dbus.Array(results, signature='(oss)') ### Playlists interface signals @dbus.service.signal(dbus_interface=PLAYLISTS_IFACE, signature='(oss)') def PlaylistChanged(self, playlist): logger.debug('%s.PlaylistChanged signaled', PLAYLISTS_IFACE) # Do nothing, as just calling the method is enough to emit the signal. ### Playlists interface properties def get_PlaylistCount(self): return len(self.core.playlists.playlists.get()) def get_Orderings(self): return [ 'Alphabetical', # Order by playlist.name 'Modified', # Order by playlist.last_modified 'User', # Don't change order ] def get_ActivePlaylist(self): playlist_is_valid = False playlist = ('/', 'None', '') return (playlist_is_valid, playlist) mopidy-mpris-1.0.1/setup.py000066400000000000000000000026351224322260300156560ustar00rootroot00000000000000from __future__ import unicode_literals import re from setuptools import setup, find_packages def get_version(filename): content = open(filename).read() metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", content)) return metadata['version'] setup( name='Mopidy-MPRIS', version=get_version('mopidy_mpris/__init__.py'), url='https://github.com/mopidy/mopidy-mpris', license='Apache License, Version 2.0', author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', description=( 'Mopidy extension for controlling Mopidy through the ' 'MPRIS D-Bus interface'), long_description=open('README.rst').read(), packages=find_packages(exclude=['tests', 'tests.*']), zip_safe=False, include_package_data=True, install_requires=[ 'setuptools', 'Mopidy >= 0.16a0', 'Pykka >= 1.1', ], test_suite='nose.collector', tests_require=[ 'nose', 'mock >= 1.0', ], entry_points={ 'mopidy.ext': [ 'mpris = mopidy_mpris:Extension', ], }, classifiers=[ 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Topic :: Multimedia :: Sound/Audio :: Players', ], ) mopidy-mpris-1.0.1/tests/000077500000000000000000000000001224322260300153005ustar00rootroot00000000000000mopidy-mpris-1.0.1/tests/__init__.py000066400000000000000000000000001224322260300173770ustar00rootroot00000000000000mopidy-mpris-1.0.1/tests/test_events.py000066400000000000000000000076321224322260300202250ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest try: import dbus except ImportError: dbus = False from mopidy.models import Playlist, TlTrack if dbus: from mopidy_mpris import frontend, objects @unittest.skipUnless(dbus, 'dbus not found') class BackendEventsTest(unittest.TestCase): def setUp(self): # As a plain class, not an actor: self.mpris_frontend = frontend.MprisFrontend(config=None, core=None) 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(TlTrack(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( 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(TlTrack(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( objects.PLAYER_IFACE, {'PlaybackStatus': 'Playing'}, []) def test_track_playback_started_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_started(TlTrack()) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ((objects.PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( objects.PLAYER_IFACE, {'Metadata': '...', 'PlaybackStatus': '...'}, []) def test_track_playback_ended_changes_playback_status_and_metadata(self): self.mpris_object.Get.return_value = '...' self.mpris_frontend.track_playback_ended(TlTrack(), 0) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYER_IFACE, 'PlaybackStatus'), {}), ((objects.PLAYER_IFACE, 'Metadata'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( 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(volume=100) self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYER_IFACE, 'Volume'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( objects.PLAYER_IFACE, {'Volume': 1.0}, []) def test_seeked_event_causes_mpris_seeked_event(self): self.mpris_frontend.seeked(31000) self.mpris_object.Seeked.assert_called_with(31000000) def test_playlists_loaded_event_changes_playlist_count(self): self.mpris_object.Get.return_value = 17 self.mpris_frontend.playlists_loaded() self.assertListEqual(self.mpris_object.Get.call_args_list, [ ((objects.PLAYLISTS_IFACE, 'PlaylistCount'), {}), ]) self.mpris_object.PropertiesChanged.assert_called_with( objects.PLAYLISTS_IFACE, {'PlaylistCount': 17}, []) def test_playlist_changed_event_causes_mpris_playlist_changed_event(self): self.mpris_object.get_playlist_id.return_value = 'id-for-dummy:foo' playlist = Playlist(uri='dummy:foo', name='foo') self.mpris_frontend.playlist_changed(playlist) self.mpris_object.PlaylistChanged.assert_called_with( ('id-for-dummy:foo', 'foo', '')) mopidy-mpris-1.0.1/tests/test_extension.py000066400000000000000000000012131224322260300207220ustar00rootroot00000000000000import unittest from mopidy_mpris import Extension, frontend as frontend_lib class ExtensionTest(unittest.TestCase): def test_get_default_config(self): ext = Extension() config = ext.get_default_config() self.assertIn('[mpris]', config) self.assertIn('enabled = true', config) def test_get_config_schema(self): ext = Extension() schema = ext.get_config_schema() self.assertIn('desktop_file', schema) def test_get_frontend_classes(self): ext = Extension() frontends = ext.get_frontend_classes() self.assertIn(frontend_lib.MprisFrontend, frontends) mopidy-mpris-1.0.1/tests/test_player_interface.py000066400000000000000000001145551224322260300222400ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest import pykka try: import dbus except ImportError: dbus = False from mopidy import core from mopidy.backends import dummy from mopidy.core import PlaybackState from mopidy.models import Album, Artist, Track if dbus: from mopidy_mpris import objects PLAYING = PlaybackState.PLAYING PAUSED = PlaybackState.PAUSED STOPPED = PlaybackState.STOPPED @unittest.skipUnless(dbus, 'dbus not found') class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(config={}, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() def test_get_playback_status_is_playing_when_playing(self): self.core.playback.state = PLAYING result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Playing', result) def test_get_playback_status_is_paused_when_paused(self): self.core.playback.state = PAUSED result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Paused', result) def test_get_playback_status_is_stopped_when_stopped(self): self.core.playback.state = STOPPED result = self.mpris.Get(objects.PLAYER_IFACE, 'PlaybackStatus') self.assertEqual('Stopped', result) def test_get_loop_status_is_none_when_not_looping(self): self.core.tracklist.repeat = False self.core.tracklist.single = False 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.core.tracklist.repeat = True self.core.tracklist.single = True result = self.mpris.Get(objects.PLAYER_IFACE, 'LoopStatus') self.assertEqual('Track', result) def test_get_loop_status_is_playlist_when_looping_tracklist(self): self.core.tracklist.repeat = True self.core.tracklist.single = False 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.core.tracklist.repeat = True self.core.tracklist.single = True self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEqual(self.core.tracklist.repeat.get(), True) self.assertEqual(self.core.tracklist.single.get(), True) def test_set_loop_status_to_none_unsets_repeat_and_single(self): self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'None') self.assertEqual(self.core.tracklist.repeat.get(), False) self.assertEqual(self.core.tracklist.single.get(), False) def test_set_loop_status_to_track_sets_repeat_and_single(self): self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Track') self.assertEqual(self.core.tracklist.repeat.get(), True) self.assertEqual(self.core.tracklist.single.get(), True) def test_set_loop_status_to_playlists_sets_repeat_and_not_single(self): self.mpris.Set(objects.PLAYER_IFACE, 'LoopStatus', 'Playlist') self.assertEqual(self.core.tracklist.repeat.get(), True) self.assertEqual(self.core.tracklist.single.get(), False) def test_get_rate_is_greater_or_equal_than_minimum_rate(self): rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate') minimum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate') self.assertGreaterEqual(rate, minimum_rate) def test_get_rate_is_less_or_equal_than_maximum_rate(self): rate = self.mpris.Get(objects.PLAYER_IFACE, 'Rate') maximum_rate = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate') self.assertGreaterEqual(rate, maximum_rate) def test_set_rate_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) self.assertEqual(self.core.playback.state.get(), PLAYING) def test_set_rate_to_zero_pauses_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Set(objects.PLAYER_IFACE, 'Rate', 0) self.assertEqual(self.core.playback.state.get(), PAUSED) def test_get_shuffle_returns_true_if_random_is_active(self): self.core.tracklist.random = True result = self.mpris.Get(objects.PLAYER_IFACE, 'Shuffle') self.assertTrue(result) def test_get_shuffle_returns_false_if_random_is_inactive(self): self.core.tracklist.random = False 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.core.tracklist.random = False self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True) self.assertFalse(self.core.tracklist.random.get()) def test_set_shuffle_to_true_activates_random_mode(self): self.core.tracklist.random = False self.assertFalse(self.core.tracklist.random.get()) self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', True) self.assertTrue(self.core.tracklist.random.get()) def test_set_shuffle_to_false_deactivates_random_mode(self): self.core.tracklist.random = True self.assertTrue(self.core.tracklist.random.get()) self.mpris.Set(objects.PLAYER_IFACE, 'Shuffle', False) self.assertFalse(self.core.tracklist.random.get()) def test_get_metadata_has_trackid_even_when_no_current_track(self): result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:trackid', result.keys()) self.assertEqual(result['mpris:trackid'], '') def test_get_metadata_has_trackid_based_on_tlid(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() (tlid, track) = self.core.playback.current_tl_track.get() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:trackid', result.keys()) self.assertEqual( result['mpris:trackid'], '/com/mopidy/track/%d' % tlid) def test_get_metadata_has_track_length(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:length', result.keys()) self.assertEqual(result['mpris:length'], 40000000) def test_get_metadata_has_track_uri(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:url', result.keys()) self.assertEqual(result['xesam:url'], 'dummy:a') def test_get_metadata_has_track_title(self): self.core.tracklist.add([Track(name='a')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:title', result.keys()) self.assertEqual(result['xesam:title'], 'a') def test_get_metadata_has_track_artists(self): self.core.tracklist.add([Track(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)])]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:artist', result.keys()) self.assertEqual(result['xesam:artist'], ['a', 'b']) def test_get_metadata_has_track_album(self): self.core.tracklist.add([Track(album=Album(name='a'))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:album', result.keys()) self.assertEqual(result['xesam:album'], 'a') def test_get_metadata_has_track_album_artists(self): self.core.tracklist.add([Track(album=Album(artists=[ Artist(name='a'), Artist(name='b'), Artist(name=None)]))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:albumArtist', result.keys()) self.assertEqual(result['xesam:albumArtist'], ['a', 'b']) def test_get_metadata_use_first_album_image_as_art_url(self): # XXX Currently, the album image order isn't preserved because they # are stored as a frozenset(). We pick the first in the set, which is # sorted alphabetically, thus we get 'bar.jpg', not 'foo.jpg', which # would probably make more sense. self.core.tracklist.add([Track(album=Album(images=[ 'http://example.com/foo.jpg', 'http://example.com/bar.jpg']))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:artUrl', result.keys()) self.assertEqual(result['mpris:artUrl'], 'http://example.com/bar.jpg') def test_get_metadata_has_no_art_url_if_no_album(self): self.core.tracklist.add([Track()]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertNotIn('mpris:artUrl', result.keys()) def test_get_metadata_has_no_art_url_if_no_album_images(self): self.core.tracklist.add([Track(Album(images=[]))]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertNotIn('mpris:artUrl', result.keys()) def test_get_metadata_has_disc_number_in_album(self): self.core.tracklist.add([Track(disc_no=2)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:discNumber', result.keys()) self.assertEqual(result['xesam:discNumber'], 2) def test_get_metadata_has_track_number_in_album(self): self.core.tracklist.add([Track(track_no=7)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('xesam:trackNumber', result.keys()) self.assertEqual(result['xesam:trackNumber'], 7) def test_get_volume_should_return_volume_between_zero_and_one(self): self.core.playback.volume = None result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEqual(result, 0) self.core.playback.volume = 0 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEqual(result, 0) self.core.playback.volume = 50 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEqual(result, 0.5) self.core.playback.volume = 100 result = self.mpris.Get(objects.PLAYER_IFACE, 'Volume') self.assertEqual(result, 1) def test_set_volume_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.core.playback.volume = 0 self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) self.assertEqual(self.core.playback.volume.get(), 0) def test_set_volume_to_one_should_set_mixer_volume_to_100(self): self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 1.0) self.assertEqual(self.core.playback.volume.get(), 100) def test_set_volume_to_anything_above_one_sets_mixer_volume_to_100(self): self.mpris.Set(objects.PLAYER_IFACE, 'Volume', 2.0) self.assertEqual(self.core.playback.volume.get(), 100) def test_set_volume_to_anything_not_a_number_does_not_change_volume(self): self.core.playback.volume = 10 self.mpris.Set(objects.PLAYER_IFACE, 'Volume', None) self.assertEqual(self.core.playback.volume.get(), 10) def test_get_position_returns_time_position_in_microseconds(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(10000) result_in_microseconds = self.mpris.Get( objects.PLAYER_IFACE, 'Position') result_in_milliseconds = result_in_microseconds // 1000 self.assertGreaterEqual(result_in_milliseconds, 10000) def test_get_position_when_no_current_track_should_be_zero(self): result_in_microseconds = self.mpris.Get( objects.PLAYER_IFACE, 'Position') result_in_milliseconds = result_in_microseconds // 1000 self.assertEqual(result_in_milliseconds, 0) def test_get_minimum_rate_is_one_or_less(self): result = self.mpris.Get(objects.PLAYER_IFACE, 'MinimumRate') self.assertLessEqual(result, 1.0) def test_get_maximum_rate_is_one_or_more(self): result = self.mpris.Get(objects.PLAYER_IFACE, 'MaximumRate') self.assertGreaterEqual(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.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() 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): self.mpris.get_CanControl = lambda *_: True self.core.tracklist.add([Track(uri='dummy:a')]) self.core.tracklist.repeat = True self.core.playback.play() 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.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'CanGoNext') self.assertFalse(result) def test_can_go_previous_is_true_if_can_control_and_previous_track(self): self.mpris.get_CanControl = lambda *_: True self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() 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): self.mpris.get_CanControl = lambda *_: True self.core.tracklist.add([Track(uri='dummy:a')]) self.core.tracklist.repeat = True self.core.playback.play() result = self.mpris.Get(objects.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.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() result = self.mpris.Get(objects.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.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() self.assertTrue(self.core.playback.current_track.get()) 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.core.playback.current_track.get()) 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(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(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(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(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(objects.PLAYER_IFACE, 'CanSeek') self.assertFalse(result) def test_can_control_is_true(self): result = self.mpris.Get(objects.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.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.mpris.Next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_next_when_playing_skips_to_next_track_and_keep_playing(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), PLAYING) def test_next_when_at_end_of_list_should_stop_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Next() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_next_when_paused_should_skip_to_next_track_and_stay_paused(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PAUSED) self.mpris.Next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), PAUSED) def test_next_when_stopped_skips_to_next_track_and_stay_stopped(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.stop() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), STOPPED) def test_previous_is_ignored_if_can_go_previous_is_false(self): self.mpris.get_CanGoPrevious = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.mpris.Previous() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') def test_previous_when_playing_skips_to_prev_track_and_keep_playing(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Previous() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PLAYING) def test_previous_when_at_start_of_list_should_stop_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Previous() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_previous_when_paused_skips_to_previous_track_and_pause(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.core.playback.pause() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), PAUSED) self.mpris.Previous() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), PAUSED) def test_previous_when_stopped_skips_to_previous_track_and_stops(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.next() self.core.playback.stop() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Previous() self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.assertEqual(self.core.playback.state.get(), STOPPED) def test_pause_is_ignored_if_can_pause_is_false(self): self.mpris.get_CanPause = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_pause_when_playing_should_pause_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PAUSED) def test_pause_when_paused_has_no_effect(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PAUSED) def test_playpause_is_ignored_if_can_pause_is_false(self): self.mpris.get_CanPause = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_playpause_when_playing_should_pause_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PAUSED) def test_playpause_when_paused_should_resume_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) at_pause = self.core.playback.time_position.get() self.assertGreaterEqual(at_pause, 0) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PLAYING) after_pause = self.core.playback.time_position.get() self.assertGreaterEqual(after_pause, at_pause) def test_playpause_when_stopped_should_start_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.PlayPause() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_stop_is_ignored_if_can_control_is_false(self): self.mpris.get_CanControl = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Stop() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_stop_when_playing_should_stop_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.mpris.Stop() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_stop_when_paused_should_stop_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) self.mpris.Stop() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_play_is_ignored_if_can_play_is_false(self): self.mpris.get_CanPlay = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_play_when_stopped_starts_playback(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), PLAYING) def test_play_after_pause_resumes_from_same_position(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_pause = self.core.playback.time_position.get() self.assertGreaterEqual(before_pause, 0) self.mpris.Pause() self.assertEqual(self.core.playback.state.get(), PAUSED) at_pause = self.core.playback.time_position.get() self.assertGreaterEqual(at_pause, before_pause) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), PLAYING) after_pause = self.core.playback.time_position.get() self.assertGreaterEqual(after_pause, at_pause) def test_play_when_there_is_no_track_has_no_effect(self): self.core.tracklist.clear() self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.Play() self.assertEqual(self.core.playback.state.get(), STOPPED) def test_seek_is_ignored_if_can_seek_is_false(self): self.mpris.get_CanSeek = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_seek = self.core.playback.time_position.get() self.assertGreaterEqual(before_seek, 0) milliseconds_to_seek = 10000 microseconds_to_seek = milliseconds_to_seek * 1000 self.mpris.Seek(microseconds_to_seek) after_seek = self.core.playback.time_position.get() self.assertLessEqual(before_seek, after_seek) self.assertLess(after_seek, before_seek + milliseconds_to_seek) def test_seek_seeks_given_microseconds_forward_in_the_current_track(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_seek = self.core.playback.time_position.get() self.assertGreaterEqual(before_seek, 0) milliseconds_to_seek = 10000 microseconds_to_seek = milliseconds_to_seek * 1000 self.mpris.Seek(microseconds_to_seek) self.assertEqual(self.core.playback.state.get(), PLAYING) after_seek = self.core.playback.time_position.get() self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek) def test_seek_seeks_given_microseconds_backward_if_negative(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) before_seek = self.core.playback.time_position.get() self.assertGreaterEqual(before_seek, 20000) milliseconds_to_seek = -10000 microseconds_to_seek = milliseconds_to_seek * 1000 self.mpris.Seek(microseconds_to_seek) self.assertEqual(self.core.playback.state.get(), PLAYING) after_seek = self.core.playback.time_position.get() self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek) self.assertLess(after_seek, before_seek) def test_seek_seeks_to_start_of_track_if_new_position_is_negative(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) before_seek = self.core.playback.time_position.get() self.assertGreaterEqual(before_seek, 20000) milliseconds_to_seek = -30000 microseconds_to_seek = milliseconds_to_seek * 1000 self.mpris.Seek(microseconds_to_seek) self.assertEqual(self.core.playback.state.get(), PLAYING) after_seek = self.core.playback.time_position.get() self.assertGreaterEqual(after_seek, before_seek + milliseconds_to_seek) self.assertLess(after_seek, before_seek) self.assertGreaterEqual(after_seek, 0) def test_seek_skips_to_next_track_if_new_position_gt_track_length(self): self.core.tracklist.add([ Track(uri='dummy:a', length=40000), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.seek(20000) before_seek = self.core.playback.time_position.get() self.assertGreaterEqual(before_seek, 20000) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') milliseconds_to_seek = 50000 microseconds_to_seek = milliseconds_to_seek * 1000 self.mpris.Seek(microseconds_to_seek) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:b') after_seek = self.core.playback.time_position.get() self.assertGreaterEqual(after_seek, 0) self.assertLess(after_seek, before_seek) def test_set_position_is_ignored_if_can_seek_is_false(self): self.mpris.get_CanSeek = lambda *_: False self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_set_position = self.core.playback.time_position.get() self.assertLessEqual(before_set_position, 5000) track_id = 'a' position_to_set_in_millisec = 20000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 self.mpris.SetPosition(track_id, position_to_set_in_microsec) after_set_position = self.core.playback.time_position.get() self.assertLessEqual(before_set_position, after_set_position) self.assertLess(after_set_position, position_to_set_in_millisec) def test_set_position_sets_the_current_track_position_in_microsecs(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() before_set_position = self.core.playback.time_position.get() self.assertLessEqual(before_set_position, 5000) self.assertEqual(self.core.playback.state.get(), PLAYING) track_id = '/com/mopidy/track/0' position_to_set_in_millisec = 20000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 self.mpris.SetPosition(track_id, position_to_set_in_microsec) self.assertEqual(self.core.playback.state.get(), PLAYING) after_set_position = self.core.playback.time_position.get() self.assertGreaterEqual( after_set_position, position_to_set_in_millisec) def test_set_position_does_nothing_if_the_position_is_negative(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) before_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(before_set_position, 20000) self.assertLessEqual(before_set_position, 25000) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') track_id = '/com/mopidy/track/0' position_to_set_in_millisec = -1000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 self.mpris.SetPosition(track_id, position_to_set_in_microsec) after_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(after_set_position, before_set_position) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_set_position_does_nothing_if_position_is_gt_track_length(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) before_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(before_set_position, 20000) self.assertLessEqual(before_set_position, 25000) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') track_id = 'a' position_to_set_in_millisec = 50000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 self.mpris.SetPosition(track_id, position_to_set_in_microsec) after_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(after_set_position, before_set_position) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_set_position_is_noop_if_track_id_isnt_current_track(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(20000) before_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(before_set_position, 20000) self.assertLessEqual(before_set_position, 25000) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') track_id = 'b' position_to_set_in_millisec = 0 position_to_set_in_microsec = position_to_set_in_millisec * 1000 self.mpris.SetPosition(track_id, position_to_set_in_microsec) after_set_position = self.core.playback.time_position.get() self.assertGreaterEqual(after_set_position, before_set_position) self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') def test_open_uri_is_ignored_if_can_play_is_false(self): self.mpris.get_CanPlay = lambda *_: False self.backend.library.dummy_library = [ Track(uri='dummy:/test/uri')] self.mpris.OpenUri('dummy:/test/uri') self.assertEqual(len(self.core.tracklist.tracks.get()), 0) def test_open_uri_ignores_uris_with_unknown_uri_scheme(self): self.assertListEqual(self.core.uri_schemes.get(), ['dummy']) self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='notdummy:/test/uri')] self.mpris.OpenUri('notdummy:/test/uri') self.assertEqual(len(self.core.tracklist.tracks.get()), 0) def test_open_uri_adds_uri_to_tracklist(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] self.mpris.OpenUri('dummy:/test/uri') self.assertEqual( self.core.tracklist.tracks.get()[0].uri, 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_stopped(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.state.get(), STOPPED) self.mpris.OpenUri('dummy:/test/uri') self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual( self.core.playback.current_track.get().uri, 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_paused(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.core.playback.pause() self.assertEqual(self.core.playback.state.get(), PAUSED) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.mpris.OpenUri('dummy:/test/uri') self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual( self.core.playback.current_track.get().uri, 'dummy:/test/uri') def test_open_uri_starts_playback_of_new_track_if_playing(self): self.mpris.get_CanPlay = lambda *_: True self.backend.library.dummy_library = [Track(uri='dummy:/test/uri')] self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.core.playback.play() self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual(self.core.playback.current_track.get().uri, 'dummy:a') self.mpris.OpenUri('dummy:/test/uri') self.assertEqual(self.core.playback.state.get(), PLAYING) self.assertEqual( self.core.playback.current_track.get().uri, 'dummy:/test/uri') mopidy-mpris-1.0.1/tests/test_playlist_interface.py000066400000000000000000000141251224322260300225750ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import mock import unittest import pykka try: import dbus except ImportError: dbus = False from mopidy import core from mopidy.audio import PlaybackState from mopidy.backends import dummy from mopidy.models import Track if dbus: from mopidy_mpris import objects @unittest.skipUnless(dbus, 'dbus not found') class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(config={}, core=self.core) foo = self.core.playlists.create('foo').get() foo = foo.copy(last_modified=datetime.datetime(2012, 3, 1, 6, 0, 0)) foo = self.core.playlists.save(foo).get() bar = self.core.playlists.create('bar').get() bar = bar.copy(last_modified=datetime.datetime(2012, 2, 1, 6, 0, 0)) bar = self.core.playlists.save(bar).get() baz = self.core.playlists.create('baz').get() baz = baz.copy(last_modified=datetime.datetime(2012, 1, 1, 6, 0, 0)) baz = self.core.playlists.save(baz).get() self.playlist = baz def tearDown(self): pykka.ActorRegistry.stop_all() def test_activate_playlist_appends_tracks_to_tracklist(self): self.core.tracklist.add([ Track(uri='dummy:old-a'), Track(uri='dummy:old-b'), ]) self.playlist = self.playlist.copy(tracks=[ Track(uri='dummy:baz-a'), Track(uri='dummy:baz-b'), Track(uri='dummy:baz-c'), ]) self.playlist = self.core.playlists.save(self.playlist).get() self.assertEqual(2, self.core.tracklist.length.get()) playlists = self.mpris.GetPlaylists(0, 100, 'User', False) playlist_id = playlists[2][0] self.mpris.ActivatePlaylist(playlist_id) self.assertEqual(5, self.core.tracklist.length.get()) self.assertEqual( PlaybackState.PLAYING, self.core.playback.state.get()) self.assertEqual( self.playlist.tracks[0], self.core.playback.current_track.get()) def test_activate_empty_playlist_is_harmless(self): self.assertEqual(0, self.core.tracklist.length.get()) playlists = self.mpris.GetPlaylists(0, 100, 'User', False) playlist_id = playlists[2][0] self.mpris.ActivatePlaylist(playlist_id) self.assertEqual(0, self.core.tracklist.length.get()) self.assertEqual( PlaybackState.STOPPED, self.core.playback.state.get()) self.assertIsNone(self.core.playback.current_track.get()) def test_get_playlists_in_alphabetical_order(self): result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', False) self.assertEqual(3, len(result)) self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC4Q_', result[0][0]) self.assertEqual('bar', result[0][1]) self.assertEqual('/com/mopidy/playlist/MR2W23LZHJRGC6Q_', result[1][0]) self.assertEqual('baz', result[1][1]) self.assertEqual('/com/mopidy/playlist/MR2W23LZHJTG63Y_', result[2][0]) self.assertEqual('foo', result[2][1]) def test_get_playlists_in_reverse_alphabetical_order(self): result = self.mpris.GetPlaylists(0, 100, 'Alphabetical', True) self.assertEqual(3, len(result)) self.assertEqual('foo', result[0][1]) self.assertEqual('baz', result[1][1]) self.assertEqual('bar', result[2][1]) def test_get_playlists_in_modified_order(self): result = self.mpris.GetPlaylists(0, 100, 'Modified', False) self.assertEqual(3, len(result)) self.assertEqual('baz', result[0][1]) self.assertEqual('bar', result[1][1]) self.assertEqual('foo', result[2][1]) def test_get_playlists_in_reverse_modified_order(self): result = self.mpris.GetPlaylists(0, 100, 'Modified', True) self.assertEqual(3, len(result)) self.assertEqual('foo', result[0][1]) self.assertEqual('bar', result[1][1]) self.assertEqual('baz', result[2][1]) def test_get_playlists_in_user_order(self): result = self.mpris.GetPlaylists(0, 100, 'User', False) self.assertEqual(3, len(result)) self.assertEqual('foo', result[0][1]) self.assertEqual('bar', result[1][1]) self.assertEqual('baz', result[2][1]) def test_get_playlists_in_reverse_user_order(self): result = self.mpris.GetPlaylists(0, 100, 'User', True) self.assertEqual(3, len(result)) self.assertEqual('baz', result[0][1]) self.assertEqual('bar', result[1][1]) self.assertEqual('foo', result[2][1]) def test_get_playlists_slice_on_start_of_list(self): result = self.mpris.GetPlaylists(0, 2, 'User', False) self.assertEqual(2, len(result)) self.assertEqual('foo', result[0][1]) self.assertEqual('bar', result[1][1]) def test_get_playlists_slice_later_in_list(self): result = self.mpris.GetPlaylists(2, 2, 'User', False) self.assertEqual(1, len(result)) self.assertEqual('baz', result[0][1]) def test_get_playlist_count_returns_number_of_playlists(self): result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'PlaylistCount') self.assertEqual(3, result) def test_get_orderings_includes_alpha_modified_and_user(self): result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'Orderings') self.assertIn('Alphabetical', result) self.assertNotIn('Created', result) self.assertIn('Modified', result) self.assertNotIn('Played', result) self.assertIn('User', result) def test_get_active_playlist_does_not_return_a_playlist(self): result = self.mpris.Get(objects.PLAYLISTS_IFACE, 'ActivePlaylist') valid, playlist = result playlist_id, playlist_name, playlist_icon_uri = playlist self.assertEqual(False, valid) self.assertEqual('/', playlist_id) self.assertEqual('None', playlist_name) self.assertEqual('', playlist_icon_uri) mopidy-mpris-1.0.1/tests/test_root_interface.py000066400000000000000000000053301224322260300217150ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest import pykka try: import dbus except ImportError: dbus = False from mopidy import core from mopidy.backends import dummy if dbus: from mopidy_mpris import objects @unittest.skipUnless(dbus, 'dbus not found') class RootInterfaceTest(unittest.TestCase): def setUp(self): config = { 'mpris': { 'desktop_file': '/tmp/foo.desktop', } } objects.exit_process = mock.Mock() objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.mpris = objects.MprisObject(config=config, core=self.core) def tearDown(self): pykka.ActorRegistry.stop_all() def test_constructor_connects_to_dbus(self): self.assert_(self.mpris._connect_to_dbus.called) def test_fullscreen_returns_false(self): result = self.mpris.Get(objects.ROOT_IFACE, 'Fullscreen') self.assertFalse(result) def test_setting_fullscreen_fails_and_returns_none(self): result = self.mpris.Set(objects.ROOT_IFACE, 'Fullscreen', 'True') self.assertIsNone(result) def test_can_set_fullscreen_returns_false(self): result = self.mpris.Get(objects.ROOT_IFACE, 'CanSetFullscreen') self.assertFalse(result) def test_can_raise_returns_false(self): 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(objects.ROOT_IFACE, 'CanQuit') self.assertTrue(result) def test_quit_should_stop_all_actors(self): self.mpris.Quit() self.assert_(objects.exit_process.called) def test_has_track_list_returns_false(self): result = self.mpris.Get(objects.ROOT_IFACE, 'HasTrackList') self.assertFalse(result) def test_identify_is_mopidy(self): result = self.mpris.Get(objects.ROOT_IFACE, 'Identity') self.assertEquals(result, 'Mopidy') def test_desktop_entry_is_based_on_DESKTOP_FILE_setting(self): result = self.mpris.Get(objects.ROOT_IFACE, 'DesktopEntry') self.assertEquals(result, 'foo') def test_supported_uri_schemes_includes_backend_uri_schemes(self): 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(objects.ROOT_IFACE, 'SupportedMimeTypes') self.assertEquals(len(result), 0)