pax_global_header 0000666 0000000 0000000 00000000064 12243222603 0014506 g ustar 00root root 0000000 0000000 52 comment=990ec0cbb00e73c9b2f669280d80ef5608634703
mopidy-mpris-1.0.1/ 0000775 0000000 0000000 00000000000 12243222603 0014136 5 ustar 00root root 0000000 0000000 mopidy-mpris-1.0.1/.coveragerc 0000664 0000000 0000000 00000000115 12243222603 0016254 0 ustar 00root root 0000000 0000000 [report]
omit =
*/pyshared/*
*/python?.?/*
*/site-packages/nose/* mopidy-mpris-1.0.1/.gitignore 0000664 0000000 0000000 00000000066 12243222603 0016130 0 ustar 00root root 0000000 0000000 *.egg-info
*.pyc
*.swp
.coverage
MANIFEST
build/
dist/ mopidy-mpris-1.0.1/.travis.yml 0000664 0000000 0000000 00000001410 12243222603 0016243 0 ustar 00root root 0000000 0000000 language: 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/LICENSE 0000664 0000000 0000000 00000026135 12243222603 0015152 0 ustar 00root root 0000000 0000000
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.in 0000664 0000000 0000000 00000000124 12243222603 0015671 0 ustar 00root root 0000000 0000000 include LICENSE
include MANIFEST.in
include README.rst
include mopidy_mpris/ext.conf mopidy-mpris-1.0.1/README.rst 0000664 0000000 0000000 00000010426 12243222603 0015630 0 ustar 00root root 0000000 0000000 ************
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/ 0000775 0000000 0000000 00000000000 12243222603 0016651 5 ustar 00root root 0000000 0000000 mopidy-mpris-1.0.1/mopidy_mpris/__init__.py 0000664 0000000 0000000 00000001732 12243222603 0020765 0 ustar 00root root 0000000 0000000 from __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.conf 0000664 0000000 0000000 00000000115 12243222603 0020315 0 ustar 00root root 0000000 0000000 [mpris]
enabled = true
desktop_file = /usr/share/applications/mopidy.desktop
mopidy-mpris-1.0.1/mopidy_mpris/frontend.py 0000664 0000000 0000000 00000007677 12243222603 0021063 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000044474 12243222603 0020671 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000002635 12243222603 0015656 0 ustar 00root root 0000000 0000000 from __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/ 0000775 0000000 0000000 00000000000 12243222603 0015300 5 ustar 00root root 0000000 0000000 mopidy-mpris-1.0.1/tests/__init__.py 0000664 0000000 0000000 00000000000 12243222603 0017377 0 ustar 00root root 0000000 0000000 mopidy-mpris-1.0.1/tests/test_events.py 0000664 0000000 0000000 00000007632 12243222603 0020225 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000001213 12243222603 0020722 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000114555 12243222603 0022240 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000014125 12243222603 0022575 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000005330 12243222603 0021715 0 ustar 00root root 0000000 0000000 from __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)