pax_global_header00006660000000000000000000000064125646242310014517gustar00rootroot0000000000000052 comment=4baaf11bc07ba15b1115e71e09c8c8c11f03052b mopidy-mpris-1.3.1/000077500000000000000000000000001256462423100141525ustar00rootroot00000000000000mopidy-mpris-1.3.1/.coveragerc000066400000000000000000000001151256462423100162700ustar00rootroot00000000000000[report] omit = */pyshared/* */python?.?/* */site-packages/nose/*mopidy-mpris-1.3.1/.gitignore000066400000000000000000000001111256462423100161330ustar00rootroot00000000000000*.egg-info *.pyc *.swp .coverage .tox/ MANIFEST build/ dist/ xunit-*.xml mopidy-mpris-1.3.1/.mailmap000066400000000000000000000001131256462423100155660ustar00rootroot00000000000000Tobias Laundal Marcin Klimczak mopidy-mpris-1.3.1/.travis.yml000066400000000000000000000011131256462423100162570ustar00rootroot00000000000000sudo: false language: python python: - "2.7_with_system_site_packages" addons: apt: sources: - mopidy-stable packages: - mopidy - python-dbus - python-indicate env: - TOX_ENV=py27 - TOX_ENV=flake8 install: - "pip install tox" script: - "tox -e $TOX_ENV" after_success: - "if [ $TOX_ENV == 'py27' ]; then pip install coveralls; coveralls; fi" branches: except: - debian notifications: irc: channels: - "irc.freenode.org#mopidy" on_success: change on_failure: change use_notice: true skip_join: true mopidy-mpris-1.3.1/LICENSE000066400000000000000000000261351256462423100151660ustar00rootroot00000000000000 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.3.1/MANIFEST.in000066400000000000000000000002741256462423100157130ustar00rootroot00000000000000include .coveragerc include .mailmap include .travis.yml include LICENSE include MANIFEST.in include README.rst include mopidy_mpris/ext.conf include tox.ini recursive-include tests *.py mopidy-mpris-1.3.1/README.rst000066400000000000000000000155041256462423100156460ustar00rootroot00000000000000************ Mopidy-MPRIS ************ .. image:: https://img.shields.io/pypi/v/Mopidy-MPRIS.svg?style=flat :target: https://pypi.python.org/pypi/Mopidy-MPRIS/ :alt: Latest PyPI version .. image:: https://img.shields.io/pypi/dm/Mopidy-MPRIS.svg?style=flat :target: https://pypi.python.org/pypi/Mopidy-MPRIS/ :alt: Number of PyPI downloads .. image:: https://img.shields.io/travis/mopidy/mopidy-mpris/master.svg?style=flat :target: https://travis-ci.org/mopidy/mopidy-mpris :alt: Travis CI build status .. image:: https://img.shields.io/coveralls/mopidy/mopidy-mpris/master.svg?style=flat :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 ============ Debian/Ubuntu/Raspbian: Install the ``mopidy-mpris`` package from `apt.mopidy.com `_:: sudo apt-get install mopidy-mpris Arch Linux: Install the ``mopidy-mpris`` package from `AUR `_:: yaourt -S mopidy-mpris Else: Install the dependencies listed above yourself, and then install the package from PyPI:: pip install Mopidy-MPRIS 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. - ``mpris/bus_type``: The type of D-Bus bus Mopidy-MPRIS should connect to. Choices include ``session`` (the default) and ``system``. Usage ===== The extension is enabled by default if all dependencies are available. Running as a service and connecting to the system bus ----------------------------------------------------- If Mopidy is running as an user without an X display, e.g. as a system service, then Mopidy-MPRIS will fail with the default config. To fix this, you can set the ``mpris/bus_type`` config value to ``system``. This will lead to Mopidy-MPRIS making itself available on the system bus instead of the logged in user's session bus. Note that few MPRIS clients will try to access MPRIS devices on the system bus, so this will give you limited functionality. 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 instances running as your own user through the Ubuntu Sound Menu without any additional setup. 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 extra/desktop/mopidy.desktop /usr/share/applications/ If the correct path to the installed ``mopidy.desktop`` file on your system isn't ``/usr/share/applications/mopidy.desktop``, 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.3.1 (2015-08-18) ------------------- - Make tests pass with Mopidy >= 1.1. v1.3.0 (2015-08-11) ------------------- - No longer allow ``Quit()`` to shut down the Mopidy server process. Mopidy has no public API for Mopidy extensions to shut down the server. v1.2.0 (2015-05-05) ------------------- - Fix crash on seek event: Update ``seeked`` event handler to accept the ``time_position`` keyword argument. Recent versions of Mopidy passes all arguments to event handlers as keyword arguments, not positional arguments. (Fixes: #12) - Fix crash on tracks longer than 35 minutes: The ``mpris:length`` attribute in the ``Metadata`` property is now typed to a 64-bit integer. - Update ``Seek()`` implementation to only pass positive numbers to Mopidy, as Mopidy 1.1 is stricter about its input validation and no longer accepts seeks to negative positions. - Add a hardcoded list of MIME types to the root interface ``SupportedMimeTypes`` property. This is a temporary solution to be able to play audio through UPnP using Rygel and Mopidy-MPRIS. Long term, mopidy/mopidy#812 is the proper solution. (Fixes: #7, PR: #11) - Add a ``mpris/bus_type`` config value for making Mopidy-MPRIS connect to the D-Bus system bus instead of the session bus. (Fixes: #9, PR: #10) - Update tests to pass with Mopidy 1.0. v1.1.1 (2014-01-22) ------------------- - Fix: Make ``OpenUri()`` work even if the tracklist is empty. v1.1.0 (2014-01-20) ------------------- - Updated extension API to match Mopidy 0.18. 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.3.1/mopidy_mpris/000077500000000000000000000000001256462423100166655ustar00rootroot00000000000000mopidy-mpris-1.3.1/mopidy_mpris/__init__.py000066400000000000000000000016401256462423100207770ustar00rootroot00000000000000from __future__ import unicode_literals import os from mopidy import config, exceptions, ext __version__ = '1.3.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() schema['bus_type'] = config.String(choices=['session', 'system']) return schema def validate_environment(self): try: import dbus # noqa except ImportError as e: raise exceptions.ExtensionError('dbus library not found', e) def setup(self, registry): from .frontend import MprisFrontend registry.add('frontend', MprisFrontend) mopidy-mpris-1.3.1/mopidy_mpris/ext.conf000066400000000000000000000001401256462423100203270ustar00rootroot00000000000000[mpris] enabled = true desktop_file = /usr/share/applications/mopidy.desktop bus_type = session mopidy-mpris-1.3.1/mopidy_mpris/frontend.py000066400000000000000000000077651256462423100210750ustar00rootroot00000000000000from __future__ import unicode_literals import logging import os from mopidy.core import CoreListener import pykka from mopidy_mpris import objects logger = logging.getLogger(__name__) 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): logger.debug('Received seeked event') time_position_in_microseconds = time_position * 1000 self.mpris_object.Seeked(time_position_in_microseconds) 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.3.1/mopidy_mpris/objects.py000066400000000000000000000457171256462423100207060ustar00rootroot00000000000000from __future__ import unicode_literals import base64 import logging import os import dbus import dbus.service from mopidy.core import PlaybackState logger = logging.getLogger(__name__) 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([ dbus.String('audio/mpeg'), dbus.String('audio/x-ms-wma'), dbus.String('audio/x-ms-asf'), dbus.String('audio/x-flac'), dbus.String('audio/flac'), dbus.String('audio/l16;channels=2;rate=44100'), dbus.String('audio/l16;rate=44100;channels=2'), ], 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): bus_type = self.config['mpris']['bus_type'] logger.debug('Connecting to D-Bus %s bus...', bus_type) if bus_type == 'system': bus = dbus.SystemBus() else: bus = dbus.SessionBus() bus_name = dbus.service.BusName(BUS_NAME, bus) logger.info('MPRIS server connected to D-Bus %s bus', bus_type) 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) # Do nothing, as we do not allow MPRIS clients to shut down Mopidy # --- 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 if new_position < 0: new_position = 0 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_CanControl(): # 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.OpenUri 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'] = dbus.Int64(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 = sorted(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.3.1/setup.cfg000066400000000000000000000001411256462423100157670ustar00rootroot00000000000000[flake8] application-import-names = mopidy_mpris,tests exclude = .git,tox [wheel] universal = 1 mopidy-mpris-1.3.1/setup.py000066400000000000000000000026341256462423100156710ustar00rootroot00000000000000from __future__ import unicode_literals import re from setuptools import find_packages, setup 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.18', '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.3.1/tests/000077500000000000000000000000001256462423100153145ustar00rootroot00000000000000mopidy-mpris-1.3.1/tests/__init__.py000066400000000000000000000000001256462423100174130ustar00rootroot00000000000000mopidy-mpris-1.3.1/tests/dummy_backend.py000066400000000000000000000077411256462423100205010ustar00rootroot00000000000000"""A dummy backend for use in tests. This backend implements the backend API in the simplest way possible. It is used in tests of the frontends. """ from __future__ import absolute_import, unicode_literals from mopidy import backend from mopidy.models import Playlist, Ref, SearchResult import pykka def create_proxy(config=None, audio=None): return DummyBackend.start(config=config, audio=audio).proxy() class DummyBackend(pykka.ThreadingActor, backend.Backend): def __init__(self, config, audio): super(DummyBackend, self).__init__() self.library = DummyLibraryProvider(backend=self) self.playback = DummyPlaybackProvider(audio=audio, backend=self) self.playlists = DummyPlaylistsProvider(backend=self) self.uri_schemes = ['dummy'] class DummyLibraryProvider(backend.LibraryProvider): root_directory = Ref.directory(uri='dummy:/', name='dummy') def __init__(self, *args, **kwargs): super(DummyLibraryProvider, self).__init__(*args, **kwargs) self.dummy_library = [] self.dummy_get_distinct_result = {} self.dummy_browse_result = {} self.dummy_find_exact_result = SearchResult() self.dummy_search_result = SearchResult() def browse(self, path): return self.dummy_browse_result.get(path, []) def get_distinct(self, field, query=None): return self.dummy_get_distinct_result.get(field, set()) def lookup(self, uri): return [t for t in self.dummy_library if uri == t.uri] def refresh(self, uri=None): pass def search(self, query=None, uris=None, exact=False): if exact: # TODO: remove uses of dummy_find_exact_result return self.dummy_find_exact_result return self.dummy_search_result class DummyPlaybackProvider(backend.PlaybackProvider): def __init__(self, *args, **kwargs): super(DummyPlaybackProvider, self).__init__(*args, **kwargs) self._uri = None self._time_position = 0 def pause(self): return True def play(self): return self._uri and self._uri != 'dummy:error' def change_track(self, track): """Pass a track with URI 'dummy:error' to force failure""" self._uri = track.uri self._time_position = 0 return True def prepare_change(self): pass def resume(self): return True def seek(self, time_position): self._time_position = time_position return True def stop(self): self._uri = None return True def get_time_position(self): return self._time_position class DummyPlaylistsProvider(backend.PlaylistsProvider): def __init__(self, backend): super(DummyPlaylistsProvider, self).__init__(backend) self._playlists = [] def set_dummy_playlists(self, playlists): """For tests using the dummy provider through an actor proxy.""" self._playlists = playlists def as_list(self): return [ Ref.playlist(uri=pl.uri, name=pl.name) for pl in self._playlists] def get_items(self, uri): playlist = self.lookup(uri) if playlist is None: return return [ Ref.track(uri=t.uri, name=t.name) for t in playlist.tracks] def lookup(self, uri): for playlist in self._playlists: if playlist.uri == uri: return playlist def refresh(self): pass def create(self, name): playlist = Playlist(name=name, uri='dummy:%s' % name) self._playlists.append(playlist) return playlist def delete(self, uri): playlist = self.lookup(uri) if playlist: self._playlists.remove(playlist) def save(self, playlist): old_playlist = self.lookup(playlist.uri) if old_playlist is not None: index = self._playlists.index(old_playlist) self._playlists[index] = playlist else: self._playlists.append(playlist) return playlist mopidy-mpris-1.3.1/tests/dummy_mixer.py000066400000000000000000000013071256462423100202260ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy import mixer import pykka def create_proxy(config=None): return DummyMixer.start(config=None).proxy() class DummyMixer(pykka.ThreadingActor, mixer.Mixer): def __init__(self, config): super(DummyMixer, self).__init__() self._volume = None self._mute = None def get_volume(self): return self._volume def set_volume(self, volume): self._volume = volume self.trigger_volume_changed(volume=volume) return True def get_mute(self): return self._mute def set_mute(self, mute): self._mute = mute self.trigger_mute_changed(mute=mute) return True mopidy-mpris-1.3.1/tests/test_events.py000066400000000000000000000076621256462423100202440ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import mock from mopidy.models import Playlist, TlTrack from mopidy_mpris import frontend, objects 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( tl_track=TlTrack(), time_position=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( tl_track=TlTrack(), time_position=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(tl_track=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( tl_track=TlTrack(), time_position=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(time_position=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=playlist) self.mpris_object.PlaylistChanged.assert_called_with( ('id-for-dummy:foo', 'foo', '')) mopidy-mpris-1.3.1/tests/test_extension.py000066400000000000000000000014471256462423100207470ustar00rootroot00000000000000import unittest import mock 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) self.assertIn('bus_type = session', config) def test_get_config_schema(self): ext = Extension() schema = ext.get_config_schema() self.assertIn('desktop_file', schema) self.assertIn('bus_type', schema) def test_get_frontend_classes(self): ext = Extension() registry = mock.Mock() ext.setup(registry) registry.add.assert_called_once_with( 'frontend', frontend_lib.MprisFrontend) mopidy-mpris-1.3.1/tests/test_player_interface.py000066400000000000000000001144211256462423100222440ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import dbus import mock from mopidy import core from mopidy.core import PlaybackState from mopidy.models import Album, Artist, Track import pykka from mopidy_mpris import objects from tests import dummy_backend, dummy_mixer PLAYING = PlaybackState.PLAYING PAUSED = PlaybackState.PAUSED STOPPED = PlaybackState.STOPPED class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy_backend.create_proxy() self.mixer = dummy_mixer.create_proxy() config = {'core': {'max_tracklist_length': 10000}} self.core = core.Core.start( config=config, backends=[self.backend], mixer=self.mixer).proxy() self.mpris = objects.MprisObject(config=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=3600000)]) self.core.playback.play() result = self.mpris.Get(objects.PLAYER_IFACE, 'Metadata') self.assertIn('mpris:length', result.keys()) self.assertEqual(result['mpris:length'], 3600000000) self.assertIsInstance(result['mpris:length'], dbus.Int64) 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): # dummy_mixer starts out with None as the volume 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_control_is_false(self): self.mpris.get_CanControl = 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.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.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.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.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.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.3.1/tests/test_playlist_interface.py000066400000000000000000000137041256462423100226130ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import mock from mopidy import core from mopidy.audio import PlaybackState from mopidy.models import Track import pykka from mopidy_mpris import objects from tests import dummy_backend class PlayerInterfaceTest(unittest.TestCase): def setUp(self): objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy_backend.create_proxy() config = {'core': {'max_tracklist_length': 10000}} self.core = core.Core.start( config=config, backends=[self.backend]).proxy() self.mpris = objects.MprisObject(config={}, core=self.core) foo = self.core.playlists.create('foo').get() foo = foo.copy(last_modified=3000000) foo = self.core.playlists.save(foo).get() bar = self.core.playlists.create('bar').get() bar = bar.copy(last_modified=2000000) bar = self.core.playlists.save(bar).get() baz = self.core.playlists.create('baz').get() baz = baz.copy(last_modified=1000000) 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.3.1/tests/test_root_interface.py000066400000000000000000000053611256462423100217350ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import mock from mopidy import core import pykka from mopidy_mpris import objects from tests import dummy_backend class RootInterfaceTest(unittest.TestCase): def setUp(self): config = { 'mpris': { 'desktop_file': '/tmp/foo.desktop', } } objects.MprisObject._connect_to_dbus = mock.Mock() self.backend = dummy_backend.create_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_does_nothing(self): self.mpris.Quit() 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_has_hardcoded_entries(self): result = self.mpris.Get(objects.ROOT_IFACE, 'SupportedMimeTypes') self.assertEqual(result, [ 'audio/mpeg', 'audio/x-ms-wma', 'audio/x-ms-asf', 'audio/x-flac', 'audio/flac', 'audio/l16;channels=2;rate=44100', 'audio/l16;rate=44100;channels=2', ]) mopidy-mpris-1.3.1/tox.ini000066400000000000000000000007461256462423100154740ustar00rootroot00000000000000[tox] envlist = py27, flake8 [testenv] sitepackages = true deps = mock mopidy==dev pytest pytest-cov pytest-xdist install_command = pip install --allow-unverified=mopidy --pre {opts} {packages} commands = py.test \ --basetemp={envtmpdir} \ --junit-xml=xunit-{envname}.xml \ --cov=mopidy_mpris --cov-report=term-missing \ {posargs} [testenv:flake8] deps = flake8 flake8-import-order skip_install = true commands = flake8