Mopidy-MPRIS-3.0.1/0000775000175000017500000000000013577721047014067 5ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/.circleci/0000775000175000017500000000000013577721047015722 5ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/.circleci/config.yml0000664000175000017500000000206513577631635017720 0ustar jodaljodal00000000000000version: 2.1 orbs: codecov: codecov/codecov@1.0.5 workflows: version: 2 test: jobs: - py38 - py37 - black - check-manifest - flake8 jobs: py38: &test-template docker: - image: mopidy/ci-python:3.8 steps: - checkout - restore_cache: name: Restoring tox cache key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }} - run: name: Run tests command: | tox -e $CIRCLE_JOB -- \ --junit-xml=test-results/pytest/results.xml \ --cov-report=xml - save_cache: name: Saving tox cache key: tox-v1-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.cfg" }} paths: - ./.tox - ~/.cache/pip - codecov/upload: file: coverage.xml - store_test_results: path: test-results py37: <<: *test-template docker: - image: mopidy/ci-python:3.7 black: *test-template check-manifest: *test-template flake8: *test-template Mopidy-MPRIS-3.0.1/.mailmap0000664000175000017500000000011313577631635015506 0ustar jodaljodal00000000000000Tobias Laundal Marcin Klimczak Mopidy-MPRIS-3.0.1/CHANGELOG.rst0000664000175000017500000001104113577720676016114 0ustar jodaljodal00000000000000********* Changelog ********* v3.0.1 (2019-12-22) =================== - Fix link in README. - Fix tests by updating a mock. v3.0.0 (2019-12-22) =================== - Depend on final release of Mopidy 3.0.0. v3.0.0rc1 (2019-11-12) ====================== - Require Mopidy >= 3.0.0a4, which required the following changes: - Stop using removed ``Album.images`` field. - Use ``uris`` instead of ``uri`` when calling ``core.tracklist.add()``. - Require Python >= 3.7. No major changes required. - Update project setup. v2.0.0 (2018-12-07) =================== Major feature release. Dependencies ------------ - Require Mopidy >= 1.1. - Replace python-dbus with python-pydbus. Configuration ------------- - Remove config value ``mpris/desktop_file``. It is marked as deprecated in the config schema, so it will be ignored if present in the config file. Functionality ------------- - Ordering of playlists by playlist modification time is no longer supported. - Update UIs when playback options change: On the Mopidy event ``options_changed``, emit ``PropertiesChanged`` for ``LoopStatus``, ``Shuffle``, ``CanGoPrevious``, and ``CanGoNext``. - Update UIs when playback is stopped: On the Mopidy event ``playback_state_changed``, emit ``PropertiesChanged`` for ``PlaybackStatus`` and ``Metadata``. (Fixes: #23) - Update UIs when playlists are deleted: On the Mopidy event ``playlist_deleted``, emit``PropertiesChanged`` for ``PlaylistCount``. - Update track name when stream title changes: - The ``Metadata`` property now uses ``core.playback.get_stream_title()`` as ``xesam:title`` if available. - On the Mopidy event ``stream_title_changed``, emit ``PropertiesChanged`` for ``Metadata``. - Control mixer mute through the volume control: - The ``Volume`` property is now ``0.0`` if the mixer is muted. - When setting the ``Volume`` property to a positive value, the mixer is unmuted. - On the Mopidy event ``mute_changed``, emit ``PropertiesChanged`` for ``Volume``. - Fallback to get cover art from ``core.library.get_images()`` if ``track.album.images`` is blank. - Do not expose Mopidy's desktop file through the ``DesktopEntry`` property. If we set this to "mopidy", the basename of "mopidy.desktop", some MPRIS clients will start a new Mopidy instance in a terminal window if one clicks outside the buttons of the UI. This is probably never what the user wants. Internals --------- - Improved documentation. - Port tests to pytest. - Replace all usage of Mopidy APIs deprecated as of Mopidy 2.2. v1.4.0 (2018-04-10) =================== - Remove dependency on python-indicate and libindicate, as it is deprecated and it no longer seems to no be necessary to send a startup notification with libindicate. - Make tests pass with Mopidy >= 2.0. 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-3.0.1/LICENSE0000664000175000017500000002613513577631635015106 0ustar jodaljodal00000000000000 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-3.0.1/MANIFEST.in0000664000175000017500000000041213577631635015625 0ustar jodaljodal00000000000000include *.py include *.rst include .mailmap include LICENSE include MANIFEST.in include pyproject.toml include tox.ini recursive-include .circleci * recursive-include .github * include mopidy_*/ext.conf recursive-include tests *.py recursive-include tests/data * Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/0000775000175000017500000000000013577721047017774 5ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/PKG-INFO0000664000175000017500000003577013577721047021105 0ustar jodaljodal00000000000000Metadata-Version: 2.1 Name: Mopidy-MPRIS Version: 3.0.1 Summary: Mopidy extension for controlling Mopidy through the MPRIS D-Bus interface Home-page: https://github.com/mopidy/mopidy-mpris Author: Stein Magnus Jodal Author-email: stein.magnus@jodal.no License: Apache License, Version 2.0 Description: ************ Mopidy-MPRIS ************ .. image:: https://img.shields.io/pypi/v/Mopidy-MPRIS :target: https://pypi.org/project/Mopidy-MPRIS/ :alt: Latest PyPI version .. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpris :target: https://circleci.com/gh/mopidy/mopidy-mpris :alt: CircleCI build status .. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpris :target: https://codecov.io/gh/mopidy/mopidy-mpris :alt: Test coverage `Mopidy`_ extension for controlling Mopidy through D-Bus using the `MPRIS specification`_. Mopidy-MPRIS supports the minimum requirements of the `MPRIS specification`_ as well as the optional `Playlists interface`_. The `TrackList interface`_ is currently not supported. .. _Mopidy: https://www.mopidy.com/ .. _MPRIS specification: https://specifications.freedesktop.org/mpris-spec/latest/ .. _Playlists interface: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html .. _TrackList interface: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html Table of contents ================= - Requirements_ - Installation_ - Configuration_ - Usage_ - Clients_ - `GNOME Shell builtin`_ - `gnome-shell-extensions-mediaplayer`_ - `gnome-shell-extensions-mpris-indicator-button`_ - `Ubuntu Sound Menu`_ - `Advanced setups`_ - `Running as a service`_ - `MPRIS on the system bus`_ - `UPnP/DLNA with Rygel`_ - `Development tips`_ - `Browsing the MPRIS API with D-Feet`_ - `Testing the MPRIS API with pydbus`_ - `Project resources`_ - Credits_ Requirements ============ - `pydbus`_ D-Bus Python bindings, which again depends on ``python-gi``. Thus it is usually easiest to install with your distribution's package manager. .. _pydbus: https://github.com/LEW21/pydbus Installation ============ Install by running:: sudo python3 -m pip install Mopidy-MPRIS See https://mopidy.com/ext/mpris/ for alternative installation methods. Configuration ============= No configuration is required for the MPRIS extension to work. The following configuration values are available: - ``mpris/enabled``: If the MPRIS extension should be enabled or not. Defaults to ``true``. - ``mpris/bus_type``: The type of D-Bus bus Mopidy-MPRIS should connect to. Choices include ``session`` (the default) and ``system``. Usage ===== Once Mopidy-MPRIS has been installed and your Mopidy server has been restarted, the Mopidy-MPRIS extension announces its presence on D-Bus so that any MPRIS compatible clients on your system can interact with it. Exactly how you control Mopidy through MPRIS depends on which MPRIS client you use. Clients ======= The following clients have been tested with Mopidy-MPRIS. GNOME Shell builtin ------------------- State: Not working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, Mopidy-MPRIS 2.0.0 GNOME Shell, which is the default desktop on Ubuntu 18.04 onwards, has a builtin MPRIS client. This client seems to work well with Spotify's player, but Mopidy-MPRIS does not show up here. If you have any tips on what's missing to get this working, please open an issue. gnome-shell-extensions-mediaplayer ---------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extension-mediaplayer 63, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mediaplayer gnome-shell-extensions-mediaplayer is a quite feature rich MPRIS client built as an extension to GNOME Shell. With the improvements to Mopidy-MPRIS in v2.0, this extension works very well with Mopidy. gnome-shell-extensions-mpris-indicator-button --------------------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extensions-mpris-indicator-button 5, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mpris-indicator-button/ gnome-shell-extensions-mpris-indicator-button is a minimalistic version of gnome-shell-extensions-mediaplayer. It works with Mopidy-MPRIS, with the exception of the play/pause button not changing state when Mopidy starts playing. If you have any tips on what's missing to get the play/pause button display correctly, please open an issue. Ubuntu Sound Menu ----------------- State: Unknown Historically, Ubuntu Sound Menu was the primary target for Mopidy-MPRIS' development. Since Ubuntu 18.04 replaced Unity with GNOME Shell, this is no longer the case. It is currently unknown to what degree Mopidy-MPRIS works with old Ubuntu setups. If you run an Ubuntu setup with Unity and have tested Mopidy-MPRIS, please open an issue to share your results. Advanced setups =============== Running as a service -------------------- If you have input on how to best configure Mopidy-MPRIS when Mopidy is running as a service, please add a comment to `issue #15`_. .. _issue #15: https://github.com/mopidy/mopidy-mpris/issues/15 MPRIS on the system bus ----------------------- 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:: Few MPRIS clients will try to access MPRIS devices on the system bus, so this will give you limited functionality. For example, media keys in GNOME Shell does not work with media players that expose their MPRIS interface on the system bus instead of the user's session bus. The default setup will often not permit Mopidy to publish its service on the D-Bus system bus, causing a warning similar to this in Mopidy's log:: MPRIS frontend setup failed (g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Connection ":1.3071" is not allowed to own the service "org.mpris.MediaPlayer2.mopidy" due to security policies in the configuration file (9)) To solve this, create the file ``/etc/dbus-1/system.d/org.mpris.MediaPlayer2.mopidy.conf`` with the following contents: .. code:: xml If you run Mopidy as another user than ``mopidy``, you must update ``user="mopidy"`` in the above file accordingly. Once the file is in place, you must restart Mopidy for the change to take effect. To test the setup, you can run the following command as any user on the system to play/pause the music:: dbus-send --system --print-reply \ --dest=org.mpris.MediaPlayer2.mopidy \ /org/mpris/MediaPlayer2 \ org.mpris.MediaPlayer2.Player.PlayPause UPnP/DLNA with Rygel -------------------- Rygel_ is an application that will translate between Mopidy's MPRIS interface and UPnP. Rygel must be run on the same machine as Mopidy, but will make Mopidy controllable by any device on the local network that can control a UPnP/DLNA MediaRenderer. .. _Rygel: https://wiki.gnome.org/Projects/Rygel The setup process is approximately as follows: 1. Install Rygel. On Debian/Ubuntu/Raspbian:: sudo apt install rygel 2. Enable Rygel's MPRIS plugin. On Debian/Ubuntu/Raspbian, edit ``/etc/rygel.conf``, find the ``[MPRIS]`` section, and change ``enabled=false`` to ``enabled=true``. 3. Start Rygel. To start it as the current user:: systemctl --user start rygel To make Rygel start as the current user on boot:: systemctl --user enable rygel 4. Configure your system's firewall to allow the local network to reach Rygel. Exactly how is out of scope for this document. 5. Start Mopidy with Mopidy-MPRIS enabled. 6. If you view Rygel's log output with:: journalctl --user -feu rygel You should see a log statement similar to:: New plugin "org.mpris.MediaPlayer2.mopidy" available 6. If everything went well, you should now be able to control Mopidy from a device on your local network that can control an UPnP/DLNA MediaRenderer, for example the Android app BubbleUPnP. Alternatively, `upmpdcli combined with Mopidy-MPD`_ serves the same purpose as this setup. .. _upmpdcli combined with Mopidy-MPD: https://docs.mopidy.com/en/latest/clients/upnp/ Development tips ================ Mopidy-MPRIS has an extensive test suite, so the first step for all changes or additions is to add a test exercising the new code. However, making the tests pass doesn't ensure that what comes out on the D-Bus bus is correct. To introspect this through the bus, there's a couple of useful tools. Browsing the MPRIS API with D-Feet ---------------------------------- D-Feet is a graphical D-Bus browser. On Debian/Ubuntu systems it can be installed by running:: sudo apt install d-feet Then run the ``d-feet`` command. In the D-Feet window, select the tab corresponding to the bus you run Mopidy-MPRIS on, usually the session bus. Then search for "MediaPlayer2" to find all available MPRIS interfaces. To get the current value of a property, double-click it. To execute a method, double-click it, provide any required arguments, and click "Execute". For more information on D-Feet, see the `GNOME wiki `_. Testing the MPRIS API with pydbus --------------------------------- To use the MPRIS API directly, start Mopidy, and then run the following in a Python shell to use ``pydbus`` as an MPRIS client:: >>> import pydbus >>> bus = pydbus.SessionBus() >>> player = bus.get('org.mpris.MediaPlayer2.mopidy', '/org/mpris/MediaPlayer2') Now you can control Mopidy through the player object. To get properties from Mopidy, run for example:: >>> player.PlaybackStatus 'Playing' >>> player.Metadata {'mpris:artUrl': 'https://i.scdn.co/image/8eb49b41eeb45c1cf53e1ddfea7973d9ca257777', 'mpris:length': 342000000, 'mpris:trackid': '/com/mopidy/track/36', 'xesam:album': '65/Milo', 'xesam:albumArtist': ['Kiasmos'], 'xesam:artist': ['Rival Consoles'], 'xesam:discNumber': 1, 'xesam:title': 'Arp', 'xesam:trackNumber': 5, 'xesam:url': 'spotify:track:7CoxEEsqo3XdvUsScRV4WD'} >>> To pause Mopidy's playback through D-Bus, run:: >>> player.Pause() >>> For details on the API, please refer to the `MPRIS specification `__. Project resources ================= - `Source code `_ - `Issue tracker `_ - `Changelog `_ Credits ======= - Original author: `Stein Magnus Jodal `__ - Current maintainer: `Stein Magnus Jodal `__ - `Contributors `_ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Multimedia :: Sound/Audio :: Players Requires-Python: >=3.7 Provides-Extra: lint Provides-Extra: release Provides-Extra: test Provides-Extra: dev Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/SOURCES.txt0000664000175000017500000000140213577721047021655 0ustar jodaljodal00000000000000.mailmap CHANGELOG.rst LICENSE MANIFEST.in README.rst pyproject.toml setup.cfg setup.py tox.ini .circleci/config.yml Mopidy_MPRIS.egg-info/PKG-INFO Mopidy_MPRIS.egg-info/SOURCES.txt Mopidy_MPRIS.egg-info/dependency_links.txt Mopidy_MPRIS.egg-info/entry_points.txt Mopidy_MPRIS.egg-info/not-zip-safe Mopidy_MPRIS.egg-info/requires.txt Mopidy_MPRIS.egg-info/top_level.txt mopidy_mpris/__init__.py mopidy_mpris/ext.conf mopidy_mpris/frontend.py mopidy_mpris/interface.py mopidy_mpris/player.py mopidy_mpris/playlists.py mopidy_mpris/root.py mopidy_mpris/server.py tests/__init__.py tests/conftest.py tests/dummy_audio.py tests/dummy_backend.py tests/dummy_mixer.py tests/test_events.py tests/test_extension.py tests/test_player.py tests/test_playlists.py tests/test_root.pyMopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/dependency_links.txt0000664000175000017500000000000113577721047024042 0ustar jodaljodal00000000000000 Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/entry_points.txt0000664000175000017500000000005513577721047023272 0ustar jodaljodal00000000000000[mopidy.ext] mpris = mopidy_mpris:Extension Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/not-zip-safe0000664000175000017500000000000113577720035022216 0ustar jodaljodal00000000000000 Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/requires.txt0000664000175000017500000000046213577721047022376 0ustar jodaljodal00000000000000Mopidy>=3.0.0 Pykka>=2.0.1 setuptools pydbus>=0.6.0 [dev] black check-manifest flake8 flake8-bugbear flake8-import-order isort[pyproject] twine wheel pytest pytest-cov [lint] black check-manifest flake8 flake8-bugbear flake8-import-order isort[pyproject] [release] twine wheel [test] pytest pytest-cov Mopidy-MPRIS-3.0.1/Mopidy_MPRIS.egg-info/top_level.txt0000664000175000017500000000001513577721047022522 0ustar jodaljodal00000000000000mopidy_mpris Mopidy-MPRIS-3.0.1/PKG-INFO0000664000175000017500000003577013577721047015200 0ustar jodaljodal00000000000000Metadata-Version: 2.1 Name: Mopidy-MPRIS Version: 3.0.1 Summary: Mopidy extension for controlling Mopidy through the MPRIS D-Bus interface Home-page: https://github.com/mopidy/mopidy-mpris Author: Stein Magnus Jodal Author-email: stein.magnus@jodal.no License: Apache License, Version 2.0 Description: ************ Mopidy-MPRIS ************ .. image:: https://img.shields.io/pypi/v/Mopidy-MPRIS :target: https://pypi.org/project/Mopidy-MPRIS/ :alt: Latest PyPI version .. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpris :target: https://circleci.com/gh/mopidy/mopidy-mpris :alt: CircleCI build status .. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpris :target: https://codecov.io/gh/mopidy/mopidy-mpris :alt: Test coverage `Mopidy`_ extension for controlling Mopidy through D-Bus using the `MPRIS specification`_. Mopidy-MPRIS supports the minimum requirements of the `MPRIS specification`_ as well as the optional `Playlists interface`_. The `TrackList interface`_ is currently not supported. .. _Mopidy: https://www.mopidy.com/ .. _MPRIS specification: https://specifications.freedesktop.org/mpris-spec/latest/ .. _Playlists interface: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html .. _TrackList interface: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html Table of contents ================= - Requirements_ - Installation_ - Configuration_ - Usage_ - Clients_ - `GNOME Shell builtin`_ - `gnome-shell-extensions-mediaplayer`_ - `gnome-shell-extensions-mpris-indicator-button`_ - `Ubuntu Sound Menu`_ - `Advanced setups`_ - `Running as a service`_ - `MPRIS on the system bus`_ - `UPnP/DLNA with Rygel`_ - `Development tips`_ - `Browsing the MPRIS API with D-Feet`_ - `Testing the MPRIS API with pydbus`_ - `Project resources`_ - Credits_ Requirements ============ - `pydbus`_ D-Bus Python bindings, which again depends on ``python-gi``. Thus it is usually easiest to install with your distribution's package manager. .. _pydbus: https://github.com/LEW21/pydbus Installation ============ Install by running:: sudo python3 -m pip install Mopidy-MPRIS See https://mopidy.com/ext/mpris/ for alternative installation methods. Configuration ============= No configuration is required for the MPRIS extension to work. The following configuration values are available: - ``mpris/enabled``: If the MPRIS extension should be enabled or not. Defaults to ``true``. - ``mpris/bus_type``: The type of D-Bus bus Mopidy-MPRIS should connect to. Choices include ``session`` (the default) and ``system``. Usage ===== Once Mopidy-MPRIS has been installed and your Mopidy server has been restarted, the Mopidy-MPRIS extension announces its presence on D-Bus so that any MPRIS compatible clients on your system can interact with it. Exactly how you control Mopidy through MPRIS depends on which MPRIS client you use. Clients ======= The following clients have been tested with Mopidy-MPRIS. GNOME Shell builtin ------------------- State: Not working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, Mopidy-MPRIS 2.0.0 GNOME Shell, which is the default desktop on Ubuntu 18.04 onwards, has a builtin MPRIS client. This client seems to work well with Spotify's player, but Mopidy-MPRIS does not show up here. If you have any tips on what's missing to get this working, please open an issue. gnome-shell-extensions-mediaplayer ---------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extension-mediaplayer 63, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mediaplayer gnome-shell-extensions-mediaplayer is a quite feature rich MPRIS client built as an extension to GNOME Shell. With the improvements to Mopidy-MPRIS in v2.0, this extension works very well with Mopidy. gnome-shell-extensions-mpris-indicator-button --------------------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extensions-mpris-indicator-button 5, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mpris-indicator-button/ gnome-shell-extensions-mpris-indicator-button is a minimalistic version of gnome-shell-extensions-mediaplayer. It works with Mopidy-MPRIS, with the exception of the play/pause button not changing state when Mopidy starts playing. If you have any tips on what's missing to get the play/pause button display correctly, please open an issue. Ubuntu Sound Menu ----------------- State: Unknown Historically, Ubuntu Sound Menu was the primary target for Mopidy-MPRIS' development. Since Ubuntu 18.04 replaced Unity with GNOME Shell, this is no longer the case. It is currently unknown to what degree Mopidy-MPRIS works with old Ubuntu setups. If you run an Ubuntu setup with Unity and have tested Mopidy-MPRIS, please open an issue to share your results. Advanced setups =============== Running as a service -------------------- If you have input on how to best configure Mopidy-MPRIS when Mopidy is running as a service, please add a comment to `issue #15`_. .. _issue #15: https://github.com/mopidy/mopidy-mpris/issues/15 MPRIS on the system bus ----------------------- 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:: Few MPRIS clients will try to access MPRIS devices on the system bus, so this will give you limited functionality. For example, media keys in GNOME Shell does not work with media players that expose their MPRIS interface on the system bus instead of the user's session bus. The default setup will often not permit Mopidy to publish its service on the D-Bus system bus, causing a warning similar to this in Mopidy's log:: MPRIS frontend setup failed (g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Connection ":1.3071" is not allowed to own the service "org.mpris.MediaPlayer2.mopidy" due to security policies in the configuration file (9)) To solve this, create the file ``/etc/dbus-1/system.d/org.mpris.MediaPlayer2.mopidy.conf`` with the following contents: .. code:: xml If you run Mopidy as another user than ``mopidy``, you must update ``user="mopidy"`` in the above file accordingly. Once the file is in place, you must restart Mopidy for the change to take effect. To test the setup, you can run the following command as any user on the system to play/pause the music:: dbus-send --system --print-reply \ --dest=org.mpris.MediaPlayer2.mopidy \ /org/mpris/MediaPlayer2 \ org.mpris.MediaPlayer2.Player.PlayPause UPnP/DLNA with Rygel -------------------- Rygel_ is an application that will translate between Mopidy's MPRIS interface and UPnP. Rygel must be run on the same machine as Mopidy, but will make Mopidy controllable by any device on the local network that can control a UPnP/DLNA MediaRenderer. .. _Rygel: https://wiki.gnome.org/Projects/Rygel The setup process is approximately as follows: 1. Install Rygel. On Debian/Ubuntu/Raspbian:: sudo apt install rygel 2. Enable Rygel's MPRIS plugin. On Debian/Ubuntu/Raspbian, edit ``/etc/rygel.conf``, find the ``[MPRIS]`` section, and change ``enabled=false`` to ``enabled=true``. 3. Start Rygel. To start it as the current user:: systemctl --user start rygel To make Rygel start as the current user on boot:: systemctl --user enable rygel 4. Configure your system's firewall to allow the local network to reach Rygel. Exactly how is out of scope for this document. 5. Start Mopidy with Mopidy-MPRIS enabled. 6. If you view Rygel's log output with:: journalctl --user -feu rygel You should see a log statement similar to:: New plugin "org.mpris.MediaPlayer2.mopidy" available 6. If everything went well, you should now be able to control Mopidy from a device on your local network that can control an UPnP/DLNA MediaRenderer, for example the Android app BubbleUPnP. Alternatively, `upmpdcli combined with Mopidy-MPD`_ serves the same purpose as this setup. .. _upmpdcli combined with Mopidy-MPD: https://docs.mopidy.com/en/latest/clients/upnp/ Development tips ================ Mopidy-MPRIS has an extensive test suite, so the first step for all changes or additions is to add a test exercising the new code. However, making the tests pass doesn't ensure that what comes out on the D-Bus bus is correct. To introspect this through the bus, there's a couple of useful tools. Browsing the MPRIS API with D-Feet ---------------------------------- D-Feet is a graphical D-Bus browser. On Debian/Ubuntu systems it can be installed by running:: sudo apt install d-feet Then run the ``d-feet`` command. In the D-Feet window, select the tab corresponding to the bus you run Mopidy-MPRIS on, usually the session bus. Then search for "MediaPlayer2" to find all available MPRIS interfaces. To get the current value of a property, double-click it. To execute a method, double-click it, provide any required arguments, and click "Execute". For more information on D-Feet, see the `GNOME wiki `_. Testing the MPRIS API with pydbus --------------------------------- To use the MPRIS API directly, start Mopidy, and then run the following in a Python shell to use ``pydbus`` as an MPRIS client:: >>> import pydbus >>> bus = pydbus.SessionBus() >>> player = bus.get('org.mpris.MediaPlayer2.mopidy', '/org/mpris/MediaPlayer2') Now you can control Mopidy through the player object. To get properties from Mopidy, run for example:: >>> player.PlaybackStatus 'Playing' >>> player.Metadata {'mpris:artUrl': 'https://i.scdn.co/image/8eb49b41eeb45c1cf53e1ddfea7973d9ca257777', 'mpris:length': 342000000, 'mpris:trackid': '/com/mopidy/track/36', 'xesam:album': '65/Milo', 'xesam:albumArtist': ['Kiasmos'], 'xesam:artist': ['Rival Consoles'], 'xesam:discNumber': 1, 'xesam:title': 'Arp', 'xesam:trackNumber': 5, 'xesam:url': 'spotify:track:7CoxEEsqo3XdvUsScRV4WD'} >>> To pause Mopidy's playback through D-Bus, run:: >>> player.Pause() >>> For details on the API, please refer to the `MPRIS specification `__. Project resources ================= - `Source code `_ - `Issue tracker `_ - `Changelog `_ Credits ======= - Original author: `Stein Magnus Jodal `__ - Current maintainer: `Stein Magnus Jodal `__ - `Contributors `_ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: No Input/Output (Daemon) Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Multimedia :: Sound/Audio :: Players Requires-Python: >=3.7 Provides-Extra: lint Provides-Extra: release Provides-Extra: test Provides-Extra: dev Mopidy-MPRIS-3.0.1/README.rst0000664000175000017500000002624713577720311015562 0ustar jodaljodal00000000000000************ Mopidy-MPRIS ************ .. image:: https://img.shields.io/pypi/v/Mopidy-MPRIS :target: https://pypi.org/project/Mopidy-MPRIS/ :alt: Latest PyPI version .. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpris :target: https://circleci.com/gh/mopidy/mopidy-mpris :alt: CircleCI build status .. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpris :target: https://codecov.io/gh/mopidy/mopidy-mpris :alt: Test coverage `Mopidy`_ extension for controlling Mopidy through D-Bus using the `MPRIS specification`_. Mopidy-MPRIS supports the minimum requirements of the `MPRIS specification`_ as well as the optional `Playlists interface`_. The `TrackList interface`_ is currently not supported. .. _Mopidy: https://www.mopidy.com/ .. _MPRIS specification: https://specifications.freedesktop.org/mpris-spec/latest/ .. _Playlists interface: https://specifications.freedesktop.org/mpris-spec/latest/Playlists_Interface.html .. _TrackList interface: https://specifications.freedesktop.org/mpris-spec/latest/Track_List_Interface.html Table of contents ================= - Requirements_ - Installation_ - Configuration_ - Usage_ - Clients_ - `GNOME Shell builtin`_ - `gnome-shell-extensions-mediaplayer`_ - `gnome-shell-extensions-mpris-indicator-button`_ - `Ubuntu Sound Menu`_ - `Advanced setups`_ - `Running as a service`_ - `MPRIS on the system bus`_ - `UPnP/DLNA with Rygel`_ - `Development tips`_ - `Browsing the MPRIS API with D-Feet`_ - `Testing the MPRIS API with pydbus`_ - `Project resources`_ - Credits_ Requirements ============ - `pydbus`_ D-Bus Python bindings, which again depends on ``python-gi``. Thus it is usually easiest to install with your distribution's package manager. .. _pydbus: https://github.com/LEW21/pydbus Installation ============ Install by running:: sudo python3 -m pip install Mopidy-MPRIS See https://mopidy.com/ext/mpris/ for alternative installation methods. Configuration ============= No configuration is required for the MPRIS extension to work. The following configuration values are available: - ``mpris/enabled``: If the MPRIS extension should be enabled or not. Defaults to ``true``. - ``mpris/bus_type``: The type of D-Bus bus Mopidy-MPRIS should connect to. Choices include ``session`` (the default) and ``system``. Usage ===== Once Mopidy-MPRIS has been installed and your Mopidy server has been restarted, the Mopidy-MPRIS extension announces its presence on D-Bus so that any MPRIS compatible clients on your system can interact with it. Exactly how you control Mopidy through MPRIS depends on which MPRIS client you use. Clients ======= The following clients have been tested with Mopidy-MPRIS. GNOME Shell builtin ------------------- State: Not working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, Mopidy-MPRIS 2.0.0 GNOME Shell, which is the default desktop on Ubuntu 18.04 onwards, has a builtin MPRIS client. This client seems to work well with Spotify's player, but Mopidy-MPRIS does not show up here. If you have any tips on what's missing to get this working, please open an issue. gnome-shell-extensions-mediaplayer ---------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extension-mediaplayer 63, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mediaplayer gnome-shell-extensions-mediaplayer is a quite feature rich MPRIS client built as an extension to GNOME Shell. With the improvements to Mopidy-MPRIS in v2.0, this extension works very well with Mopidy. gnome-shell-extensions-mpris-indicator-button --------------------------------------------- State: Working Tested versions: Ubuntu 18.10, GNOME Shell 3.30.1-2ubuntu1, gnome-shell-extensions-mpris-indicator-button 5, Mopidy-MPRIS 2.0.0 Website: https://github.com/JasonLG1979/gnome-shell-extensions-mpris-indicator-button/ gnome-shell-extensions-mpris-indicator-button is a minimalistic version of gnome-shell-extensions-mediaplayer. It works with Mopidy-MPRIS, with the exception of the play/pause button not changing state when Mopidy starts playing. If you have any tips on what's missing to get the play/pause button display correctly, please open an issue. Ubuntu Sound Menu ----------------- State: Unknown Historically, Ubuntu Sound Menu was the primary target for Mopidy-MPRIS' development. Since Ubuntu 18.04 replaced Unity with GNOME Shell, this is no longer the case. It is currently unknown to what degree Mopidy-MPRIS works with old Ubuntu setups. If you run an Ubuntu setup with Unity and have tested Mopidy-MPRIS, please open an issue to share your results. Advanced setups =============== Running as a service -------------------- If you have input on how to best configure Mopidy-MPRIS when Mopidy is running as a service, please add a comment to `issue #15`_. .. _issue #15: https://github.com/mopidy/mopidy-mpris/issues/15 MPRIS on the system bus ----------------------- 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:: Few MPRIS clients will try to access MPRIS devices on the system bus, so this will give you limited functionality. For example, media keys in GNOME Shell does not work with media players that expose their MPRIS interface on the system bus instead of the user's session bus. The default setup will often not permit Mopidy to publish its service on the D-Bus system bus, causing a warning similar to this in Mopidy's log:: MPRIS frontend setup failed (g-dbus-error-quark: GDBus.Error:org.freedesktop.DBus.Error.AccessDenied: Connection ":1.3071" is not allowed to own the service "org.mpris.MediaPlayer2.mopidy" due to security policies in the configuration file (9)) To solve this, create the file ``/etc/dbus-1/system.d/org.mpris.MediaPlayer2.mopidy.conf`` with the following contents: .. code:: xml If you run Mopidy as another user than ``mopidy``, you must update ``user="mopidy"`` in the above file accordingly. Once the file is in place, you must restart Mopidy for the change to take effect. To test the setup, you can run the following command as any user on the system to play/pause the music:: dbus-send --system --print-reply \ --dest=org.mpris.MediaPlayer2.mopidy \ /org/mpris/MediaPlayer2 \ org.mpris.MediaPlayer2.Player.PlayPause UPnP/DLNA with Rygel -------------------- Rygel_ is an application that will translate between Mopidy's MPRIS interface and UPnP. Rygel must be run on the same machine as Mopidy, but will make Mopidy controllable by any device on the local network that can control a UPnP/DLNA MediaRenderer. .. _Rygel: https://wiki.gnome.org/Projects/Rygel The setup process is approximately as follows: 1. Install Rygel. On Debian/Ubuntu/Raspbian:: sudo apt install rygel 2. Enable Rygel's MPRIS plugin. On Debian/Ubuntu/Raspbian, edit ``/etc/rygel.conf``, find the ``[MPRIS]`` section, and change ``enabled=false`` to ``enabled=true``. 3. Start Rygel. To start it as the current user:: systemctl --user start rygel To make Rygel start as the current user on boot:: systemctl --user enable rygel 4. Configure your system's firewall to allow the local network to reach Rygel. Exactly how is out of scope for this document. 5. Start Mopidy with Mopidy-MPRIS enabled. 6. If you view Rygel's log output with:: journalctl --user -feu rygel You should see a log statement similar to:: New plugin "org.mpris.MediaPlayer2.mopidy" available 6. If everything went well, you should now be able to control Mopidy from a device on your local network that can control an UPnP/DLNA MediaRenderer, for example the Android app BubbleUPnP. Alternatively, `upmpdcli combined with Mopidy-MPD`_ serves the same purpose as this setup. .. _upmpdcli combined with Mopidy-MPD: https://docs.mopidy.com/en/latest/clients/upnp/ Development tips ================ Mopidy-MPRIS has an extensive test suite, so the first step for all changes or additions is to add a test exercising the new code. However, making the tests pass doesn't ensure that what comes out on the D-Bus bus is correct. To introspect this through the bus, there's a couple of useful tools. Browsing the MPRIS API with D-Feet ---------------------------------- D-Feet is a graphical D-Bus browser. On Debian/Ubuntu systems it can be installed by running:: sudo apt install d-feet Then run the ``d-feet`` command. In the D-Feet window, select the tab corresponding to the bus you run Mopidy-MPRIS on, usually the session bus. Then search for "MediaPlayer2" to find all available MPRIS interfaces. To get the current value of a property, double-click it. To execute a method, double-click it, provide any required arguments, and click "Execute". For more information on D-Feet, see the `GNOME wiki `_. Testing the MPRIS API with pydbus --------------------------------- To use the MPRIS API directly, start Mopidy, and then run the following in a Python shell to use ``pydbus`` as an MPRIS client:: >>> import pydbus >>> bus = pydbus.SessionBus() >>> player = bus.get('org.mpris.MediaPlayer2.mopidy', '/org/mpris/MediaPlayer2') Now you can control Mopidy through the player object. To get properties from Mopidy, run for example:: >>> player.PlaybackStatus 'Playing' >>> player.Metadata {'mpris:artUrl': 'https://i.scdn.co/image/8eb49b41eeb45c1cf53e1ddfea7973d9ca257777', 'mpris:length': 342000000, 'mpris:trackid': '/com/mopidy/track/36', 'xesam:album': '65/Milo', 'xesam:albumArtist': ['Kiasmos'], 'xesam:artist': ['Rival Consoles'], 'xesam:discNumber': 1, 'xesam:title': 'Arp', 'xesam:trackNumber': 5, 'xesam:url': 'spotify:track:7CoxEEsqo3XdvUsScRV4WD'} >>> To pause Mopidy's playback through D-Bus, run:: >>> player.Pause() >>> For details on the API, please refer to the `MPRIS specification `__. Project resources ================= - `Source code `_ - `Issue tracker `_ - `Changelog `_ Credits ======= - Original author: `Stein Magnus Jodal `__ - Current maintainer: `Stein Magnus Jodal `__ - `Contributors `_ Mopidy-MPRIS-3.0.1/mopidy_mpris/0000775000175000017500000000000013577721047016602 5ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/mopidy_mpris/__init__.py0000664000175000017500000000162513577631635020722 0ustar jodaljodal00000000000000import pathlib import pkg_resources from mopidy import config, exceptions, ext __version__ = pkg_resources.get_distribution("Mopidy-MPRIS").version class Extension(ext.Extension): dist_name = "Mopidy-MPRIS" ext_name = "mpris" version = __version__ def get_default_config(self): return config.read(pathlib.Path(__file__).parent / "ext.conf") def get_config_schema(self): schema = super().get_config_schema() schema["desktop_file"] = config.Deprecated() schema["bus_type"] = config.String(choices=["session", "system"]) return schema def validate_environment(self): try: import pydbus # noqa except ImportError as e: raise exceptions.ExtensionError("pydbus library not found", e) def setup(self, registry): from .frontend import MprisFrontend registry.add("frontend", MprisFrontend) Mopidy-MPRIS-3.0.1/mopidy_mpris/ext.conf0000664000175000017500000000005213577631635020251 0ustar jodaljodal00000000000000[mpris] enabled = true bus_type = session Mopidy-MPRIS-3.0.1/mopidy_mpris/frontend.py0000664000175000017500000000625513577631635021006 0ustar jodaljodal00000000000000import logging import pykka from mopidy.core import CoreListener from mopidy_mpris.playlists import get_playlist_id from mopidy_mpris.server import Server logger = logging.getLogger(__name__) class MprisFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super().__init__() self.config = config self.core = core self.mpris = None def on_start(self): try: self.mpris = Server(self.config, self.core) self.mpris.publish() 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: self.mpris.unpublish() self.mpris = None logger.debug("Removed MPRIS object from D-Bus connection") def on_event(self, event, **kwargs): logger.debug("Received %s event", event) if self.mpris is None: return return super().on_event(event, **kwargs) def track_playback_paused(self, tl_track, time_position): _emit_properties_changed(self.mpris.player, ["PlaybackStatus"]) def track_playback_resumed(self, tl_track, time_position): _emit_properties_changed(self.mpris.player, ["PlaybackStatus"]) def track_playback_started(self, tl_track): _emit_properties_changed( self.mpris.player, ["PlaybackStatus", "Metadata"] ) def track_playback_ended(self, tl_track, time_position): _emit_properties_changed( self.mpris.player, ["PlaybackStatus", "Metadata"] ) def playback_state_changed(self, old_state, new_state): _emit_properties_changed( self.mpris.player, ["PlaybackStatus", "Metadata"] ) def tracklist_changed(self): pass # TODO Implement if adding tracklist support def playlists_loaded(self): _emit_properties_changed(self.mpris.playlists, ["PlaylistCount"]) def playlist_changed(self, playlist): playlist_id = get_playlist_id(playlist.uri) self.mpris.playlists.PlaylistChanged(playlist_id, playlist.name, "") def playlist_deleted(self, uri): _emit_properties_changed(self.mpris.playlists, ["PlaylistCount"]) def options_changed(self): _emit_properties_changed( self.mpris.player, ["LoopStatus", "Shuffle", "CanGoPrevious", "CanGoNext"], ) def volume_changed(self, volume): _emit_properties_changed(self.mpris.player, ["Volume"]) def mute_changed(self, mute): _emit_properties_changed(self.mpris.player, ["Volume"]) def seeked(self, time_position): time_position_in_microseconds = time_position * 1000 self.mpris.player.Seeked(time_position_in_microseconds) def stream_title_changed(self, title): _emit_properties_changed(self.mpris.player, ["Metadata"]) def _emit_properties_changed(interface, changed_properties): props_with_new_values = [ (p, getattr(interface, p)) for p in changed_properties ] interface.PropertiesChanged( interface.INTERFACE, dict(props_with_new_values), [] ) Mopidy-MPRIS-3.0.1/mopidy_mpris/interface.py0000664000175000017500000000064513577631635021124 0ustar jodaljodal00000000000000import logging from pydbus.generic import signal logger = logging.getLogger(__name__) # This should be kept in sync with mopidy.internal.log.TRACE_LOG_LEVEL TRACE_LOG_LEVEL = 5 class Interface: def __init__(self, config, core): self.config = config self.core = core PropertiesChanged = signal() def log_trace(self, *args, **kwargs): logger.log(TRACE_LOG_LEVEL, *args, **kwargs) Mopidy-MPRIS-3.0.1/mopidy_mpris/player.py0000664000175000017500000003221513577631635020456 0ustar jodaljodal00000000000000"""Implementation of org.mpris.MediaPlayer2.Player interface. https://specifications.freedesktop.org/mpris-spec/2.2/Player_Interface.html """ import logging from gi.repository.GLib import Variant from pydbus.generic import signal from mopidy.core import PlaybackState from mopidy_mpris.interface import Interface logger = logging.getLogger(__name__) class Player(Interface): """ """ INTERFACE = "org.mpris.MediaPlayer2.Player" # To override from tests. _CanControl = True def Next(self): logger.debug("%s.Next called", self.INTERFACE) if not self.CanGoNext: logger.debug("%s.Next not allowed", self.INTERFACE) return self.core.playback.next().get() def Previous(self): logger.debug("%s.Previous called", self.INTERFACE) if not self.CanGoPrevious: logger.debug("%s.Previous not allowed", self.INTERFACE) return self.core.playback.previous().get() def Pause(self): logger.debug("%s.Pause called", self.INTERFACE) if not self.CanPause: logger.debug("%s.Pause not allowed", self.INTERFACE) return self.core.playback.pause().get() def PlayPause(self): logger.debug("%s.PlayPause called", self.INTERFACE) if not self.CanPause: logger.debug("%s.PlayPause not allowed", self.INTERFACE) return state = self.core.playback.get_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() def Stop(self): logger.debug("%s.Stop called", self.INTERFACE) if not self.CanControl: logger.debug("%s.Stop not allowed", self.INTERFACE) return self.core.playback.stop().get() def Play(self): logger.debug("%s.Play called", self.INTERFACE) if not self.CanPlay: logger.debug("%s.Play not allowed", self.INTERFACE) return state = self.core.playback.get_state().get() if state == PlaybackState.PAUSED: self.core.playback.resume().get() else: self.core.playback.play().get() def Seek(self, offset): logger.debug("%s.Seek called", self.INTERFACE) if not self.CanSeek: logger.debug("%s.Seek not allowed", self.INTERFACE) return offset_in_milliseconds = offset // 1000 current_position = self.core.playback.get_time_position().get() new_position = current_position + offset_in_milliseconds if new_position < 0: new_position = 0 self.core.playback.seek(new_position).get() def SetPosition(self, track_id, position): logger.debug("%s.SetPosition called", self.INTERFACE) if not self.CanSeek: logger.debug("%s.SetPosition not allowed", self.INTERFACE) return position = position // 1000 current_tl_track = self.core.playback.get_current_tl_track().get() if current_tl_track is None: return if track_id != get_track_id(current_tl_track.tlid): return if position < 0: return if current_tl_track.track.length < position: return self.core.playback.seek(position).get() def OpenUri(self, uri): logger.debug("%s.OpenUri called", self.INTERFACE) if not self.CanControl: # NOTE The spec does not explicitly require this check, but # guarding the other methods doesn't help much if OpenUri is open # for use. logger.debug("%s.OpenUri not allowed", self.INTERFACE) 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(uris=[uri]).get() if tl_tracks: self.core.playback.play(tlid=tl_tracks[0].tlid).get() else: logger.debug('Track with URI "%s" not found in library.', uri) Seeked = signal() @property def PlaybackStatus(self): self.log_trace("Getting %s.PlaybackStatus", self.INTERFACE) state = self.core.playback.get_state().get() if state == PlaybackState.PLAYING: return "Playing" elif state == PlaybackState.PAUSED: return "Paused" elif state == PlaybackState.STOPPED: return "Stopped" @property def LoopStatus(self): self.log_trace("Getting %s.LoopStatus", self.INTERFACE) repeat = self.core.tracklist.get_repeat().get() single = self.core.tracklist.get_single().get() if not repeat: return "None" else: if single: return "Track" else: return "Playlist" @LoopStatus.setter def LoopStatus(self, value): if not self.CanControl: logger.debug("Setting %s.LoopStatus not allowed", self.INTERFACE) return logger.debug("Setting %s.LoopStatus to %s", self.INTERFACE, value) if value == "None": self.core.tracklist.set_repeat(False) self.core.tracklist.set_single(False) elif value == "Track": self.core.tracklist.set_repeat(True) self.core.tracklist.set_single(True) elif value == "Playlist": self.core.tracklist.set_repeat(True) self.core.tracklist.set_single(False) @property def Rate(self): self.log_trace("Getting %s.Rate", self.INTERFACE) return 1.0 @Rate.setter def Rate(self, value): if not self.CanControl: # NOTE The spec does not explicitly require this check, but it was # added to be consistent with all the other property setters. logger.debug("Setting %s.Rate not allowed", self.INTERFACE) return logger.debug("Setting %s.Rate to %s", self.INTERFACE, value) if value == 0: self.Pause() @property def Shuffle(self): self.log_trace("Getting %s.Shuffle", self.INTERFACE) return self.core.tracklist.get_random().get() @Shuffle.setter def Shuffle(self, value): if not self.CanControl: logger.debug("Setting %s.Shuffle not allowed", self.INTERFACE) return logger.debug("Setting %s.Shuffle to %s", self.INTERFACE, value) self.core.tracklist.set_random(bool(value)) @property def Metadata(self): self.log_trace("Getting %s.Metadata", self.INTERFACE) current_tl_track = self.core.playback.get_current_tl_track().get() stream_title = self.core.playback.get_stream_title().get() if current_tl_track is None: return {} else: (tlid, track) = current_tl_track track_id = get_track_id(tlid) res = {"mpris:trackid": Variant("o", track_id)} if track.length: res["mpris:length"] = Variant("x", track.length * 1000) if track.uri: res["xesam:url"] = Variant("s", track.uri) if stream_title or track.name: res["xesam:title"] = Variant("s", stream_title or track.name) if track.artists: artists = list(track.artists) artists.sort(key=lambda a: a.name or "") res["xesam:artist"] = Variant( "as", [a.name for a in artists if a.name] ) if track.album and track.album.name: res["xesam:album"] = Variant("s", track.album.name) if track.album and track.album.artists: artists = list(track.album.artists) artists.sort(key=lambda a: a.name or "") res["xesam:albumArtist"] = Variant( "as", [a.name for a in artists if a.name] ) art_url = self._get_art_url(track) if art_url: res["mpris:artUrl"] = Variant("s", art_url) if track.disc_no: res["xesam:discNumber"] = Variant("i", track.disc_no) if track.track_no: res["xesam:trackNumber"] = Variant("i", track.track_no) return res def _get_art_url(self, track): images = self.core.library.get_images([track.uri]).get() if images[track.uri]: largest_image = sorted( images[track.uri], key=lambda i: i.width, reverse=True )[0] return largest_image.uri @property def Volume(self): self.log_trace("Getting %s.Volume", self.INTERFACE) mute = self.core.mixer.get_mute().get() volume = self.core.mixer.get_volume().get() if volume is None or mute is True: return 0 return volume / 100.0 @Volume.setter def Volume(self, value): if not self.CanControl: logger.debug("Setting %s.Volume not allowed", self.INTERFACE) return logger.debug("Setting %s.Volume to %s", self.INTERFACE, value) if value is None: return if value < 0: value = 0 elif value > 1: value = 1 self.core.mixer.set_volume(int(value * 100)) if value > 0: self.core.mixer.set_mute(False) @property def Position(self): self.log_trace("Getting %s.Position", self.INTERFACE) return self.core.playback.get_time_position().get() * 1000 MinimumRate = 1.0 MaximumRate = 1.0 @property def CanGoNext(self): self.log_trace("Getting %s.CanGoNext", self.INTERFACE) if not self.CanControl: return False current_tlid = self.core.playback.get_current_tlid().get() next_tlid = self.core.tracklist.get_next_tlid().get() return next_tlid != current_tlid @property def CanGoPrevious(self): self.log_trace("Getting %s.CanGoPrevious", self.INTERFACE) if not self.CanControl: return False current_tlid = self.core.playback.get_current_tlid().get() previous_tlid = self.core.tracklist.get_previous_tlid().get() return previous_tlid != current_tlid @property def CanPlay(self): self.log_trace("Getting %s.CanPlay", self.INTERFACE) if not self.CanControl: return False current_tlid = self.core.playback.get_current_tlid().get() next_tlid = self.core.tracklist.get_next_tlid().get() return current_tlid is not None or next_tlid is not None @property def CanPause(self): self.log_trace("Getting %s.CanPause", self.INTERFACE) if not self.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 @property def CanSeek(self): self.log_trace("Getting %s.CanSeek", self.INTERFACE) if not self.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 @property def CanControl(self): # NOTE This could be a setting for the end user to change. return self._CanControl def get_track_id(tlid): return "/com/mopidy/track/%d" % tlid def get_track_tlid(track_id): assert track_id.startswith("/com/mopidy/track/") return track_id.split("/")[-1] Mopidy-MPRIS-3.0.1/mopidy_mpris/playlists.py0000664000175000017500000000743213577631635021211 0ustar jodaljodal00000000000000"""Implementation of org.mpris.MediaPlayer2.Playlists interface. https://specifications.freedesktop.org/mpris-spec/2.2/Playlists_Interface.html """ import base64 import logging from typing import Union from pydbus.generic import signal from mopidy_mpris.interface import Interface logger = logging.getLogger(__name__) class Playlists(Interface): """ """ INTERFACE = "org.mpris.MediaPlayer2.Playlists" def ActivatePlaylist(self, playlist_id): logger.debug( "%s.ActivatePlaylist(%r) called", self.INTERFACE, playlist_id ) playlist_uri = 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(tlid=tl_tracks[0].tlid).get() def GetPlaylists(self, index, max_count, order, reverse): logger.debug( "%s.GetPlaylists(%r, %r, %r, %r) called", self.INTERFACE, index, max_count, order, reverse, ) playlists = self.core.playlists.as_list().get() if order == "Alphabetical": playlists.sort(key=lambda p: p.name, reverse=reverse) elif order == "User" and reverse: playlists.reverse() slice_end = index + max_count playlists = playlists[index:slice_end] results = [(get_playlist_id(p.uri), p.name, "") for p in playlists] return results PlaylistChanged = signal() @property def PlaylistCount(self): self.log_trace("Getting %s.PlaylistCount", self.INTERFACE) return len(self.core.playlists.as_list().get()) @property def Orderings(self): self.log_trace("Getting %s.Orderings", self.INTERFACE) return [ "Alphabetical", # Order by playlist.name "User", # Don't change order ] @property def ActivePlaylist(self): self.log_trace("Getting %s.ActivePlaylist", self.INTERFACE) playlist_is_valid = False playlist = ("/", "None", "") return (playlist_is_valid, playlist) def get_playlist_id(playlist_uri: Union[str, bytes]) -> str: # 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 "_". if isinstance(playlist_uri, str): playlist_uri = playlist_uri.encode() encoded_uri = base64.b32encode(playlist_uri).decode().replace("=", "_") return "/com/mopidy/playlist/%s" % encoded_uri def get_playlist_uri(playlist_id: Union[str, bytes]) -> str: if isinstance(playlist_id, bytes): playlist_id = playlist_id.decode() encoded_uri = playlist_id.split("/")[-1].replace("_", "=").encode() return base64.b32decode(encoded_uri).decode() Mopidy-MPRIS-3.0.1/mopidy_mpris/root.py0000664000175000017500000000546713577631635020156 0ustar jodaljodal00000000000000"""Implementation of org.mpris.MediaPlayer2 interface. https://specifications.freedesktop.org/mpris-spec/2.2/Media_Player.html """ import logging from mopidy_mpris.interface import Interface logger = logging.getLogger(__name__) class Root(Interface): """ """ INTERFACE = "org.mpris.MediaPlayer2" def Raise(self): logger.debug("%s.Raise called", self.INTERFACE) # Do nothing, as we do not have a GUI def Quit(self): logger.debug("%s.Quit called", self.INTERFACE) # Do nothing, as we do not allow MPRIS clients to shut down Mopidy CanQuit = False @property def Fullscreen(self): self.log_trace("Getting %s.Fullscreen", self.INTERFACE) return False @Fullscreen.setter def Fullscreen(self, value): logger.debug("Setting %s.Fullscreen to %s", self.INTERFACE, value) pass CanSetFullscreen = False CanRaise = False HasTrackList = False # NOTE Change if adding optional track list support Identity = "Mopidy" @property def DesktopEntry(self): self.log_trace("Getting %s.DesktopEntry", self.INTERFACE) # This property is optional to expose. If we set this to "mopidy", the # basename of "mopidy.desktop", some MPRIS clients will start a new # Mopidy instance in a terminal window if one clicks outside the # buttons of the UI. This is probably never what the user wants. return "" @property def SupportedUriSchemes(self): self.log_trace("Getting %s.SupportedUriSchemes", self.INTERFACE) return self.core.get_uri_schemes().get() @property def SupportedMimeTypes(self): # NOTE Return MIME types supported by local backend if support for # reporting supported MIME types is added. self.log_trace("Getting %s.SupportedMimeTypes", self.INTERFACE) return [ "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-3.0.1/mopidy_mpris/server.py0000664000175000017500000000230713577631635020467 0ustar jodaljodal00000000000000import logging import pydbus from mopidy_mpris.player import Player from mopidy_mpris.playlists import Playlists from mopidy_mpris.root import Root logger = logging.getLogger(__name__) class Server: def __init__(self, config, core): self.config = config self.core = core self.root = Root(config, core) self.player = Player(config, core) self.playlists = Playlists(config, core) self._publication_token = None def publish(self): bus_type = self.config["mpris"]["bus_type"] logger.debug("Connecting to D-Bus %s bus...", bus_type) if bus_type == "system": bus = pydbus.SystemBus() else: bus = pydbus.SessionBus() logger.info("MPRIS server connected to D-Bus %s bus", bus_type) self._publication_token = bus.publish( "org.mpris.MediaPlayer2.mopidy", ("/org/mpris/MediaPlayer2", self.root), ("/org/mpris/MediaPlayer2", self.player), ("/org/mpris/MediaPlayer2", self.playlists), ) def unpublish(self): if self._publication_token: self._publication_token.unpublish() self._publication_token = None Mopidy-MPRIS-3.0.1/pyproject.toml0000664000175000017500000000052613577631635017011 0ustar jodaljodal00000000000000[build-system] requires = ["setuptools >= 30.3.0", "wheel"] [tool.black] target-version = ["py37", "py38"] line-length = 80 [tool.isort] multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true line_length = 88 known_tests = "tests" sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,TESTS,LOCALFOLDER" Mopidy-MPRIS-3.0.1/setup.cfg0000664000175000017500000000267313577721047015720 0ustar jodaljodal00000000000000[metadata] name = Mopidy-MPRIS version = 3.0.1 url = https://github.com/mopidy/mopidy-mpris author = Stein Magnus Jodal author_email = stein.magnus@jodal.no license = Apache License, Version 2.0 license_file = LICENSE description = Mopidy extension for controlling Mopidy through the MPRIS D-Bus interface long_description = file: README.rst classifiers = Development Status :: 5 - Production/Stable Environment :: No Input/Output (Daemon) Intended Audience :: End Users/Desktop License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python :: 3 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Multimedia :: Sound/Audio :: Players [options] zip_safe = False include_package_data = True packages = find: python_requires = >= 3.7 install_requires = Mopidy >= 3.0.0 Pykka >= 2.0.1 setuptools pydbus >= 0.6.0 [options.extras_require] lint = black check-manifest flake8 flake8-bugbear flake8-import-order isort[pyproject] release = twine wheel test = pytest pytest-cov dev = %(lint)s %(release)s %(test)s [options.packages.find] exclude = tests tests.* [options.entry_points] mopidy.ext = mpris = mopidy_mpris:Extension [flake8] application-import-names = mopidy_mpris, tests max-line-length = 80 exclude = .git, .tox, build select = C, E, F, W B B950 N ignore = E203 E501 W503 B305 N802 [egg_info] tag_build = tag_date = 0 Mopidy-MPRIS-3.0.1/setup.py0000664000175000017500000000004613577631635015604 0ustar jodaljodal00000000000000from setuptools import setup setup() Mopidy-MPRIS-3.0.1/tests/0000775000175000017500000000000013577721047015231 5ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/tests/__init__.py0000664000175000017500000000000013577631635017333 0ustar jodaljodal00000000000000Mopidy-MPRIS-3.0.1/tests/conftest.py0000664000175000017500000000133213577631635017432 0ustar jodaljodal00000000000000import pytest from mopidy.core import Core from tests import dummy_audio, dummy_backend, dummy_mixer @pytest.fixture def config(): return { "core": {"max_tracklist_length": 10000}, } @pytest.fixture def audio(): actor = dummy_audio.create_proxy() yield actor actor.stop() @pytest.fixture def backend(audio): actor = dummy_backend.create_proxy(audio=audio) yield actor actor.stop() @pytest.fixture def mixer(): actor = dummy_mixer.create_proxy() yield actor actor.stop() @pytest.fixture def core(config, backend, mixer, audio): actor = Core.start( config=config, backends=[backend], mixer=mixer, audio=audio ).proxy() yield actor actor.stop() Mopidy-MPRIS-3.0.1/tests/dummy_audio.py0000664000175000017500000000747213577720551020130 0ustar jodaljodal00000000000000"""A dummy audio actor for use in tests. This class implements the audio API in the simplest way possible. It is used in tests of the core and backends. """ import pykka from mopidy import audio def create_proxy(config=None, mixer=None): return DummyAudio.start(config, mixer).proxy() # TODO: reset position on track change? class DummyAudio(pykka.ThreadingActor): def __init__(self, config=None, mixer=None): super().__init__() self.state = audio.PlaybackState.STOPPED self._volume = 0 self._position = 0 self._callback = None self._uri = None self._stream_changed = False self._tags = {} self._bad_uris = set() def set_uri(self, uri, live_stream=False): assert self._uri is None, "prepare change not called before set" self._position = 0 self._uri = uri self._stream_changed = True self._tags = {} def set_appsrc(self, *args, **kwargs): pass def emit_data(self, buffer_): pass def emit_end_of_stream(self): pass def get_position(self): return self._position def set_position(self, position): print("set_position", position) self._position = position audio.AudioListener.send("position_changed", position=position) return True def start_playback(self): return self._change_state(audio.PlaybackState.PLAYING) def pause_playback(self): return self._change_state(audio.PlaybackState.PAUSED) def prepare_change(self): self._uri = None return True def stop_playback(self): return self._change_state(audio.PlaybackState.STOPPED) def get_volume(self): return self._volume def set_volume(self, volume): self._volume = volume return True def set_metadata(self, track): pass def get_current_tags(self): return self._tags def set_about_to_finish_callback(self, callback): self._callback = callback def enable_sync_handler(self): pass def wait_for_state_change(self): pass def _change_state(self, new_state): if not self._uri: return False if new_state == audio.PlaybackState.STOPPED and self._uri: self._stream_changed = True self._uri = None if self._uri is not None: audio.AudioListener.send("position_changed", position=0) if self._stream_changed: self._stream_changed = False audio.AudioListener.send("stream_changed", uri=self._uri) old_state, self.state = self.state, new_state audio.AudioListener.send( "state_changed", old_state=old_state, new_state=new_state, target_state=None, ) if new_state == audio.PlaybackState.PLAYING: self._tags["audio-codec"] = ["fake info..."] audio.AudioListener.send("tags_changed", tags=["audio-codec"]) return self._uri not in self._bad_uris def trigger_fake_playback_failure(self, uri): self._bad_uris.add(uri) def trigger_fake_tags_changed(self, tags): self._tags.update(tags) audio.AudioListener.send("tags_changed", tags=self._tags.keys()) def get_about_to_finish_callback(self): # This needs to be called from outside the actor or we lock up. def wrapper(): if self._callback: self.prepare_change() self._callback() if not self._uri or not self._callback: self._tags = {} audio.AudioListener.send("reached_end_of_stream") else: audio.AudioListener.send("position_changed", position=0) audio.AudioListener.send("stream_changed", uri=self._uri) return wrapper Mopidy-MPRIS-3.0.1/tests/dummy_backend.py0000664000175000017500000001005113577631635020405 0ustar jodaljodal00000000000000"""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. """ import pykka from mopidy import backend from mopidy.models import Playlist, Ref, SearchResult 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().__init__() self.library = DummyLibraryProvider(backend=self) if audio: self.playback = backend.PlaybackProvider(audio=audio, backend=self) else: 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().__init__(*args, **kwargs) self.dummy_library = [] self.dummy_get_distinct_result = {} self.dummy_get_images_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 get_images(self, uris): return self.dummy_get_images_result 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().__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().__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-3.0.1/tests/dummy_mixer.py0000664000175000017500000000121513577631635020144 0ustar jodaljodal00000000000000import pykka from mopidy import mixer def create_proxy(config=None): return DummyMixer.start(config=None).proxy() class DummyMixer(pykka.ThreadingActor, mixer.Mixer): def __init__(self, config): super().__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-3.0.1/tests/test_events.py0000664000175000017500000001206113577631635020151 0ustar jodaljodal00000000000000from unittest import mock import pytest from mopidy.core.playback import PlaybackState from mopidy.models import Playlist, TlTrack from mopidy_mpris import frontend as frontend_mod from mopidy_mpris import player, playlists, root, server @pytest.fixture def frontend(): # As a plain class, not an actor: result = frontend_mod.MprisFrontend(config=None, core=None) result.mpris = mock.Mock(spec=server.Server) result.mpris.root = mock.Mock(spec=root.Root) result.mpris.root.INTERFACE = root.Root.INTERFACE result.mpris.player = mock.Mock(spec=player.Player) result.mpris.player.INTERFACE = player.Player.INTERFACE result.mpris.playlists = mock.Mock(spec=playlists.Playlists) result.mpris.playlists.INTERFACE = playlists.Playlists.INTERFACE return result def test_track_playback_paused_event_changes_playback_status(frontend): frontend.mpris.player.PlaybackStatus = "Paused" frontend.track_playback_paused(tl_track=TlTrack(), time_position=0) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"PlaybackStatus": "Paused"}, [] ) def test_track_playback_resumed_event_changes_playback_status(frontend): frontend.mpris.player.PlaybackStatus = "Playing" frontend.track_playback_resumed(tl_track=TlTrack(), time_position=0) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"PlaybackStatus": "Playing"}, [] ) def test_track_playback_started_changes_playback_status_and_metadata(frontend): frontend.mpris.player.Metadata = "..." frontend.mpris.player.PlaybackStatus = "Playing" frontend.track_playback_started(tl_track=TlTrack()) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Metadata": "...", "PlaybackStatus": "Playing"}, [], ) def test_track_playback_ended_changes_playback_status_and_metadata(frontend): frontend.mpris.player.Metadata = "..." frontend.mpris.player.PlaybackStatus = "Stopped" frontend.track_playback_ended(tl_track=TlTrack(), time_position=0) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Metadata": "...", "PlaybackStatus": "Stopped"}, [], ) def test_playback_state_changed_changes_playback_status_and_metadata(frontend): frontend.mpris.player.Metadata = "..." frontend.mpris.player.PlaybackStatus = "Stopped" frontend.playback_state_changed( PlaybackState.PLAYING, PlaybackState.STOPPED ) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Metadata": "...", "PlaybackStatus": "Stopped"}, [], ) def test_playlists_loaded_event_changes_playlist_count(frontend): frontend.mpris.playlists.PlaylistCount = 17 frontend.playlists_loaded() frontend.mpris.playlists.PropertiesChanged.assert_called_with( playlists.Playlists.INTERFACE, {"PlaylistCount": 17}, [] ) def test_playlist_changed_event_causes_mpris_playlist_changed_event(frontend): playlist = Playlist(uri="dummy:foo", name="foo") frontend.playlist_changed(playlist=playlist) frontend.mpris.playlists.PlaylistChanged.assert_called_with( "/com/mopidy/playlist/MR2W23LZHJTG63Y_", "foo", "" ) def test_playlist_deleted_event_changes_playlist_count(frontend): frontend.mpris.playlists.PlaylistCount = 17 frontend.playlist_deleted("dummy:foo") frontend.mpris.playlists.PropertiesChanged.assert_called_with( playlists.Playlists.INTERFACE, {"PlaylistCount": 17}, [] ) def test_options_changed_event_changes_loopstatus_and_shuffle(frontend): frontend.mpris.player.CanGoPrevious = False frontend.mpris.player.CanGoNext = True frontend.mpris.player.LoopStatus = "Track" frontend.mpris.player.Shuffle = True frontend.options_changed() frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, { "LoopStatus": "Track", "Shuffle": True, "CanGoPrevious": False, "CanGoNext": True, }, [], ) def test_volume_changed_event_changes_volume(frontend): frontend.mpris.player.Volume = 1.0 frontend.volume_changed(volume=100) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Volume": 1.0}, [] ) def test_mute_changed_event_changes_volume(frontend): frontend.mpris.player.Volume = 0.0 frontend.mute_changed(True) frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Volume": 0.0}, [] ) def test_seeked_event_causes_mpris_seeked_event(frontend): frontend.seeked(time_position=31000) frontend.mpris.player.Seeked.assert_called_with(31000000) def test_stream_title_changed_changes_metadata(frontend): frontend.mpris.player.Metadata = "..." frontend.stream_title_changed("a new title") frontend.mpris.player.PropertiesChanged.assert_called_with( player.Player.INTERFACE, {"Metadata": "..."}, [] ) Mopidy-MPRIS-3.0.1/tests/test_extension.py0000664000175000017500000000152413577631635020663 0ustar jodaljodal00000000000000import unittest from unittest import mock from mopidy_mpris import Extension from mopidy_mpris import 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-3.0.1/tests/test_player.py0000664000175000017500000007320313577631635020146 0ustar jodaljodal00000000000000import pytest from gi.repository import GLib from mopidy.core import PlaybackState from mopidy.models import Album, Artist, Image, Track from mopidy_mpris.player import Player PLAYING = PlaybackState.PLAYING PAUSED = PlaybackState.PAUSED STOPPED = PlaybackState.STOPPED @pytest.fixture def player(config, core): return Player(config, core) @pytest.mark.parametrize( "state, expected", [(PLAYING, "Playing"), (PAUSED, "Paused"), (STOPPED, "Stopped")], ) def test_get_playback_status(core, player, state, expected): core.playback.set_state(state) assert player.PlaybackStatus == expected @pytest.mark.parametrize( "repeat, single, expected", [ (False, False, "None"), (False, True, "None"), (True, False, "Playlist"), (True, True, "Track"), ], ) def test_get_loop_status(core, player, repeat, single, expected): core.tracklist.set_repeat(repeat) core.tracklist.set_single(single) assert player.LoopStatus == expected @pytest.mark.parametrize( "status, expected_repeat, expected_single", [("None", False, False), ("Track", True, True), ("Playlist", True, False)], ) def test_set_loop_status( core, player, status, expected_repeat, expected_single ): player.LoopStatus = status assert core.tracklist.get_repeat().get() is expected_repeat assert core.tracklist.get_single().get() is expected_single def test_set_loop_status_is_ignored_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.set_repeat(True) core.tracklist.set_single(True) player.LoopStatus = "None" assert core.tracklist.get_repeat().get() is True assert core.tracklist.get_single().get() is True def test_get_rate_is_greater_or_equal_than_minimum_rate(player): assert player.Rate >= player.MinimumRate def test_get_rate_is_less_or_equal_than_maximum_rate(player): assert player.Rate <= player.MaximumRate def test_set_rate_to_zero_pauses_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Rate = 0 assert core.playback.get_state().get() == PAUSED def test_set_rate_is_ignored_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Rate == 0 assert core.playback.get_state().get() == PLAYING @pytest.mark.parametrize("random", [True, False]) def test_get_shuffle(core, player, random): core.tracklist.set_random(random) assert player.Shuffle is random @pytest.mark.parametrize("value", [True, False]) def test_set_shuffle(core, player, value): core.tracklist.set_random(not value) assert core.tracklist.get_random().get() is not value player.Shuffle = value assert core.tracklist.get_random().get() is value def test_set_shuffle_is_ignored_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.set_random(False) player.Shuffle = True assert core.tracklist.get_random().get() is False def test_get_metadata_is_empty_when_no_current_track(player): assert player.Metadata == {} def test_get_metadata(core, player): core.tracklist.add( [ Track( uri="dummy:a", length=3600000, name="a", artists=[Artist(name="b"), Artist(name="c"), Artist(name=None)], album=Album( name="d", artists=[Artist(name="e"), Artist(name=None)], ), ) ] ) core.playback.play().get() (tlid, track) = core.playback.get_current_tl_track().get() result = player.Metadata assert result["mpris:trackid"] == GLib.Variant( "o", "/com/mopidy/track/%d" % tlid ) assert result["mpris:length"] == GLib.Variant("x", 3600000000) assert result["xesam:url"] == GLib.Variant("s", "dummy:a") assert result["xesam:title"] == GLib.Variant("s", "a") assert result["xesam:artist"] == GLib.Variant("as", ["b", "c"]) assert result["xesam:album"] == GLib.Variant("s", "d") assert result["xesam:albumArtist"] == GLib.Variant("as", ["e"]) def test_get_metadata_prefers_stream_title_over_track_name(audio, core, player): core.tracklist.add([Track(uri="dummy:a", name="Track name",)]) core.playback.play().get() result = player.Metadata assert result["xesam:title"] == GLib.Variant("s", "Track name") audio.trigger_fake_tags_changed( { "organization": [ "Required for Mopidy core to care about the title" ], "title": ["Stream title"], } ).get() result = player.Metadata assert result["xesam:title"] == GLib.Variant("s", "Stream title") def test_get_metadata_use_library_image_as_art_url(backend, core, player): backend.library.dummy_get_images_result = { "dummy:a": [ Image(uri="http://example.com/small.jpg", width=100, height=100), Image(uri="http://example.com/large.jpg", width=200, height=200), ], } core.tracklist.add([Track(uri="dummy:a")]) core.playback.play().get() result = player.Metadata assert result["mpris:artUrl"] == GLib.Variant( "s", "http://example.com/large.jpg" ) def test_get_metadata_has_disc_number_in_album(core, player): core.tracklist.add([Track(uri="dummy:a", disc_no=2)]) core.playback.play().get() assert player.Metadata["xesam:discNumber"] == GLib.Variant("i", 2) def test_get_metadata_has_track_number_in_album(core, player): core.tracklist.add([Track(uri="dummy:a", track_no=7)]) core.playback.play().get() assert player.Metadata["xesam:trackNumber"] == GLib.Variant("i", 7) def test_get_volume_should_return_volume_between_zero_and_one(core, player): # dummy_mixer starts out with None as the volume assert player.Volume == 0 core.mixer.set_volume(0) assert player.Volume == 0 core.mixer.set_volume(50) assert player.Volume == 0.5 core.mixer.set_volume(100) assert player.Volume == 1 def test_get_volume_should_return_0_if_muted(core, player): assert player.Volume == 0 core.mixer.set_volume(100) assert player.Volume == 1 core.mixer.set_mute(True) assert player.Volume == 0 core.mixer.set_mute(False) assert player.Volume == 1 @pytest.mark.parametrize( "volume, expected", [(-1.0, 0), (0, 0), (0.5, 50), (1.0, 100), (2.0, 100)] ) def test_set_volume(core, player, volume, expected): player.Volume = volume assert core.mixer.get_volume().get() == expected def test_set_volume_to_not_a_number_does_not_change_volume(core, player): core.mixer.set_volume(10).get() player.Volume = None assert core.mixer.get_volume().get() == 10 def test_set_volume_is_ignored_if_can_control_is_false(core, player): player._CanControl = False core.mixer.set_volume(0) player.Volume = 1.0 assert core.mixer.get_volume().get() == 0 def test_set_volume_to_positive_value_unmutes_if_muted(core, player): core.mixer.set_volume(10).get() core.mixer.set_mute(True).get() player.Volume = 1.0 assert core.mixer.get_volume().get() == 100 assert core.mixer.get_mute().get() is False def test_set_volume_to_zero_does_not_unmute_if_muted(core, player): core.mixer.set_volume(10).get() core.mixer.set_mute(True).get() player.Volume = 0.0 assert core.mixer.get_volume().get() == 0 assert core.mixer.get_mute().get() is True def test_get_position_returns_time_position_in_microseconds(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(10000).get() result_in_microseconds = player.Position result_in_milliseconds = result_in_microseconds // 1000 assert result_in_milliseconds >= 10000 def test_get_position_when_no_current_track_should_be_zero(player): result_in_microseconds = player.Position result_in_milliseconds = result_in_microseconds // 1000 assert result_in_milliseconds == 0 def test_get_minimum_rate_is_one_or_less(player): assert player.MinimumRate <= 1.0 def test_get_maximum_rate_is_one_or_more(player): assert player.MaximumRate >= 1.0 def test_can_go_next_is_true_if_can_control_and_other_next_track(core, player): player._CanControl = True core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert player.CanGoNext def test_can_go_next_is_false_if_next_track_is_the_same(core, player): player._CanControl = True core.tracklist.add([Track(uri="dummy:a")]) core.tracklist.set_repeat(True) core.playback.play().get() assert not player.CanGoNext def test_can_go_next_is_false_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert not player.CanGoNext def test_can_go_previous_is_true_if_can_control_and_previous_track( core, player ): player._CanControl = True core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() assert player.CanGoPrevious def test_can_go_previous_is_false_if_previous_track_is_the_same(core, player): player._CanControl = True core.tracklist.add([Track(uri="dummy:a")]) core.tracklist.set_repeat(True) core.playback.play().get() assert not player.CanGoPrevious def test_can_go_previous_is_false_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() assert not player.CanGoPrevious def test_can_play_is_true_if_can_control_and_current_track(core, player): player._CanControl = True core.tracklist.add([Track(uri="dummy:a")]) core.playback.play().get() assert core.playback.get_current_track().get() assert player.CanPlay def test_can_play_is_false_if_no_current_track(core, player): player._CanControl = True assert not core.playback.get_current_track().get() assert not player.CanPlay def test_can_play_if_false_if_can_control_is_false(core, player): player._CanControl = False assert not player.CanPlay def test_can_pause_is_true_if_can_control_and_track_can_be_paused(core, player): player._CanControl = True assert player.CanPause def test_can_pause_if_false_if_can_control_is_false(core, player): player._CanControl = False assert not player.CanPause def test_can_seek_is_true_if_can_control_is_true(core, player): player._CanControl = True assert player.CanSeek def test_can_seek_is_false_if_can_control_is_false(core, player): player._CanControl = False result = player.CanSeek assert not result def test_can_control_is_true(core, player): result = player.CanControl assert result def test_next_is_ignored_if_can_go_next_is_false(core, player): player._CanControl = False assert not player.CanGoNext core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_current_track().get().uri == "dummy:a" player.Next() assert core.playback.get_current_track().get().uri == "dummy:a" def test_next_when_playing_skips_to_next_track_and_keep_playing(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == PLAYING player.Next() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == PLAYING def test_next_when_at_end_of_list_should_stop_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == PLAYING player.Next() assert core.playback.get_state().get() == STOPPED def test_next_when_paused_should_skip_to_next_track_and_stay_paused( core, player ): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.pause().get() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == PAUSED player.Next() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == PAUSED def test_next_when_stopped_skips_to_next_track_and_stay_stopped(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.stop() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == STOPPED player.Next() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == STOPPED def test_previous_is_ignored_if_can_go_previous_is_false(core, player): player._CanControl = False assert not player.CanGoPrevious core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() assert core.playback.get_current_track().get().uri == "dummy:b" player.Previous() assert core.playback.get_current_track().get().uri == "dummy:b" def test_previous_when_playing_skips_to_prev_track_and_keep_playing( core, player ): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == PLAYING player.Previous() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == PLAYING def test_previous_when_at_start_of_list_should_stop_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == PLAYING player.Previous() assert core.playback.get_state().get() == STOPPED def test_previous_when_paused_skips_to_previous_track_and_pause(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() core.playback.pause().get() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == PAUSED player.Previous() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == PAUSED def test_previous_when_stopped_skips_to_previous_track_and_stops(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.next().get() core.playback.stop() assert core.playback.get_current_track().get().uri == "dummy:b" assert core.playback.get_state().get() == STOPPED player.Previous() assert core.playback.get_current_track().get().uri == "dummy:a" assert core.playback.get_state().get() == STOPPED def test_pause_is_ignored_if_can_pause_is_false(core, player): player._CanControl = False assert not player.CanPause core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Pause() assert core.playback.get_state().get() == PLAYING def test_pause_when_playing_should_pause_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Pause() assert core.playback.get_state().get() == PAUSED def test_pause_when_paused_has_no_effect(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.pause().get() assert core.playback.get_state().get() == PAUSED player.Pause() assert core.playback.get_state().get() == PAUSED def test_playpause_is_ignored_if_can_pause_is_false(core, player): player._CanControl = False assert not player.CanPause core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.PlayPause() assert core.playback.get_state().get() == PLAYING def test_playpause_when_playing_should_pause_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.PlayPause() assert core.playback.get_state().get() == PAUSED def test_playpause_when_paused_should_resume_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.pause().get() assert core.playback.get_state().get() == PAUSED at_pause = core.playback.get_time_position().get() assert at_pause >= 0 player.PlayPause() assert core.playback.get_state().get() == PLAYING after_pause = core.playback.get_time_position().get() assert after_pause >= at_pause def test_playpause_when_stopped_should_start_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) assert core.playback.get_state().get() == STOPPED player.PlayPause() assert core.playback.get_state().get() == PLAYING def test_stop_is_ignored_if_can_control_is_false(core, player): player._CanControl = False core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Stop() assert core.playback.get_state().get() == PLAYING def test_stop_when_playing_should_stop_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING player.Stop() assert core.playback.get_state().get() == STOPPED def test_stop_when_paused_should_stop_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.pause().get() assert core.playback.get_state().get() == PAUSED player.Stop() assert core.playback.get_state().get() == STOPPED def test_play_is_ignored_if_can_play_is_false(core, player): player._CanControl = False assert not player.CanPlay core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) assert core.playback.get_state().get() == STOPPED player.Play() assert core.playback.get_state().get() == STOPPED def test_play_when_stopped_starts_playback(core, player): core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) assert core.playback.get_state().get() == STOPPED player.Play() assert core.playback.get_state().get() == PLAYING def test_play_after_pause_resumes_from_same_position(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() before_pause = core.playback.get_time_position().get() assert before_pause >= 0 player.Pause() assert core.playback.get_state().get() == PAUSED at_pause = core.playback.get_time_position().get() assert at_pause >= before_pause player.Play() assert core.playback.get_state().get() == PLAYING after_pause = core.playback.get_time_position().get() assert after_pause >= at_pause def test_play_when_there_is_no_track_has_no_effect(core, player): core.tracklist.clear() assert core.playback.get_state().get() == STOPPED player.Play() assert core.playback.get_state().get() == STOPPED def test_seek_is_ignored_if_can_seek_is_false(core, player): player._CanControl = False assert not player.CanSeek core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() before_seek = core.playback.get_time_position().get() assert before_seek >= 0 milliseconds_to_seek = 10000 microseconds_to_seek = milliseconds_to_seek * 1000 player.Seek(microseconds_to_seek) after_seek = core.playback.get_time_position().get() assert before_seek <= after_seek assert after_seek < before_seek + milliseconds_to_seek def test_seek_seeks_given_microseconds_forward_in_the_current_track( core, player ): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() before_seek = core.playback.get_time_position().get() assert before_seek >= 0 milliseconds_to_seek = 10000 microseconds_to_seek = milliseconds_to_seek * 1000 player.Seek(microseconds_to_seek) assert core.playback.get_state().get() == PLAYING after_seek = core.playback.get_time_position().get() assert after_seek >= before_seek + milliseconds_to_seek def test_seek_seeks_given_microseconds_backward_if_negative(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(20000).get() before_seek = core.playback.get_time_position().get() assert before_seek >= 20000 milliseconds_to_seek = -10000 microseconds_to_seek = milliseconds_to_seek * 1000 player.Seek(microseconds_to_seek) assert core.playback.get_state().get() == PLAYING after_seek = core.playback.get_time_position().get() assert after_seek >= before_seek + milliseconds_to_seek assert after_seek < before_seek def test_seek_seeks_to_start_of_track_if_new_position_is_negative(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(20000).get() before_seek = core.playback.get_time_position().get() assert before_seek >= 20000 milliseconds_to_seek = -30000 microseconds_to_seek = milliseconds_to_seek * 1000 player.Seek(microseconds_to_seek) assert core.playback.get_state().get() == PLAYING after_seek = core.playback.get_time_position().get() assert after_seek >= before_seek + milliseconds_to_seek assert after_seek < before_seek assert after_seek >= 0 def test_seek_skips_to_next_track_if_new_position_gt_track_length(core, player): core.tracklist.add( [Track(uri="dummy:a", length=40000), Track(uri="dummy:b")] ) core.playback.play().get() core.playback.seek(20000).get() before_seek = core.playback.get_time_position().get() assert before_seek >= 20000 assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" milliseconds_to_seek = 50000 microseconds_to_seek = milliseconds_to_seek * 1000 player.Seek(microseconds_to_seek) assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:b" after_seek = core.playback.get_time_position().get() assert after_seek >= 0 assert after_seek < before_seek def test_set_position_is_ignored_if_can_seek_is_false(core, player): player.get_CanSeek = lambda *_: False core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() before_set_position = core.playback.get_time_position().get() assert before_set_position <= 5000 track_id = "a" position_to_set_in_millisec = 20000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 player.SetPosition(track_id, position_to_set_in_microsec) after_set_position = core.playback.get_time_position().get() assert before_set_position <= after_set_position assert after_set_position < position_to_set_in_millisec def test_set_position_sets_the_current_track_position_in_microsecs( core, player ): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() before_set_position = core.playback.get_time_position().get() assert before_set_position <= 5000 assert core.playback.get_state().get() == PLAYING track_id = "/com/mopidy/track/1" position_to_set_in_millisec = 20000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 player.SetPosition(track_id, position_to_set_in_microsec) assert core.playback.get_state().get() == PLAYING after_set_position = core.playback.get_time_position().get() assert after_set_position >= position_to_set_in_millisec def test_set_position_does_nothing_if_the_position_is_negative(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(20000) before_set_position = core.playback.get_time_position().get() assert before_set_position >= 20000 assert before_set_position <= 25000 assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" track_id = "/com/mopidy/track/1" position_to_set_in_millisec = -1000 position_to_set_in_microsec = position_to_set_in_millisec * 1000 player.SetPosition(track_id, position_to_set_in_microsec) after_set_position = core.playback.get_time_position().get() assert after_set_position >= before_set_position assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" def test_set_position_does_nothing_if_position_is_gt_track_length(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(20000) before_set_position = core.playback.get_time_position().get() assert before_set_position >= 20000 assert before_set_position <= 25000 assert core.playback.get_state().get() == PLAYING assert core.playback.get_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 player.SetPosition(track_id, position_to_set_in_microsec) after_set_position = core.playback.get_time_position().get() assert after_set_position >= before_set_position assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" def test_set_position_is_noop_if_track_id_isnt_current_track(core, player): core.tracklist.add([Track(uri="dummy:a", length=40000)]) core.playback.play().get() core.playback.seek(20000) before_set_position = core.playback.get_time_position().get() assert before_set_position >= 20000 assert before_set_position <= 25000 assert core.playback.get_state().get() == PLAYING assert core.playback.get_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 player.SetPosition(track_id, position_to_set_in_microsec) after_set_position = core.playback.get_time_position().get() assert after_set_position >= before_set_position assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" def test_open_uri_is_ignored_if_can_control_is_false(backend, core, player): player._CanControl = False backend.library.dummy_library = [Track(uri="dummy:/test/uri")] player.OpenUri("dummy:/test/uri") assert core.tracklist.get_length().get() == 0 def test_open_uri_ignores_uris_with_unknown_uri_scheme(backend, core, player): assert core.get_uri_schemes().get() == ["dummy"] backend.library.dummy_library = [Track(uri="notdummy:/test/uri")] player.OpenUri("notdummy:/test/uri") assert core.tracklist.get_length().get() == 0 def test_open_uri_adds_uri_to_tracklist(backend, core, player): backend.library.dummy_library = [Track(uri="dummy:/test/uri")] player.OpenUri("dummy:/test/uri") assert core.tracklist.get_length().get() == 1 assert core.tracklist.get_tracks().get()[0].uri == "dummy:/test/uri" def test_open_uri_starts_playback_of_new_track_if_stopped( backend, core, player ): backend.library.dummy_library = [Track(uri="dummy:/test/uri")] core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) assert core.playback.get_state().get() == STOPPED player.OpenUri("dummy:/test/uri") assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:/test/uri" def test_open_uri_starts_playback_of_new_track_if_paused(backend, core, player): backend.library.dummy_library = [Track(uri="dummy:/test/uri")] core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() core.playback.pause().get() assert core.playback.get_state().get() == PAUSED assert core.playback.get_current_track().get().uri == "dummy:a" player.OpenUri("dummy:/test/uri") assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:/test/uri" def test_open_uri_starts_playback_of_new_track_if_playing( backend, core, player ): backend.library.dummy_library = [Track(uri="dummy:/test/uri")] core.tracklist.add([Track(uri="dummy:a"), Track(uri="dummy:b")]) core.playback.play().get() assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:a" player.OpenUri("dummy:/test/uri") assert core.playback.get_state().get() == PLAYING assert core.playback.get_current_track().get().uri == "dummy:/test/uri" Mopidy-MPRIS-3.0.1/tests/test_playlists.py0000664000175000017500000000745613577631635020705 0ustar jodaljodal00000000000000import pytest from mopidy.audio import PlaybackState from mopidy.models import Track from mopidy_mpris.playlists import Playlists @pytest.fixture def dummy_playlists(core): result = {} for name, lm in [("foo", 3000000), ("bar", 2000000), ("baz", 1000000)]: pl = core.playlists.create(name).get() pl = pl.replace(last_modified=lm) result[name] = core.playlists.save(pl).get() return result @pytest.fixture def playlists(config, core, dummy_playlists): return Playlists(config, core) def test_activate_playlist_appends_tracks_to_tracklist( core, playlists, dummy_playlists ): core.tracklist.add([Track(uri="dummy:old-a"), Track(uri="dummy:old-b")]) assert core.tracklist.get_length().get() == 2 pl = dummy_playlists["baz"] pl = pl.replace( tracks=[ Track(uri="dummy:baz-a"), Track(uri="dummy:baz-b"), Track(uri="dummy:baz-c"), ] ) pl = core.playlists.save(pl).get() playlist_id = playlists.GetPlaylists(0, 100, "User", False)[2][0] playlists.ActivatePlaylist(playlist_id) assert core.tracklist.get_length().get() == 5 assert core.playback.get_state().get() == PlaybackState.PLAYING assert core.playback.get_current_track().get() == pl.tracks[0] def test_activate_empty_playlist_is_harmless(core, playlists): assert core.tracklist.get_length().get() == 0 playlist_id = playlists.GetPlaylists(0, 100, "User", False)[2][0] playlists.ActivatePlaylist(playlist_id) assert core.tracklist.get_length().get() == 0 assert core.playback.get_state().get() == PlaybackState.STOPPED assert core.playback.get_current_track().get() is None def test_get_playlists_in_alphabetical_order(playlists): result = playlists.GetPlaylists(0, 100, "Alphabetical", False) assert result == [ ("/com/mopidy/playlist/MR2W23LZHJRGC4Q_", "bar", ""), ("/com/mopidy/playlist/MR2W23LZHJRGC6Q_", "baz", ""), ("/com/mopidy/playlist/MR2W23LZHJTG63Y_", "foo", ""), ] def test_get_playlists_in_reverse_alphabetical_order(playlists): result = playlists.GetPlaylists(0, 100, "Alphabetical", True) assert len(result) == 3 assert result[0][1] == "foo" assert result[1][1] == "baz" assert result[2][1] == "bar" def test_get_playlists_in_user_order(playlists): result = playlists.GetPlaylists(0, 100, "User", False) assert len(result) == 3 assert result[0][1] == "foo" assert result[1][1] == "bar" assert result[2][1] == "baz" def test_get_playlists_in_reverse_user_order(playlists): result = playlists.GetPlaylists(0, 100, "User", True) assert len(result) == 3 assert result[0][1] == "baz" assert result[1][1] == "bar" assert result[2][1] == "foo" def test_get_playlists_slice_on_start_of_list(playlists): result = playlists.GetPlaylists(0, 2, "User", False) assert len(result) == 2 assert result[0][1] == "foo" assert result[1][1] == "bar" def test_get_playlists_slice_later_in_list(playlists): result = playlists.GetPlaylists(2, 2, "User", False) assert len(result) == 1 assert result[0][1] == "baz" def test_get_playlist_count_returns_number_of_playlists(playlists): assert playlists.PlaylistCount == 3 def test_get_orderings_includes_alpha_modified_and_user(playlists): result = playlists.Orderings assert "Alphabetical" in result assert "Created" not in result assert "Modified" not in result assert "Played" not in result assert "User" in result def test_get_active_playlist_does_not_return_a_playlist(playlists): result = playlists.ActivePlaylist valid, playlist = result playlist_id, playlist_name, playlist_icon_uri = playlist assert valid is False assert playlist_id == "/" assert playlist_name == "None" assert playlist_icon_uri == "" Mopidy-MPRIS-3.0.1/tests/test_root.py0000664000175000017500000000246313577631635017635 0ustar jodaljodal00000000000000import pytest from mopidy_mpris.root import Root @pytest.fixture def root(config, core): return Root(config, core) def test_fullscreen_returns_false(root): assert root.Fullscreen is False def test_setting_fullscreen_fails(root): root.Fullscreen = True assert root.Fullscreen is False def test_can_set_fullscreen_returns_false(root): assert root.CanSetFullscreen is False def test_can_raise_returns_false(root): assert root.CanRaise is False def test_raise_does_nothing(root): root.Raise() def test_can_quit_returns_false(root): assert root.CanQuit is False def test_quit_does_nothing(root): root.Quit() def test_has_track_list_returns_false(root): assert root.HasTrackList is False def test_identify_is_mopidy(root): assert root.Identity == "Mopidy" def test_desktop_entry_is_blank(root, config): assert root.DesktopEntry == "" def test_supported_uri_schemes_includes_backend_uri_schemes(root): assert root.SupportedUriSchemes == ["dummy"] def test_supported_mime_types_has_hardcoded_entries(root): assert root.SupportedMimeTypes == [ "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-3.0.1/tox.ini0000664000175000017500000000073613577631635015413 0ustar jodaljodal00000000000000[tox] envlist = py37, py38, black, check-manifest, flake8 [testenv] sitepackages = true deps = .[test] commands = python -m pytest \ --basetemp={envtmpdir} \ --cov=mopidy_mpris --cov-report=term-missing \ {posargs} [testenv:black] deps = .[lint] commands = python -m black --check . [testenv:check-manifest] deps = .[lint] commands = python -m check_manifest [testenv:flake8] deps = .[lint] commands = python -m flake8 --show-source --statistics