././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/ 0000755 0001751 0000171 00000000000 00000000000 012732 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9770465
Mopidy-MPD-3.1.0/.circleci/ 0000755 0001751 0000171 00000000000 00000000000 014565 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/.circleci/config.yml 0000644 0001751 0000171 00000002222 00000000000 016553 0 ustar 00runner docker version: 2.1
orbs:
codecov: codecov/codecov@1.0.5
workflows:
version: 2
test:
jobs:
- py39
- py38
- py37
- black
- check-manifest
- flake8
jobs:
py39: &test-template
docker:
- image: mopidy/ci-python:3.9
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
py38:
<<: *test-template
docker:
- image: mopidy/ci-python:3.8
py37:
<<: *test-template
docker:
- image: mopidy/ci-python:3.7
black: *test-template
check-manifest: *test-template
flake8: *test-template
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9730465
Mopidy-MPD-3.1.0/.github/ 0000755 0001751 0000171 00000000000 00000000000 014272 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9770465
Mopidy-MPD-3.1.0/.github/workflows/ 0000755 0001751 0000171 00000000000 00000000000 016327 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/.github/workflows/release.yml 0000644 0001751 0000171 00000000757 00000000000 020503 0 ustar 00runner docker name: Release
on:
release:
types: [published]
jobs:
release:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: "Install dependencies"
run: python3 -m pip install wheel
- name: "Build package"
run: python3 setup.py sdist bdist_wheel
- uses: pypa/gh-action-pypi-publish@v1.4.1
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/LICENSE 0000644 0001751 0000171 00000026136 00000000000 013747 0 ustar 00runner docker
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.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/MANIFEST.in 0000644 0001751 0000171 00000000412 00000000000 014465 0 ustar 00runner docker include *.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 *
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9770465
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/ 0000755 0001751 0000171 00000000000 00000000000 016365 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/PKG-INFO 0000644 0001751 0000171 00000020332 00000000000 017462 0 ustar 00runner docker Metadata-Version: 2.1
Name: Mopidy-MPD
Version: 3.1.0
Summary: Mopidy extension for controlling Mopidy from MPD clients
Home-page: https://github.com/mopidy/mopidy-mpd
Author: Stein Magnus Jodal
Author-email: stein.magnus@jodal.no
License: Apache License, Version 2.0
Description: **********
Mopidy-MPD
**********
.. image:: https://img.shields.io/pypi/v/Mopidy-MPD
:target: https://pypi.org/project/Mopidy-MPD/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpd
:target: https://circleci.com/gh/mopidy/mopidy-mpd
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpd
:target: https://codecov.io/gh/mopidy/mopidy-mpd
:alt: Test coverage
`Mopidy`_ extension for controlling Mopidy from MPD clients.
MPD stands for Music Player Daemon, which is also the name of the `original MPD
server project `_. Mopidy does not depend on the
original MPD server, but implements the MPD protocol itself, and is thus
compatible with most clients built for the original MPD server
.. _Mopidy: https://mopidy.com/
Maintainer wanted
=================
Mopidy-MPD is currently kept on life support by the Mopidy core
developers. It is in need of a more dedicated maintainer.
If you want to be the maintainer of Mopidy-Local, please:
1. Make 2-3 good pull requests improving any part of the project.
2. Read and get familiar with all of the project's open issues.
3. Send a pull request removing this section and adding yourself as the
"Current maintainer" in the "Credits" section below. In the pull request
description, please refer to the previous pull requests and state that
you've familiarized yourself with the open issues.
As a maintainer, you'll be given push access to the repo and the authority
to make releases to PyPI when you see fit.
Installation
============
Install by running::
sudo python3 -m pip install Mopidy-MPD
See https://mopidy.com/ext/mpd/ for alternative installation methods.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-MPD to your Mopidy configuration file::
[mpd]
hostname = ::
.. warning::
As a simple security measure, the MPD server is by default only available
from localhost. To make it available from other computers, change the
``mpd/hostname`` config value. Before you do so, note that the MPD
server does not support any form of encryption and only a single clear
text password (see ``mpd/password``) for weak authentication. Anyone
able to access the MPD server can control music playback on your computer.
Thus, you probably only want to make the MPD server available from your
local network. You have been warned.
The following configuration values are available:
- ``mpd/enabled``:
If the MPD extension should be enabled or not.
- ``mpd/hostname``:
Which address the MPD server should bind to.
This can be a network address or the path toa Unix socket:
- ``127.0.0.1``: Listens only on the IPv4 loopback interface (default).
- ``::1``: Listens only on the IPv6 loopback interface.
- ``0.0.0.0``: Listens on all IPv4 interfaces.
- ``::``: Listens on all interfaces, both IPv4 and IPv6.
- ``unix:/path/to/unix/socket.sock``: Listen on the Unix socket at the
specified path. Must be prefixed with ``unix:``.
- ``mpd/port``:
Which TCP port the MPD server should listen to.
Default: 6600.
- ``mpd/password``:
The password required for connecting to the MPD server.
If blank, no password is required.
Default: blank.
- ``mpd/max_connections``:
The maximum number of concurrent connections the MPD server will accept.
Default: 20.
- ``mpd/connection_timeout``:
Number of seconds an MPD client can stay inactive before the connection is
closed by the server.
Default: 60.
- ``mpd/zeroconf``:
Name of the MPD service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
Set to an empty string to disable Zeroconf for MPD.
Default: ``Mopidy MPD server on $hostname``
- ``mpd/command_blacklist``:
List of MPD commands which are disabled by the server.
By default this blacklists ``listall`` and ``listallinfo``.
These commands don't fit well with many of Mopidy's backends and are better
left disabled unless you know what youare doing.
- ``mpd/default_playlist_scheme``:
The URI scheme used if the server cannot find a backend appropriate for
creating a playlist from the given tracks.
Default: ``m3u``
Limitations
===========
This is a non-exhaustive list of MPD features that Mopidy doesn't support.
- Only a single password is supported. It gives all-or-nothing access.
- Toggling of audio outputs is not supported.
- Channels for client-to-client communication are not supported.
- Stickers are not supported.
- Crossfade is not supported.
- Replay gain is not supported.
- ``stats`` does not provide any statistics.
- ``decoders`` does not provide information about available decoders.
- ``tagtypes`` is not supported.
- Live update of the music database is not supported.
Clients
=======
Over the years, a huge number of MPD clients have been built for every thinkable
platform. As always, the quality and state of maintenance varies between clients,
so you might have to try a couple before you find one you like for your purpose.
In general, they should all work with Mopidy-MPD.
The `Wikipedia article on MPD `_
has a short list of well-known clients.
In the MPD wiki there is a
`more complete list `_
of the available MPD clients.
Both lists are grouped by user interface, e.g. terminal, graphical, or web-based.
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original authors:
`Stein Magnus Jodal `__ and
`Thomas Adamcik `__
for the Mopidy-MPD extension in Mopidy core.
- Current maintainer: None. Maintainer wanted, see section above.
- `Contributors `_
Platform: UNKNOWN
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Requires-Python: >=3.7
Provides-Extra: lint
Provides-Extra: release
Provides-Extra: test
Provides-Extra: dev
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/SOURCES.txt 0000644 0001751 0000171 00000004102 00000000000 020246 0 ustar 00runner docker LICENSE
MANIFEST.in
README.rst
pyproject.toml
setup.cfg
setup.py
tox.ini
.circleci/config.yml
.github/workflows/release.yml
Mopidy_MPD.egg-info/PKG-INFO
Mopidy_MPD.egg-info/SOURCES.txt
Mopidy_MPD.egg-info/dependency_links.txt
Mopidy_MPD.egg-info/entry_points.txt
Mopidy_MPD.egg-info/not-zip-safe
Mopidy_MPD.egg-info/requires.txt
Mopidy_MPD.egg-info/top_level.txt
mopidy_mpd/__init__.py
mopidy_mpd/actor.py
mopidy_mpd/dispatcher.py
mopidy_mpd/exceptions.py
mopidy_mpd/ext.conf
mopidy_mpd/formatting.py
mopidy_mpd/network.py
mopidy_mpd/session.py
mopidy_mpd/tokenize.py
mopidy_mpd/translator.py
mopidy_mpd/uri_mapper.py
mopidy_mpd/protocol/__init__.py
mopidy_mpd/protocol/audio_output.py
mopidy_mpd/protocol/channels.py
mopidy_mpd/protocol/command_list.py
mopidy_mpd/protocol/connection.py
mopidy_mpd/protocol/current_playlist.py
mopidy_mpd/protocol/mount.py
mopidy_mpd/protocol/music_db.py
mopidy_mpd/protocol/playback.py
mopidy_mpd/protocol/reflection.py
mopidy_mpd/protocol/status.py
mopidy_mpd/protocol/stickers.py
mopidy_mpd/protocol/stored_playlists.py
mopidy_mpd/protocol/tagtype_list.py
tests/__init__.py
tests/dummy_audio.py
tests/dummy_backend.py
tests/dummy_mixer.py
tests/path_utils.py
tests/test_actor.py
tests/test_commands.py
tests/test_dispatcher.py
tests/test_exceptions.py
tests/test_extension.py
tests/test_path_utils.py
tests/test_session.py
tests/test_status.py
tests/test_tokenizer.py
tests/test_translator.py
tests/network/__init__.py
tests/network/test_connection.py
tests/network/test_lineprotocol.py
tests/network/test_server.py
tests/network/test_utils.py
tests/protocol/__init__.py
tests/protocol/test_audio_output.py
tests/protocol/test_authentication.py
tests/protocol/test_channels.py
tests/protocol/test_command_list.py
tests/protocol/test_connection.py
tests/protocol/test_current_playlist.py
tests/protocol/test_idle.py
tests/protocol/test_mount.py
tests/protocol/test_music_db.py
tests/protocol/test_playback.py
tests/protocol/test_reflection.py
tests/protocol/test_regression.py
tests/protocol/test_status.py
tests/protocol/test_stickers.py
tests/protocol/test_stored_playlists.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/dependency_links.txt 0000644 0001751 0000171 00000000001 00000000000 022433 0 ustar 00runner docker
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/entry_points.txt 0000644 0001751 0000171 00000000051 00000000000 021657 0 ustar 00runner docker [mopidy.ext]
mpd = mopidy_mpd:Extension
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/not-zip-safe 0000644 0001751 0000171 00000000001 00000000000 020613 0 ustar 00runner docker
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/requires.txt 0000644 0001751 0000171 00000000444 00000000000 020767 0 ustar 00runner docker Mopidy>=3.0.0
Pykka>=2.0.1
setuptools
[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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133604.0
Mopidy-MPD-3.1.0/Mopidy_MPD.egg-info/top_level.txt 0000644 0001751 0000171 00000000013 00000000000 021111 0 ustar 00runner docker mopidy_mpd
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/PKG-INFO 0000644 0001751 0000171 00000020332 00000000000 014027 0 ustar 00runner docker Metadata-Version: 2.1
Name: Mopidy-MPD
Version: 3.1.0
Summary: Mopidy extension for controlling Mopidy from MPD clients
Home-page: https://github.com/mopidy/mopidy-mpd
Author: Stein Magnus Jodal
Author-email: stein.magnus@jodal.no
License: Apache License, Version 2.0
Description: **********
Mopidy-MPD
**********
.. image:: https://img.shields.io/pypi/v/Mopidy-MPD
:target: https://pypi.org/project/Mopidy-MPD/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpd
:target: https://circleci.com/gh/mopidy/mopidy-mpd
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpd
:target: https://codecov.io/gh/mopidy/mopidy-mpd
:alt: Test coverage
`Mopidy`_ extension for controlling Mopidy from MPD clients.
MPD stands for Music Player Daemon, which is also the name of the `original MPD
server project `_. Mopidy does not depend on the
original MPD server, but implements the MPD protocol itself, and is thus
compatible with most clients built for the original MPD server
.. _Mopidy: https://mopidy.com/
Maintainer wanted
=================
Mopidy-MPD is currently kept on life support by the Mopidy core
developers. It is in need of a more dedicated maintainer.
If you want to be the maintainer of Mopidy-Local, please:
1. Make 2-3 good pull requests improving any part of the project.
2. Read and get familiar with all of the project's open issues.
3. Send a pull request removing this section and adding yourself as the
"Current maintainer" in the "Credits" section below. In the pull request
description, please refer to the previous pull requests and state that
you've familiarized yourself with the open issues.
As a maintainer, you'll be given push access to the repo and the authority
to make releases to PyPI when you see fit.
Installation
============
Install by running::
sudo python3 -m pip install Mopidy-MPD
See https://mopidy.com/ext/mpd/ for alternative installation methods.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-MPD to your Mopidy configuration file::
[mpd]
hostname = ::
.. warning::
As a simple security measure, the MPD server is by default only available
from localhost. To make it available from other computers, change the
``mpd/hostname`` config value. Before you do so, note that the MPD
server does not support any form of encryption and only a single clear
text password (see ``mpd/password``) for weak authentication. Anyone
able to access the MPD server can control music playback on your computer.
Thus, you probably only want to make the MPD server available from your
local network. You have been warned.
The following configuration values are available:
- ``mpd/enabled``:
If the MPD extension should be enabled or not.
- ``mpd/hostname``:
Which address the MPD server should bind to.
This can be a network address or the path toa Unix socket:
- ``127.0.0.1``: Listens only on the IPv4 loopback interface (default).
- ``::1``: Listens only on the IPv6 loopback interface.
- ``0.0.0.0``: Listens on all IPv4 interfaces.
- ``::``: Listens on all interfaces, both IPv4 and IPv6.
- ``unix:/path/to/unix/socket.sock``: Listen on the Unix socket at the
specified path. Must be prefixed with ``unix:``.
- ``mpd/port``:
Which TCP port the MPD server should listen to.
Default: 6600.
- ``mpd/password``:
The password required for connecting to the MPD server.
If blank, no password is required.
Default: blank.
- ``mpd/max_connections``:
The maximum number of concurrent connections the MPD server will accept.
Default: 20.
- ``mpd/connection_timeout``:
Number of seconds an MPD client can stay inactive before the connection is
closed by the server.
Default: 60.
- ``mpd/zeroconf``:
Name of the MPD service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
Set to an empty string to disable Zeroconf for MPD.
Default: ``Mopidy MPD server on $hostname``
- ``mpd/command_blacklist``:
List of MPD commands which are disabled by the server.
By default this blacklists ``listall`` and ``listallinfo``.
These commands don't fit well with many of Mopidy's backends and are better
left disabled unless you know what youare doing.
- ``mpd/default_playlist_scheme``:
The URI scheme used if the server cannot find a backend appropriate for
creating a playlist from the given tracks.
Default: ``m3u``
Limitations
===========
This is a non-exhaustive list of MPD features that Mopidy doesn't support.
- Only a single password is supported. It gives all-or-nothing access.
- Toggling of audio outputs is not supported.
- Channels for client-to-client communication are not supported.
- Stickers are not supported.
- Crossfade is not supported.
- Replay gain is not supported.
- ``stats`` does not provide any statistics.
- ``decoders`` does not provide information about available decoders.
- ``tagtypes`` is not supported.
- Live update of the music database is not supported.
Clients
=======
Over the years, a huge number of MPD clients have been built for every thinkable
platform. As always, the quality and state of maintenance varies between clients,
so you might have to try a couple before you find one you like for your purpose.
In general, they should all work with Mopidy-MPD.
The `Wikipedia article on MPD `_
has a short list of well-known clients.
In the MPD wiki there is a
`more complete list `_
of the available MPD clients.
Both lists are grouped by user interface, e.g. terminal, graphical, or web-based.
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original authors:
`Stein Magnus Jodal `__ and
`Thomas Adamcik `__
for the Mopidy-MPD extension in Mopidy core.
- Current maintainer: None. Maintainer wanted, see section above.
- `Contributors `_
Platform: UNKNOWN
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
Requires-Python: >=3.7
Provides-Extra: lint
Provides-Extra: release
Provides-Extra: test
Provides-Extra: dev
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/README.rst 0000644 0001751 0000171 00000013724 00000000000 014430 0 ustar 00runner docker **********
Mopidy-MPD
**********
.. image:: https://img.shields.io/pypi/v/Mopidy-MPD
:target: https://pypi.org/project/Mopidy-MPD/
:alt: Latest PyPI version
.. image:: https://img.shields.io/circleci/build/gh/mopidy/mopidy-mpd
:target: https://circleci.com/gh/mopidy/mopidy-mpd
:alt: CircleCI build status
.. image:: https://img.shields.io/codecov/c/gh/mopidy/mopidy-mpd
:target: https://codecov.io/gh/mopidy/mopidy-mpd
:alt: Test coverage
`Mopidy`_ extension for controlling Mopidy from MPD clients.
MPD stands for Music Player Daemon, which is also the name of the `original MPD
server project `_. Mopidy does not depend on the
original MPD server, but implements the MPD protocol itself, and is thus
compatible with most clients built for the original MPD server
.. _Mopidy: https://mopidy.com/
Maintainer wanted
=================
Mopidy-MPD is currently kept on life support by the Mopidy core
developers. It is in need of a more dedicated maintainer.
If you want to be the maintainer of Mopidy-Local, please:
1. Make 2-3 good pull requests improving any part of the project.
2. Read and get familiar with all of the project's open issues.
3. Send a pull request removing this section and adding yourself as the
"Current maintainer" in the "Credits" section below. In the pull request
description, please refer to the previous pull requests and state that
you've familiarized yourself with the open issues.
As a maintainer, you'll be given push access to the repo and the authority
to make releases to PyPI when you see fit.
Installation
============
Install by running::
sudo python3 -m pip install Mopidy-MPD
See https://mopidy.com/ext/mpd/ for alternative installation methods.
Configuration
=============
Before starting Mopidy, you must add configuration for
Mopidy-MPD to your Mopidy configuration file::
[mpd]
hostname = ::
.. warning::
As a simple security measure, the MPD server is by default only available
from localhost. To make it available from other computers, change the
``mpd/hostname`` config value. Before you do so, note that the MPD
server does not support any form of encryption and only a single clear
text password (see ``mpd/password``) for weak authentication. Anyone
able to access the MPD server can control music playback on your computer.
Thus, you probably only want to make the MPD server available from your
local network. You have been warned.
The following configuration values are available:
- ``mpd/enabled``:
If the MPD extension should be enabled or not.
- ``mpd/hostname``:
Which address the MPD server should bind to.
This can be a network address or the path toa Unix socket:
- ``127.0.0.1``: Listens only on the IPv4 loopback interface (default).
- ``::1``: Listens only on the IPv6 loopback interface.
- ``0.0.0.0``: Listens on all IPv4 interfaces.
- ``::``: Listens on all interfaces, both IPv4 and IPv6.
- ``unix:/path/to/unix/socket.sock``: Listen on the Unix socket at the
specified path. Must be prefixed with ``unix:``.
- ``mpd/port``:
Which TCP port the MPD server should listen to.
Default: 6600.
- ``mpd/password``:
The password required for connecting to the MPD server.
If blank, no password is required.
Default: blank.
- ``mpd/max_connections``:
The maximum number of concurrent connections the MPD server will accept.
Default: 20.
- ``mpd/connection_timeout``:
Number of seconds an MPD client can stay inactive before the connection is
closed by the server.
Default: 60.
- ``mpd/zeroconf``:
Name of the MPD service when published through Zeroconf. The variables
``$hostname`` and ``$port`` can be used in the name.
Set to an empty string to disable Zeroconf for MPD.
Default: ``Mopidy MPD server on $hostname``
- ``mpd/command_blacklist``:
List of MPD commands which are disabled by the server.
By default this blacklists ``listall`` and ``listallinfo``.
These commands don't fit well with many of Mopidy's backends and are better
left disabled unless you know what youare doing.
- ``mpd/default_playlist_scheme``:
The URI scheme used if the server cannot find a backend appropriate for
creating a playlist from the given tracks.
Default: ``m3u``
Limitations
===========
This is a non-exhaustive list of MPD features that Mopidy doesn't support.
- Only a single password is supported. It gives all-or-nothing access.
- Toggling of audio outputs is not supported.
- Channels for client-to-client communication are not supported.
- Stickers are not supported.
- Crossfade is not supported.
- Replay gain is not supported.
- ``stats`` does not provide any statistics.
- ``decoders`` does not provide information about available decoders.
- ``tagtypes`` is not supported.
- Live update of the music database is not supported.
Clients
=======
Over the years, a huge number of MPD clients have been built for every thinkable
platform. As always, the quality and state of maintenance varies between clients,
so you might have to try a couple before you find one you like for your purpose.
In general, they should all work with Mopidy-MPD.
The `Wikipedia article on MPD `_
has a short list of well-known clients.
In the MPD wiki there is a
`more complete list `_
of the available MPD clients.
Both lists are grouped by user interface, e.g. terminal, graphical, or web-based.
Project resources
=================
- `Source code `_
- `Issue tracker `_
- `Changelog `_
Credits
=======
- Original authors:
`Stein Magnus Jodal `__ and
`Thomas Adamcik `__
for the Mopidy-MPD extension in Mopidy core.
- Current maintainer: None. Maintainer wanted, see section above.
- `Contributors `_
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9810467
Mopidy-MPD-3.1.0/mopidy_mpd/ 0000755 0001751 0000171 00000000000 00000000000 015073 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/__init__.py 0000644 0001751 0000171 00000002020 00000000000 017176 0 ustar 00runner docker import pathlib
import pkg_resources
from mopidy import config, ext
__version__ = pkg_resources.get_distribution("Mopidy-MPD").version
class Extension(ext.Extension):
dist_name = "Mopidy-MPD"
ext_name = "mpd"
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["hostname"] = config.Hostname()
schema["port"] = config.Port(optional=True)
schema["password"] = config.Secret(optional=True)
schema["max_connections"] = config.Integer(minimum=1)
schema["connection_timeout"] = config.Integer(minimum=1)
schema["zeroconf"] = config.String(optional=True)
schema["command_blacklist"] = config.List(optional=True)
schema["default_playlist_scheme"] = config.String()
return schema
def setup(self, registry):
from .actor import MpdFrontend
registry.add("frontend", MpdFrontend)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/actor.py 0000644 0001751 0000171 00000005632 00000000000 016563 0 ustar 00runner docker import logging
import pykka
from mopidy import exceptions, listener, zeroconf
from mopidy.core import CoreListener
from mopidy_mpd import network, session, uri_mapper
logger = logging.getLogger(__name__)
_CORE_EVENTS_TO_IDLE_SUBSYSTEMS = {
"track_playback_paused": None,
"track_playback_resumed": None,
"track_playback_started": None,
"track_playback_ended": None,
"playback_state_changed": "player",
"tracklist_changed": "playlist",
"playlists_loaded": "stored_playlist",
"playlist_changed": "stored_playlist",
"playlist_deleted": "stored_playlist",
"options_changed": "options",
"volume_changed": "mixer",
"mute_changed": "output",
"seeked": "player",
"stream_title_changed": "playlist",
}
class MpdFrontend(pykka.ThreadingActor, CoreListener):
def __init__(self, config, core):
super().__init__()
self.hostname = network.format_hostname(config["mpd"]["hostname"])
self.port = config["mpd"]["port"]
self.uri_map = uri_mapper.MpdUriMapper(core)
self.zeroconf_name = config["mpd"]["zeroconf"]
self.zeroconf_service = None
self.server = self._setup_server(config, core)
def _setup_server(self, config, core):
try:
server = network.Server(
self.hostname,
self.port,
protocol=session.MpdSession,
protocol_kwargs={
"config": config,
"core": core,
"uri_map": self.uri_map,
},
max_connections=config["mpd"]["max_connections"],
timeout=config["mpd"]["connection_timeout"],
)
except OSError as exc:
raise exceptions.FrontendError(f"MPD server startup failed: {exc}")
logger.info(
f"MPD server running at {network.format_address(server.address)}"
)
return server
def on_start(self):
if self.zeroconf_name and not network.is_unix_socket(
self.server.server_socket
):
self.zeroconf_service = zeroconf.Zeroconf(
name=self.zeroconf_name, stype="_mpd._tcp", port=self.port
)
self.zeroconf_service.publish()
def on_stop(self):
if self.zeroconf_service:
self.zeroconf_service.unpublish()
session_actors = pykka.ActorRegistry.get_by_class(session.MpdSession)
for session_actor in session_actors:
session_actor.stop()
self.server.stop()
def on_event(self, event, **kwargs):
if event not in _CORE_EVENTS_TO_IDLE_SUBSYSTEMS:
logger.warning(
"Got unexpected event: %s(%s)", event, ", ".join(kwargs)
)
else:
self.send_idle(_CORE_EVENTS_TO_IDLE_SUBSYSTEMS[event])
def send_idle(self, subsystem):
if subsystem:
listener.send(session.MpdSession, subsystem)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/dispatcher.py 0000644 0001751 0000171 00000025765 00000000000 017612 0 ustar 00runner docker import logging
import re
import pykka
from mopidy_mpd import exceptions, protocol, tokenize
logger = logging.getLogger(__name__)
protocol.load_protocol_modules()
class MpdDispatcher:
"""
The MPD session feeds the MPD dispatcher with requests. The dispatcher
finds the correct handler, processes the request and sends the response
back to the MPD session.
"""
_noidle = re.compile(r"^noidle$")
def __init__(self, session=None, config=None, core=None, uri_map=None):
self.config = config
self.authenticated = False
self.command_list_receiving = False
self.command_list_ok = False
self.command_list = []
self.command_list_index = None
self.context = MpdContext(
self, session=session, config=config, core=core, uri_map=uri_map
)
def handle_request(self, request, current_command_list_index=None):
"""Dispatch incoming requests to the correct handler."""
self.command_list_index = current_command_list_index
response = []
filter_chain = [
self._catch_mpd_ack_errors_filter,
self._authenticate_filter,
self._command_list_filter,
self._idle_filter,
self._add_ok_filter,
self._call_handler_filter,
]
return self._call_next_filter(request, response, filter_chain)
def handle_idle(self, subsystem):
# TODO: validate against mopidy_mpd/protocol/status.SUBSYSTEMS
self.context.events.add(subsystem)
subsystems = self.context.subscriptions.intersection(
self.context.events
)
if not subsystems:
return
response = []
for subsystem in subsystems:
response.append(f"changed: {subsystem}")
response.append("OK")
self.context.subscriptions = set()
self.context.events = set()
self.context.session.send_lines(response)
def _call_next_filter(self, request, response, filter_chain):
if filter_chain:
next_filter = filter_chain.pop(0)
return next_filter(request, response, filter_chain)
else:
return response
# Filter: catch MPD ACK errors
def _catch_mpd_ack_errors_filter(self, request, response, filter_chain):
try:
return self._call_next_filter(request, response, filter_chain)
except exceptions.MpdAckError as mpd_ack_error:
if self.command_list_index is not None:
mpd_ack_error.index = self.command_list_index
return [mpd_ack_error.get_mpd_ack()]
# Filter: authenticate
def _authenticate_filter(self, request, response, filter_chain):
if self.authenticated:
return self._call_next_filter(request, response, filter_chain)
elif self.config["mpd"]["password"] is None:
self.authenticated = True
return self._call_next_filter(request, response, filter_chain)
else:
command_name = request.split(" ")[0]
command = protocol.commands.handlers.get(command_name)
if command and not command.auth_required:
return self._call_next_filter(request, response, filter_chain)
else:
raise exceptions.MpdPermissionError(command=command_name)
# Filter: command list
def _command_list_filter(self, request, response, filter_chain):
if self._is_receiving_command_list(request):
self.command_list.append(request)
return []
else:
response = self._call_next_filter(request, response, filter_chain)
if self._is_receiving_command_list(
request
) or self._is_processing_command_list(request):
if response and response[-1] == "OK":
response = response[:-1]
return response
def _is_receiving_command_list(self, request):
return self.command_list_receiving and request != "command_list_end"
def _is_processing_command_list(self, request):
return (
self.command_list_index is not None
and request != "command_list_end"
)
# Filter: idle
def _idle_filter(self, request, response, filter_chain):
if self._is_currently_idle() and not self._noidle.match(request):
logger.debug(
"Client sent us %s, only %s is allowed while in "
"the idle state",
repr(request),
repr("noidle"),
)
self.context.session.close()
return []
if not self._is_currently_idle() and self._noidle.match(request):
return [] # noidle was called before idle
response = self._call_next_filter(request, response, filter_chain)
if self._is_currently_idle():
return []
else:
return response
def _is_currently_idle(self):
return bool(self.context.subscriptions)
# Filter: add OK
def _add_ok_filter(self, request, response, filter_chain):
response = self._call_next_filter(request, response, filter_chain)
if not self._has_error(response):
response.append("OK")
return response
def _has_error(self, response):
return response and response[-1].startswith("ACK")
# Filter: call handler
def _call_handler_filter(self, request, response, filter_chain):
try:
response = self._format_response(self._call_handler(request))
return self._call_next_filter(request, response, filter_chain)
except pykka.ActorDeadError as e:
logger.warning("Tried to communicate with dead actor.")
raise exceptions.MpdSystemError(e)
def _call_handler(self, request):
tokens = tokenize.split(request)
# TODO: check that blacklist items are valid commands?
blacklist = self.config["mpd"].get("command_blacklist", [])
if tokens and tokens[0] in blacklist:
logger.warning("MPD client used blacklisted command: %s", tokens[0])
raise exceptions.MpdDisabled(command=tokens[0])
try:
return protocol.commands.call(tokens, context=self.context)
except exceptions.MpdAckError as exc:
if exc.command is None:
exc.command = tokens[0]
raise
def _format_response(self, response):
formatted_response = []
for element in self._listify_result(response):
formatted_response.extend(self._format_lines(element))
return formatted_response
def _listify_result(self, result):
if result is None:
return []
if isinstance(result, set):
return self._flatten(list(result))
if not isinstance(result, list):
return [result]
return self._flatten(result)
def _flatten(self, the_list):
result = []
for element in the_list:
if isinstance(element, list):
result.extend(self._flatten(element))
else:
result.append(element)
return result
def _format_lines(self, line):
if isinstance(line, dict):
return [f"{key}: {value}" for (key, value) in line.items()]
if isinstance(line, tuple):
(key, value) = line
return [f"{key}: {value}"]
return [line]
class MpdContext:
"""
This object is passed as the first argument to all MPD command handlers to
give the command handlers access to important parts of Mopidy.
"""
#: The current :class:`MpdDispatcher`.
dispatcher = None
#: The current :class:`mopidy_mpd.MpdSession`.
session = None
#: The MPD password
password = None
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
core = None
#: The active subsystems that have pending events.
events = None
#: The subsytems that we want to be notified about in idle mode.
subscriptions = None
_uri_map = None
def __init__(
self, dispatcher, session=None, config=None, core=None, uri_map=None
):
self.dispatcher = dispatcher
self.session = session
if config is not None:
self.password = config["mpd"]["password"]
self.core = core
self.events = set()
self.subscriptions = set()
self._uri_map = uri_map
def lookup_playlist_uri_from_name(self, name):
"""
Helper function to retrieve a playlist from its unique MPD name.
"""
return self._uri_map.playlist_uri_from_name(name)
def lookup_playlist_name_from_uri(self, uri):
"""
Helper function to retrieve the unique MPD playlist name from its uri.
"""
return self._uri_map.playlist_name_from_uri(uri)
def browse(self, path, recursive=True, lookup=True):
"""
Browse the contents of a given directory path.
Returns a sequence of two-tuples ``(path, data)``.
If ``recursive`` is true, it returns results for all entries in the
given path.
If ``lookup`` is true and the ``path`` is to a track, the returned
``data`` is a future which will contain the results from looking up
the URI with :meth:`mopidy.core.LibraryController.lookup`. If
``lookup`` is false and the ``path`` is to a track, the returned
``data`` will be a :class:`mopidy.models.Ref` for the track.
For all entries that are not tracks, the returned ``data`` will be
:class:`None`.
"""
path_parts = re.findall(r"[^/]+", path or "")
root_path = "/".join([""] + path_parts)
uri = self._uri_map.uri_from_name(root_path)
if uri is None:
for part in path_parts:
for ref in self.core.library.browse(uri).get():
if ref.type != ref.TRACK and ref.name == part:
uri = ref.uri
break
else:
raise exceptions.MpdNoExistError("Not found")
root_path = self._uri_map.insert(root_path, uri)
if recursive:
yield (root_path, None)
path_and_futures = [(root_path, self.core.library.browse(uri))]
while path_and_futures:
base_path, future = path_and_futures.pop()
for ref in future.get():
if ref.name is None or ref.uri is None:
continue
path = "/".join([base_path, ref.name.replace("/", "")])
path = self._uri_map.insert(path, ref.uri)
if ref.type == ref.TRACK:
if lookup:
# TODO: can we lookup all the refs at once now?
yield (path, self.core.library.lookup(uris=[ref.uri]))
else:
yield (path, ref)
else:
yield (path, None)
if recursive:
path_and_futures.append(
(path, self.core.library.browse(ref.uri))
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/exceptions.py 0000644 0001751 0000171 00000007560 00000000000 017636 0 ustar 00runner docker from mopidy.exceptions import MopidyException
class MpdAckError(MopidyException):
"""See fields on this class for available MPD error codes"""
ACK_ERROR_NOT_LIST = 1
ACK_ERROR_ARG = 2
ACK_ERROR_PASSWORD = 3
ACK_ERROR_PERMISSION = 4
ACK_ERROR_UNKNOWN = 5
ACK_ERROR_NO_EXIST = 50
ACK_ERROR_PLAYLIST_MAX = 51
ACK_ERROR_SYSTEM = 52
ACK_ERROR_PLAYLIST_LOAD = 53
ACK_ERROR_UPDATE_ALREADY = 54
ACK_ERROR_PLAYER_SYNC = 55
ACK_ERROR_EXIST = 56
error_code = 0
def __init__(self, message="", index=0, command=None):
super().__init__(message, index, command)
self.message = message
self.index = index
self.command = command
def get_mpd_ack(self):
"""
MPD error code format::
ACK [%(error_code)i@%(index)i] {%(command)s} description
"""
return (
f"ACK [{self.__class__.error_code:d}@{self.index:d}] "
f"{{{self.command}}} {self.message}"
)
class MpdArgError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_ARG
class MpdPasswordError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PASSWORD
class MpdPermissionError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_PERMISSION
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.command is not None, "command must be given explicitly"
self.message = f'you don\'t have permission for "{self.command}"'
class MpdUnknownError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_UNKNOWN
class MpdUnknownCommand(MpdUnknownError):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.command is not None, "command must be given explicitly"
self.message = f'unknown command "{self.command}"'
self.command = ""
class MpdNoCommand(MpdUnknownCommand):
def __init__(self, *args, **kwargs):
kwargs["command"] = ""
super().__init__(*args, **kwargs)
self.message = "No command given"
class MpdNoExistError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_NO_EXIST
class MpdExistError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_EXIST
class MpdSystemError(MpdAckError):
error_code = MpdAckError.ACK_ERROR_SYSTEM
class MpdInvalidPlaylistName(MpdAckError):
error_code = MpdAckError.ACK_ERROR_ARG
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message = (
"playlist name is invalid: playlist names may not "
"contain slashes, newlines or carriage returns"
)
class MpdNotImplemented(MpdAckError):
error_code = 0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message = "Not implemented"
class MpdInvalidTrackForPlaylist(MpdAckError):
# NOTE: This is a custom error for Mopidy that does not exist in MPD.
error_code = 0
def __init__(self, playlist_scheme, track_scheme, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message = (
f'Playlist with scheme "{playlist_scheme}" '
f'can\'t store track scheme "{track_scheme}"'
)
class MpdFailedToSavePlaylist(MpdAckError):
# NOTE: This is a custom error for Mopidy that does not exist in MPD.
error_code = 0
def __init__(self, backend_scheme, *args, **kwargs):
super().__init__(*args, **kwargs)
self.message = (
f'Backend with scheme "{backend_scheme}" failed to save playlist'
)
class MpdDisabled(MpdAckError):
# NOTE: This is a custom error for Mopidy that does not exist in MPD.
error_code = 0
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
assert self.command is not None, "command must be given explicitly"
self.message = f'"{self.command}" has been disabled in the server'
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/ext.conf 0000644 0001751 0000171 00000000336 00000000000 016544 0 ustar 00runner docker [mpd]
enabled = true
hostname = 127.0.0.1
port = 6600
password =
max_connections = 20
connection_timeout = 60
zeroconf = Mopidy MPD server on $hostname
command_blacklist = listall,listallinfo
default_playlist_scheme = m3u
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/formatting.py 0000644 0001751 0000171 00000000530 00000000000 017615 0 ustar 00runner docker def indent(string, places=4, linebreak="\n", singles=False):
lines = string.split(linebreak)
if not singles and len(lines) == 1:
return string
for i, line in enumerate(lines):
lines[i] = " " * places + line
result = linebreak.join(lines)
if not singles:
result = linebreak + result
return result
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/network.py 0000644 0001751 0000171 00000035075 00000000000 017150 0 ustar 00runner docker import errno
import logging
import os
import re
import socket
import sys
import threading
import pykka
from gi.repository import GLib
logger = logging.getLogger(__name__)
def get_unix_socket_path(socket_path):
match = re.search("^unix:(.*)", socket_path)
if not match:
return None
return match.group(1)
def is_unix_socket(sock):
"""Check if the provided socket is a Unix domain socket"""
if hasattr(socket, "AF_UNIX"):
return sock.family == socket.AF_UNIX
return False
def get_socket_address(host, port):
unix_socket_path = get_unix_socket_path(host)
if unix_socket_path is not None:
return (unix_socket_path, None)
else:
return (host, port)
class ShouldRetrySocketCall(Exception):
"""Indicate that attempted socket call should be retried"""
def try_ipv6_socket():
"""Determine if system really supports IPv6"""
if not socket.has_ipv6:
return False
try:
socket.socket(socket.AF_INET6).close()
return True
except OSError as exc:
logger.debug(
f"Platform supports IPv6, but socket creation failed, "
f"disabling: {exc}"
)
return False
#: Boolean value that indicates if creating an IPv6 socket will succeed.
has_ipv6 = try_ipv6_socket()
def create_tcp_socket():
"""Create a TCP socket with or without IPv6 depending on system support"""
if has_ipv6:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
# Explicitly configure socket to work for both IPv4 and IPv6
if hasattr(socket, "IPPROTO_IPV6"):
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
elif sys.platform == "win32": # also match 64bit windows.
# Python 2.7 on windows does not have the IPPROTO_IPV6 constant
# Use values extracted from Windows Vista/7/8's header
sock.setsockopt(41, 27, 0)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return sock
def create_unix_socket():
"""Create a Unix domain socket"""
return socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
def format_address(address):
"""Format socket address for display."""
host, port = address[:2]
if port is not None:
return f"[{host}]:{port}"
else:
return f"[{host}]"
def format_hostname(hostname):
"""Format hostname for display."""
if has_ipv6 and re.match(r"\d+.\d+.\d+.\d+", hostname) is not None:
hostname = f"::ffff:{hostname}"
return hostname
class Server:
"""Setup listener and register it with GLib's event loop."""
def __init__(
self,
host,
port,
protocol,
protocol_kwargs=None,
max_connections=5,
timeout=30,
):
self.protocol = protocol
self.protocol_kwargs = protocol_kwargs or {}
self.max_connections = max_connections
self.timeout = timeout
self.server_socket = self.create_server_socket(host, port)
self.address = get_socket_address(host, port)
self.watcher = self.register_server_socket(self.server_socket.fileno())
def create_server_socket(self, host, port):
socket_path = get_unix_socket_path(host)
if socket_path is not None: # host is a path so use unix socket
sock = create_unix_socket()
sock.bind(socket_path)
else:
# ensure the port is supplied
if not isinstance(port, int):
raise TypeError(f"Expected an integer, not {port!r}")
sock = create_tcp_socket()
sock.bind((host, port))
sock.setblocking(False)
sock.listen(1)
return sock
def stop(self):
GLib.source_remove(self.watcher)
if is_unix_socket(self.server_socket):
unix_socket_path = self.server_socket.getsockname()
else:
unix_socket_path = None
self.server_socket.shutdown(socket.SHUT_RDWR)
self.server_socket.close()
# clean up the socket file
if unix_socket_path is not None:
os.unlink(unix_socket_path)
def register_server_socket(self, fileno):
return GLib.io_add_watch(fileno, GLib.IO_IN, self.handle_connection)
def handle_connection(self, fd, flags):
try:
sock, addr = self.accept_connection()
except ShouldRetrySocketCall:
return True
if self.maximum_connections_exceeded():
self.reject_connection(sock, addr)
else:
self.init_connection(sock, addr)
return True
def accept_connection(self):
try:
sock, addr = self.server_socket.accept()
if is_unix_socket(sock):
addr = (sock.getsockname(), None)
return sock, addr
except OSError as e:
if e.errno in (errno.EAGAIN, errno.EINTR):
raise ShouldRetrySocketCall
raise
def maximum_connections_exceeded(self):
return (
self.max_connections is not None
and self.number_of_connections() >= self.max_connections
)
def number_of_connections(self):
return len(pykka.ActorRegistry.get_by_class(self.protocol))
def reject_connection(self, sock, addr):
# FIXME provide more context in logging?
logger.warning("Rejected connection from %s", format_address(addr))
try:
sock.close()
except OSError:
pass
def init_connection(self, sock, addr):
Connection(
self.protocol, self.protocol_kwargs, sock, addr, self.timeout
)
class Connection:
# NOTE: the callback code is _not_ run in the actor's thread, but in the
# same one as the event loop. If code in the callbacks blocks, the rest of
# GLib code will likely be blocked as well...
#
# Also note that source_remove() return values are ignored on purpose, a
# false return value would only tell us that what we thought was registered
# is already gone, there is really nothing more we can do.
def __init__(self, protocol, protocol_kwargs, sock, addr, timeout):
sock.setblocking(False)
self.host, self.port = addr[:2] # IPv6 has larger addr
self._sock = sock
self.protocol = protocol
self.protocol_kwargs = protocol_kwargs
self.timeout = timeout
self.send_lock = threading.Lock()
self.send_buffer = b""
self.stopping = False
self.recv_id = None
self.send_id = None
self.timeout_id = None
self.actor_ref = self.protocol.start(self, **self.protocol_kwargs)
self.enable_recv()
self.enable_timeout()
def stop(self, reason, level=logging.DEBUG):
if self.stopping:
logger.log(level, f"Already stopping: {reason}")
return
else:
self.stopping = True
logger.log(level, reason)
try:
self.actor_ref.stop(block=False)
except pykka.ActorDeadError:
pass
self.disable_timeout()
self.disable_recv()
self.disable_send()
try:
self._sock.close()
except OSError:
pass
def queue_send(self, data):
"""Try to send data to client exactly as is and queue rest."""
self.send_lock.acquire(True)
self.send_buffer = self.send(self.send_buffer + data)
self.send_lock.release()
if self.send_buffer:
self.enable_send()
def send(self, data):
"""Send data to client, return any unsent data."""
try:
sent = self._sock.send(data)
return data[sent:]
except OSError as exc:
if exc.errno in (errno.EWOULDBLOCK, errno.EINTR):
return data
self.stop(f"Unexpected client error: {exc}")
return b""
def enable_timeout(self):
"""Reactivate timeout mechanism."""
if self.timeout is None or self.timeout <= 0:
return
self.disable_timeout()
self.timeout_id = GLib.timeout_add_seconds(
self.timeout, self.timeout_callback
)
def disable_timeout(self):
"""Deactivate timeout mechanism."""
if self.timeout_id is None:
return
GLib.source_remove(self.timeout_id)
self.timeout_id = None
def enable_recv(self):
if self.recv_id is not None:
return
try:
self.recv_id = GLib.io_add_watch(
self._sock.fileno(),
GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
self.recv_callback,
)
except OSError as exc:
self.stop(f"Problem with connection: {exc}")
def disable_recv(self):
if self.recv_id is None:
return
GLib.source_remove(self.recv_id)
self.recv_id = None
def enable_send(self):
if self.send_id is not None:
return
try:
self.send_id = GLib.io_add_watch(
self._sock.fileno(),
GLib.IO_OUT | GLib.IO_ERR | GLib.IO_HUP,
self.send_callback,
)
except OSError as exc:
self.stop(f"Problem with connection: {exc}")
def disable_send(self):
if self.send_id is None:
return
GLib.source_remove(self.send_id)
self.send_id = None
def recv_callback(self, fd, flags):
if flags & (GLib.IO_ERR | GLib.IO_HUP):
self.stop(f"Bad client flags: {flags}")
return True
try:
data = self._sock.recv(4096)
except OSError as exc:
if exc.errno not in (errno.EWOULDBLOCK, errno.EINTR):
self.stop(f"Unexpected client error: {exc}")
return True
if not data:
self.disable_recv()
self.actor_ref.tell({"close": True})
return True
try:
self.actor_ref.tell({"received": data})
except pykka.ActorDeadError:
self.stop("Actor is dead.")
return True
def send_callback(self, fd, flags):
if flags & (GLib.IO_ERR | GLib.IO_HUP):
self.stop(f"Bad client flags: {flags}")
return True
# If with can't get the lock, simply try again next time socket is
# ready for sending.
if not self.send_lock.acquire(False):
return True
try:
self.send_buffer = self.send(self.send_buffer)
if not self.send_buffer:
self.disable_send()
finally:
self.send_lock.release()
return True
def timeout_callback(self):
self.stop(f"Client inactive for {self.timeout:d}s; closing connection")
return False
def __str__(self):
return format_address((self.host, self.port))
class LineProtocol(pykka.ThreadingActor):
"""
Base class for handling line based protocols.
Takes care of receiving new data from server's client code, decoding and
then splitting data along line boundaries.
"""
#: Line terminator to use for outputed lines.
terminator = b"\n"
#: Regex to use for spliting lines, will be set compiled version of its
#: own value, or to ``terminator``s value if it is not set itself.
delimiter = None
#: What encoding to expect incoming data to be in, can be :class:`None`.
encoding = "utf-8"
def __init__(self, connection):
super().__init__()
self.connection = connection
self.prevent_timeout = False
self.recv_buffer = b""
if self.delimiter:
self.delimiter = re.compile(self.delimiter)
else:
self.delimiter = re.compile(self.terminator)
@property
def host(self):
return self.connection.host
@property
def port(self):
return self.connection.port
def on_line_received(self, line):
"""
Called whenever a new line is found.
Should be implemented by subclasses.
"""
raise NotImplementedError
def on_receive(self, message):
"""Handle messages with new data from server."""
if "close" in message:
self.connection.stop("Client most likely disconnected.")
return
if "received" not in message:
return
self.connection.disable_timeout()
self.recv_buffer += message["received"]
for line in self.parse_lines():
line = self.decode(line)
if line is not None:
self.on_line_received(line)
if not self.prevent_timeout:
self.connection.enable_timeout()
def on_failure(self, exception_type, exception_value, traceback):
"""Clean up connection resouces when actor fails."""
self.connection.stop("Actor failed.")
def on_stop(self):
"""Clean up connection resouces when actor stops."""
self.connection.stop("Actor is shutting down.")
def parse_lines(self):
"""Consume new data and yield any lines found."""
while re.search(self.terminator, self.recv_buffer):
line, self.recv_buffer = self.delimiter.split(self.recv_buffer, 1)
yield line
def encode(self, line):
"""
Handle encoding of line.
Can be overridden by subclasses to change encoding behaviour.
"""
try:
return line.encode(self.encoding)
except UnicodeError:
logger.warning(
"Stopping actor due to encode problem, data "
"supplied by client was not valid %s",
self.encoding,
)
self.stop()
def decode(self, line):
"""
Handle decoding of line.
Can be overridden by subclasses to change decoding behaviour.
"""
try:
return line.decode(self.encoding)
except UnicodeError:
logger.warning(
"Stopping actor due to decode problem, data "
"supplied by client was not valid %s",
self.encoding,
)
self.stop()
def join_lines(self, lines):
if not lines:
return ""
line_terminator = self.decode(self.terminator)
return line_terminator.join(lines) + line_terminator
def send_lines(self, lines):
"""
Send array of lines to client via connection.
Join lines using the terminator that is set for this class, encode it
and send it to the client.
"""
if not lines:
return
data = self.join_lines(lines)
self.connection.queue_send(self.encode(data))
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9810467
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/ 0000755 0001751 0000171 00000000000 00000000000 016734 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/__init__.py 0000644 0001751 0000171 00000015637 00000000000 021061 0 ustar 00runner docker """
This is Mopidy's MPD protocol implementation.
This is partly based upon the `MPD protocol documentation
`_, which is a useful resource, but it is
rather incomplete with regards to data formats, both for requests and
responses. Thus, we have had to talk a great deal with the the original `MPD
server `_ using telnet to get the details we need to
implement our own MPD server which is compatible with the numerous existing
`MPD clients `_.
"""
import inspect
from mopidy_mpd import exceptions
#: The MPD protocol uses UTF-8 for encoding all data.
ENCODING = "UTF-8"
#: The MPD protocol uses ``\n`` as line terminator.
LINE_TERMINATOR = b"\n"
#: The MPD protocol version is 0.19.0.
VERSION = "0.19.0"
def load_protocol_modules():
"""
The protocol modules must be imported to get them registered in
:attr:`commands`.
"""
from . import ( # noqa
audio_output,
channels,
command_list,
connection,
current_playlist,
mount,
music_db,
playback,
reflection,
status,
stickers,
stored_playlists,
)
def INT(value): # noqa: N802
r"""Converts a value that matches [+-]?\d+ into an integer."""
if value is None:
raise ValueError("None is not a valid integer")
# TODO: check for whitespace via value != value.strip()?
return int(value)
def UINT(value): # noqa: N802
r"""Converts a value that matches \d+ into an integer."""
if value is None:
raise ValueError("None is not a valid integer")
if not value.isdigit():
raise ValueError("Only positive numbers are allowed")
return int(value)
def FLOAT(value): # noqa: N802
r"""Converts a value that matches [+-]\d+(.\d+)? into a float."""
if value is None:
raise ValueError("None is not a valid float")
return float(value)
def UFLOAT(value): # noqa: N802
r"""Converts a value that matches \d+(.\d+)? into a float."""
if value is None:
raise ValueError("None is not a valid float")
value = float(value)
if value < 0:
raise ValueError("Only positive numbers are allowed")
return value
def BOOL(value): # noqa: N802
"""Convert the values 0 and 1 into booleans."""
if value in ("1", "0"):
return bool(int(value))
raise ValueError(f"{value!r} is not 0 or 1")
def RANGE(value): # noqa: N802
"""Convert a single integer or range spec into a slice
``n`` should become ``slice(n, n+1)``
``n:`` should become ``slice(n, None)``
``n:m`` should become ``slice(n, m)`` and ``m > n`` must hold
"""
if ":" in value:
start, stop = value.split(":", 1)
start = UINT(start)
if stop.strip():
stop = UINT(stop)
if start >= stop:
raise ValueError("End must be larger than start")
else:
stop = None
else:
start = UINT(value)
stop = start + 1
return slice(start, stop)
class Commands:
"""Collection of MPD commands to expose to users.
Normally used through the global instance which command handlers have been
installed into.
"""
def __init__(self):
self.handlers = {}
# TODO: consider removing auth_required and list_command in favour of
# additional command instances to register in?
def add(self, name, auth_required=True, list_command=True, **validators):
"""Create a decorator that registers a handler and validation rules.
Additional keyword arguments are treated as converters/validators to
apply to tokens converting them to proper Python types.
Requirements for valid handlers:
- must accept a context argument as the first arg.
- may not use variable keyword arguments, ``**kwargs``.
- may use variable arguments ``*args`` *or* a mix of required and
optional arguments.
Decorator returns the unwrapped function so that tests etc can use the
functions with values with correct python types instead of strings.
:param string name: Name of the command being registered.
:param bool auth_required: If authorization is required.
:param bool list_command: If command should be listed in reflection.
"""
def wrapper(func):
if name in self.handlers:
raise ValueError(f"{name} already registered")
spec = inspect.getfullargspec(func)
defaults = dict(
zip(spec.args[-len(spec.defaults or []) :], spec.defaults or [])
)
if not spec.args and not spec.varargs:
raise TypeError("Handler must accept at least one argument.")
if len(spec.args) > 1 and spec.varargs:
raise TypeError(
"*args may not be combined with regular arguments"
)
if not set(validators.keys()).issubset(spec.args):
raise TypeError("Validator for non-existent arg passed")
if spec.varkw or spec.kwonlyargs:
raise TypeError("Keyword arguments are not permitted")
def validate(*args, **kwargs):
if spec.varargs:
return func(*args, **kwargs)
try:
ba = inspect.signature(func).bind(*args, **kwargs)
ba.apply_defaults()
callargs = ba.arguments
except TypeError:
raise exceptions.MpdArgError(
f'wrong number of arguments for "{name}"'
)
for key, value in callargs.items():
default = defaults.get(key, object())
if key in validators and value != default:
try:
callargs[key] = validators[key](value)
except ValueError:
raise exceptions.MpdArgError("incorrect arguments")
return func(**callargs)
validate.auth_required = auth_required
validate.list_command = list_command
self.handlers[name] = validate
return func
return wrapper
def call(self, tokens, context=None):
"""Find and run the handler registered for the given command.
If the handler was registered with any converters/validators they will
be run before calling the real handler.
:param list tokens: List of tokens to process
:param context: MPD context.
:type context: :class:`~mopidy_mpd.dispatcher.MpdContext`
"""
if not tokens:
raise exceptions.MpdNoCommand()
if tokens[0] not in self.handlers:
raise exceptions.MpdUnknownCommand(command=tokens[0])
return self.handlers[tokens[0]](context, *tokens[1:])
#: Global instance to install commands into
commands = Commands()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/audio_output.py 0000644 0001751 0000171 00000003572 00000000000 022036 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("disableoutput", outputid=protocol.UINT)
def disableoutput(context, outputid):
"""
*musicpd.org, audio output section:*
``disableoutput {ID}``
Turns an output off.
"""
if outputid == 0:
success = context.core.mixer.set_mute(False).get()
if not success:
raise exceptions.MpdSystemError("problems disabling output")
else:
raise exceptions.MpdNoExistError("No such audio output")
@protocol.commands.add("enableoutput", outputid=protocol.UINT)
def enableoutput(context, outputid):
"""
*musicpd.org, audio output section:*
``enableoutput {ID}``
Turns an output on.
"""
if outputid == 0:
success = context.core.mixer.set_mute(True).get()
if not success:
raise exceptions.MpdSystemError("problems enabling output")
else:
raise exceptions.MpdNoExistError("No such audio output")
@protocol.commands.add("toggleoutput", outputid=protocol.UINT)
def toggleoutput(context, outputid):
"""
*musicpd.org, audio output section:*
``toggleoutput {ID}``
Turns an output on or off, depending on the current state.
"""
if outputid == 0:
mute_status = context.core.mixer.get_mute().get()
success = context.core.mixer.set_mute(not mute_status)
if not success:
raise exceptions.MpdSystemError("problems toggling output")
else:
raise exceptions.MpdNoExistError("No such audio output")
@protocol.commands.add("outputs")
def outputs(context):
"""
*musicpd.org, audio output section:*
``outputs``
Shows information about all outputs.
"""
muted = 1 if context.core.mixer.get_mute().get() else 0
return [
("outputid", 0),
("outputname", "Mute"),
("outputenabled", muted),
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/channels.py 0000644 0001751 0000171 00000003275 00000000000 021110 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("subscribe")
def subscribe(context, channel):
"""
*musicpd.org, client to client section:*
``subscribe {NAME}``
Subscribe to a channel. The channel is created if it does not exist
already. The name may consist of alphanumeric ASCII characters plus
underscore, dash, dot and colon.
"""
# TODO: match channel against [A-Za-z0-9:._-]+
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("unsubscribe")
def unsubscribe(context, channel):
"""
*musicpd.org, client to client section:*
``unsubscribe {NAME}``
Unsubscribe from a channel.
"""
# TODO: match channel against [A-Za-z0-9:._-]+
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("channels")
def channels(context):
"""
*musicpd.org, client to client section:*
``channels``
Obtain a list of all channels. The response is a list of "channel:"
lines.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("readmessages")
def readmessages(context):
"""
*musicpd.org, client to client section:*
``readmessages``
Reads messages for this client. The response is a list of "channel:"
and "message:" lines.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("sendmessage")
def sendmessage(context, channel, text):
"""
*musicpd.org, client to client section:*
``sendmessage {CHANNEL} {TEXT}``
Send a message to the specified channel.
"""
# TODO: match channel against [A-Za-z0-9:._-]+
raise exceptions.MpdNotImplemented # TODO
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/command_list.py 0000644 0001751 0000171 00000004610 00000000000 021760 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("command_list_begin", list_command=False)
def command_list_begin(context):
"""
*musicpd.org, command list section:*
To facilitate faster adding of files etc. you can pass a list of
commands all at once using a command list. The command list begins
with ``command_list_begin`` or ``command_list_ok_begin`` and ends
with ``command_list_end``.
It does not execute any commands until the list has ended. The
return value is whatever the return for a list of commands is. On
success for all commands, ``OK`` is returned. If a command fails,
no more commands are executed and the appropriate ``ACK`` error is
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
returned for each successful command executed in the command list.
"""
context.dispatcher.command_list_receiving = True
context.dispatcher.command_list_ok = False
context.dispatcher.command_list = []
@protocol.commands.add("command_list_end", list_command=False)
def command_list_end(context):
"""See :meth:`command_list_begin()`."""
# TODO: batch consecutive add commands
if not context.dispatcher.command_list_receiving:
raise exceptions.MpdUnknownCommand(command="command_list_end")
context.dispatcher.command_list_receiving = False
(command_list, context.dispatcher.command_list) = (
context.dispatcher.command_list,
[],
)
(command_list_ok, context.dispatcher.command_list_ok) = (
context.dispatcher.command_list_ok,
False,
)
command_list_response = []
for index, command in enumerate(command_list):
response = context.dispatcher.handle_request(
command, current_command_list_index=index
)
command_list_response.extend(response)
if command_list_response and command_list_response[-1].startswith(
"ACK"
):
return command_list_response
if command_list_ok:
command_list_response.append("list_OK")
return command_list_response
@protocol.commands.add("command_list_ok_begin", list_command=False)
def command_list_ok_begin(context):
"""See :meth:`command_list_begin()`."""
context.dispatcher.command_list_receiving = True
context.dispatcher.command_list_ok = True
context.dispatcher.command_list = []
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/connection.py 0000644 0001751 0000171 00000002150 00000000000 021443 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("close", auth_required=False)
def close(context):
"""
*musicpd.org, connection section:*
``close``
Closes the connection to MPD.
"""
context.session.close()
@protocol.commands.add("kill", list_command=False)
def kill(context):
"""
*musicpd.org, connection section:*
``kill``
Kills MPD.
"""
raise exceptions.MpdPermissionError(command="kill")
@protocol.commands.add("password", auth_required=False)
def password(context, password):
"""
*musicpd.org, connection section:*
``password {PASSWORD}``
This is used for authentication with the server. ``PASSWORD`` is
simply the plaintext password.
"""
if password == context.password:
context.dispatcher.authenticated = True
else:
raise exceptions.MpdPasswordError("incorrect password")
@protocol.commands.add("ping", auth_required=False)
def ping(context):
"""
*musicpd.org, connection section:*
``ping``
Does nothing but return ``OK``.
"""
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/current_playlist.py 0000644 0001751 0000171 00000033200 00000000000 022707 0 ustar 00runner docker import urllib
from mopidy_mpd import exceptions, protocol, translator
@protocol.commands.add("add")
def add(context, uri):
"""
*musicpd.org, current playlist section:*
``add {URI}``
Adds the file ``URI`` to the playlist (directories add recursively).
``URI`` can also be a single file.
*Clarifications:*
- ``add ""`` should add all tracks in the library to the current playlist.
"""
if not uri.strip("/"):
return
# If we have an URI just try and add it directly without bothering with
# jumping through browse...
if urllib.parse.urlparse(uri).scheme != "":
if context.core.tracklist.add(uris=[uri]).get():
return
try:
uris = []
for _path, ref in context.browse(uri, lookup=False):
if ref:
uris.append(ref.uri)
except exceptions.MpdNoExistError as exc:
exc.message = ( # noqa B306: Our own exception
"directory or file not found"
)
raise
if not uris:
raise exceptions.MpdNoExistError("directory or file not found")
context.core.tracklist.add(uris=uris).get()
@protocol.commands.add("addid", songpos=protocol.UINT)
def addid(context, uri, songpos=None):
"""
*musicpd.org, current playlist section:*
``addid {URI} [POSITION]``
Adds a song to the playlist (non-recursive) and returns the song id.
``URI`` is always a single file or URL. For example::
addid "foo.mp3"
Id: 999
OK
*Clarifications:*
- ``addid ""`` should return an error.
"""
if not uri:
raise exceptions.MpdNoExistError("No such song")
length = context.core.tracklist.get_length()
if songpos is not None and songpos > length.get():
raise exceptions.MpdArgError("Bad song index")
tl_tracks = context.core.tracklist.add(
uris=[uri], at_position=songpos
).get()
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
return ("Id", tl_tracks[0].tlid)
@protocol.commands.add("delete", songrange=protocol.RANGE)
def delete(context, songrange):
"""
*musicpd.org, current playlist section:*
``delete [{POS} | {START:END}]``
Deletes a song from the playlist.
"""
start = songrange.start
end = songrange.stop
if end is None:
end = context.core.tracklist.get_length().get()
tl_tracks = context.core.tracklist.slice(start, end).get()
if not tl_tracks:
raise exceptions.MpdArgError("Bad song index", command="delete")
for (tlid, _) in tl_tracks:
context.core.tracklist.remove({"tlid": [tlid]})
@protocol.commands.add("deleteid", tlid=protocol.UINT)
def deleteid(context, tlid):
"""
*musicpd.org, current playlist section:*
``deleteid {SONGID}``
Deletes the song ``SONGID`` from the playlist
"""
tl_tracks = context.core.tracklist.remove({"tlid": [tlid]}).get()
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
@protocol.commands.add("clear")
def clear(context):
"""
*musicpd.org, current playlist section:*
``clear``
Clears the current playlist.
"""
context.core.tracklist.clear()
@protocol.commands.add("move", songrange=protocol.RANGE, to=protocol.UINT)
def move_range(context, songrange, to):
"""
*musicpd.org, current playlist section:*
``move [{FROM} | {START:END}] {TO}``
Moves the song at ``FROM`` or range of songs at ``START:END`` to
``TO`` in the playlist.
"""
start = songrange.start
end = songrange.stop
if end is None:
end = context.core.tracklist.get_length().get()
context.core.tracklist.move(start, end, to)
@protocol.commands.add("moveid", tlid=protocol.UINT, to=protocol.UINT)
def moveid(context, tlid, to):
"""
*musicpd.org, current playlist section:*
``moveid {FROM} {TO}``
Moves the song with ``FROM`` (songid) to ``TO`` (playlist index) in
the playlist. If ``TO`` is negative, it is relative to the current
song in the playlist (if there is one).
"""
tl_tracks = context.core.tracklist.filter({"tlid": [tlid]}).get()
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
position = context.core.tracklist.index(tl_tracks[0]).get()
context.core.tracklist.move(position, position + 1, to)
@protocol.commands.add("playlist")
def playlist(context):
"""
*musicpd.org, current playlist section:*
``playlist``
Displays the current playlist.
.. note::
Do not use this, instead use ``playlistinfo``.
"""
return playlistinfo(context)
@protocol.commands.add("playlistfind")
def playlistfind(context, tag, needle):
"""
*musicpd.org, current playlist section:*
``playlistfind {TAG} {NEEDLE}``
Finds songs in the current playlist with strict matching.
"""
if tag == "filename":
tl_tracks = context.core.tracklist.filter({"uri": [needle]}).get()
if not tl_tracks:
return None
position = context.core.tracklist.index(tl_tracks[0]).get()
return translator.track_to_mpd_format(tl_tracks[0], position=position)
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("playlistid", tlid=protocol.UINT)
def playlistid(context, tlid=None):
"""
*musicpd.org, current playlist section:*
``playlistid {SONGID}``
Displays a list of songs in the playlist. ``SONGID`` is optional
and specifies a single song to display info for.
"""
if tlid is not None:
tl_tracks = context.core.tracklist.filter({"tlid": [tlid]}).get()
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
position = context.core.tracklist.index(tl_tracks[0]).get()
return translator.track_to_mpd_format(tl_tracks[0], position=position)
else:
return translator.tracks_to_mpd_format(
context.core.tracklist.get_tl_tracks().get()
)
@protocol.commands.add("playlistinfo")
def playlistinfo(context, parameter=None):
"""
*musicpd.org, current playlist section:*
``playlistinfo [[SONGPOS] | [START:END]]``
Displays a list of all songs in the playlist, or if the optional
argument is given, displays information only for the song
``SONGPOS`` or the range of songs ``START:END``.
*ncmpc and mpc:*
- uses negative indexes, like ``playlistinfo "-1"``, to request
the entire playlist
"""
if parameter is None or parameter == "-1":
start, end = 0, None
else:
tracklist_slice = protocol.RANGE(parameter)
start, end = tracklist_slice.start, tracklist_slice.stop
tl_tracks = context.core.tracklist.get_tl_tracks().get()
if start and start > len(tl_tracks):
raise exceptions.MpdArgError("Bad song index")
if end and end > len(tl_tracks):
end = None
return translator.tracks_to_mpd_format(tl_tracks, start, end)
@protocol.commands.add("playlistsearch")
def playlistsearch(context, tag, needle):
"""
*musicpd.org, current playlist section:*
``playlistsearch {TAG} {NEEDLE}``
Searches case-sensitively for partial matches in the current
playlist.
*GMPC:*
- uses ``filename`` and ``any`` as tags
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("plchanges", version=protocol.INT)
def plchanges(context, version):
"""
*musicpd.org, current playlist section:*
``plchanges {VERSION}``
Displays changed songs currently in the playlist since ``VERSION``.
To detect songs that were deleted at the end of the playlist, use
``playlistlength`` returned by status command.
*MPDroid:*
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
"""
# XXX Naive implementation that returns all tracks as changed
tracklist_version = context.core.tracklist.get_version().get()
if version < tracklist_version:
return translator.tracks_to_mpd_format(
context.core.tracklist.get_tl_tracks().get()
)
elif version == tracklist_version:
# A version match could indicate this is just a metadata update, so
# check for a stream ref and let the client know about the change.
stream_title = context.core.playback.get_stream_title().get()
if stream_title is None:
return None
tl_track = context.core.playback.get_current_tl_track().get()
position = context.core.tracklist.index(tl_track).get()
return translator.track_to_mpd_format(
tl_track, position=position, stream_title=stream_title
)
@protocol.commands.add("plchangesposid", version=protocol.INT)
def plchangesposid(context, version):
"""
*musicpd.org, current playlist section:*
``plchangesposid {VERSION}``
Displays changed songs currently in the playlist since ``VERSION``.
This function only returns the position and the id of the changed
song, not the complete metadata. This is more bandwidth efficient.
To detect songs that were deleted at the end of the playlist, use
``playlistlength`` returned by status command.
"""
# XXX Naive implementation that returns all tracks as changed
if int(version) != context.core.tracklist.get_version().get():
result = []
for (position, (tlid, _)) in enumerate(
context.core.tracklist.get_tl_tracks().get()
):
result.append(("cpos", position))
result.append(("Id", tlid))
return result
@protocol.commands.add("prio", priority=protocol.UINT, position=protocol.RANGE)
def prio(context, priority, position):
"""
*musicpd.org, current playlist section:*
``prio {PRIORITY} {START:END...}``
Set the priority of the specified songs. A higher priority means that
it will be played first when "random" mode is enabled.
A priority is an integer between 0 and 255. The default priority of new
songs is 0.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("prioid")
def prioid(context, *args):
"""
*musicpd.org, current playlist section:*
``prioid {PRIORITY} {ID...}``
Same as prio, but address the songs with their id.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("rangeid", tlid=protocol.UINT, songrange=protocol.RANGE)
def rangeid(context, tlid, songrange):
"""
*musicpd.org, current playlist section:*
``rangeid {ID} {START:END}``
Specifies the portion of the song that shall be played. START and END
are offsets in seconds (fractional seconds allowed); both are optional.
Omitting both (i.e. sending just ":") means "remove the range, play
everything". A song that is currently playing cannot be manipulated
this way.
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("shuffle", songrange=protocol.RANGE)
def shuffle(context, songrange=None):
"""
*musicpd.org, current playlist section:*
``shuffle [START:END]``
Shuffles the current playlist. ``START:END`` is optional and
specifies a range of songs.
"""
if songrange is None:
start, end = None, None
else:
start, end = songrange.start, songrange.stop
context.core.tracklist.shuffle(start, end)
@protocol.commands.add("swap", songpos1=protocol.UINT, songpos2=protocol.UINT)
def swap(context, songpos1, songpos2):
"""
*musicpd.org, current playlist section:*
``swap {SONG1} {SONG2}``
Swaps the positions of ``SONG1`` and ``SONG2``.
"""
if songpos2 < songpos1:
songpos1, songpos2 = songpos2, songpos1
context.core.tracklist.move(songpos1, songpos1 + 1, songpos2)
context.core.tracklist.move(songpos2 - 1, songpos2, songpos1)
@protocol.commands.add("swapid", tlid1=protocol.UINT, tlid2=protocol.UINT)
def swapid(context, tlid1, tlid2):
"""
*musicpd.org, current playlist section:*
``swapid {SONG1} {SONG2}``
Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids).
"""
tl_tracks1 = context.core.tracklist.filter({"tlid": [tlid1]}).get()
tl_tracks2 = context.core.tracklist.filter({"tlid": [tlid2]}).get()
if not tl_tracks1 or not tl_tracks2:
raise exceptions.MpdNoExistError("No such song")
position1 = context.core.tracklist.index(tl_tracks1[0]).get()
position2 = context.core.tracklist.index(tl_tracks2[0]).get()
swap(context, position1, position2)
@protocol.commands.add("addtagid", tlid=protocol.UINT)
def addtagid(context, tlid, tag, value):
"""
*musicpd.org, current playlist section:*
``addtagid {SONGID} {TAG} {VALUE}``
Adds a tag to the specified song. Editing song tags is only possible
for remote songs. This change is volatile: it may be overwritten by
tags received from the server, and the data is gone when the song gets
removed from the queue.
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("cleartagid", tlid=protocol.UINT)
def cleartagid(context, tlid, tag):
"""
*musicpd.org, current playlist section:*
``cleartagid {SONGID} [TAG]``
Removes tags from the specified song. If TAG is not specified, then all
tag values will be removed. Editing song tags is only possible for
remote songs.
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/mount.py 0000644 0001751 0000171 00000003577 00000000000 020464 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("mount")
def mount(context, path, uri):
"""
*musicpd.org, mounts and neighbors section:*
``mount {PATH} {URI}``
Mount the specified remote storage URI at the given path. Example::
mount foo nfs://192.168.1.4/export/mp3
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("unmount")
def unmount(context, path):
"""
*musicpd.org, mounts and neighbors section:*
``unmount {PATH}``
Unmounts the specified path. Example::
unmount foo
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("listmounts")
def listmounts(context):
"""
*musicpd.org, mounts and neighbors section:*
``listmounts``
Queries a list of all mounts. By default, this contains just the
configured music_directory. Example::
listmounts
mount:
storage: /home/foo/music
mount: foo
storage: nfs://192.168.1.4/export/mp3
OK
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("listneighbors")
def listneighbors(context):
"""
*musicpd.org, mounts and neighbors section:*
``listneighbors``
Queries a list of "neighbors" (e.g. accessible file servers on the
local net). Items on that list may be used with the mount command.
Example::
listneighbors
neighbor: smb://FOO
name: FOO (Samba 4.1.11-Debian)
OK
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/music_db.py 0000644 0001751 0000171 00000040143 00000000000 021075 0 ustar 00runner docker import functools
import itertools
from mopidy.models import Track
from mopidy_mpd import exceptions, protocol, translator
_SEARCH_MAPPING = {
"album": "album",
"albumartist": "albumartist",
"any": "any",
"artist": "artist",
"comment": "comment",
"composer": "composer",
"date": "date",
"file": "uri",
"filename": "uri",
"genre": "genre",
"performer": "performer",
"title": "track_name",
"track": "track_no",
}
_LIST_MAPPING = {
"title": "track",
"album": "album",
"albumartist": "albumartist",
"artist": "artist",
"composer": "composer",
"date": "date",
"genre": "genre",
"performer": "performer",
}
_LIST_NAME_MAPPING = {
"track": "Title",
"album": "Album",
"albumartist": "AlbumArtist",
"artist": "Artist",
"composer": "Composer",
"date": "Date",
"genre": "Genre",
"performer": "Performer",
}
def _query_from_mpd_search_parameters(parameters, mapping):
query = {}
parameters = list(parameters)
while parameters:
# TODO: does it matter that this is now case insensitive
field = mapping.get(parameters.pop(0).lower())
if not field:
raise exceptions.MpdArgError("incorrect arguments")
if not parameters:
raise ValueError
value = parameters.pop(0)
if value.strip():
query.setdefault(field, []).append(value)
return query
def _get_field(field, search_results):
return list(itertools.chain(*[getattr(r, field) for r in search_results]))
_get_albums = functools.partial(_get_field, "albums")
_get_artists = functools.partial(_get_field, "artists")
_get_tracks = functools.partial(_get_field, "tracks")
def _album_as_track(album):
return Track(
uri=album.uri,
name="Album: " + album.name,
artists=album.artists,
album=album,
date=album.date,
)
def _artist_as_track(artist):
return Track(
uri=artist.uri, name="Artist: " + artist.name, artists=[artist]
)
@protocol.commands.add("count")
def count(context, *args):
"""
*musicpd.org, music database section:*
``count {TAG} {NEEDLE}``
Counts the number of songs and their total playtime in the db
matching ``TAG`` exactly.
*GMPC:*
- use multiple tag-needle pairs to make more specific searches.
"""
try:
query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
except ValueError:
raise exceptions.MpdArgError("incorrect arguments")
results = context.core.library.search(query=query, exact=True).get()
result_tracks = _get_tracks(results)
total_length = sum(t.length for t in result_tracks if t.length)
return [
("songs", len(result_tracks)),
("playtime", int(total_length / 1000)),
]
@protocol.commands.add("find")
def find(context, *args):
"""
*musicpd.org, music database section:*
``find {TYPE} {WHAT}``
Finds songs in the db that are exactly ``WHAT``. ``TYPE`` can be any
tag supported by MPD, or one of the two special parameters - ``file``
to search by full path (relative to database root), and ``any`` to
match against all available tags. ``WHAT`` is what to find.
*GMPC:*
- also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album
tracks.
*ncmpc:*
- capitalizes the type argument.
*ncmpcpp:*
- also uses the search type "date".
- uses "file" instead of "filename".
"""
try:
query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
except ValueError:
return
results = context.core.library.search(query=query, exact=True).get()
result_tracks = []
if (
"artist" not in query
and "albumartist" not in query
and "composer" not in query
and "performer" not in query
):
result_tracks += [_artist_as_track(a) for a in _get_artists(results)]
if "album" not in query:
result_tracks += [_album_as_track(a) for a in _get_albums(results)]
result_tracks += _get_tracks(results)
return translator.tracks_to_mpd_format(result_tracks)
@protocol.commands.add("findadd")
def findadd(context, *args):
"""
*musicpd.org, music database section:*
``findadd {TYPE} {WHAT}``
Finds songs in the db that are exactly ``WHAT`` and adds them to
current playlist. Parameters have the same meaning as for ``find``.
"""
try:
query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
except ValueError:
return
results = context.core.library.search(query=query, exact=True).get()
context.core.tracklist.add(
uris=[track.uri for track in _get_tracks(results)]
).get()
@protocol.commands.add("list")
def list_(context, *args):
"""
*musicpd.org, music database section:*
``list {TYPE} [ARTIST]``
Lists all tags of the specified type. ``TYPE`` should be ``album``,
``artist``, ``albumartist``, ``date``, or ``genre``.
``ARTIST`` is an optional parameter when type is ``album``,
``date``, or ``genre``. This filters the result list by an artist.
*Clarifications:*
The musicpd.org documentation for ``list`` is far from complete. The
command also supports the following variant:
``list {TYPE} {QUERY}``
Where ``QUERY`` applies to all ``TYPE``. ``QUERY`` is one or more pairs
of a field name and a value. If the ``QUERY`` consists of more than one
pair, the pairs are AND-ed together to find the result. Examples of
valid queries and what they should return:
``list "artist" "artist" "ABBA"``
List artists where the artist name is "ABBA". Response::
Artist: ABBA
OK
``list "album" "artist" "ABBA"``
Lists albums where the artist name is "ABBA". Response::
Album: More ABBA Gold: More ABBA Hits
Album: Absolute More Christmas
Album: Gold: Greatest Hits
OK
``list "artist" "album" "Gold: Greatest Hits"``
Lists artists where the album name is "Gold: Greatest Hits".
Response::
Artist: ABBA
OK
``list "artist" "artist" "ABBA" "artist" "TLC"``
Lists artists where the artist name is "ABBA" *and* "TLC". Should
never match anything. Response::
OK
``list "date" "artist" "ABBA"``
Lists dates where artist name is "ABBA". Response::
Date:
Date: 1992
Date: 1993
OK
``list "date" "artist" "ABBA" "album" "Gold: Greatest Hits"``
Lists dates where artist name is "ABBA" and album name is "Gold:
Greatest Hits". Response::
Date: 1992
OK
``list "genre" "artist" "The Rolling Stones"``
Lists genres where artist name is "The Rolling Stones". Response::
Genre:
Genre: Rock
OK
*ncmpc:*
- capitalizes the field argument.
"""
params = list(args)
if not params:
raise exceptions.MpdArgError('too few arguments for "list"')
field_arg = params.pop(0).lower()
field = _LIST_MAPPING.get(field_arg)
if field is None:
raise exceptions.MpdArgError(f"Unknown tag type: {field_arg}")
query = None
if len(params) == 1:
if field != "album":
raise exceptions.MpdArgError('should be "Album" for 3 arguments')
if params[0].strip():
query = {"artist": params}
else:
try:
query = _query_from_mpd_search_parameters(params, _SEARCH_MAPPING)
except exceptions.MpdArgError as exc:
exc.message = "Unknown filter type" # noqa B306: Our own exception
raise
except ValueError:
return
name = _LIST_NAME_MAPPING[field]
result = context.core.library.get_distinct(field, query)
return [(name, value) for value in result.get()]
@protocol.commands.add("listall")
def listall(context, uri=None):
"""
*musicpd.org, music database section:*
``listall [URI]``
Lists all songs and directories in ``URI``.
Do not use this command. Do not manage a client-side copy of MPD's
database. That is fragile and adds huge overhead. It will break with
large databases. Instead, query MPD whenever you need something.
.. warning:: This command is disabled by default in Mopidy installs.
"""
result = []
for path, track_ref in context.browse(uri, lookup=False):
if not track_ref:
result.append(("directory", path.lstrip("/")))
else:
result.append(("file", track_ref.uri))
if not result:
raise exceptions.MpdNoExistError("Not found")
return result
@protocol.commands.add("listallinfo")
def listallinfo(context, uri=None):
"""
*musicpd.org, music database section:*
``listallinfo [URI]``
Same as ``listall``, except it also returns metadata info in the
same format as ``lsinfo``.
Do not use this command. Do not manage a client-side copy of MPD's
database. That is fragile and adds huge overhead. It will break with
large databases. Instead, query MPD whenever you need something.
.. warning:: This command is disabled by default in Mopidy installs.
"""
result = []
for path, lookup_future in context.browse(uri):
if not lookup_future:
result.append(("directory", path.lstrip("/")))
else:
for tracks in lookup_future.get().values():
for track in tracks:
result.extend(translator.track_to_mpd_format(track))
return result
@protocol.commands.add("listfiles")
def listfiles(context, uri=None):
"""
*musicpd.org, music database section:*
``listfiles [URI]``
Lists the contents of the directory URI, including files are not
recognized by MPD. URI can be a path relative to the music directory or
an URI understood by one of the storage plugins. The response contains
at least one line for each directory entry with the prefix "file: " or
"directory: ", and may be followed by file attributes such as
"Last-Modified" and "size".
For example, "smb://SERVER" returns a list of all shares on the given
SMB/CIFS server; "nfs://servername/path" obtains a directory listing
from the NFS server.
.. versionadded:: 0.19
New in MPD protocol version 0.19
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("lsinfo")
def lsinfo(context, uri=None):
"""
*musicpd.org, music database section:*
``lsinfo [URI]``
Lists the contents of the directory ``URI``.
When listing the root directory, this currently returns the list of
stored playlists. This behavior is deprecated; use
``listplaylists`` instead.
MPD returns the same result, including both playlists and the files and
directories located at the root level, for both ``lsinfo``, ``lsinfo
""``, and ``lsinfo "/"``.
"""
result = []
for path, lookup_future in context.browse(uri, recursive=False):
if not lookup_future:
result.append(("directory", path.lstrip("/")))
else:
for tracks in lookup_future.get().values():
if tracks:
result.extend(translator.track_to_mpd_format(tracks[0]))
if uri in (None, "", "/"):
result.extend(protocol.stored_playlists.listplaylists(context))
return result
@protocol.commands.add("rescan")
def rescan(context, uri=None):
"""
*musicpd.org, music database section:*
``rescan [URI]``
Same as ``update``, but also rescans unmodified files.
"""
return {"updating_db": 0} # TODO
@protocol.commands.add("search")
def search(context, *args):
"""
*musicpd.org, music database section:*
``search {TYPE} {WHAT} [...]``
Searches for any song that contains ``WHAT``. Parameters have the same
meaning as for ``find``, except that search is not case sensitive.
*GMPC:*
- uses the undocumented field ``any``.
- searches for multiple words like this::
search any "foo" any "bar" any "baz"
*ncmpc:*
- capitalizes the field argument.
*ncmpcpp:*
- also uses the search type "date".
- uses "file" instead of "filename".
"""
try:
query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
except ValueError:
return
results = context.core.library.search(query).get()
artists = [_artist_as_track(a) for a in _get_artists(results)]
albums = [_album_as_track(a) for a in _get_albums(results)]
tracks = _get_tracks(results)
return translator.tracks_to_mpd_format(artists + albums + tracks)
@protocol.commands.add("searchadd")
def searchadd(context, *args):
"""
*musicpd.org, music database section:*
``searchadd {TYPE} {WHAT} [...]``
Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds
them to current playlist.
Parameters have the same meaning as for ``find``, except that search is
not case sensitive.
"""
try:
query = _query_from_mpd_search_parameters(args, _SEARCH_MAPPING)
except ValueError:
return
results = context.core.library.search(query).get()
context.core.tracklist.add(
uris=[track.uri for track in _get_tracks(results)]
).get()
@protocol.commands.add("searchaddpl")
def searchaddpl(context, *args):
"""
*musicpd.org, music database section:*
``searchaddpl {NAME} {TYPE} {WHAT} [...]``
Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds
them to the playlist named ``NAME``.
If a playlist by that name doesn't exist it is created.
Parameters have the same meaning as for ``find``, except that search is
not case sensitive.
"""
parameters = list(args)
if not parameters:
raise exceptions.MpdArgError("incorrect arguments")
playlist_name = parameters.pop(0)
try:
query = _query_from_mpd_search_parameters(parameters, _SEARCH_MAPPING)
except ValueError:
return
results = context.core.library.search(query).get()
uri = context.lookup_playlist_uri_from_name(playlist_name)
playlist = uri is not None and context.core.playlists.lookup(uri).get()
if not playlist:
playlist = context.core.playlists.create(playlist_name).get()
tracks = list(playlist.tracks) + _get_tracks(results)
playlist = playlist.replace(tracks=tracks)
context.core.playlists.save(playlist)
@protocol.commands.add("update")
def update(context, uri=None):
"""
*musicpd.org, music database section:*
``update [URI]``
Updates the music database: find new files, remove deleted files,
update modified files.
``URI`` is a particular directory or song/file to update. If you do
not specify it, everything is updated.
Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number
identifying the update job. You can read the current job id in the
``status`` response.
"""
return {"updating_db": 0} # TODO
# TODO: add at least reflection tests before adding NotImplemented version
# @protocol.commands.add('readcomments')
def readcomments(context, uri):
"""
*musicpd.org, music database section:*
``readcomments [URI]``
Read "comments" (i.e. key-value pairs) from the file specified by
"URI". This "URI" can be a path relative to the music directory or a
URL in the form "file:///foo/bar.ogg".
This command may be used to list metadata of remote files (e.g. URI
beginning with "http://" or "smb://").
The response consists of lines in the form "KEY: VALUE". Comments with
suspicious characters (e.g. newlines) are ignored silently.
The meaning of these depends on the codec, and not all decoder plugins
support it. For example, on Ogg files, this lists the Vorbis comments.
"""
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/playback.py 0000644 0001751 0000171 00000034450 00000000000 021102 0 ustar 00runner docker from mopidy.core import PlaybackState
from mopidy_mpd import exceptions, protocol
@protocol.commands.add("consume", state=protocol.BOOL)
def consume(context, state):
"""
*musicpd.org, playback section:*
``consume {STATE}``
Sets consume state to ``STATE``, ``STATE`` should be 0 or
1. When consume is activated, each song played is removed from
playlist.
"""
context.core.tracklist.set_consume(state)
@protocol.commands.add("crossfade", seconds=protocol.UINT)
def crossfade(context, seconds):
"""
*musicpd.org, playback section:*
``crossfade {SECONDS}``
Sets crossfading between songs.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("mixrampdb")
def mixrampdb(context, decibels):
"""
*musicpd.org, playback section:*
``mixrampdb {deciBels}``
Sets the threshold at which songs will be overlapped. Like crossfading but
doesn't fade the track volume, just overlaps. The songs need to have
MixRamp tags added by an external tool. 0dB is the normalized maximum
volume so use negative values, I prefer -17dB. In the absence of mixramp
tags crossfading will be used. See
https://sourceforge.net/projects/mixramp/
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("mixrampdelay", seconds=protocol.UINT)
def mixrampdelay(context, seconds):
"""
*musicpd.org, playback section:*
``mixrampdelay {SECONDS}``
Additional time subtracted from the overlap calculated by mixrampdb. A
value of "nan" disables MixRamp overlapping and falls back to
crossfading.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("next")
def next_(context):
"""
*musicpd.org, playback section:*
``next``
Plays next song in the playlist.
*MPD's behaviour when affected by repeat/random/single/consume:*
Given a playlist of three tracks numbered 1, 2, 3, and a currently
playing track ``c``. ``next_track`` is defined at the track that
will be played upon calls to ``next``.
Tests performed on MPD 0.15.4-1ubuntu3.
====== ====== ====== ======= ===== ===== ===== =====
Inputs next_track
------------------------------- ------------------- -----
repeat random single consume c = 1 c = 2 c = 3 Notes
====== ====== ====== ======= ===== ===== ===== =====
T T T T 2 3 EOPL
T T T . Rand Rand Rand [1]
T T . T Rand Rand Rand [4]
T T . . Rand Rand Rand [4]
T . T T 2 3 EOPL
T . T . 2 3 1
T . . T 3 3 EOPL
T . . . 2 3 1
. T T T Rand Rand Rand [3]
. T T . Rand Rand Rand [3]
. T . T Rand Rand Rand [2]
. T . . Rand Rand Rand [2]
. . T T 2 3 EOPL
. . T . 2 3 EOPL
. . . T 2 3 EOPL
. . . . 2 3 EOPL
====== ====== ====== ======= ===== ===== ===== =====
- When end of playlist (EOPL) is reached, the current track is
unset.
- [1] When *random* and *single* is combined, ``next`` selects
a track randomly at each invocation, and not just the next track
in an internal prerandomized playlist.
- [2] When *random* is active, ``next`` will skip through
all tracks in the playlist in random order, and finally EOPL is
reached.
- [3] *single* has no effect in combination with *random*
alone, or *random* and *consume*.
- [4] When *random* and *repeat* is active, EOPL is never
reached, but the playlist is played again, in the same random
order as the first time.
"""
return context.core.playback.next().get()
@protocol.commands.add("pause", state=protocol.BOOL)
def pause(context, state=None):
"""
*musicpd.org, playback section:*
``pause {PAUSE}``
Toggles pause/resumes playing, ``PAUSE`` is 0 or 1.
*MPDroid:*
- Calls ``pause`` without any arguments to toogle pause.
"""
if state is None:
# Deprecated: Calling `pause` without any arguments
playback_state = context.core.playback.get_state().get()
if playback_state == PlaybackState.PLAYING:
context.core.playback.pause().get()
elif playback_state == PlaybackState.PAUSED:
context.core.playback.resume().get()
elif state:
context.core.playback.pause().get()
else:
context.core.playback.resume().get()
@protocol.commands.add("play", songpos=protocol.INT)
def play(context, songpos=None):
"""
*musicpd.org, playback section:*
``play [SONGPOS]``
Begins playing the playlist at song number ``SONGPOS``.
The original MPD server resumes from the paused state on ``play``
without arguments.
*Clarifications:*
- ``play "-1"`` when playing is ignored.
- ``play "-1"`` when paused resumes playback.
- ``play "-1"`` when stopped with a current track starts playback at the
current track.
- ``play "-1"`` when stopped without a current track, e.g. after playlist
replacement, starts playback at the first track.
*BitMPC:*
- issues ``play 6`` without quotes around the argument.
"""
if songpos is None:
return context.core.playback.play().get()
elif songpos == -1:
return _play_minus_one(context)
try:
tl_track = context.core.tracklist.slice(songpos, songpos + 1).get()[0]
return context.core.playback.play(tl_track).get()
except IndexError:
raise exceptions.MpdArgError("Bad song index")
def _play_minus_one(context):
playback_state = context.core.playback.get_state().get()
if playback_state == PlaybackState.PLAYING:
return # Nothing to do
elif playback_state == PlaybackState.PAUSED:
return context.core.playback.resume().get()
current_tl_track = context.core.playback.get_current_tl_track().get()
if current_tl_track is not None:
return context.core.playback.play(current_tl_track).get()
tl_tracks = context.core.tracklist.slice(0, 1).get()
if tl_tracks:
return context.core.playback.play(tl_tracks[0]).get()
return # Fail silently
@protocol.commands.add("playid", tlid=protocol.INT)
def playid(context, tlid):
"""
*musicpd.org, playback section:*
``playid [SONGID]``
Begins playing the playlist at song ``SONGID``.
*Clarifications:*
- ``playid "-1"`` when playing is ignored.
- ``playid "-1"`` when paused resumes playback.
- ``playid "-1"`` when stopped with a current track starts playback at the
current track.
- ``playid "-1"`` when stopped without a current track, e.g. after playlist
replacement, starts playback at the first track.
"""
if tlid == -1:
return _play_minus_one(context)
tl_tracks = context.core.tracklist.filter({"tlid": [tlid]}).get()
if not tl_tracks:
raise exceptions.MpdNoExistError("No such song")
return context.core.playback.play(tl_tracks[0]).get()
@protocol.commands.add("previous")
def previous(context):
"""
*musicpd.org, playback section:*
``previous``
Plays previous song in the playlist.
*MPD's behaviour when affected by repeat/random/single/consume:*
Given a playlist of three tracks numbered 1, 2, 3, and a currently
playing track ``c``. ``previous_track`` is defined at the track
that will be played upon ``previous`` calls.
Tests performed on MPD 0.15.4-1ubuntu3.
====== ====== ====== ======= ===== ===== =====
Inputs previous_track
------------------------------- -------------------
repeat random single consume c = 1 c = 2 c = 3
====== ====== ====== ======= ===== ===== =====
T T T T Rand? Rand? Rand?
T T T . 3 1 2
T T . T Rand? Rand? Rand?
T T . . 3 1 2
T . T T 3 1 2
T . T . 3 1 2
T . . T 3 1 2
T . . . 3 1 2
. T T T c c c
. T T . c c c
. T . T c c c
. T . . c c c
. . T T 1 1 2
. . T . 1 1 2
. . . T 1 1 2
. . . . 1 1 2
====== ====== ====== ======= ===== ===== =====
- If :attr:`time_position` of the current track is 15s or more,
``previous`` should do a seek to time position 0.
"""
return context.core.playback.previous().get()
@protocol.commands.add("random", state=protocol.BOOL)
def random(context, state):
"""
*musicpd.org, playback section:*
``random {STATE}``
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
"""
context.core.tracklist.set_random(state)
@protocol.commands.add("repeat", state=protocol.BOOL)
def repeat(context, state):
"""
*musicpd.org, playback section:*
``repeat {STATE}``
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
"""
context.core.tracklist.set_repeat(state)
@protocol.commands.add("replay_gain_mode")
def replay_gain_mode(context, mode):
"""
*musicpd.org, playback section:*
``replay_gain_mode {MODE}``
Sets the replay gain mode. One of ``off``, ``track``, ``album``.
Changing the mode during playback may take several seconds, because
the new settings does not affect the buffered data.
This command triggers the options idle event.
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("replay_gain_status")
def replay_gain_status(context):
"""
*musicpd.org, playback section:*
``replay_gain_status``
Prints replay gain options. Currently, only the variable
``replay_gain_mode`` is returned.
"""
return "replay_gain_mode: off" # TODO
@protocol.commands.add("seek", songpos=protocol.UINT, seconds=protocol.UFLOAT)
def seek(context, songpos, seconds):
"""
*musicpd.org, playback section:*
``seek {SONGPOS} {TIME}``
Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in
the playlist.
*Droid MPD:*
- issues ``seek 1 120`` without quotes around the arguments.
"""
tl_track = context.core.playback.get_current_tl_track().get()
if context.core.tracklist.index(tl_track).get() != songpos:
play(context, songpos)
context.core.playback.seek(int(seconds * 1000)).get()
@protocol.commands.add("seekid", tlid=protocol.UINT, seconds=protocol.UFLOAT)
def seekid(context, tlid, seconds):
"""
*musicpd.org, playback section:*
``seekid {SONGID} {TIME}``
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
"""
tl_track = context.core.playback.get_current_tl_track().get()
if not tl_track or tl_track.tlid != tlid:
playid(context, tlid)
context.core.playback.seek(int(seconds * 1000)).get()
@protocol.commands.add("seekcur")
def seekcur(context, time):
"""
*musicpd.org, playback section:*
``seekcur {TIME}``
Seeks to the position ``TIME`` within the current song. If prefixed by
'+' or '-', then the time is relative to the current playing position.
"""
if time.startswith(("+", "-")):
position = context.core.playback.get_time_position().get()
position += int(protocol.FLOAT(time) * 1000)
context.core.playback.seek(position).get()
else:
position = int(protocol.UFLOAT(time) * 1000)
context.core.playback.seek(position).get()
@protocol.commands.add("setvol", volume=protocol.INT)
def setvol(context, volume):
"""
*musicpd.org, playback section:*
``setvol {VOL}``
Sets volume to ``VOL``, the range of volume is 0-100.
*Droid MPD:*
- issues ``setvol 50`` without quotes around the argument.
"""
# NOTE: we use INT as clients can pass in +N etc.
value = min(max(0, volume), 100)
success = context.core.mixer.set_volume(value).get()
if not success:
raise exceptions.MpdSystemError("problems setting volume")
@protocol.commands.add("single", state=protocol.BOOL)
def single(context, state):
"""
*musicpd.org, playback section:*
``single {STATE}``
Sets single state to ``STATE``, ``STATE`` should be 0 or 1. When
single is activated, playback is stopped after current song, or
song is repeated if the ``repeat`` mode is enabled.
"""
context.core.tracklist.set_single(state)
@protocol.commands.add("stop")
def stop(context):
"""
*musicpd.org, playback section:*
``stop``
Stops playing.
"""
context.core.playback.stop()
@protocol.commands.add("volume", change=protocol.INT)
def volume(context, change):
"""
*musicpd.org, playback section:*
``volume {CHANGE}``
Changes volume by amount ``CHANGE``.
Note: ``volume`` is deprecated, use ``setvol`` instead.
"""
if change < -100 or change > 100:
raise exceptions.MpdArgError("Invalid volume value")
old_volume = context.core.mixer.get_volume().get()
if old_volume is None:
raise exceptions.MpdSystemError("problems setting volume")
new_volume = min(max(0, old_volume + change), 100)
success = context.core.mixer.set_volume(new_volume).get()
if not success:
raise exceptions.MpdSystemError("problems setting volume")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/reflection.py 0000644 0001751 0000171 00000005646 00000000000 021453 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
from mopidy_mpd.protocol import tagtype_list
@protocol.commands.add("config", list_command=False)
def config(context):
"""
*musicpd.org, reflection section:*
``config``
Dumps configuration values that may be interesting for the client. This
command is only permitted to "local" clients (connected via UNIX domain
socket).
"""
raise exceptions.MpdPermissionError(command="config")
@protocol.commands.add("commands", auth_required=False)
def commands(context):
"""
*musicpd.org, reflection section:*
``commands``
Shows which commands the current user has access to.
"""
command_names = set()
for name, handler in protocol.commands.handlers.items():
if not handler.list_command:
continue
if context.dispatcher.authenticated or not handler.auth_required:
command_names.add(name)
return [("command", command_name) for command_name in sorted(command_names)]
@protocol.commands.add("decoders")
def decoders(context):
"""
*musicpd.org, reflection section:*
``decoders``
Print a list of decoder plugins, followed by their supported
suffixes and MIME types. Example response::
plugin: mad
suffix: mp3
suffix: mp2
mime_type: audio/mpeg
plugin: mpcdec
suffix: mpc
*Clarifications:*
- ncmpcpp asks for decoders the first time you open the browse view. By
returning nothing and OK instead of an not implemented error, we avoid
"Not implemented" showing up in the ncmpcpp interface, and we get the
list of playlists without having to enter the browse interface twice.
"""
return # TODO
@protocol.commands.add("notcommands", auth_required=False)
def notcommands(context):
"""
*musicpd.org, reflection section:*
``notcommands``
Shows which commands the current user does not have access to.
"""
command_names = {"config", "kill"} # No permission to use
for name, handler in protocol.commands.handlers.items():
if not handler.list_command:
continue
if not context.dispatcher.authenticated and handler.auth_required:
command_names.add(name)
return [("command", command_name) for command_name in sorted(command_names)]
@protocol.commands.add("tagtypes")
def tagtypes(context):
"""
*musicpd.org, reflection section:*
``tagtypes``
Shows a list of available song metadata.
"""
return [("tagtype", tagtype) for tagtype in tagtype_list.TAGTYPE_LIST]
@protocol.commands.add("urlhandlers")
def urlhandlers(context):
"""
*musicpd.org, reflection section:*
``urlhandlers``
Gets a list of available URL handlers.
"""
return [
("handler", uri_scheme)
for uri_scheme in context.core.get_uri_schemes().get()
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/status.py 0000644 0001751 0000171 00000023777 00000000000 020651 0 ustar 00runner docker import pykka
from mopidy.core import PlaybackState
from mopidy_mpd import exceptions, protocol, translator
#: Subsystems that can be registered with idle command.
SUBSYSTEMS = [
"database",
"mixer",
"options",
"output",
"player",
"playlist",
"stored_playlist",
"update",
]
@protocol.commands.add("clearerror")
def clearerror(context):
"""
*musicpd.org, status section:*
``clearerror``
Clears the current error message in status (this is also
accomplished by any command that starts playback).
"""
raise exceptions.MpdNotImplemented # TODO
@protocol.commands.add("currentsong")
def currentsong(context):
"""
*musicpd.org, status section:*
``currentsong``
Displays the song info of the current song (same song that is
identified in status).
"""
tl_track = context.core.playback.get_current_tl_track().get()
stream_title = context.core.playback.get_stream_title().get()
if tl_track is not None:
position = context.core.tracklist.index(tl_track).get()
return translator.track_to_mpd_format(
tl_track, position=position, stream_title=stream_title
)
@protocol.commands.add("idle")
def idle(context, *subsystems):
"""
*musicpd.org, status section:*
``idle [SUBSYSTEMS...]``
Waits until there is a noteworthy change in one or more of MPD's
subsystems. As soon as there is one, it lists all changed systems
in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM``
is one of the following:
- ``database``: the song database has been modified after update.
- ``update``: a database update has started or finished. If the
database was modified during the update, the database event is
also emitted.
- ``stored_playlist``: a stored playlist has been modified,
renamed, created or deleted
- ``playlist``: the current playlist has been modified
- ``player``: the player has been started, stopped or seeked
- ``mixer``: the volume has been changed
- ``output``: an audio output has been enabled or disabled
- ``options``: options like repeat, random, crossfade, replay gain
While a client is waiting for idle results, the server disables
timeouts, allowing a client to wait for events as long as MPD runs.
The idle command can be canceled by sending the command ``noidle``
(no other commands are allowed). MPD will then leave idle mode and
print results immediately; might be empty at this time.
If the optional ``SUBSYSTEMS`` argument is used, MPD will only send
notifications when something changed in one of the specified
subsystems.
"""
# TODO: test against valid subsystems
if not subsystems:
subsystems = SUBSYSTEMS
for subsystem in subsystems:
context.subscriptions.add(subsystem)
active = context.subscriptions.intersection(context.events)
if not active:
context.session.prevent_timeout = True
return
response = []
context.events = set()
context.subscriptions = set()
for subsystem in active:
response.append(f"changed: {subsystem}")
return response
@protocol.commands.add("noidle", list_command=False)
def noidle(context):
"""See :meth:`_status_idle`."""
if not context.subscriptions:
return
context.subscriptions = set()
context.events = set()
context.session.prevent_timeout = False
@protocol.commands.add("stats")
def stats(context):
"""
*musicpd.org, status section:*
``stats``
Displays statistics.
- ``artists``: number of artists
- ``songs``: number of albums
- ``uptime``: daemon uptime in seconds
- ``db_playtime``: sum of all song times in the db
- ``db_update``: last db update in UNIX time
- ``playtime``: time length of music played
"""
return {
"artists": 0, # TODO
"albums": 0, # TODO
"songs": 0, # TODO
"uptime": 0, # TODO
"db_playtime": 0, # TODO
"db_update": 0, # TODO
"playtime": 0, # TODO
}
@protocol.commands.add("status")
def status(context):
"""
*musicpd.org, status section:*
``status``
Reports the current status of the player and the volume level.
- ``volume``: 0-100 or -1
- ``repeat``: 0 or 1
- ``single``: 0 or 1
- ``consume``: 0 or 1
- ``playlist``: 31-bit unsigned integer, the playlist version
number
- ``playlistlength``: integer, the length of the playlist
- ``state``: play, stop, or pause
- ``song``: playlist song number of the current song stopped on or
playing
- ``songid``: playlist songid of the current song stopped on or
playing
- ``nextsong``: playlist song number of the next song to be played
- ``nextsongid``: playlist songid of the next song to be played
- ``time``: total time elapsed (of current playing/paused song)
- ``elapsed``: Total time elapsed within the current song, but with
higher resolution.
- ``bitrate``: instantaneous bitrate in kbps
- ``xfade``: crossfade in seconds
- ``audio``: sampleRate``:bits``:channels
- ``updatings_db``: job id
- ``error``: if there is an error, returns message here
*Clarifications based on experience implementing*
- ``volume``: can also be -1 if no output is set.
- ``elapsed``: Higher resolution means time in seconds with three
decimal places for millisecond precision.
"""
tl_track = context.core.playback.get_current_tl_track()
next_tlid = context.core.tracklist.get_next_tlid()
futures = {
"tracklist.length": context.core.tracklist.get_length(),
"tracklist.version": context.core.tracklist.get_version(),
"mixer.volume": context.core.mixer.get_volume(),
"tracklist.consume": context.core.tracklist.get_consume(),
"tracklist.random": context.core.tracklist.get_random(),
"tracklist.repeat": context.core.tracklist.get_repeat(),
"tracklist.single": context.core.tracklist.get_single(),
"playback.state": context.core.playback.get_state(),
"playback.current_tl_track": tl_track,
"tracklist.index": context.core.tracklist.index(tl_track.get()),
"tracklist.next_tlid": next_tlid,
"tracklist.next_index": context.core.tracklist.index(
tlid=next_tlid.get()
),
"playback.time_position": context.core.playback.get_time_position(),
}
pykka.get_all(futures.values())
result = [
("volume", _status_volume(futures)),
("repeat", _status_repeat(futures)),
("random", _status_random(futures)),
("single", _status_single(futures)),
("consume", _status_consume(futures)),
("playlist", _status_playlist_version(futures)),
("playlistlength", _status_playlist_length(futures)),
("xfade", _status_xfade(futures)),
("state", _status_state(futures)),
]
if futures["playback.current_tl_track"].get() is not None:
result.append(("song", _status_songpos(futures)))
result.append(("songid", _status_songid(futures)))
if futures["tracklist.next_tlid"].get() is not None:
result.append(("nextsong", _status_nextsongpos(futures)))
result.append(("nextsongid", _status_nextsongid(futures)))
if futures["playback.state"].get() in (
PlaybackState.PLAYING,
PlaybackState.PAUSED,
):
result.append(("time", _status_time(futures)))
result.append(("elapsed", _status_time_elapsed(futures)))
result.append(("bitrate", _status_bitrate(futures)))
return result
def _status_bitrate(futures):
current_tl_track = futures["playback.current_tl_track"].get()
if current_tl_track is None:
return 0
if current_tl_track.track.bitrate is None:
return 0
return current_tl_track.track.bitrate
def _status_consume(futures):
if futures["tracklist.consume"].get():
return 1
else:
return 0
def _status_playlist_length(futures):
return futures["tracklist.length"].get()
def _status_playlist_version(futures):
return futures["tracklist.version"].get()
def _status_random(futures):
return int(futures["tracklist.random"].get())
def _status_repeat(futures):
return int(futures["tracklist.repeat"].get())
def _status_single(futures):
return int(futures["tracklist.single"].get())
def _status_songid(futures):
current_tl_track = futures["playback.current_tl_track"].get()
if current_tl_track is not None:
return current_tl_track.tlid
else:
return _status_songpos(futures)
def _status_songpos(futures):
return futures["tracklist.index"].get()
def _status_nextsongid(futures):
return futures["tracklist.next_tlid"].get()
def _status_nextsongpos(futures):
return futures["tracklist.next_index"].get()
def _status_state(futures):
state = futures["playback.state"].get()
if state == PlaybackState.PLAYING:
return "play"
elif state == PlaybackState.STOPPED:
return "stop"
elif state == PlaybackState.PAUSED:
return "pause"
def _status_time(futures):
position = futures["playback.time_position"].get() // 1000
total = _status_time_total(futures) // 1000
return f"{position:d}:{total:d}"
def _status_time_elapsed(futures):
elapsed = futures["playback.time_position"].get() / 1000.0
return f"{elapsed:.3f}"
def _status_time_total(futures):
current_tl_track = futures["playback.current_tl_track"].get()
if current_tl_track is None:
return 0
elif current_tl_track.track.length is None:
return 0
else:
return current_tl_track.track.length
def _status_volume(futures):
volume = futures["mixer.volume"].get()
if volume is not None:
return volume
else:
return -1
def _status_xfade(futures):
return 0 # Not supported
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/stickers.py 0000644 0001751 0000171 00000002266 00000000000 021143 0 ustar 00runner docker from mopidy_mpd import exceptions, protocol
@protocol.commands.add("sticker", list_command=False)
def sticker(context, action, field, uri, name=None, value=None):
"""
*musicpd.org, sticker section:*
``sticker list {TYPE} {URI}``
Lists the stickers for the specified object.
``sticker find {TYPE} {URI} {NAME}``
Searches the sticker database for stickers with the specified name,
below the specified directory (``URI``). For each matching song, it
prints the ``URI`` and that one sticker's value.
``sticker get {TYPE} {URI} {NAME}``
Reads a sticker value for the specified object.
``sticker set {TYPE} {URI} {NAME} {VALUE}``
Adds a sticker value to the specified object. If a sticker item
with that name already exists, it is replaced.
``sticker delete {TYPE} {URI} [NAME]``
Deletes a sticker value from the specified object. If you do not
specify a sticker name, all sticker values are deleted.
"""
# TODO: check that action in ('list', 'find', 'get', 'set', 'delete')
# TODO: check name/value matches with action
raise exceptions.MpdNotImplemented # TODO
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/stored_playlists.py 0000644 0001751 0000171 00000030463 00000000000 022720 0 ustar 00runner docker import datetime
import logging
import re
import urllib
from mopidy_mpd import exceptions, protocol, translator
logger = logging.getLogger(__name__)
def _check_playlist_name(name):
if re.search("[/\n\r]", name):
raise exceptions.MpdInvalidPlaylistName()
def _get_playlist(context, name, must_exist=True):
playlist = None
uri = context.lookup_playlist_uri_from_name(name)
if uri:
playlist = context.core.playlists.lookup(uri).get()
if must_exist and playlist is None:
raise exceptions.MpdNoExistError("No such playlist")
return playlist
@protocol.commands.add("listplaylist")
def listplaylist(context, name):
"""
*musicpd.org, stored playlists section:*
``listplaylist {NAME}``
Lists the files in the playlist ``NAME.m3u``.
Output format::
file: relative/path/to/file1.flac
file: relative/path/to/file2.ogg
file: relative/path/to/file3.mp3
"""
playlist = _get_playlist(context, name)
return [f"file: {track.uri}" for track in playlist.tracks]
@protocol.commands.add("listplaylistinfo")
def listplaylistinfo(context, name):
"""
*musicpd.org, stored playlists section:*
``listplaylistinfo {NAME}``
Lists songs in the playlist ``NAME.m3u``.
Output format:
Standard track listing, with fields: file, Time, Title, Date,
Album, Artist, Track
"""
playlist = _get_playlist(context, name)
track_uris = [track.uri for track in playlist.tracks]
tracks_map = context.core.library.lookup(uris=track_uris).get()
tracks = []
for uri in track_uris:
tracks.extend(tracks_map[uri])
playlist = playlist.replace(tracks=tracks)
return translator.playlist_to_mpd_format(playlist)
@protocol.commands.add("listplaylists")
def listplaylists(context):
"""
*musicpd.org, stored playlists section:*
``listplaylists``
Prints a list of the playlist directory.
After each playlist name the server sends its last modification
time as attribute ``Last-Modified`` in ISO 8601 format. To avoid
problems due to clock differences between clients and the server,
clients should not compare this value with their local clock.
Output format::
playlist: a
Last-Modified: 2010-02-06T02:10:25Z
playlist: b
Last-Modified: 2010-02-06T02:11:08Z
*Clarifications:*
- ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must
ignore playlists without names, which isn't very useful anyway.
"""
last_modified = _get_last_modified()
result = []
for playlist_ref in context.core.playlists.as_list().get():
if not playlist_ref.name:
continue
name = context.lookup_playlist_name_from_uri(playlist_ref.uri)
result.append(("playlist", name))
result.append(("Last-Modified", last_modified))
return result
# TODO: move to translators?
def _get_last_modified(last_modified=None):
"""Formats last modified timestamp of a playlist for MPD.
Time in UTC with second precision, formatted in the ISO 8601 format, with
the "Z" time zone marker for UTC. For example, "1970-01-01T00:00:00Z".
"""
if last_modified is None:
# If unknown, assume the playlist is modified
dt = datetime.datetime.utcnow()
else:
dt = datetime.datetime.utcfromtimestamp(last_modified / 1000.0)
dt = dt.replace(microsecond=0)
return f"{dt.isoformat()}Z"
DEFAULT_PLAYLIST_SLICE = slice(0, None)
@protocol.commands.add("load", playlist_slice=protocol.RANGE)
def load(context, name, playlist_slice=DEFAULT_PLAYLIST_SLICE):
"""
*musicpd.org, stored playlists section:*
``load {NAME} [START:END]``
Loads the playlist into the current queue. Playlist plugins are
supported. A range may be specified to load only a part of the
playlist.
*Clarifications:*
- ``load`` appends the given playlist to the current playlist.
- MPD 0.17.1 does not support open-ended ranges, i.e. without end
specified, for the ``load`` command, even though MPD's general range docs
allows open-ended ranges.
- MPD 0.17.1 does not fail if the specified range is outside the playlist,
in either or both ends.
"""
playlist = _get_playlist(context, name)
track_uris = [track.uri for track in playlist.tracks[playlist_slice]]
context.core.tracklist.add(uris=track_uris).get()
@protocol.commands.add("playlistadd")
def playlistadd(context, name, track_uri):
"""
*musicpd.org, stored playlists section:*
``playlistadd {NAME} {URI}``
Adds ``URI`` to the playlist ``NAME.m3u``.
``NAME.m3u`` will be created if it does not exist.
"""
_check_playlist_name(name)
old_playlist = _get_playlist(context, name, must_exist=False)
if not old_playlist:
# Create new playlist with this single track
lookup_res = context.core.library.lookup(uris=[track_uri]).get()
tracks = [
track for uri_tracks in lookup_res.values() for track in uri_tracks
]
_create_playlist(context, name, tracks)
else:
# Add track to existing playlist
lookup_res = context.core.library.lookup(uris=[track_uri]).get()
new_tracks = [
track for uri_tracks in lookup_res.values() for track in uri_tracks
]
new_playlist = old_playlist.replace(
tracks=list(old_playlist.tracks) + new_tracks
)
saved_playlist = context.core.playlists.save(new_playlist).get()
if saved_playlist is None:
playlist_scheme = urllib.parse.urlparse(old_playlist.uri).scheme
uri_scheme = urllib.parse.urlparse(track_uri).scheme
raise exceptions.MpdInvalidTrackForPlaylist(
playlist_scheme, uri_scheme
)
def _create_playlist(context, name, tracks):
"""
Creates new playlist using backend appropriate for the given tracks
"""
uri_schemes = {urllib.parse.urlparse(t.uri).scheme for t in tracks}
for scheme in uri_schemes:
new_playlist = context.core.playlists.create(name, scheme).get()
if new_playlist is None:
logger.debug("Backend for scheme %s can't create playlists", scheme)
continue # Backend can't create playlists at all
new_playlist = new_playlist.replace(tracks=tracks)
saved_playlist = context.core.playlists.save(new_playlist).get()
if saved_playlist is not None:
return # Created and saved
else:
continue # Failed to save using this backend
# Can't use backend appropriate for passed URI schemes, use default one
default_scheme = context.dispatcher.config["mpd"]["default_playlist_scheme"]
new_playlist = context.core.playlists.create(name, default_scheme).get()
if new_playlist is None:
# If even MPD's default backend can't save playlist, everything is lost
logger.warning("MPD's default backend can't create playlists")
raise exceptions.MpdFailedToSavePlaylist(default_scheme)
new_playlist = new_playlist.replace(tracks=tracks)
saved_playlist = context.core.playlists.save(new_playlist).get()
if saved_playlist is None:
uri_scheme = urllib.parse.urlparse(new_playlist.uri).scheme
raise exceptions.MpdFailedToSavePlaylist(uri_scheme)
@protocol.commands.add("playlistclear")
def playlistclear(context, name):
"""
*musicpd.org, stored playlists section:*
``playlistclear {NAME}``
Clears the playlist ``NAME.m3u``.
The playlist will be created if it does not exist.
"""
_check_playlist_name(name)
playlist = _get_playlist(context, name, must_exist=False)
if not playlist:
playlist = context.core.playlists.create(name).get()
# Just replace tracks with empty list and save
playlist = playlist.replace(tracks=[])
if context.core.playlists.save(playlist).get() is None:
raise exceptions.MpdFailedToSavePlaylist(
urllib.parse.urlparse(playlist.uri).scheme
)
@protocol.commands.add("playlistdelete", songpos=protocol.UINT)
def playlistdelete(context, name, songpos):
"""
*musicpd.org, stored playlists section:*
``playlistdelete {NAME} {SONGPOS}``
Deletes ``SONGPOS`` from the playlist ``NAME.m3u``.
"""
_check_playlist_name(name)
playlist = _get_playlist(context, name)
try:
# Convert tracks to list and remove requested
tracks = list(playlist.tracks)
tracks.pop(songpos)
except IndexError:
raise exceptions.MpdArgError("Bad song index")
# Replace tracks and save playlist
playlist = playlist.replace(tracks=tracks)
saved_playlist = context.core.playlists.save(playlist).get()
if saved_playlist is None:
raise exceptions.MpdFailedToSavePlaylist(
urllib.parse.urlparse(playlist.uri).scheme
)
@protocol.commands.add(
"playlistmove", from_pos=protocol.UINT, to_pos=protocol.UINT
)
def playlistmove(context, name, from_pos, to_pos):
"""
*musicpd.org, stored playlists section:*
``playlistmove {NAME} {SONGID} {SONGPOS}``
Moves ``SONGID`` in the playlist ``NAME.m3u`` to the position
``SONGPOS``.
*Clarifications:*
- The second argument is not a ``SONGID`` as used elsewhere in the protocol
documentation, but just the ``SONGPOS`` to move *from*, i.e.
``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``.
"""
if from_pos == to_pos:
return
_check_playlist_name(name)
playlist = _get_playlist(context, name)
if from_pos == to_pos:
return # Nothing to do
try:
# Convert tracks to list and perform move
tracks = list(playlist.tracks)
track = tracks.pop(from_pos)
tracks.insert(to_pos, track)
except IndexError:
raise exceptions.MpdArgError("Bad song index")
# Replace tracks and save playlist
playlist = playlist.replace(tracks=tracks)
saved_playlist = context.core.playlists.save(playlist).get()
if saved_playlist is None:
raise exceptions.MpdFailedToSavePlaylist(
urllib.parse.urlparse(playlist.uri).scheme
)
@protocol.commands.add("rename")
def rename(context, old_name, new_name):
"""
*musicpd.org, stored playlists section:*
``rename {NAME} {NEW_NAME}``
Renames the playlist ``NAME.m3u`` to ``NEW_NAME.m3u``.
"""
_check_playlist_name(old_name)
_check_playlist_name(new_name)
old_playlist = _get_playlist(context, old_name)
if _get_playlist(context, new_name, must_exist=False):
raise exceptions.MpdExistError("Playlist already exists")
# TODO: should we purge the mapping in an else?
# Create copy of the playlist and remove original
uri_scheme = urllib.parse.urlparse(old_playlist.uri).scheme
new_playlist = context.core.playlists.create(new_name, uri_scheme).get()
new_playlist = new_playlist.replace(tracks=old_playlist.tracks)
saved_playlist = context.core.playlists.save(new_playlist).get()
if saved_playlist is None:
raise exceptions.MpdFailedToSavePlaylist(uri_scheme)
context.core.playlists.delete(old_playlist.uri).get()
@protocol.commands.add("rm")
def rm(context, name):
"""
*musicpd.org, stored playlists section:*
``rm {NAME}``
Removes the playlist ``NAME.m3u`` from the playlist directory.
"""
_check_playlist_name(name)
uri = context.lookup_playlist_uri_from_name(name)
if not uri:
raise exceptions.MpdNoExistError("No such playlist")
context.core.playlists.delete(uri).get()
@protocol.commands.add("save")
def save(context, name):
"""
*musicpd.org, stored playlists section:*
``save {NAME}``
Saves the current playlist to ``NAME.m3u`` in the playlist
directory.
"""
_check_playlist_name(name)
tracks = context.core.tracklist.get_tracks().get()
playlist = _get_playlist(context, name, must_exist=False)
if not playlist:
# Create new playlist
_create_playlist(context, name, tracks)
else:
# Overwrite existing playlist
new_playlist = playlist.replace(tracks=tracks)
saved_playlist = context.core.playlists.save(new_playlist).get()
if saved_playlist is None:
raise exceptions.MpdFailedToSavePlaylist(
urllib.parse.urlparse(playlist.uri).scheme
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/protocol/tagtype_list.py 0000644 0001751 0000171 00000000557 00000000000 022025 0 ustar 00runner docker TAGTYPE_LIST = [
"Artist",
"ArtistSort",
"Album",
"AlbumArtist",
"AlbumArtistSort",
"Title",
"Track",
"Name",
"Genre",
"Date",
"Composer",
"Performer",
"Disc",
"MUSICBRAINZ_ARTISTID",
"MUSICBRAINZ_ALBUMID",
"MUSICBRAINZ_ALBUMARTISTID",
"MUSICBRAINZ_TRACKID",
"X-AlbumUri",
"X-AlbumImage",
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/session.py 0000644 0001751 0000171 00000003155 00000000000 017134 0 ustar 00runner docker import logging
from mopidy_mpd import dispatcher, formatting, network, protocol
logger = logging.getLogger(__name__)
class MpdSession(network.LineProtocol):
"""
The MPD client session. Keeps track of a single client session. Any
requests from the client is passed on to the MPD request dispatcher.
"""
terminator = protocol.LINE_TERMINATOR
encoding = protocol.ENCODING
delimiter = br"\r?\n"
def __init__(self, connection, config=None, core=None, uri_map=None):
super().__init__(connection)
self.dispatcher = dispatcher.MpdDispatcher(
session=self, config=config, core=core, uri_map=uri_map
)
def on_start(self):
logger.info("New MPD connection from %s", self.connection)
self.send_lines([f"OK MPD {protocol.VERSION}"])
def on_line_received(self, line):
logger.debug("Request from %s: %s", self.connection, line)
response = self.dispatcher.handle_request(line)
if not response:
return
logger.debug(
"Response to %s: %s",
self.connection,
formatting.indent(self.decode(self.terminator).join(response)),
)
self.send_lines(response)
def on_event(self, subsystem):
self.dispatcher.handle_idle(subsystem)
def decode(self, line):
try:
return super().decode(line)
except ValueError:
logger.warning(
"Stopping actor due to unescaping error, data "
"supplied by client was not valid."
)
self.stop()
def close(self):
self.stop()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/tokenize.py 0000644 0001751 0000171 00000006242 00000000000 017301 0 ustar 00runner docker import re
from mopidy_mpd import exceptions
WORD_RE = re.compile(
r"""
^
(\s*) # Leading whitespace not allowed, capture it to report.
([a-z][a-z0-9_]*) # A command name
(?:\s+|$) # trailing whitespace or EOS
(.*) # Possibly a remainder to be parsed
""",
re.VERBOSE,
)
# Quotes matching is an unrolled version of "(?:[^"\\]|\\.)*"
PARAM_RE = re.compile(
r"""
^ # Leading whitespace is not allowed
(?:
([^%(unprintable)s"']+) # ord(char) < 0x20, not ", not '
| # or
"([^"\\]*(?:\\.[^"\\]*)*)" # anything surrounded by quotes
)
(?:\s+|$) # trailing whitespace or EOS
(.*) # Possibly a remainder to be parsed
"""
% {"unprintable": "".join(map(chr, range(0x21)))},
re.VERBOSE,
)
BAD_QUOTED_PARAM_RE = re.compile(
r"""
^
"[^"\\]*(?:\\.[^"\\]*)* # start of a quoted value
(?: # followed by:
("[^\s]) # non-escaped quote, followed by non-whitespace
| # or
([^"]) # anything that is not a quote
)
""",
re.VERBOSE,
)
UNESCAPE_RE = re.compile(r"\\(.)") # Backslash escapes any following char.
def split(line):
"""Splits a line into tokens using same rules as MPD.
- Lines may not start with whitespace
- Tokens are split by arbitrary amount of spaces or tabs
- First token must match `[a-z][a-z0-9_]*`
- Remaining tokens can be unquoted or quoted tokens.
- Unquoted tokens consist of all printable characters except double quotes,
single quotes, spaces and tabs.
- Quoted tokens are surrounded by a matching pair of double quotes.
- The closing quote must be followed by space, tab or end of line.
- Any value is allowed inside a quoted token. Including double quotes,
assuming it is correctly escaped.
- Backslash inside a quoted token is used to escape the following
character.
For examples see the tests for this function.
"""
if not line.strip():
raise exceptions.MpdNoCommand("No command given")
match = WORD_RE.match(line)
if not match:
raise exceptions.MpdUnknownError("Invalid word character")
whitespace, command, remainder = match.groups()
if whitespace:
raise exceptions.MpdUnknownError("Letter expected")
result = [command]
while remainder:
match = PARAM_RE.match(remainder)
if not match:
msg = _determine_error_message(remainder)
raise exceptions.MpdArgError(msg, command=command)
unquoted, quoted, remainder = match.groups()
result.append(unquoted or UNESCAPE_RE.sub(r"\g<1>", quoted))
return result
def _determine_error_message(remainder):
"""Helper to emulate MPD errors."""
# Following checks are simply to match MPD error messages:
match = BAD_QUOTED_PARAM_RE.match(remainder)
if match:
if match.group(1):
return "Space expected after closing '\"'"
else:
return "Missing closing '\"'"
return "Invalid unquoted character"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/translator.py 0000644 0001751 0000171 00000013316 00000000000 017642 0 ustar 00runner docker import datetime
import logging
import re
from mopidy.models import TlTrack
from mopidy_mpd.protocol import tagtype_list
logger = logging.getLogger(__name__)
# TODO: special handling of local:// uri scheme
normalize_path_re = re.compile(r"[^/]+")
def normalize_path(path, relative=False):
parts = normalize_path_re.findall(path or "")
if not relative:
parts.insert(0, "")
return "/".join(parts)
def track_to_mpd_format(track, position=None, stream_title=None):
"""
Format track for output to MPD client.
:param track: the track
:type track: :class:`mopidy.models.Track` or :class:`mopidy.models.TlTrack`
:param position: track's position in playlist
:type position: integer
:param stream_title: the current streams title
:type position: string
:rtype: list of two-tuples
"""
if isinstance(track, TlTrack):
(tlid, track) = track
else:
(tlid, track) = (None, track)
if not track.uri:
logger.warning("Ignoring track without uri")
return []
result = [
("file", track.uri),
("Time", track.length and (track.length // 1000) or 0),
("Artist", concat_multi_values(track.artists, "name")),
("Album", track.album and track.album.name or ""),
]
if stream_title is not None:
result.append(("Title", stream_title))
if track.name:
result.append(("Name", track.name))
else:
result.append(("Title", track.name or ""))
if track.date:
result.append(("Date", track.date))
if track.album is not None and track.album.num_tracks is not None:
result.append(
("Track", f"{track.track_no or 0}/{track.album.num_tracks}")
)
else:
result.append(("Track", track.track_no or 0))
if position is not None and tlid is not None:
result.append(("Pos", position))
result.append(("Id", tlid))
if track.album is not None and track.album.musicbrainz_id is not None:
result.append(("MUSICBRAINZ_ALBUMID", track.album.musicbrainz_id))
if track.album is not None and track.album.artists:
result.append(
("AlbumArtist", concat_multi_values(track.album.artists, "name"))
)
musicbrainz_ids = concat_multi_values(
track.album.artists, "musicbrainz_id"
)
if musicbrainz_ids:
result.append(("MUSICBRAINZ_ALBUMARTISTID", musicbrainz_ids))
if track.artists:
musicbrainz_ids = concat_multi_values(track.artists, "musicbrainz_id")
if musicbrainz_ids:
result.append(("MUSICBRAINZ_ARTISTID", musicbrainz_ids))
if track.composers:
result.append(
("Composer", concat_multi_values(track.composers, "name"))
)
if track.performers:
result.append(
("Performer", concat_multi_values(track.performers, "name"))
)
if track.genre:
result.append(("Genre", track.genre))
if track.disc_no:
result.append(("Disc", track.disc_no))
if track.last_modified:
datestring = datetime.datetime.utcfromtimestamp(
track.last_modified // 1000
).isoformat()
result.append(("Last-Modified", datestring + "Z"))
if track.musicbrainz_id is not None:
result.append(("MUSICBRAINZ_TRACKID", track.musicbrainz_id))
if track.album and track.album.uri:
result.append(("X-AlbumUri", track.album.uri))
result = [element for element in result if _has_value(*element)]
return result
def _has_value(tagtype, value):
"""
Determine whether to add the tagtype to the output or not.
:param tagtype: the MPD tagtype
:type tagtype: string
:param value: the tag value
:rtype: bool
"""
if tagtype in tagtype_list.TAGTYPE_LIST:
return bool(value)
return True
def concat_multi_values(models, attribute):
"""
Format Mopidy model values for output to MPD client.
:param models: the models
:type models: array of :class:`mopidy.models.Artist`,
:class:`mopidy.models.Album` or :class:`mopidy.models.Track`
:param attribute: the attribute to use
:type attribute: string
:rtype: string
"""
# Don't sort the values. MPD doesn't appear to (or if it does it's not
# strict alphabetical). If we just use them in the order in which they come
# in then the musicbrainz ids have a higher chance of staying in sync
return ";".join(
getattr(m, attribute)
for m in models
if getattr(m, attribute, None) is not None
)
def tracks_to_mpd_format(tracks, start=0, end=None):
"""
Format list of tracks for output to MPD client.
Optionally limit output to the slice ``[start:end]`` of the list.
:param tracks: the tracks
:type tracks: list of :class:`mopidy.models.Track` or
:class:`mopidy.models.TlTrack`
:param start: position of first track to include in output
:type start: int (positive or negative)
:param end: position after last track to include in output
:type end: int (positive or negative) or :class:`None` for end of list
:rtype: list of lists of two-tuples
"""
if end is None:
end = len(tracks)
tracks = tracks[start:end]
positions = range(start, end)
assert len(tracks) == len(positions)
result = []
for track, position in zip(tracks, positions):
formatted_track = track_to_mpd_format(track, position)
if formatted_track:
result.append(formatted_track)
return result
def playlist_to_mpd_format(playlist, *args, **kwargs):
"""
Format playlist for output to MPD client.
Arguments as for :func:`tracks_to_mpd_format`, except the first one.
"""
return tracks_to_mpd_format(playlist.tracks, *args, **kwargs)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/mopidy_mpd/uri_mapper.py 0000644 0001751 0000171 00000005014 00000000000 017610 0 ustar 00runner docker import re
# TOOD: refactor this into a generic mapper that does not know about browse
# or playlists and then use one instance for each case?
class MpdUriMapper:
"""
Maintains the mappings between uniquified MPD names and URIs.
"""
#: The Mopidy core API. An instance of :class:`mopidy.core.Core`.
core = None
_invalid_browse_chars = re.compile(r"[\n\r]")
_invalid_playlist_chars = re.compile(r"[/]")
def __init__(self, core=None):
self.core = core
self._uri_from_name = {}
self._browse_name_from_uri = {}
self._playlist_name_from_uri = {}
def _create_unique_name(self, name, uri):
stripped_name = self._invalid_browse_chars.sub(" ", name)
name = stripped_name
i = 2
while name in self._uri_from_name:
if self._uri_from_name[name] == uri:
return name
name = f"{stripped_name} [{i:d}]"
i += 1
return name
def insert(self, name, uri, playlist=False):
"""
Create a unique and MPD compatible name that maps to the given URI.
"""
name = self._create_unique_name(name, uri)
self._uri_from_name[name] = uri
if playlist:
self._playlist_name_from_uri[uri] = name
else:
self._browse_name_from_uri[uri] = name
return name
def uri_from_name(self, name):
"""
Return the uri for the given MPD name.
"""
return self._uri_from_name.get(name)
def refresh_playlists_mapping(self):
"""
Maintain map between playlists and unique playlist names to be used by
MPD.
"""
if self.core is None:
return
for playlist_ref in self.core.playlists.as_list().get():
if not playlist_ref.name:
continue
name = self._invalid_playlist_chars.sub("|", playlist_ref.name)
self.insert(name, playlist_ref.uri, playlist=True)
def playlist_uri_from_name(self, name):
"""
Helper function to retrieve a playlist URI from its unique MPD name.
"""
if name not in self._uri_from_name:
self.refresh_playlists_mapping()
return self._uri_from_name.get(name)
def playlist_name_from_uri(self, uri):
"""
Helper function to retrieve the unique MPD playlist name from its URI.
"""
if uri not in self._playlist_name_from_uri:
self.refresh_playlists_mapping()
return self._playlist_name_from_uri[uri]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/pyproject.toml 0000644 0001751 0000171 00000000526 00000000000 015651 0 ustar 00runner docker [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"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/setup.cfg 0000644 0001751 0000171 00000002603 00000000000 014554 0 ustar 00runner docker [metadata]
name = Mopidy-MPD
version = 3.1.0
url = https://github.com/mopidy/mopidy-mpd
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 from MPD clients
long_description = file: README.rst
classifiers =
Environment :: No Input/Output (Daemon)
Intended Audience :: End Users/Desktop
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
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
[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 =
mpd = mopidy_mpd:Extension
[flake8]
application-import-names = mopidy_mpd, tests
max-line-length = 80
exclude = .git, .tox, build
select =
C, E, F, W
B
B950
N
ignore =
E203
E501
W503
B305
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/setup.py 0000644 0001751 0000171 00000000046 00000000000 014444 0 ustar 00runner docker from setuptools import setup
setup()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/tests/ 0000755 0001751 0000171 00000000000 00000000000 014074 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/__init__.py 0000644 0001751 0000171 00000000665 00000000000 016214 0 ustar 00runner docker class IsA:
def __init__(self, klass):
self.klass = klass
def __eq__(self, rhs):
try:
return isinstance(rhs, self.klass)
except TypeError:
return type(rhs) == type(self.klass) # noqa
def __ne__(self, rhs):
return not self.__eq__(rhs)
def __repr__(self):
return str(self.klass)
any_int = IsA(int)
any_str = IsA(str)
any_unicode = any_str # TODO remove
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/dummy_audio.py 0000644 0001751 0000171 00000007474 00000000000 016776 0 ustar 00runner docker """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._live_stream = False
self._tags = {}
self._bad_uris = set()
def set_uri(self, uri, live_stream=False, download=False):
assert self._uri is None, "prepare change not called before set"
self._position = 0
self._uri = uri
self._stream_changed = True
self._live_stream = live_stream
self._tags = {}
def set_appsrc(self, *args, **kwargs):
pass
def emit_data(self, buffer_):
pass
def get_position(self):
return self._position
def set_position(self, 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/dummy_backend.py 0000644 0001751 0000171 00000010242 00000000000 017247 0 ustar 00runner docker """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_browse_result = {}
self.dummy_find_exact_result = SearchResult()
self.dummy_search_result = SearchResult()
def browse(self, path):
return self.dummy_browse_result.get(path, [])
def get_distinct(self, field, query=None):
return self.dummy_get_distinct_result.get(field, set())
def lookup(self, uri):
uri = Ref.track(uri=uri).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 = []
self._allow_save = True
def set_dummy_playlists(self, playlists):
"""For tests using the dummy provider through an actor proxy."""
self._playlists = playlists
def set_allow_save(self, enabled):
self._allow_save = enabled
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):
uri = Ref.playlist(uri=uri).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=f"dummy:{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):
if not self._allow_save:
return None
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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/dummy_mixer.py 0000644 0001751 0000171 00000001215 00000000000 017004 0 ustar 00runner docker import 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/tests/network/ 0000755 0001751 0000171 00000000000 00000000000 015565 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/network/__init__.py 0000644 0001751 0000171 00000000000 00000000000 017664 0 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/network/test_connection.py 0000644 0001751 0000171 00000053156 00000000000 021347 0 ustar 00runner docker import errno
import logging
import socket
import unittest
from unittest.mock import Mock, call, patch, sentinel
import pykka
from gi.repository import GLib
from mopidy_mpd import network
from tests import any_int, any_unicode
class ConnectionTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.mock = Mock(spec=network.Connection)
def test_init_ensure_nonblocking_io(self):
sock = Mock(spec=socket.SocketType)
network.Connection.__init__(
self.mock,
Mock(),
{},
sock,
(sentinel.host, sentinel.port),
sentinel.timeout,
)
sock.setblocking.assert_called_once_with(False)
def test_init_starts_actor(self):
protocol = Mock(spec=network.LineProtocol)
network.Connection.__init__(
self.mock,
protocol,
{},
Mock(),
(sentinel.host, sentinel.port),
sentinel.timeout,
)
protocol.start.assert_called_once_with(self.mock)
def test_init_enables_recv_and_timeout(self):
network.Connection.__init__(
self.mock,
Mock(),
{},
Mock(),
(sentinel.host, sentinel.port),
sentinel.timeout,
)
self.mock.enable_recv.assert_called_once_with()
self.mock.enable_timeout.assert_called_once_with()
def test_init_stores_values_in_attributes(self):
addr = (sentinel.host, sentinel.port)
protocol = Mock(spec=network.LineProtocol)
protocol_kwargs = {}
sock = Mock(spec=socket.SocketType)
network.Connection.__init__(
self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout
)
assert sock == self.mock._sock
assert protocol == self.mock.protocol
assert protocol_kwargs == self.mock.protocol_kwargs
assert sentinel.timeout == self.mock.timeout
assert sentinel.host == self.mock.host
assert sentinel.port == self.mock.port
def test_init_handles_ipv6_addr(self):
addr = (
sentinel.host,
sentinel.port,
sentinel.flowinfo,
sentinel.scopeid,
)
protocol = Mock(spec=network.LineProtocol)
protocol_kwargs = {}
sock = Mock(spec=socket.SocketType)
network.Connection.__init__(
self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout
)
assert sentinel.host == self.mock.host
assert sentinel.port == self.mock.port
def test_stop_disables_recv_send_and_timeout(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
self.mock.disable_timeout.assert_called_once_with()
self.mock.disable_recv.assert_called_once_with()
self.mock.disable_send.assert_called_once_with()
def test_stop_closes_socket(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
self.mock._sock.close.assert_called_once_with()
def test_stop_closes_socket_error(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.close.side_effect = socket.error
network.Connection.stop(self.mock, sentinel.reason)
self.mock._sock.close.assert_called_once_with()
def test_stop_stops_actor(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
self.mock.actor_ref.stop.assert_called_once_with(block=False)
def test_stop_handles_actor_already_being_stopped(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
self.mock.actor_ref.stop.assert_called_once_with(block=False)
def test_stop_sets_stopping_to_true(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
assert self.mock.stopping is True
def test_stop_does_not_proceed_when_already_stopping(self):
self.mock.stopping = True
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
assert 0 == self.mock.actor_ref.stop.call_count
assert 0 == self.mock._sock.close.call_count
@patch.object(network.logger, "log", new=Mock())
def test_stop_logs_reason(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
network.logger.log.assert_called_once_with(
logging.DEBUG, sentinel.reason
)
@patch.object(network.logger, "log", new=Mock())
def test_stop_logs_reason_with_level(self):
self.mock.stopping = False
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(
self.mock, sentinel.reason, level=sentinel.level
)
network.logger.log.assert_called_once_with(
sentinel.level, sentinel.reason
)
@patch.object(network.logger, "log", new=Mock())
def test_stop_logs_that_it_is_calling_itself(self):
self.mock.stopping = True
self.mock.actor_ref = Mock()
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.stop(self.mock, sentinel.reason)
network.logger.log(any_int, any_unicode)
@patch.object(GLib, "io_add_watch", new=Mock())
def test_enable_recv_registers_with_glib(self):
self.mock.recv_id = None
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.fileno.return_value = sentinel.fileno
GLib.io_add_watch.return_value = sentinel.tag
network.Connection.enable_recv(self.mock)
GLib.io_add_watch.assert_called_once_with(
sentinel.fileno,
GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP,
self.mock.recv_callback,
)
assert sentinel.tag == self.mock.recv_id
@patch.object(GLib, "io_add_watch", new=Mock())
def test_enable_recv_already_registered(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock.recv_id = sentinel.tag
network.Connection.enable_recv(self.mock)
assert 0 == GLib.io_add_watch.call_count
def test_enable_recv_does_not_change_tag(self):
self.mock.recv_id = sentinel.tag
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.enable_recv(self.mock)
assert sentinel.tag == self.mock.recv_id
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_recv_deregisters(self):
self.mock.recv_id = sentinel.tag
network.Connection.disable_recv(self.mock)
GLib.source_remove.assert_called_once_with(sentinel.tag)
assert self.mock.recv_id is None
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_recv_already_deregistered(self):
self.mock.recv_id = None
network.Connection.disable_recv(self.mock)
assert 0 == GLib.source_remove.call_count
assert self.mock.recv_id is None
def test_enable_recv_on_closed_socket(self):
self.mock.recv_id = None
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.fileno.side_effect = socket.error(errno.EBADF, "")
network.Connection.enable_recv(self.mock)
self.mock.stop.assert_called_once_with(any_unicode)
assert self.mock.recv_id is None
@patch.object(GLib, "io_add_watch", new=Mock())
def test_enable_send_registers_with_glib(self):
self.mock.send_id = None
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.fileno.return_value = sentinel.fileno
GLib.io_add_watch.return_value = sentinel.tag
network.Connection.enable_send(self.mock)
GLib.io_add_watch.assert_called_once_with(
sentinel.fileno,
GLib.IO_OUT | GLib.IO_ERR | GLib.IO_HUP,
self.mock.send_callback,
)
assert sentinel.tag == self.mock.send_id
@patch.object(GLib, "io_add_watch", new=Mock())
def test_enable_send_already_registered(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock.send_id = sentinel.tag
network.Connection.enable_send(self.mock)
assert 0 == GLib.io_add_watch.call_count
def test_enable_send_does_not_change_tag(self):
self.mock.send_id = sentinel.tag
self.mock._sock = Mock(spec=socket.SocketType)
network.Connection.enable_send(self.mock)
assert sentinel.tag == self.mock.send_id
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_send_deregisters(self):
self.mock.send_id = sentinel.tag
network.Connection.disable_send(self.mock)
GLib.source_remove.assert_called_once_with(sentinel.tag)
assert self.mock.send_id is None
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_send_already_deregistered(self):
self.mock.send_id = None
network.Connection.disable_send(self.mock)
assert 0 == GLib.source_remove.call_count
assert self.mock.send_id is None
def test_enable_send_on_closed_socket(self):
self.mock.send_id = None
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.fileno.side_effect = socket.error(errno.EBADF, "")
network.Connection.enable_send(self.mock)
assert self.mock.send_id is None
@patch.object(GLib, "timeout_add_seconds", new=Mock())
def test_enable_timeout_clears_existing_timeouts(self):
self.mock.timeout = 10
network.Connection.enable_timeout(self.mock)
self.mock.disable_timeout.assert_called_once_with()
@patch.object(GLib, "timeout_add_seconds", new=Mock())
def test_enable_timeout_add_glib_timeout(self):
self.mock.timeout = 10
GLib.timeout_add_seconds.return_value = sentinel.tag
network.Connection.enable_timeout(self.mock)
GLib.timeout_add_seconds.assert_called_once_with(
10, self.mock.timeout_callback
)
assert sentinel.tag == self.mock.timeout_id
@patch.object(GLib, "timeout_add_seconds", new=Mock())
def test_enable_timeout_does_not_add_timeout(self):
self.mock.timeout = 0
network.Connection.enable_timeout(self.mock)
assert 0 == GLib.timeout_add_seconds.call_count
self.mock.timeout = -1
network.Connection.enable_timeout(self.mock)
assert 0 == GLib.timeout_add_seconds.call_count
self.mock.timeout = None
network.Connection.enable_timeout(self.mock)
assert 0 == GLib.timeout_add_seconds.call_count
def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self):
self.mock.timeout = 0
network.Connection.enable_timeout(self.mock)
assert 0 == self.mock.disable_timeout.call_count
self.mock.timeout = -1
network.Connection.enable_timeout(self.mock)
assert 0 == self.mock.disable_timeout.call_count
self.mock.timeout = None
network.Connection.enable_timeout(self.mock)
assert 0 == self.mock.disable_timeout.call_count
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_timeout_deregisters(self):
self.mock.timeout_id = sentinel.tag
network.Connection.disable_timeout(self.mock)
GLib.source_remove.assert_called_once_with(sentinel.tag)
assert self.mock.timeout_id is None
@patch.object(GLib, "source_remove", new=Mock())
def test_disable_timeout_already_deregistered(self):
self.mock.timeout_id = None
network.Connection.disable_timeout(self.mock)
assert 0 == GLib.source_remove.call_count
assert self.mock.timeout_id is None
def test_queue_send_acquires_and_releases_lock(self):
self.mock.send_lock = Mock()
self.mock.send_buffer = b""
network.Connection.queue_send(self.mock, b"data")
self.mock.send_lock.acquire.assert_called_once_with(True)
self.mock.send_lock.release.assert_called_once_with()
def test_queue_send_calls_send(self):
self.mock.send_buffer = b""
self.mock.send_lock = Mock()
self.mock.send.return_value = b""
network.Connection.queue_send(self.mock, b"data")
self.mock.send.assert_called_once_with(b"data")
assert 0 == self.mock.enable_send.call_count
assert b"" == self.mock.send_buffer
def test_queue_send_calls_enable_send_for_partial_send(self):
self.mock.send_buffer = b""
self.mock.send_lock = Mock()
self.mock.send.return_value = b"ta"
network.Connection.queue_send(self.mock, b"data")
self.mock.send.assert_called_once_with(b"data")
self.mock.enable_send.assert_called_once_with()
assert b"ta" == self.mock.send_buffer
def test_queue_send_calls_send_with_existing_buffer(self):
self.mock.send_buffer = b"foo"
self.mock.send_lock = Mock()
self.mock.send.return_value = b""
network.Connection.queue_send(self.mock, b"bar")
self.mock.send.assert_called_once_with(b"foobar")
assert 0 == self.mock.enable_send.call_count
assert b"" == self.mock.send_buffer
def test_recv_callback_respects_io_err(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, (GLib.IO_IN | GLib.IO_ERR)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, (GLib.IO_IN | GLib.IO_HUP)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_respects_io_hup_and_io_err(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock.actor_ref = Mock()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, ((GLib.IO_IN | GLib.IO_HUP) | GLib.IO_ERR)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_sends_data_to_actor(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.recv.return_value = b"data"
self.mock.actor_ref = Mock()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.actor_ref.tell.assert_called_once_with({"received": b"data"})
def test_recv_callback_handles_dead_actors(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.recv.return_value = b"data"
self.mock.actor_ref = Mock()
self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_recv_callback_gets_no_data(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.recv.return_value = b""
self.mock.actor_ref = Mock()
assert network.Connection.recv_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
assert self.mock.mock_calls == [
call._sock.recv(any_int),
call.disable_recv(),
call.actor_ref.tell({"close": True}),
]
def test_recv_callback_recoverable_error(self):
self.mock._sock = Mock(spec=socket.SocketType)
for error in (errno.EWOULDBLOCK, errno.EINTR):
self.mock._sock.recv.side_effect = socket.error(error, "")
assert network.Connection.recv_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
assert 0 == self.mock.stop.call_count
def test_recv_callback_unrecoverable_error(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.recv.side_effect = socket.error
assert network.Connection.recv_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_err(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 1
self.mock.send_lock = Mock()
self.mock.actor_ref = Mock()
self.mock.send_buffer = b""
assert network.Connection.send_callback(
self.mock, sentinel.fd, (GLib.IO_IN | GLib.IO_ERR)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 1
self.mock.send_lock = Mock()
self.mock.actor_ref = Mock()
self.mock.send_buffer = b""
assert network.Connection.send_callback(
self.mock, sentinel.fd, (GLib.IO_IN | GLib.IO_HUP)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_respects_io_hup_and_io_err(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 1
self.mock.send_lock = Mock()
self.mock.actor_ref = Mock()
self.mock.send_buffer = b""
assert network.Connection.send_callback(
self.mock, sentinel.fd, ((GLib.IO_IN | GLib.IO_HUP) | GLib.IO_ERR)
)
self.mock.stop.assert_called_once_with(any_unicode)
def test_send_callback_acquires_and_releases_lock(self):
self.mock.send_lock = Mock()
self.mock.send_lock.acquire.return_value = True
self.mock.send_buffer = b""
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 0
assert network.Connection.send_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.send_lock.acquire.assert_called_once_with(False)
self.mock.send_lock.release.assert_called_once_with()
def test_send_callback_fails_to_acquire_lock(self):
self.mock.send_lock = Mock()
self.mock.send_lock.acquire.return_value = False
self.mock.send_buffer = b""
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 0
assert network.Connection.send_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.send_lock.acquire.assert_called_once_with(False)
assert 0 == self.mock._sock.send.call_count
def test_send_callback_sends_all_data(self):
self.mock.send_lock = Mock()
self.mock.send_lock.acquire.return_value = True
self.mock.send_buffer = b"data"
self.mock.send.return_value = b""
assert network.Connection.send_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.disable_send.assert_called_once_with()
self.mock.send.assert_called_once_with(b"data")
assert b"" == self.mock.send_buffer
def test_send_callback_sends_partial_data(self):
self.mock.send_lock = Mock()
self.mock.send_lock.acquire.return_value = True
self.mock.send_buffer = b"data"
self.mock.send.return_value = b"ta"
assert network.Connection.send_callback(
self.mock, sentinel.fd, GLib.IO_IN
)
self.mock.send.assert_called_once_with(b"data")
assert b"ta" == self.mock.send_buffer
def test_send_recoverable_error(self):
self.mock._sock = Mock(spec=socket.SocketType)
for error in (errno.EWOULDBLOCK, errno.EINTR):
self.mock._sock.send.side_effect = socket.error(error, "")
network.Connection.send(self.mock, b"data")
assert 0 == self.mock.stop.call_count
def test_send_calls_socket_send(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 4
assert b"" == network.Connection.send(self.mock, b"data")
self.mock._sock.send.assert_called_once_with(b"data")
def test_send_calls_socket_send_partial_send(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.return_value = 2
assert b"ta" == network.Connection.send(self.mock, b"data")
self.mock._sock.send.assert_called_once_with(b"data")
def test_send_unrecoverable_error(self):
self.mock._sock = Mock(spec=socket.SocketType)
self.mock._sock.send.side_effect = socket.error
assert b"" == network.Connection.send(self.mock, b"data")
self.mock.stop.assert_called_once_with(any_unicode)
def test_timeout_callback(self):
self.mock.timeout = 10
assert not network.Connection.timeout_callback(self.mock)
self.mock.stop.assert_called_once_with(any_unicode)
def test_str(self):
self.mock.host = "foo"
self.mock.port = 999
assert "[foo]:999" == network.Connection.__str__(self.mock)
def test_str_without_port(self):
self.mock.host = "foo"
self.mock.port = None
assert "[foo]" == network.Connection.__str__(self.mock)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/network/test_lineprotocol.py 0000644 0001751 0000171 00000025647 00000000000 021725 0 ustar 00runner docker import re
import unittest
from unittest.mock import Mock, sentinel
from mopidy_mpd import network
from tests import any_unicode
class LineProtocolTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.mock = Mock(spec=network.LineProtocol)
self.mock.terminator = network.LineProtocol.terminator
self.mock.encoding = network.LineProtocol.encoding
self.mock.delimiter = network.LineProtocol.delimiter
self.mock.prevent_timeout = False
def prepare_on_receive_test(self, return_value=None):
self.mock.connection = Mock(spec=network.Connection)
self.mock.recv_buffer = b""
self.mock.parse_lines.return_value = return_value or []
def test_init_stores_values_in_attributes(self):
delimiter = re.compile(network.LineProtocol.terminator)
network.LineProtocol.__init__(self.mock, sentinel.connection)
assert sentinel.connection == self.mock.connection
assert b"" == self.mock.recv_buffer
assert delimiter == self.mock.delimiter
assert not self.mock.prevent_timeout
def test_init_compiles_delimiter(self):
self.mock.delimiter = "\r?\n"
delimiter = re.compile("\r?\n")
network.LineProtocol.__init__(self.mock, sentinel.connection)
assert delimiter == self.mock.delimiter
def test_on_receive_close_calls_stop(self):
self.prepare_on_receive_test()
network.LineProtocol.on_receive(self.mock, {"close": True})
self.mock.connection.stop.assert_called_once_with(any_unicode)
def test_on_receive_no_new_lines_adds_to_recv_buffer(self):
self.prepare_on_receive_test()
network.LineProtocol.on_receive(self.mock, {"received": b"data"})
assert b"data" == self.mock.recv_buffer
self.mock.parse_lines.assert_called_once_with()
assert 0 == self.mock.on_line_received.call_count
def test_on_receive_toggles_timeout(self):
self.prepare_on_receive_test()
network.LineProtocol.on_receive(self.mock, {"received": b"data"})
self.mock.connection.disable_timeout.assert_called_once_with()
self.mock.connection.enable_timeout.assert_called_once_with()
def test_on_receive_toggles_unless_prevent_timeout_is_set(self):
self.prepare_on_receive_test()
self.mock.prevent_timeout = True
network.LineProtocol.on_receive(self.mock, {"received": b"data"})
self.mock.connection.disable_timeout.assert_called_once_with()
assert 0 == self.mock.connection.enable_timeout.call_count
def test_on_receive_no_new_lines_calls_parse_lines(self):
self.prepare_on_receive_test()
network.LineProtocol.on_receive(self.mock, {"received": b"data"})
self.mock.parse_lines.assert_called_once_with()
assert 0 == self.mock.on_line_received.call_count
def test_on_receive_with_new_line_calls_decode(self):
self.prepare_on_receive_test([sentinel.line])
network.LineProtocol.on_receive(self.mock, {"received": b"data\n"})
self.mock.parse_lines.assert_called_once_with()
self.mock.decode.assert_called_once_with(sentinel.line)
def test_on_receive_with_new_line_calls_on_recieve(self):
self.prepare_on_receive_test([sentinel.line])
self.mock.decode.return_value = sentinel.decoded
network.LineProtocol.on_receive(self.mock, {"received": b"data\n"})
self.mock.on_line_received.assert_called_once_with(sentinel.decoded)
def test_on_receive_with_new_line_with_failed_decode(self):
self.prepare_on_receive_test([sentinel.line])
self.mock.decode.return_value = None
network.LineProtocol.on_receive(self.mock, {"received": b"data\n"})
assert 0 == self.mock.on_line_received.call_count
def test_on_receive_with_new_lines_calls_on_recieve(self):
self.prepare_on_receive_test(["line1", "line2"])
self.mock.decode.return_value = sentinel.decoded
network.LineProtocol.on_receive(
self.mock, {"received": b"line1\nline2\n"}
)
assert 2 == self.mock.on_line_received.call_count
def test_on_failure_calls_stop(self):
self.mock.connection = Mock(spec=network.Connection)
network.LineProtocol.on_failure(self.mock, None, None, None)
self.mock.connection.stop.assert_called_once_with("Actor failed.")
def prepare_parse_lines_test(self, recv_data=""):
self.mock.terminator = b"\n"
self.mock.delimiter = re.compile(br"\n")
self.mock.recv_buffer = recv_data.encode()
def test_parse_lines_emtpy_buffer(self):
self.prepare_parse_lines_test()
lines = network.LineProtocol.parse_lines(self.mock)
with self.assertRaises(StopIteration):
next(lines)
def test_parse_lines_no_terminator(self):
self.prepare_parse_lines_test("data")
lines = network.LineProtocol.parse_lines(self.mock)
with self.assertRaises(StopIteration):
next(lines)
def test_parse_lines_terminator(self):
self.prepare_parse_lines_test("data\n")
lines = network.LineProtocol.parse_lines(self.mock)
assert b"data" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"" == self.mock.recv_buffer
def test_parse_lines_terminator_with_carriage_return(self):
self.prepare_parse_lines_test("data\r\n")
self.mock.delimiter = re.compile(br"\r?\n")
lines = network.LineProtocol.parse_lines(self.mock)
assert b"data" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"" == self.mock.recv_buffer
def test_parse_lines_no_data_before_terminator(self):
self.prepare_parse_lines_test("\n")
lines = network.LineProtocol.parse_lines(self.mock)
assert b"" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"" == self.mock.recv_buffer
def test_parse_lines_extra_data_after_terminator(self):
self.prepare_parse_lines_test("data1\ndata2")
lines = network.LineProtocol.parse_lines(self.mock)
assert b"data1" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"data2" == self.mock.recv_buffer
def test_parse_lines_non_ascii(self):
self.prepare_parse_lines_test("æøå\n")
lines = network.LineProtocol.parse_lines(self.mock)
assert "æøå".encode() == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"" == self.mock.recv_buffer
def test_parse_lines_multiple_lines(self):
self.prepare_parse_lines_test("abc\ndef\nghi\njkl")
lines = network.LineProtocol.parse_lines(self.mock)
assert b"abc" == next(lines)
assert b"def" == next(lines)
assert b"ghi" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"jkl" == self.mock.recv_buffer
def test_parse_lines_multiple_calls(self):
self.prepare_parse_lines_test("data1")
lines = network.LineProtocol.parse_lines(self.mock)
with self.assertRaises(StopIteration):
next(lines)
assert b"data1" == self.mock.recv_buffer
self.mock.recv_buffer += b"\ndata2"
lines = network.LineProtocol.parse_lines(self.mock)
assert b"data1" == next(lines)
with self.assertRaises(StopIteration):
next(lines)
assert b"data2" == self.mock.recv_buffer
def test_send_lines_called_with_no_lines(self):
self.mock.connection = Mock(spec=network.Connection)
network.LineProtocol.send_lines(self.mock, [])
assert 0 == self.mock.encode.call_count
assert 0 == self.mock.connection.queue_send.call_count
def test_send_lines_calls_join_lines(self):
self.mock.connection = Mock(spec=network.Connection)
self.mock.join_lines.return_value = "lines"
network.LineProtocol.send_lines(self.mock, sentinel.lines)
self.mock.join_lines.assert_called_once_with(sentinel.lines)
def test_send_line_encodes_joined_lines_with_final_terminator(self):
self.mock.connection = Mock(spec=network.Connection)
self.mock.join_lines.return_value = "lines\n"
network.LineProtocol.send_lines(self.mock, sentinel.lines)
self.mock.encode.assert_called_once_with("lines\n")
def test_send_lines_sends_encoded_string(self):
self.mock.connection = Mock(spec=network.Connection)
self.mock.join_lines.return_value = "lines"
self.mock.encode.return_value = sentinel.data
network.LineProtocol.send_lines(self.mock, sentinel.lines)
self.mock.connection.queue_send.assert_called_once_with(sentinel.data)
def test_join_lines_returns_empty_string_for_no_lines(self):
assert "" == network.LineProtocol.join_lines(self.mock, [])
def test_join_lines_returns_joined_lines(self):
self.mock.decode.return_value = "\n"
assert "1\n2\n" == network.LineProtocol.join_lines(
self.mock, ["1", "2"]
)
def test_decode_calls_decode_on_string(self):
string = Mock()
network.LineProtocol.decode(self.mock, string)
string.decode.assert_called_once_with(self.mock.encoding)
def test_decode_plain_ascii(self):
result = network.LineProtocol.decode(self.mock, b"abc")
assert "abc" == result
assert str == type(result)
def test_decode_utf8(self):
result = network.LineProtocol.decode(self.mock, "æøå".encode())
assert "æøå" == result
assert str == type(result)
def test_decode_invalid_data(self):
string = Mock()
string.decode.side_effect = UnicodeError
network.LineProtocol.decode(self.mock, string)
self.mock.stop.assert_called_once_with()
def test_encode_calls_encode_on_string(self):
string = Mock()
network.LineProtocol.encode(self.mock, string)
string.encode.assert_called_once_with(self.mock.encoding)
def test_encode_plain_ascii(self):
result = network.LineProtocol.encode(self.mock, "abc")
assert b"abc" == result
assert bytes == type(result)
def test_encode_utf8(self):
result = network.LineProtocol.encode(self.mock, "æøå")
assert "æøå".encode() == result
assert bytes == type(result)
def test_encode_invalid_data(self):
string = Mock()
string.encode.side_effect = UnicodeError
network.LineProtocol.encode(self.mock, string)
self.mock.stop.assert_called_once_with()
def test_host_property(self):
mock = Mock(spec=network.Connection)
mock.host = sentinel.host
lineprotocol = network.LineProtocol(mock)
assert sentinel.host == lineprotocol.host
def test_port_property(self):
mock = Mock(spec=network.Connection)
mock.port = sentinel.port
lineprotocol = network.LineProtocol(mock)
assert sentinel.port == lineprotocol.port
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/network/test_server.py 0000644 0001751 0000171 00000030445 00000000000 020512 0 ustar 00runner docker import errno
import os
import socket
import unittest
from unittest.mock import Mock, patch, sentinel
from gi.repository import GLib
from mopidy_mpd import network
from tests import any_int
class ServerTest(unittest.TestCase):
def setUp(self): # noqa: N802
self.mock = Mock(spec=network.Server)
@patch.object(network, "get_socket_address", new=Mock())
def test_init_calls_create_server_socket(self):
network.Server.__init__(
self.mock, sentinel.host, sentinel.port, sentinel.protocol
)
self.mock.create_server_socket.assert_called_once_with(
sentinel.host, sentinel.port
)
self.mock.stop()
@patch.object(network, "get_socket_address", new=Mock())
def test_init_calls_get_socket_address(self):
network.Server.__init__(
self.mock, sentinel.host, sentinel.port, sentinel.protocol
)
self.mock.create_server_socket.return_value = None
network.get_socket_address.assert_called_once_with(
sentinel.host, sentinel.port
)
self.mock.stop()
@patch.object(network, "get_socket_address", new=Mock())
def test_init_calls_register_server(self):
sock = Mock(spec=socket.socket)
sock.fileno.return_value = sentinel.fileno
self.mock.create_server_socket.return_value = sock
network.Server.__init__(
self.mock, sentinel.host, sentinel.port, sentinel.protocol
)
self.mock.register_server_socket.assert_called_once_with(
sentinel.fileno
)
@patch.object(network, "get_socket_address", new=Mock())
def test_init_fails_on_fileno_call(self):
sock = Mock(spec=socket.socket)
sock.fileno.side_effect = socket.error
self.mock.create_server_socket.return_value = sock
with self.assertRaises(socket.error):
network.Server.__init__(
self.mock, sentinel.host, sentinel.port, sentinel.protocol
)
def test_init_stores_values_in_attributes(self):
# This need to be a mock and no a sentinel as fileno() is called on it
sock = Mock(spec=socket.socket)
self.mock.create_server_socket.return_value = sock
network.Server.__init__(
self.mock,
str(sentinel.host),
sentinel.port,
sentinel.protocol,
max_connections=sentinel.max_connections,
timeout=sentinel.timeout,
)
assert sentinel.protocol == self.mock.protocol
assert sentinel.max_connections == self.mock.max_connections
assert sentinel.timeout == self.mock.timeout
assert sock == self.mock.server_socket
assert (str(sentinel.host), sentinel.port) == self.mock.address
def test_create_server_socket_no_port(self):
with self.assertRaises(TypeError):
network.Server.create_server_socket(
self.mock, str(sentinel.host), None
)
def test_create_server_socket_invalid_port(self):
with self.assertRaises(TypeError):
network.Server.create_server_socket(
self.mock, str(sentinel.host), str(sentinel.port)
)
@patch.object(network, "create_tcp_socket", spec=socket.socket)
def test_create_server_socket_sets_up_listener(self, create_tcp_socket):
sock = create_tcp_socket.return_value
network.Server.create_server_socket(self.mock, str(sentinel.host), 1234)
sock.setblocking.assert_called_once_with(False)
sock.bind.assert_called_once_with((str(sentinel.host), 1234))
sock.listen.assert_called_once_with(any_int)
create_tcp_socket.assert_called_once()
@patch.object(network, "create_unix_socket", spec=socket.socket)
def test_create_server_socket_sets_up_listener_unix(
self, create_unix_socket
):
sock = create_unix_socket.return_value
network.Server.create_server_socket(
self.mock, "unix:" + str(sentinel.host), sentinel.port
)
sock.setblocking.assert_called_once_with(False)
sock.bind.assert_called_once_with(str(sentinel.host))
sock.listen.assert_called_once_with(any_int)
create_unix_socket.assert_called_once()
@patch.object(network, "create_tcp_socket", new=Mock())
def test_create_server_socket_fails(self):
network.create_tcp_socket.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, str(sentinel.host), 1234
)
@patch.object(network, "create_unix_socket", new=Mock())
def test_create_server_socket_fails_unix(self):
network.create_unix_socket.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, "unix:" + str(sentinel.host), sentinel.port
)
@patch.object(network, "create_tcp_socket", new=Mock())
def test_create_server_bind_fails(self):
sock = network.create_tcp_socket.return_value
sock.bind.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, str(sentinel.host), 1234
)
@patch.object(network, "create_unix_socket", new=Mock())
def test_create_server_bind_fails_unix(self):
sock = network.create_unix_socket.return_value
sock.bind.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, "unix:" + str(sentinel.host), sentinel.port
)
@patch.object(network, "create_tcp_socket", new=Mock())
def test_create_server_listen_fails(self):
sock = network.create_tcp_socket.return_value
sock.listen.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, str(sentinel.host), 1234
)
@patch.object(network, "create_unix_socket", new=Mock())
def test_create_server_listen_fails_unix(self):
sock = network.create_unix_socket.return_value
sock.listen.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.create_server_socket(
self.mock, "unix:" + str(sentinel.host), sentinel.port
)
@patch.object(os, "unlink", new=Mock())
@patch.object(GLib, "source_remove", new=Mock())
def test_stop_server_cleans_unix_socket(self):
self.mock.watcher = Mock()
sock = Mock()
sock.family = socket.AF_UNIX
self.mock.server_socket = sock
network.Server.stop(self.mock)
os.unlink.assert_called_once_with(sock.getsockname())
@patch.object(GLib, "io_add_watch", new=Mock())
def test_register_server_socket_sets_up_io_watch(self):
network.Server.register_server_socket(self.mock, sentinel.fileno)
GLib.io_add_watch.assert_called_once_with(
sentinel.fileno, GLib.IO_IN, self.mock.handle_connection
)
def test_handle_connection(self):
self.mock.accept_connection.return_value = (
sentinel.sock,
sentinel.addr,
)
self.mock.maximum_connections_exceeded.return_value = False
assert network.Server.handle_connection(
self.mock, sentinel.fileno, GLib.IO_IN
)
self.mock.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.init_connection.assert_called_once_with(
sentinel.sock, sentinel.addr
)
assert 0 == self.mock.reject_connection.call_count
def test_handle_connection_exceeded_connections(self):
self.mock.accept_connection.return_value = (
sentinel.sock,
sentinel.addr,
)
self.mock.maximum_connections_exceeded.return_value = True
assert network.Server.handle_connection(
self.mock, sentinel.fileno, GLib.IO_IN
)
self.mock.accept_connection.assert_called_once_with()
self.mock.maximum_connections_exceeded.assert_called_once_with()
self.mock.reject_connection.assert_called_once_with(
sentinel.sock, sentinel.addr
)
assert 0 == self.mock.init_connection.call_count
def test_accept_connection(self):
sock = Mock(spec=socket.socket)
connected_sock = Mock(spec=socket.socket)
sock.accept.return_value = (connected_sock, sentinel.addr)
self.mock.server_socket = sock
sock, addr = network.Server.accept_connection(self.mock)
assert connected_sock == sock
assert sentinel.addr == addr
def test_accept_connection_unix(self):
sock = Mock(spec=socket.socket)
connected_sock = Mock(spec=socket.socket)
connected_sock.family = socket.AF_UNIX
connected_sock.getsockname.return_value = sentinel.sockname
sock.accept.return_value = (connected_sock, sentinel.addr)
self.mock.server_socket = sock
sock, addr = network.Server.accept_connection(self.mock)
assert connected_sock == sock
assert (sentinel.sockname, None) == addr
def test_accept_connection_recoverable_error(self):
sock = Mock(spec=socket.socket)
self.mock.server_socket = sock
for error in (errno.EAGAIN, errno.EINTR):
sock.accept.side_effect = socket.error(error, "")
with self.assertRaises(network.ShouldRetrySocketCall):
network.Server.accept_connection(self.mock)
# FIXME decide if this should be allowed to propegate
def test_accept_connection_unrecoverable_error(self):
sock = Mock(spec=socket.socket)
self.mock.server_socket = sock
sock.accept.side_effect = socket.error
with self.assertRaises(socket.error):
network.Server.accept_connection(self.mock)
def test_maximum_connections_exceeded(self):
self.mock.max_connections = 10
self.mock.number_of_connections.return_value = 11
assert network.Server.maximum_connections_exceeded(self.mock)
self.mock.number_of_connections.return_value = 10
assert network.Server.maximum_connections_exceeded(self.mock)
self.mock.number_of_connections.return_value = 9
assert not network.Server.maximum_connections_exceeded(self.mock)
@patch("pykka.ActorRegistry.get_by_class")
def test_number_of_connections(self, get_by_class):
self.mock.protocol = sentinel.protocol
get_by_class.return_value = [1, 2, 3]
assert 3 == network.Server.number_of_connections(self.mock)
get_by_class.return_value = []
assert 0 == network.Server.number_of_connections(self.mock)
@patch.object(network, "Connection", new=Mock())
def test_init_connection(self):
self.mock.protocol = sentinel.protocol
self.mock.protocol_kwargs = {}
self.mock.timeout = sentinel.timeout
network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr)
network.Connection.assert_called_once_with(
sentinel.protocol,
{},
sentinel.sock,
sentinel.addr,
sentinel.timeout,
)
def test_reject_connection(self):
sock = Mock(spec=socket.socket)
network.Server.reject_connection(
self.mock, sock, (sentinel.host, sentinel.port)
)
sock.close.assert_called_once_with()
@patch.object(network, "format_address", new=Mock())
@patch.object(network.logger, "warning", new=Mock())
def test_reject_connection_message(self):
sock = Mock(spec=socket.socket)
network.format_address.return_value = sentinel.formatted
network.Server.reject_connection(
self.mock, sock, (sentinel.host, sentinel.port)
)
network.format_address.assert_called_once_with(
(sentinel.host, sentinel.port)
)
network.logger.warning.assert_called_once_with(
"Rejected connection from %s", sentinel.formatted
)
def test_reject_connection_error(self):
sock = Mock(spec=socket.socket)
sock.close.side_effect = socket.error
network.Server.reject_connection(
self.mock, sock, (sentinel.host, sentinel.port)
)
sock.close.assert_called_once_with()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/network/test_utils.py 0000644 0001751 0000171 00000005622 00000000000 020343 0 ustar 00runner docker import socket
import unittest
from unittest.mock import Mock, patch, sentinel
from mopidy_mpd import network
class FormatHostnameTest(unittest.TestCase):
@patch("mopidy_mpd.network.has_ipv6", True)
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
network.has_ipv6 = True
assert network.format_hostname("0.0.0.0") == "::ffff:0.0.0.0"
assert network.format_hostname("1.0.0.1") == "::ffff:1.0.0.1"
@patch("mopidy_mpd.network.has_ipv6", False)
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
network.has_ipv6 = False
assert network.format_hostname("0.0.0.0") == "0.0.0.0"
class FormatAddressTest(unittest.TestCase):
def test_format_address_ipv4(self):
address = (sentinel.host, sentinel.port)
assert (
network.format_address(address)
== f"[{sentinel.host}]:{sentinel.port}"
)
def test_format_address_ipv6(self):
address = (sentinel.host, sentinel.port, sentinel.flow, sentinel.scope)
assert (
network.format_address(address)
== f"[{sentinel.host}]:{sentinel.port}"
)
def test_format_address_unix(self):
address = (sentinel.path, None)
assert network.format_address(address) == f"[{sentinel.path}]"
class GetSocketAddress(unittest.TestCase):
def test_get_socket_address(self):
host = str(sentinel.host)
port = sentinel.port
assert network.get_socket_address(host, port) == (host, port)
def test_get_socket_address_unix(self):
host = str(sentinel.host)
port = sentinel.port
assert network.get_socket_address(("unix:" + host), port) == (
host,
None,
)
class TryIPv6SocketTest(unittest.TestCase):
@patch("socket.has_ipv6", False)
def test_system_that_claims_no_ipv6_support(self):
assert not network.try_ipv6_socket()
@patch("socket.has_ipv6", True)
@patch("socket.socket")
def test_system_with_broken_ipv6(self, socket_mock):
socket_mock.side_effect = IOError()
assert not network.try_ipv6_socket()
@patch("socket.has_ipv6", True)
@patch("socket.socket")
def test_with_working_ipv6(self, socket_mock):
socket_mock.return_value = Mock()
assert network.try_ipv6_socket()
class CreateSocketTest(unittest.TestCase):
@patch("mopidy_mpd.network.has_ipv6", False)
@patch("socket.socket")
def test_ipv4_socket(self, socket_mock):
network.create_tcp_socket()
assert socket_mock.call_args[0] == (socket.AF_INET, socket.SOCK_STREAM)
@patch("mopidy_mpd.network.has_ipv6", True)
@patch("socket.socket")
def test_ipv6_socket(self, socket_mock):
network.create_tcp_socket()
assert socket_mock.call_args[0] == (socket.AF_INET6, socket.SOCK_STREAM)
@unittest.SkipTest
def test_ipv6_only_is_set(self):
pass
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/path_utils.py 0000644 0001751 0000171 00000000577 00000000000 016633 0 ustar 00runner docker import os
# FIXME replace with mock usage in tests.
class Mtime:
def __init__(self):
self.fake = None
def __call__(self, path):
if self.fake is not None:
return self.fake
return int(os.stat(path).st_mtime)
def set_fake_time(self, time):
self.fake = time
def undo_fake(self):
self.fake = None
mtime = Mtime()
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1607133604.9850466
Mopidy-MPD-3.1.0/tests/protocol/ 0000755 0001751 0000171 00000000000 00000000000 015735 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/__init__.py 0000644 0001751 0000171 00000005406 00000000000 020053 0 ustar 00runner docker import unittest
from unittest import mock
import pykka
from mopidy import core
from mopidy_mpd import session, uri_mapper
from tests import dummy_audio, dummy_backend, dummy_mixer
class MockConnection(mock.Mock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.host = mock.sentinel.host
self.port = mock.sentinel.port
self.response = []
def queue_send(self, data):
data = data.decode()
lines = (line for line in data.split("\n") if line)
self.response.extend(lines)
class BaseTestCase(unittest.TestCase):
enable_mixer = True
def get_config(self):
return {
"core": {"max_tracklist_length": 10000},
"mpd": {"password": None, "default_playlist_scheme": "dummy"},
}
def setUp(self): # noqa: N802
if self.enable_mixer:
self.mixer = dummy_mixer.create_proxy()
else:
self.mixer = None
self.audio = dummy_audio.create_proxy()
self.backend = dummy_backend.create_proxy(audio=self.audio)
self.core = core.Core.start(
self.get_config(),
audio=self.audio,
mixer=self.mixer,
backends=[self.backend],
).proxy()
self.uri_map = uri_mapper.MpdUriMapper(self.core)
self.connection = MockConnection()
self.session = session.MpdSession(
self.connection,
config=self.get_config(),
core=self.core,
uri_map=self.uri_map,
)
self.dispatcher = self.session.dispatcher
self.context = self.dispatcher.context
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
def send_request(self, request):
self.connection.response = []
request = f"{request}\n".encode()
self.session.on_receive({"received": request})
return self.connection.response
def assertNoResponse(self): # noqa: N802
assert [] == self.connection.response
def assertInResponse(self, value): # noqa: N802
assert (
value in self.connection.response
), f"Did not find {value!r} in {self.connection.response!r}"
def assertOnceInResponse(self, value): # noqa: N802
matched = len([r for r in self.connection.response if r == value])
assert (
1 == matched
), f"Expected to find {value!r} once in {self.connection.response!r}"
def assertNotInResponse(self, value): # noqa: N802
assert (
value not in self.connection.response
), f"Found {value!r} in {self.connection.response!r}"
def assertEqualResponse(self, value): # noqa: N802
assert 1 == len(self.connection.response)
assert value == self.connection.response[0]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_audio_output.py 0000644 0001751 0000171 00000010535 00000000000 022073 0 ustar 00runner docker from tests import protocol
class AudioOutputHandlerTest(protocol.BaseTestCase):
def test_enableoutput(self):
self.core.mixer.set_mute(False)
self.send_request('enableoutput "0"')
self.assertInResponse("OK")
assert self.core.mixer.get_mute().get() is True
def test_enableoutput_unknown_outputid(self):
self.send_request('enableoutput "7"')
self.assertInResponse("ACK [50@0] {enableoutput} No such audio output")
def test_disableoutput(self):
self.core.mixer.set_mute(True)
self.send_request('disableoutput "0"')
self.assertInResponse("OK")
assert self.core.mixer.get_mute().get() is False
def test_disableoutput_unknown_outputid(self):
self.send_request('disableoutput "7"')
self.assertInResponse("ACK [50@0] {disableoutput} No such audio output")
def test_outputs_when_unmuted(self):
self.core.mixer.set_mute(False)
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
def test_outputs_when_muted(self):
self.core.mixer.set_mute(True)
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 1")
self.assertInResponse("OK")
def test_outputs_toggleoutput(self):
self.core.mixer.set_mute(False)
self.send_request('toggleoutput "0"')
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 1")
self.assertInResponse("OK")
self.send_request('toggleoutput "0"')
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
self.send_request('toggleoutput "0"')
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 1")
self.assertInResponse("OK")
def test_outputs_toggleoutput_unknown_outputid(self):
self.send_request('toggleoutput "7"')
self.assertInResponse("ACK [50@0] {toggleoutput} No such audio output")
class AudioOutputHandlerNoneMixerTest(protocol.BaseTestCase):
enable_mixer = False
def test_enableoutput(self):
assert self.core.mixer.get_mute().get() is None
self.send_request('enableoutput "0"')
self.assertInResponse(
"ACK [52@0] {enableoutput} problems enabling output"
)
assert self.core.mixer.get_mute().get() is None
def test_disableoutput(self):
assert self.core.mixer.get_mute().get() is None
self.send_request('disableoutput "0"')
self.assertInResponse(
"ACK [52@0] {disableoutput} problems disabling output"
)
assert self.core.mixer.get_mute().get() is None
def test_outputs_when_unmuted(self):
self.core.mixer.set_mute(False)
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
def test_outputs_when_muted(self):
self.core.mixer.set_mute(True)
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
def test_outputs_toggleoutput(self):
self.core.mixer.set_mute(False)
self.send_request('toggleoutput "0"')
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
self.send_request('toggleoutput "0"')
self.send_request("outputs")
self.assertInResponse("outputid: 0")
self.assertInResponse("outputname: Mute")
self.assertInResponse("outputenabled: 0")
self.assertInResponse("OK")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_authentication.py 0000644 0001751 0000171 00000004555 00000000000 022376 0 ustar 00runner docker from tests import protocol
class AuthenticationActiveTest(protocol.BaseTestCase):
def get_config(self):
config = super().get_config()
config["mpd"]["password"] = "topsecret"
return config
def test_authentication_with_valid_password_is_accepted(self):
self.send_request('password "topsecret"')
assert self.dispatcher.authenticated
self.assertInResponse("OK")
def test_authentication_with_invalid_password_is_not_accepted(self):
self.send_request('password "secret"')
assert not self.dispatcher.authenticated
self.assertEqualResponse("ACK [3@0] {password} incorrect password")
def test_authentication_without_password_fails(self):
self.send_request("password")
assert not self.dispatcher.authenticated
self.assertEqualResponse(
'ACK [2@0] {password} wrong number of arguments for "password"'
)
def test_anything_when_not_authenticated_should_fail(self):
self.send_request("any request at all")
assert not self.dispatcher.authenticated
self.assertEqualResponse(
'ACK [4@0] {any} you don\'t have permission for "any"'
)
def test_close_is_allowed_without_authentication(self):
self.send_request("close")
assert not self.dispatcher.authenticated
def test_commands_is_allowed_without_authentication(self):
self.send_request("commands")
assert not self.dispatcher.authenticated
self.assertInResponse("OK")
def test_notcommands_is_allowed_without_authentication(self):
self.send_request("notcommands")
assert not self.dispatcher.authenticated
self.assertInResponse("OK")
def test_ping_is_allowed_without_authentication(self):
self.send_request("ping")
assert not self.dispatcher.authenticated
self.assertInResponse("OK")
class AuthenticationInactiveTest(protocol.BaseTestCase):
def test_authentication_with_anything_when_password_check_turned_off(self):
self.send_request("any request at all")
assert self.dispatcher.authenticated
self.assertEqualResponse('ACK [5@0] {} unknown command "any"')
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
self.send_request('password "secret"')
self.assertEqualResponse("ACK [3@0] {password} incorrect password")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_channels.py 0000644 0001751 0000171 00000001524 00000000000 021143 0 ustar 00runner docker from tests import protocol
class ChannelsHandlerTest(protocol.BaseTestCase):
def test_subscribe(self):
self.send_request('subscribe "topic"')
self.assertEqualResponse("ACK [0@0] {subscribe} Not implemented")
def test_unsubscribe(self):
self.send_request('unsubscribe "topic"')
self.assertEqualResponse("ACK [0@0] {unsubscribe} Not implemented")
def test_channels(self):
self.send_request("channels")
self.assertEqualResponse("ACK [0@0] {channels} Not implemented")
def test_readmessages(self):
self.send_request("readmessages")
self.assertEqualResponse("ACK [0@0] {readmessages} Not implemented")
def test_sendmessage(self):
self.send_request('sendmessage "topic" "a message"')
self.assertEqualResponse("ACK [0@0] {sendmessage} Not implemented")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_command_list.py 0000644 0001751 0000171 00000004703 00000000000 022023 0 ustar 00runner docker from tests import protocol
class CommandListsTest(protocol.BaseTestCase):
def test_command_list_begin(self):
response = self.send_request("command_list_begin")
assert [] == response
def test_command_list_end(self):
self.send_request("command_list_begin")
self.send_request("command_list_end")
self.assertInResponse("OK")
def test_command_list_end_without_start_first_is_an_unknown_command(self):
self.send_request("command_list_end")
self.assertEqualResponse(
'ACK [5@0] {} unknown command "command_list_end"'
)
def test_command_list_with_ping(self):
self.send_request("command_list_begin")
assert self.dispatcher.command_list_receiving
assert not self.dispatcher.command_list_ok
assert [] == self.dispatcher.command_list
self.send_request("ping")
assert "ping" in self.dispatcher.command_list
self.send_request("command_list_end")
self.assertInResponse("OK")
assert not self.dispatcher.command_list_receiving
assert not self.dispatcher.command_list_ok
assert [] == self.dispatcher.command_list
def test_command_list_with_error_returns_ack_with_correct_index(self):
self.send_request("command_list_begin")
self.send_request("play") # Known command
self.send_request("paly") # Unknown command
self.send_request("command_list_end")
self.assertEqualResponse('ACK [5@1] {} unknown command "paly"')
def test_command_list_ok_begin(self):
response = self.send_request("command_list_ok_begin")
assert [] == response
def test_command_list_ok_with_ping(self):
self.send_request("command_list_ok_begin")
assert self.dispatcher.command_list_receiving
assert self.dispatcher.command_list_ok
assert [] == self.dispatcher.command_list
self.send_request("ping")
assert "ping" in self.dispatcher.command_list
self.send_request("command_list_end")
self.assertInResponse("list_OK")
self.assertInResponse("OK")
assert not self.dispatcher.command_list_receiving
assert not self.dispatcher.command_list_ok
assert [] == self.dispatcher.command_list
# FIXME this should also include the special handling of idle within a
# command list. That is that once a idle/noidle command is found inside a
# commad list, the rest of the list seems to be ignored.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_connection.py 0000644 0001751 0000171 00000001536 00000000000 021512 0 ustar 00runner docker from unittest.mock import patch
from tests import protocol
class ConnectionHandlerTest(protocol.BaseTestCase):
def test_close_closes_the_client_connection(self):
with patch.object(self.session, "close") as close_mock:
self.send_request("close")
close_mock.assert_called_once_with()
self.assertEqualResponse("OK")
def test_empty_request(self):
self.send_request("")
self.assertEqualResponse("ACK [5@0] {} No command given")
self.send_request(" ")
self.assertEqualResponse("ACK [5@0] {} No command given")
def test_kill(self):
self.send_request("kill")
self.assertEqualResponse(
'ACK [4@0] {kill} you don\'t have permission for "kill"'
)
def test_ping(self):
self.send_request("ping")
self.assertEqualResponse("OK")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_current_playlist.py 0000644 0001751 0000171 00000044151 00000000000 022756 0 ustar 00runner docker from mopidy.models import Ref, Track
from tests import protocol
class AddCommandsTest(protocol.BaseTestCase):
def setUp(self): # noqa: N802
super().setUp()
self.tracks = [
Track(uri="dummy:/a", name="a"),
Track(uri="dummy:/foo/b", name="b"),
Track(uri="dummy:/foo/ǂ", name="ǂ"),
]
self.refs = {
"/a": Ref.track(uri="dummy:/a", name="a"),
"/foo": Ref.directory(uri="dummy:/foo", name="foo"),
"/foo/b": Ref.track(uri="dummy:/foo/b", name="b"),
"/foo/ǂ": Ref.track(uri="dummy:/foo/ǂ", name="ǂ"),
}
self.backend.library.dummy_library = self.tracks
def test_add(self):
for track in [self.tracks[0], self.tracks[0], self.tracks[1]]:
self.send_request(f'add "{track.uri}"')
assert len(self.core.tracklist.get_tracks().get()) == 3
assert self.core.tracklist.get_tracks().get()[2] == self.tracks[1]
self.assertEqualResponse("OK")
def test_add_unicode(self):
for track in [self.tracks[0], self.tracks[1], self.tracks[2]]:
self.send_request(f'add "{track.uri}"')
assert len(self.core.tracklist.get_tracks().get()) == 3
assert self.core.tracklist.get_tracks().get()[2] == self.tracks[2]
self.assertEqualResponse("OK")
def test_add_with_uri_not_found_in_library_should_ack(self):
self.send_request('add "dummy://foo"')
self.assertEqualResponse("ACK [50@0] {add} directory or file not found")
def test_add_with_empty_uri_should_not_add_anything_and_ok(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [self.refs["/a"]]
}
self.send_request('add ""')
assert len(self.core.tracklist.get_tracks().get()) == 0
self.assertInResponse("OK")
def test_add_with_library_should_recurse(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [self.refs["/a"], self.refs["/foo"]],
"dummy:/foo": [self.refs["/foo/b"], self.refs["/foo/ǂ"]],
}
self.send_request('add "/dummy"')
assert self.core.tracklist.get_tracks().get() == self.tracks
self.assertInResponse("OK")
def test_add_root_should_not_add_anything_and_ok(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [self.refs["/a"]]
}
self.send_request('add "/"')
assert len(self.core.tracklist.get_tracks().get()) == 0
self.assertInResponse("OK")
def test_addid_without_songpos(self):
for track in [self.tracks[0], self.tracks[0], self.tracks[1]]:
self.send_request(f'addid "{track.uri}"')
tl_tracks = self.core.tracklist.get_tl_tracks().get()
assert len(tl_tracks) == 3
assert tl_tracks[2].track == self.tracks[1]
self.assertInResponse(f"Id: {tl_tracks[2].tlid:d}")
self.assertInResponse("OK")
def test_addid_with_songpos(self):
for track in [self.tracks[0], self.tracks[0]]:
self.send_request(f'add "{track.uri}"')
self.send_request(f'addid "{self.tracks[1].uri}" "1"')
tl_tracks = self.core.tracklist.get_tl_tracks().get()
assert len(tl_tracks) == 3
assert tl_tracks[1].track == self.tracks[1]
self.assertInResponse(f"Id: {tl_tracks[1].tlid:d}")
self.assertInResponse("OK")
def test_addid_with_songpos_out_of_bounds_should_ack(self):
self.send_request(f'addid "{self.tracks[0].uri}" "3"')
self.assertEqualResponse("ACK [2@0] {addid} Bad song index")
def test_addid_with_empty_uri_acks(self):
self.send_request('addid ""')
self.assertEqualResponse("ACK [50@0] {addid} No such song")
def test_addid_with_uri_not_found_in_library_should_ack(self):
self.send_request('addid "dummy://foo"')
self.assertEqualResponse("ACK [50@0] {addid} No such song")
class BasePopulatedTracklistTestCase(protocol.BaseTestCase):
def setUp(self): # noqa: N802
super().setUp()
tracks = [Track(uri=f"dummy:/{x}", name=x) for x in "abcdeǂ"]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks])
class DeleteCommandsTest(BasePopulatedTracklistTestCase):
def test_clear(self):
self.send_request("clear")
assert len(self.core.tracklist.get_tracks().get()) == 0
assert self.core.playback.get_current_track().get() is None
self.assertInResponse("OK")
def test_delete_songpos(self):
tl_tracks = self.core.tracklist.get_tl_tracks().get()
self.send_request(f'delete "{tl_tracks[1].tlid}"')
assert len(self.core.tracklist.get_tracks().get()) == 5
self.assertInResponse("OK")
def test_delete_songpos_out_of_bounds(self):
self.send_request('delete "8"')
assert len(self.core.tracklist.get_tracks().get()) == 6
self.assertEqualResponse("ACK [2@0] {delete} Bad song index")
def test_delete_open_range(self):
self.send_request('delete "1:"')
assert len(self.core.tracklist.get_tracks().get()) == 1
self.assertInResponse("OK")
# TODO: check how this should work.
# def test_delete_open_upper_range(self):
# self.send_request('delete ":8"')
# self.assertEqual(len(self.core.tracklist.get_tracks().get()), 0)
# self.assertInResponse('OK')
def test_delete_closed_range(self):
self.send_request('delete "1:3"')
assert len(self.core.tracklist.get_tracks().get()) == 4
self.assertInResponse("OK")
def test_delete_entire_range_out_of_bounds(self):
self.send_request('delete "8:9"')
assert len(self.core.tracklist.get_tracks().get()) == 6
self.assertEqualResponse("ACK [2@0] {delete} Bad song index")
def test_delete_upper_range_out_of_bounds(self):
self.send_request('delete "5:9"')
assert len(self.core.tracklist.get_tracks().get()) == 5
self.assertEqualResponse("OK")
def test_deleteid(self):
self.send_request('deleteid "1"')
assert len(self.core.tracklist.get_tracks().get()) == 5
self.assertInResponse("OK")
def test_deleteid_does_not_exist(self):
self.send_request('deleteid "12345"')
assert len(self.core.tracklist.get_tracks().get()) == 6
self.assertEqualResponse("ACK [50@0] {deleteid} No such song")
class MoveCommandsTest(BasePopulatedTracklistTestCase):
def test_move_songpos(self):
self.send_request('move "1" "0"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["b", "a", "c", "d", "e", "ǂ"]
self.assertInResponse("OK")
def test_move_open_range(self):
self.send_request('move "2:" "0"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["c", "d", "e", "ǂ", "a", "b"]
self.assertInResponse("OK")
def test_move_closed_range(self):
self.send_request('move "1:3" "0"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["b", "c", "a", "d", "e", "ǂ"]
self.assertInResponse("OK")
def test_moveid(self):
self.send_request('moveid "5" "2"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["a", "b", "e", "c", "d", "ǂ"]
self.assertInResponse("OK")
def test_moveid_with_tlid_not_found_in_tracklist_should_ack(self):
self.send_request('moveid "10" "0"')
self.assertEqualResponse("ACK [50@0] {moveid} No such song")
class PlaylistFindCommandTest(protocol.BaseTestCase):
def test_playlistfind(self):
self.send_request('playlistfind "tag" "needle"')
self.assertEqualResponse("ACK [0@0] {playlistfind} Not implemented")
def test_playlistfind_by_filename_not_in_tracklist(self):
self.send_request('playlistfind "filename" "file:///dev/null"')
self.assertEqualResponse("OK")
def test_playlistfind_by_filename_without_quotes(self):
self.send_request('playlistfind filename "file:///dev/null"')
self.assertEqualResponse("OK")
def test_playlistfind_by_filename_in_tracklist(self):
track = Track(uri="dummy:///exists")
self.backend.library.dummy_library = [track]
self.core.tracklist.add(uris=[track.uri])
self.send_request('playlistfind filename "dummy:///exists"')
self.assertInResponse("file: dummy:///exists")
self.assertInResponse("Id: 1")
self.assertInResponse("Pos: 0")
self.assertInResponse("OK")
class PlaylistIdCommandTest(BasePopulatedTracklistTestCase):
def test_playlistid_without_songid(self):
self.send_request("playlistid")
self.assertInResponse("Title: a")
self.assertInResponse("Title: b")
self.assertInResponse("OK")
def test_playlistid_with_songid(self):
self.send_request('playlistid "2"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Id: 1")
self.assertInResponse("Title: b")
self.assertInResponse("Id: 2")
self.assertInResponse("OK")
def test_playlistid_with_not_existing_songid_fails(self):
self.send_request('playlistid "25"')
self.assertEqualResponse("ACK [50@0] {playlistid} No such song")
class PlaylistInfoCommandTest(BasePopulatedTracklistTestCase):
def test_playlist_returns_same_as_playlistinfo(self):
playlist_response = self.send_request("playlist")
playlistinfo_response = self.send_request("playlistinfo")
assert playlist_response == playlistinfo_response
def test_playlistinfo_without_songpos_or_range(self):
self.send_request("playlistinfo")
self.assertInResponse("Title: a")
self.assertInResponse("Pos: 0")
self.assertInResponse("Title: b")
self.assertInResponse("Pos: 1")
self.assertInResponse("Title: c")
self.assertInResponse("Pos: 2")
self.assertInResponse("Title: d")
self.assertInResponse("Pos: 3")
self.assertInResponse("Title: e")
self.assertInResponse("Pos: 4")
self.assertInResponse("Title: ǂ")
self.assertInResponse("Pos: 5")
self.assertInResponse("OK")
def test_playlistinfo_with_songpos(self):
# Make the track's CPID not match the playlist position
self.core.tracklist.tlid = 17
self.send_request('playlistinfo "4"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Pos: 0")
self.assertNotInResponse("Title: b")
self.assertNotInResponse("Pos: 1")
self.assertNotInResponse("Title: c")
self.assertNotInResponse("Pos: 2")
self.assertNotInResponse("Title: d")
self.assertNotInResponse("Pos: 3")
self.assertInResponse("Title: e")
self.assertInResponse("Pos: 4")
self.assertNotInResponse("Title: ǂ")
self.assertNotInResponse("Pos: 5")
self.assertInResponse("OK")
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
response1 = self.send_request('playlistinfo "-1"')
response2 = self.send_request("playlistinfo")
assert response1 == response2
def test_playlistinfo_with_open_range(self):
self.send_request('playlistinfo "2:"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Pos: 0")
self.assertNotInResponse("Title: b")
self.assertNotInResponse("Pos: 1")
self.assertInResponse("Title: c")
self.assertInResponse("Pos: 2")
self.assertInResponse("Title: d")
self.assertInResponse("Pos: 3")
self.assertInResponse("Title: e")
self.assertInResponse("Pos: 4")
self.assertInResponse("Title: ǂ")
self.assertInResponse("Pos: 5")
self.assertInResponse("OK")
def test_playlistinfo_with_closed_range(self):
self.send_request('playlistinfo "2:4"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Title: b")
self.assertInResponse("Title: c")
self.assertInResponse("Title: d")
self.assertNotInResponse("Title: e")
self.assertNotInResponse("Title: ǂ")
self.assertInResponse("OK")
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
self.send_request('playlistinfo "10:20"')
self.assertEqualResponse("ACK [2@0] {playlistinfo} Bad song index")
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
self.send_request('playlistinfo "0:20"')
self.assertInResponse("OK")
def test_playlistinfo_with_zero_returns_ok(self):
self.send_request('playlistinfo "0"')
self.assertInResponse("OK")
class PlaylistSearchCommandTest(protocol.BaseTestCase):
def test_playlistsearch(self):
self.send_request('playlistsearch "any" "needle"')
self.assertEqualResponse("ACK [0@0] {playlistsearch} Not implemented")
def test_playlistsearch_without_quotes(self):
self.send_request('playlistsearch any "needle"')
self.assertEqualResponse("ACK [0@0] {playlistsearch} Not implemented")
class PlChangeCommandTest(BasePopulatedTracklistTestCase):
def test_plchanges_with_lower_version_returns_changes(self):
self.send_request('plchanges "0"')
self.assertInResponse("Title: a")
self.assertInResponse("Title: b")
self.assertInResponse("Title: c")
self.assertInResponse("OK")
def test_plchanges_with_equal_version_returns_nothing(self):
assert self.core.tracklist.get_version().get() == 1
self.send_request('plchanges "1"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Title: b")
self.assertNotInResponse("Title: c")
self.assertInResponse("OK")
def test_plchanges_with_greater_version_returns_nothing(self):
assert self.core.tracklist.get_version().get() == 1
self.send_request('plchanges "2"')
self.assertNotInResponse("Title: a")
self.assertNotInResponse("Title: b")
self.assertNotInResponse("Title: c")
self.assertInResponse("OK")
def test_plchanges_with_minus_one_returns_entire_playlist(self):
self.send_request('plchanges "-1"')
self.assertInResponse("Title: a")
self.assertInResponse("Title: b")
self.assertInResponse("Title: c")
self.assertInResponse("OK")
def test_plchanges_without_quotes_works(self):
self.send_request("plchanges 0")
self.assertInResponse("Title: a")
self.assertInResponse("Title: b")
self.assertInResponse("Title: c")
self.assertInResponse("OK")
def test_plchangesposid(self):
self.send_request('plchangesposid "0"')
tl_tracks = self.core.tracklist.get_tl_tracks().get()
self.assertInResponse("cpos: 0")
self.assertInResponse(f"Id: {tl_tracks[0].tlid:d}")
self.assertInResponse("cpos: 2")
self.assertInResponse(f"Id: {tl_tracks[1].tlid:d}")
self.assertInResponse("cpos: 2")
self.assertInResponse(f"Id: {tl_tracks[2].tlid:d}")
self.assertInResponse("OK")
class PrioCommandTest(protocol.BaseTestCase):
def test_prio(self):
self.send_request("prio 255 0:10")
self.assertEqualResponse("ACK [0@0] {prio} Not implemented")
def test_prioid(self):
self.send_request("prioid 255 17 23")
self.assertEqualResponse("ACK [0@0] {prioid} Not implemented")
class RangeIdCommandTest(protocol.BaseTestCase):
def test_rangeid(self):
self.send_request("rangeid 17 0:30")
self.assertEqualResponse("ACK [0@0] {rangeid} Not implemented")
# TODO: we only seem to be testing that don't touch the non shuffled region :/
class ShuffleCommandTest(BasePopulatedTracklistTestCase):
def test_shuffle_without_range(self):
version = self.core.tracklist.get_version().get()
self.send_request("shuffle")
assert version < self.core.tracklist.get_version().get()
self.assertInResponse("OK")
def test_shuffle_with_open_range(self):
version = self.core.tracklist.get_version().get()
self.send_request('shuffle "4:"')
assert version < self.core.tracklist.get_version().get()
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result[:4] == ["a", "b", "c", "d"]
self.assertInResponse("OK")
def test_shuffle_with_closed_range(self):
version = self.core.tracklist.get_version().get()
self.send_request('shuffle "1:3"')
assert version < self.core.tracklist.get_version().get()
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result[:1] == ["a"]
assert result[3:] == ["d", "e", "ǂ"]
self.assertInResponse("OK")
class SwapCommandTest(BasePopulatedTracklistTestCase):
def test_swap(self):
self.send_request('swap "1" "4"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["a", "e", "c", "d", "b", "ǂ"]
self.assertInResponse("OK")
def test_swap_highest_position_first(self):
self.send_request('swap "4" "1"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["a", "e", "c", "d", "b", "ǂ"]
self.assertInResponse("OK")
def test_swapid(self):
self.send_request('swapid "2" "5"')
result = [t.name for t in self.core.tracklist.get_tracks().get()]
assert result == ["a", "e", "c", "d", "b", "ǂ"]
self.assertInResponse("OK")
def test_swapid_with_first_id_unknown_should_ack(self):
self.send_request('swapid "1" "8"')
self.assertEqualResponse("ACK [50@0] {swapid} No such song")
def test_swapid_with_second_id_unknown_should_ack(self):
self.send_request('swapid "8" "1"')
self.assertEqualResponse("ACK [50@0] {swapid} No such song")
class TagCommandTest(protocol.BaseTestCase):
def test_addtagid(self):
self.send_request("addtagid 17 artist Abba")
self.assertEqualResponse("ACK [0@0] {addtagid} Not implemented")
def test_cleartagid(self):
self.send_request("cleartagid 17 artist")
self.assertEqualResponse("ACK [0@0] {cleartagid} Not implemented")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_idle.py 0000644 0001751 0000171 00000017612 00000000000 020272 0 ustar 00runner docker from unittest.mock import patch
from mopidy_mpd.protocol.status import SUBSYSTEMS
from tests import protocol
class IdleHandlerTest(protocol.BaseTestCase):
def idle_event(self, subsystem):
self.session.on_event(subsystem)
def assertEqualEvents(self, events): # noqa: N802
assert set(events) == self.context.events
def assertEqualSubscriptions(self, events): # noqa: N802
assert set(events) == self.context.subscriptions
def assertNoEvents(self): # noqa: N802
self.assertEqualEvents([])
def assertNoSubscriptions(self): # noqa: N802
self.assertEqualSubscriptions([])
def test_base_state(self):
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle(self):
self.send_request("idle")
self.assertEqualSubscriptions(SUBSYSTEMS)
self.assertNoEvents()
self.assertNoResponse()
def test_idle_disables_timeout(self):
self.send_request("idle")
self.connection.disable_timeout.assert_called_once_with()
def test_noidle(self):
self.send_request("noidle")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player(self):
self.send_request("idle player")
self.assertEqualSubscriptions(["player"])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_output(self):
self.send_request("idle output")
self.assertEqualSubscriptions(["output"])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_player_playlist(self):
self.send_request("idle player playlist")
self.assertEqualSubscriptions(["player", "playlist"])
self.assertNoEvents()
self.assertNoResponse()
def test_idle_then_noidle(self):
self.send_request("idle")
self.send_request("noidle")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("OK")
def test_idle_then_noidle_enables_timeout(self):
self.send_request("idle")
self.send_request("noidle")
self.connection.enable_timeout.assert_called_once_with()
def test_idle_then_play(self):
with patch.object(self.session, "stop") as stop_mock:
self.send_request("idle")
self.send_request("play")
stop_mock.assert_called_once_with()
def test_idle_then_idle(self):
with patch.object(self.session, "stop") as stop_mock:
self.send_request("idle")
self.send_request("idle")
stop_mock.assert_called_once_with()
def test_idle_player_then_play(self):
with patch.object(self.session, "stop") as stop_mock:
self.send_request("idle player")
self.send_request("play")
stop_mock.assert_called_once_with()
def test_idle_then_player(self):
self.send_request("idle")
self.idle_event("player")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("changed: player")
self.assertOnceInResponse("OK")
def test_idle_player_then_event_player(self):
self.send_request("idle player")
self.idle_event("player")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("changed: player")
self.assertOnceInResponse("OK")
def test_idle_then_output(self):
self.send_request("idle")
self.idle_event("output")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("changed: output")
self.assertOnceInResponse("OK")
def test_idle_output_then_event_output(self):
self.send_request("idle output")
self.idle_event("output")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("changed: output")
self.assertOnceInResponse("OK")
def test_idle_player_then_noidle(self):
self.send_request("idle player")
self.send_request("noidle")
self.assertNoSubscriptions()
self.assertNoEvents()
self.assertOnceInResponse("OK")
def test_idle_player_playlist_then_noidle(self):
self.send_request("idle player playlist")
self.send_request("noidle")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("OK")
def test_idle_player_playlist_then_player(self):
self.send_request("idle player playlist")
self.idle_event("player")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("changed: player")
self.assertNotInResponse("changed: playlist")
self.assertOnceInResponse("OK")
def test_idle_playlist_then_player(self):
self.send_request("idle playlist")
self.idle_event("player")
self.assertEqualEvents(["player"])
self.assertEqualSubscriptions(["playlist"])
self.assertNoResponse()
def test_idle_playlist_then_player_then_playlist(self):
self.send_request("idle playlist")
self.idle_event("player")
self.idle_event("playlist")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse("changed: player")
self.assertOnceInResponse("changed: playlist")
self.assertOnceInResponse("OK")
def test_player(self):
self.idle_event("player")
self.assertEqualEvents(["player"])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle_player(self):
self.idle_event("player")
self.send_request("idle player")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("changed: player")
self.assertNotInResponse("changed: playlist")
self.assertOnceInResponse("OK")
def test_player_then_playlist(self):
self.idle_event("player")
self.idle_event("playlist")
self.assertEqualEvents(["player", "playlist"])
self.assertNoSubscriptions()
self.assertNoResponse()
def test_player_then_idle(self):
self.idle_event("player")
self.send_request("idle")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("changed: player")
self.assertOnceInResponse("OK")
def test_player_then_playlist_then_idle(self):
self.idle_event("player")
self.idle_event("playlist")
self.send_request("idle")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("changed: player")
self.assertOnceInResponse("changed: playlist")
self.assertOnceInResponse("OK")
def test_player_then_idle_playlist(self):
self.idle_event("player")
self.send_request("idle playlist")
self.assertEqualEvents(["player"])
self.assertEqualSubscriptions(["playlist"])
self.assertNoResponse()
def test_player_then_idle_playlist_then_noidle(self):
self.idle_event("player")
self.send_request("idle playlist")
self.send_request("noidle")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("OK")
def test_player_then_playlist_then_idle_playlist(self):
self.idle_event("player")
self.idle_event("playlist")
self.send_request("idle playlist")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertNotInResponse("changed: player")
self.assertOnceInResponse("changed: playlist")
self.assertOnceInResponse("OK")
def test_output_then_idle_toggleoutput(self):
self.idle_event("output")
self.send_request("idle output")
self.assertNoEvents()
self.assertNoSubscriptions()
self.assertOnceInResponse("changed: output")
self.assertOnceInResponse("OK")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_mount.py 0000644 0001751 0000171 00000001232 00000000000 020506 0 ustar 00runner docker from tests import protocol
class MountTest(protocol.BaseTestCase):
def test_mount(self):
self.send_request("mount my_disk /dev/sda")
self.assertEqualResponse("ACK [0@0] {mount} Not implemented")
def test_unmount(self):
self.send_request("unmount my_disk")
self.assertEqualResponse("ACK [0@0] {unmount} Not implemented")
def test_listmounts(self):
self.send_request("listmounts")
self.assertEqualResponse("ACK [0@0] {listmounts} Not implemented")
def test_listneighbors(self):
self.send_request("listneighbors")
self.assertEqualResponse("ACK [0@0] {listneighbors} Not implemented")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_music_db.py 0000644 0001751 0000171 00000137265 00000000000 021151 0 ustar 00runner docker import unittest
from unittest import mock
from mopidy.models import Album, Artist, Playlist, Ref, SearchResult, Track
from mopidy_mpd.protocol import music_db, stored_playlists
from tests import protocol
# TODO: split into more modules for faster parallel tests?
class QueryFromMpdSearchFormatTest(unittest.TestCase):
def test_dates_are_extracted(self):
result = music_db._query_from_mpd_search_parameters(
["Date", "1974-01-02", "Date", "1975"], music_db._SEARCH_MAPPING
)
assert result["date"][0] == "1974-01-02"
assert result["date"][1] == "1975"
def test_empty_value_is_ignored(self):
result = music_db._query_from_mpd_search_parameters(
["Date", ""], music_db._SEARCH_MAPPING
)
assert result == {}
def test_whitespace_value_is_ignored(self):
result = music_db._query_from_mpd_search_parameters(
["Date", " "], music_db._SEARCH_MAPPING
)
assert result == {}
# TODO Test more mappings
class QueryFromMpdListFormatTest(unittest.TestCase):
pass # TODO
# TODO: why isn't core.playlists.filter getting deprecation warnings?
class MusicDatabaseHandlerTest(protocol.BaseTestCase):
def test_count(self):
self.send_request('count "artist" "needle"')
self.assertInResponse("songs: 0")
self.assertInResponse("playtime: 0")
self.assertInResponse("OK")
def test_count_without_quotes(self):
self.send_request('count artist "needle"')
self.assertInResponse("songs: 0")
self.assertInResponse("playtime: 0")
self.assertInResponse("OK")
def test_count_with_multiple_pairs(self):
self.send_request('count "artist" "foo" "album" "bar"')
self.assertInResponse("songs: 0")
self.assertInResponse("playtime: 0")
self.assertInResponse("OK")
def test_count_correct_length(self):
# Count the lone track
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(uri="dummy:a", name="foo", date="2001", length=4000)]
)
self.send_request('count "title" "foo"')
self.assertInResponse("songs: 1")
self.assertInResponse("playtime: 4")
self.assertInResponse("OK")
# Count multiple tracks
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[
Track(uri="dummy:b", date="2001", length=50000),
Track(uri="dummy:c", date="2001", length=600000),
]
)
self.send_request('count "date" "2001"')
self.assertInResponse("songs: 2")
self.assertInResponse("playtime: 650")
self.assertInResponse("OK")
def test_count_with_track_length_none(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(uri="dummy:b", date="2001", length=None)]
)
self.send_request('count "date" "2001"')
self.assertInResponse("songs: 1")
self.assertInResponse("playtime: 0")
self.assertInResponse("OK")
def test_findadd(self):
track = Track(uri="dummy:a", name="A")
self.backend.library.dummy_library = [track]
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[track]
)
assert self.core.tracklist.get_length().get() == 0
self.send_request('findadd "title" "A"')
assert self.core.tracklist.get_length().get() == 1
assert self.core.tracklist.get_tracks().get()[0].uri == "dummy:a"
self.assertInResponse("OK")
def test_searchadd(self):
track = Track(uri="dummy:a", name="A")
self.backend.library.dummy_library = [track]
self.backend.library.dummy_search_result = SearchResult(tracks=[track])
assert self.core.tracklist.get_length().get() == 0
self.send_request('searchadd "title" "a"')
assert self.core.tracklist.get_length().get() == 1
assert self.core.tracklist.get_tracks().get()[0].uri == "dummy:a"
self.assertInResponse("OK")
def test_searchaddpl_appends_to_existing_playlist(self):
playlist = self.core.playlists.create("my favs").get()
playlist = playlist.replace(
tracks=[
Track(uri="dummy:x", name="X"),
Track(uri="dummy:y", name="y"),
]
)
self.core.playlists.save(playlist)
self.backend.library.dummy_search_result = SearchResult(
tracks=[Track(uri="dummy:a", name="A")]
)
items = self.core.playlists.get_items(playlist.uri).get()
assert len(items) == 2
self.send_request('searchaddpl "my favs" "title" "a"')
items = self.core.playlists.get_items(playlist.uri).get()
assert len(items) == 3
assert items[0].uri == "dummy:x"
assert items[1].uri == "dummy:y"
assert items[2].uri == "dummy:a"
self.assertInResponse("OK")
def test_searchaddpl_creates_missing_playlist(self):
self.backend.library.dummy_search_result = SearchResult(
tracks=[Track(uri="dummy:a", name="A")]
)
playlists = self.core.playlists.as_list().get()
assert "my favs" not in {p.name for p in playlists}
self.send_request('searchaddpl "my favs" "title" "a"')
playlists = self.core.playlists.as_list().get()
playlist = {p.name: p for p in playlists}["my favs"]
items = self.core.playlists.get_items(playlist.uri).get()
assert len(items) == 1
assert items[0].uri == "dummy:a"
self.assertInResponse("OK")
def test_listall_without_uri(self):
tracks = [
Track(uri="dummy:/a", name="a"),
Track(uri="dummy:/foo/b", name="b"),
]
self.backend.library.dummy_library = tracks
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
Ref.album(uri="dummy:/album", name="album"),
Ref.artist(uri="dummy:/artist", name="artist"),
Ref.playlist(uri="dummy:/pl", name="pl"),
],
"dummy:/foo": [Ref.track(uri="dummy:/foo/b", name="b")],
}
self.send_request("listall")
self.assertInResponse("file: dummy:/a")
self.assertInResponse("directory: dummy/foo")
self.assertInResponse("directory: dummy/album")
self.assertInResponse("directory: dummy/artist")
self.assertInResponse("directory: dummy/pl")
self.assertInResponse("file: dummy:/foo/b")
self.assertInResponse("OK")
def test_listall_with_uri(self):
tracks = [
Track(uri="dummy:/a", name="a"),
Track(uri="dummy:/foo/b", name="b"),
]
self.backend.library.dummy_library = tracks
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
],
"dummy:/foo": [Ref.track(uri="dummy:/foo/b", name="b")],
}
self.send_request('listall "/dummy/foo"')
self.assertNotInResponse("file: dummy:/a")
self.assertInResponse("directory: dummy/foo")
self.assertInResponse("file: dummy:/foo/b")
self.assertInResponse("OK")
def test_listall_with_unknown_uri(self):
self.send_request('listall "/unknown"')
self.assertEqualResponse("ACK [50@0] {listall} Not found")
def test_listall_for_dir_with_and_without_leading_slash_is_the_same(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('listall "dummy"')
response2 = self.send_request('listall "/dummy"')
assert response1 == response2
def test_listall_for_dir_with_and_without_trailing_slash_is_the_same(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('listall "dummy"')
response2 = self.send_request('listall "dummy/"')
assert response1 == response2
def test_listall_duplicate(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.directory(uri="dummy:/a1", name="a"),
Ref.directory(uri="dummy:/a2", name="a"),
]
}
self.send_request("listall")
self.assertInResponse("directory: dummy/a")
self.assertInResponse("directory: dummy/a [2]")
def test_listallinfo_without_uri(self):
tracks = [
Track(uri="dummy:/a", name="a"),
Track(uri="dummy:/foo/b", name="b"),
]
self.backend.library.dummy_library = tracks
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
Ref.album(uri="dummy:/album", name="album"),
Ref.artist(uri="dummy:/artist", name="artist"),
Ref.playlist(uri="dummy:/pl", name="pl"),
],
"dummy:/foo": [Ref.track(uri="dummy:/foo/b", name="b")],
}
self.send_request("listallinfo")
self.assertInResponse("file: dummy:/a")
self.assertInResponse("Title: a")
self.assertInResponse("directory: dummy/foo")
self.assertInResponse("directory: dummy/album")
self.assertInResponse("directory: dummy/artist")
self.assertInResponse("directory: dummy/pl")
self.assertInResponse("file: dummy:/foo/b")
self.assertInResponse("Title: b")
self.assertInResponse("OK")
def test_listallinfo_with_uri(self):
tracks = [
Track(uri="dummy:/a", name="a"),
Track(uri="dummy:/foo/b", name="b"),
]
self.backend.library.dummy_library = tracks
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
],
"dummy:/foo": [Ref.track(uri="dummy:/foo/b", name="b")],
}
self.send_request('listallinfo "/dummy/foo"')
self.assertNotInResponse("file: dummy:/a")
self.assertNotInResponse("Title: a")
self.assertInResponse("directory: dummy/foo")
self.assertInResponse("file: dummy:/foo/b")
self.assertInResponse("Title: b")
self.assertInResponse("OK")
def test_listallinfo_with_unknown_uri(self):
self.send_request('listallinfo "/unknown"')
self.assertEqualResponse("ACK [50@0] {listallinfo} Not found")
def test_listallinfo_for_dir_with_and_without_leading_slash_is_same(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('listallinfo "dummy"')
response2 = self.send_request('listallinfo "/dummy"')
assert response1 == response2
def test_listallinfo_for_dir_with_and_without_trailing_slash_is_same(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('listallinfo "dummy"')
response2 = self.send_request('listallinfo "dummy/"')
assert response1 == response2
def test_listallinfo_duplicate(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.directory(uri="dummy:/a1", name="a"),
Ref.directory(uri="dummy:/a2", name="a"),
]
}
self.send_request("listallinfo")
self.assertInResponse("directory: dummy/a")
self.assertInResponse("directory: dummy/a [2]")
def test_listfiles(self):
self.send_request("listfiles")
self.assertEqualResponse("ACK [0@0] {listfiles} Not implemented")
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_lsinfo_without_path_returns_same_as_for_root(
self, last_modified_mock
):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a", uri="dummy:/a")]
)
response1 = self.send_request("lsinfo")
response2 = self.send_request('lsinfo "/"')
assert response1 == response2
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_lsinfo_with_empty_path_returns_same_as_for_root(
self, last_modified_mock
):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a", uri="dummy:/a")]
)
response1 = self.send_request('lsinfo ""')
response2 = self.send_request('lsinfo "/"')
assert response1 == response2
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_lsinfo_for_root_includes_playlists(self, last_modified_mock):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a", uri="dummy:/a")]
)
self.send_request('lsinfo "/"')
self.assertInResponse("playlist: a")
self.assertInResponse("Last-Modified: 2015-08-05T22:51:06Z")
self.assertInResponse("OK")
def test_lsinfo_for_root_includes_dirs_for_each_lib_with_content(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
self.send_request('lsinfo "/"')
self.assertInResponse("directory: dummy")
self.assertInResponse("OK")
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_lsinfo_for_dir_with_and_without_leading_slash_is_the_same(
self, last_modified_mock
):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('lsinfo "dummy"')
response2 = self.send_request('lsinfo "/dummy"')
assert response1 == response2
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_lsinfo_for_dir_with_and_without_trailing_slash_is_the_same(
self, last_modified_mock
):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
response1 = self.send_request('lsinfo "dummy"')
response2 = self.send_request('lsinfo "dummy/"')
assert response1 == response2
def test_lsinfo_for_dir_includes_tracks(self):
self.backend.library.dummy_library = [
Track(uri="dummy:/a", name="a"),
]
self.backend.library.dummy_browse_result = {
"dummy:/": [Ref.track(uri="dummy:/a", name="a")]
}
self.send_request('lsinfo "/dummy"')
self.assertInResponse("file: dummy:/a")
self.assertInResponse("Title: a")
self.assertInResponse("OK")
def test_lsinfo_for_dir_includes_subdirs(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [Ref.directory(uri="dummy:/foo", name="foo")]
}
self.send_request('lsinfo "/dummy"')
self.assertInResponse("directory: dummy/foo")
self.assertInResponse("OK")
def test_lsinfo_for_empty_dir_returns_nothing(self):
self.backend.library.dummy_browse_result = {"dummy:/": []}
self.send_request('lsinfo "/dummy"')
self.assertInResponse("OK")
def test_lsinfo_for_dir_does_not_recurse(self):
self.backend.library.dummy_library = [
Track(uri="dummy:/a", name="a"),
]
self.backend.library.dummy_browse_result = {
"dummy:/": [Ref.directory(uri="dummy:/foo", name="foo")],
"dummy:/foo": [Ref.track(uri="dummy:/a", name="a")],
}
self.send_request('lsinfo "/dummy"')
self.assertNotInResponse("file: dummy:/a")
self.assertInResponse("OK")
def test_lsinfo_for_dir_does_not_include_self(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [Ref.directory(uri="dummy:/foo", name="foo")],
"dummy:/foo": [Ref.track(uri="dummy:/a", name="a")],
}
self.send_request('lsinfo "/dummy"')
self.assertNotInResponse("directory: dummy")
self.assertInResponse("OK")
def test_lsinfo_for_root_returns_browse_result_before_playlists(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.track(uri="dummy:/a", name="a"),
Ref.directory(uri="dummy:/foo", name="foo"),
]
}
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a", uri="dummy:/a")]
)
response = self.send_request('lsinfo "/"')
assert response.index("directory: dummy") < response.index(
"playlist: a"
)
def test_lsinfo_duplicate(self):
self.backend.library.dummy_browse_result = {
"dummy:/": [
Ref.directory(uri="dummy:/a1", name="a"),
Ref.directory(uri="dummy:/a2", name="a"),
]
}
self.send_request('lsinfo "/dummy"')
self.assertInResponse("directory: dummy/a")
self.assertInResponse("directory: dummy/a [2]")
def test_update_without_uri(self):
self.send_request("update")
self.assertInResponse("updating_db: 0")
self.assertInResponse("OK")
def test_update_with_uri(self):
self.send_request('update "file:///dev/urandom"')
self.assertInResponse("updating_db: 0")
self.assertInResponse("OK")
def test_rescan_without_uri(self):
self.send_request("rescan")
self.assertInResponse("updating_db: 0")
self.assertInResponse("OK")
def test_rescan_with_uri(self):
self.send_request('rescan "file:///dev/urandom"')
self.assertInResponse("updating_db: 0")
self.assertInResponse("OK")
class MusicDatabaseFindTest(protocol.BaseTestCase):
def test_find_includes_fake_artist_and_album_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri="dummy:album:a", name="A", date="2001")],
artists=[Artist(uri="dummy:artist:b", name="B")],
tracks=[Track(uri="dummy:track:c", name="C")],
)
self.send_request('find "any" "foo"')
self.assertInResponse("file: dummy:artist:b")
self.assertInResponse("Title: Artist: B")
self.assertInResponse("file: dummy:album:a")
self.assertInResponse("Title: Album: A")
self.assertInResponse("Date: 2001")
self.assertInResponse("file: dummy:track:c")
self.assertInResponse("Title: C")
self.assertInResponse("OK")
def test_find_artist_does_not_include_fake_artist_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri="dummy:album:a", name="A", date="2001")],
artists=[Artist(uri="dummy:artist:b", name="B")],
tracks=[Track(uri="dummy:track:c", name="C")],
)
self.send_request('find "artist" "foo"')
self.assertNotInResponse("file: dummy:artist:b")
self.assertNotInResponse("Title: Artist: B")
self.assertInResponse("file: dummy:album:a")
self.assertInResponse("Title: Album: A")
self.assertInResponse("Date: 2001")
self.assertInResponse("file: dummy:track:c")
self.assertInResponse("Title: C")
self.assertInResponse("OK")
def test_find_albumartist_does_not_include_fake_artist_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri="dummy:album:a", name="A", date="2001")],
artists=[Artist(uri="dummy:artist:b", name="B")],
tracks=[Track(uri="dummy:track:c", name="C")],
)
self.send_request('find "albumartist" "foo"')
self.assertNotInResponse("file: dummy:artist:b")
self.assertNotInResponse("Title: Artist: B")
self.assertInResponse("file: dummy:album:a")
self.assertInResponse("Title: Album: A")
self.assertInResponse("Date: 2001")
self.assertInResponse("file: dummy:track:c")
self.assertInResponse("Title: C")
self.assertInResponse("OK")
def test_find_artist_and_album_does_not_include_fake_tracks(self):
self.backend.library.dummy_find_exact_result = SearchResult(
albums=[Album(uri="dummy:album:a", name="A", date="2001")],
artists=[Artist(uri="dummy:artist:b", name="B")],
tracks=[Track(uri="dummy:track:c", name="C")],
)
self.send_request('find "artist" "foo" "album" "bar"')
self.assertNotInResponse("file: dummy:artist:b")
self.assertNotInResponse("Title: Artist: B")
self.assertNotInResponse("file: dummy:album:a")
self.assertNotInResponse("Title: Album: A")
self.assertNotInResponse("Date: 2001")
self.assertInResponse("file: dummy:track:c")
self.assertInResponse("Title: C")
self.assertInResponse("OK")
def test_find_album(self):
self.send_request('find "album" "what"')
self.assertInResponse("OK")
def test_find_album_without_quotes(self):
self.send_request('find album "what"')
self.assertInResponse("OK")
def test_find_artist(self):
self.send_request('find "artist" "what"')
self.assertInResponse("OK")
def test_find_artist_without_quotes(self):
self.send_request('find artist "what"')
self.assertInResponse("OK")
def test_find_albumartist(self):
self.send_request('find "albumartist" "what"')
self.assertInResponse("OK")
def test_find_albumartist_without_quotes(self):
self.send_request('find albumartist "what"')
self.assertInResponse("OK")
def test_find_composer(self):
self.send_request('find "composer" "what"')
self.assertInResponse("OK")
def test_find_composer_without_quotes(self):
self.send_request('find composer "what"')
self.assertInResponse("OK")
def test_find_performer(self):
self.send_request('find "performer" "what"')
self.assertInResponse("OK")
def test_find_performer_without_quotes(self):
self.send_request('find performer "what"')
self.assertInResponse("OK")
def test_find_filename(self):
self.send_request('find "filename" "afilename"')
self.assertInResponse("OK")
def test_find_filename_without_quotes(self):
self.send_request('find filename "afilename"')
self.assertInResponse("OK")
def test_find_file(self):
self.send_request('find "file" "afilename"')
self.assertInResponse("OK")
def test_find_file_without_quotes(self):
self.send_request('find file "afilename"')
self.assertInResponse("OK")
def test_find_title(self):
self.send_request('find "title" "what"')
self.assertInResponse("OK")
def test_find_title_without_quotes(self):
self.send_request('find title "what"')
self.assertInResponse("OK")
def test_find_track_no(self):
self.send_request('find "track" "10"')
self.assertInResponse("OK")
def test_find_track_no_without_quotes(self):
self.send_request('find track "10"')
self.assertInResponse("OK")
def test_find_track_no_without_filter_value(self):
self.send_request('find "track" ""')
self.assertInResponse("OK")
def test_find_genre(self):
self.send_request('find "genre" "what"')
self.assertInResponse("OK")
def test_find_genre_without_quotes(self):
self.send_request('find genre "what"')
self.assertInResponse("OK")
def test_find_date(self):
self.send_request('find "date" "2002-01-01"')
self.assertInResponse("OK")
def test_find_date_without_quotes(self):
self.send_request('find date "2002-01-01"')
self.assertInResponse("OK")
def test_find_date_with_capital_d_and_incomplete_date(self):
self.send_request('find Date "2005"')
self.assertInResponse("OK")
def test_find_else_should_fail(self):
self.send_request('find "somethingelse" "what"')
self.assertEqualResponse("ACK [2@0] {find} incorrect arguments")
def test_find_album_and_artist(self):
self.send_request('find album "album_what" artist "artist_what"')
self.assertInResponse("OK")
def test_find_without_filter_value(self):
self.send_request('find "album" ""')
self.assertInResponse("OK")
class MusicDatabaseListTest(protocol.BaseTestCase):
def test_list(self):
self.backend.library.dummy_get_distinct_result = {
"artist": {"A Artist"}
}
self.send_request('list "artist" "artist" "foo"')
self.assertInResponse("Artist: A Artist")
self.assertInResponse("OK")
def test_list_foo_returns_ack(self):
self.send_request('list "foo"')
self.assertEqualResponse("ACK [2@0] {list} Unknown tag type: foo")
def test_list_without_type_returns_ack(self):
self.send_request("list")
self.assertEqualResponse(
'ACK [2@0] {list} too few arguments for "list"'
)
# Track title
def test_list_title(self):
self.send_request('list "title"')
self.assertInResponse("OK")
def test_list_title_by_title(self):
self.send_request('list "title" "title" "atitle"')
self.assertInResponse("OK")
# Artist
def test_list_artist_with_quotes(self):
self.send_request('list "artist"')
self.assertInResponse("OK")
def test_list_artist_without_quotes(self):
self.send_request("list artist")
self.assertInResponse("OK")
def test_list_artist_without_quotes_and_capitalized(self):
self.send_request("list Artist")
self.assertInResponse("OK")
def test_list_artist_with_query_of_one_token(self):
self.send_request('list "artist" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
self.send_request('list "artist" "foo" "bar"')
self.assertEqualResponse("ACK [2@0] {list} Unknown filter type")
def test_list_artist_by_artist(self):
self.send_request('list "artist" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_artist_by_album(self):
self.send_request('list "artist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_artist_by_full_date(self):
self.send_request('list "artist" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_artist_by_year(self):
self.send_request('list "artist" "date" "2001"')
self.assertInResponse("OK")
def test_list_artist_by_genre(self):
self.send_request('list "artist" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_artist_by_artist_and_album(self):
self.send_request('list "artist" "artist" "anartist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_artist_without_filter_value(self):
self.send_request('list "artist" "artist" ""')
self.assertInResponse("OK")
def test_list_artist_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(artists=[Artist(name="")])]
)
self.send_request('list "artist"')
self.assertNotInResponse("Artist: ")
self.assertInResponse("OK")
# Albumartist
def test_list_albumartist_with_quotes(self):
self.send_request('list "albumartist"')
self.assertInResponse("OK")
def test_list_albumartist_without_quotes(self):
self.send_request("list albumartist")
self.assertInResponse("OK")
def test_list_albumartist_without_quotes_and_capitalized(self):
self.send_request("list Albumartist")
self.assertInResponse("OK")
def test_list_albumartist_with_query_of_one_token(self):
self.send_request('list "albumartist" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_albumartist_with_unknown_field_in_query_returns_ack(self):
self.send_request('list "albumartist" "foo" "bar"')
self.assertEqualResponse("ACK [2@0] {list} Unknown filter type")
def test_list_albumartist_by_artist(self):
self.send_request('list "albumartist" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_albumartist_by_album(self):
self.send_request('list "albumartist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_albumartist_by_full_date(self):
self.send_request('list "albumartist" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_albumartist_by_year(self):
self.send_request('list "albumartist" "date" "2001"')
self.assertInResponse("OK")
def test_list_albumartist_by_genre(self):
self.send_request('list "albumartist" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_albumartist_by_artist_and_album(self):
self.send_request(
'list "albumartist" "artist" "anartist" "album" "analbum"'
)
self.assertInResponse("OK")
def test_list_albumartist_without_filter_value(self):
self.send_request('list "albumartist" "artist" ""')
self.assertInResponse("OK")
def test_list_albumartist_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(album=Album(artists=[Artist(name="")]))]
)
self.send_request('list "albumartist"')
self.assertNotInResponse("Artist: ")
self.assertNotInResponse("Albumartist: ")
self.assertNotInResponse("Composer: ")
self.assertNotInResponse("Performer: ")
self.assertInResponse("OK")
# Composer
def test_list_composer_with_quotes(self):
self.send_request('list "composer"')
self.assertInResponse("OK")
def test_list_composer_without_quotes(self):
self.send_request("list composer")
self.assertInResponse("OK")
def test_list_composer_without_quotes_and_capitalized(self):
self.send_request("list Composer")
self.assertInResponse("OK")
def test_list_composer_with_query_of_one_token(self):
self.send_request('list "composer" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_composer_with_unknown_field_in_query_returns_ack(self):
self.send_request('list "composer" "foo" "bar"')
self.assertEqualResponse("ACK [2@0] {list} Unknown filter type")
def test_list_composer_by_artist(self):
self.send_request('list "composer" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_composer_by_album(self):
self.send_request('list "composer" "album" "analbum"')
self.assertInResponse("OK")
def test_list_composer_by_full_date(self):
self.send_request('list "composer" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_composer_by_year(self):
self.send_request('list "composer" "date" "2001"')
self.assertInResponse("OK")
def test_list_composer_by_genre(self):
self.send_request('list "composer" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_composer_by_artist_and_album(self):
self.send_request(
'list "composer" "artist" "anartist" "album" "analbum"'
)
self.assertInResponse("OK")
def test_list_composer_without_filter_value(self):
self.send_request('list "composer" "artist" ""')
self.assertInResponse("OK")
def test_list_composer_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(composers=[Artist(name="")])]
)
self.send_request('list "composer"')
self.assertNotInResponse("Artist: ")
self.assertNotInResponse("Albumartist: ")
self.assertNotInResponse("Composer: ")
self.assertNotInResponse("Performer: ")
self.assertInResponse("OK")
# Performer
def test_list_performer_with_quotes(self):
self.send_request('list "performer"')
self.assertInResponse("OK")
def test_list_performer_without_quotes(self):
self.send_request("list performer")
self.assertInResponse("OK")
def test_list_performer_without_quotes_and_capitalized(self):
self.send_request("list Albumartist")
self.assertInResponse("OK")
def test_list_performer_with_query_of_one_token(self):
self.send_request('list "performer" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_performer_with_unknown_field_in_query_returns_ack(self):
self.send_request('list "performer" "foo" "bar"')
self.assertEqualResponse("ACK [2@0] {list} Unknown filter type")
def test_list_performer_by_artist(self):
self.send_request('list "performer" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_performer_by_album(self):
self.send_request('list "performer" "album" "analbum"')
self.assertInResponse("OK")
def test_list_performer_by_full_date(self):
self.send_request('list "performer" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_performer_by_year(self):
self.send_request('list "performer" "date" "2001"')
self.assertInResponse("OK")
def test_list_performer_by_genre(self):
self.send_request('list "performer" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_performer_by_artist_and_album(self):
self.send_request(
'list "performer" "artist" "anartist" "album" "analbum"'
)
self.assertInResponse("OK")
def test_list_performer_without_filter_value(self):
self.send_request('list "performer" "artist" ""')
self.assertInResponse("OK")
def test_list_performer_should_not_return_artists_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(performers=[Artist(name="")])]
)
self.send_request('list "performer"')
self.assertNotInResponse("Artist: ")
self.assertNotInResponse("Albumartist: ")
self.assertNotInResponse("Composer: ")
self.assertNotInResponse("Performer: ")
self.assertInResponse("OK")
# Album
def test_list_album_with_quotes(self):
self.send_request('list "album"')
self.assertInResponse("OK")
def test_list_album_without_quotes(self):
self.send_request("list album")
self.assertInResponse("OK")
def test_list_album_without_quotes_and_capitalized(self):
self.send_request("list Album")
self.assertInResponse("OK")
def test_list_album_with_artist_name(self):
self.backend.library.dummy_get_distinct_result = {"album": {"foo"}}
self.send_request('list "album" "anartist"')
self.assertInResponse("Album: foo")
self.assertInResponse("OK")
def test_list_album_with_artist_name_without_filter_value(self):
self.send_request('list "album" ""')
self.assertInResponse("OK")
def test_list_album_by_artist(self):
self.send_request('list "album" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_album_by_album(self):
self.send_request('list "album" "album" "analbum"')
self.assertInResponse("OK")
def test_list_album_by_albumartist(self):
self.send_request('list "album" "albumartist" "anartist"')
self.assertInResponse("OK")
def test_list_album_by_composer(self):
self.send_request('list "album" "composer" "anartist"')
self.assertInResponse("OK")
def test_list_album_by_performer(self):
self.send_request('list "album" "performer" "anartist"')
self.assertInResponse("OK")
def test_list_album_by_full_date(self):
self.send_request('list "album" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_album_by_year(self):
self.send_request('list "album" "date" "2001"')
self.assertInResponse("OK")
def test_list_album_by_genre(self):
self.send_request('list "album" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_album_by_artist_and_album(self):
self.send_request('list "album" "artist" "anartist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_album_without_filter_value(self):
self.send_request('list "album" "artist" ""')
self.assertInResponse("OK")
def test_list_album_should_not_return_albums_without_names(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(album=Album(name=""))]
)
self.send_request('list "album"')
self.assertNotInResponse("Album: ")
self.assertInResponse("OK")
# Date
def test_list_date_with_quotes(self):
self.send_request('list "date"')
self.assertInResponse("OK")
def test_list_date_without_quotes(self):
self.send_request("list date")
self.assertInResponse("OK")
def test_list_date_without_quotes_and_capitalized(self):
self.send_request("list Date")
self.assertInResponse("OK")
def test_list_date_with_query_of_one_token(self):
self.send_request('list "date" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_date_by_artist(self):
self.send_request('list "date" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_date_by_album(self):
self.send_request('list "date" "album" "analbum"')
self.assertInResponse("OK")
def test_list_date_by_full_date(self):
self.send_request('list "date" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_date_by_year(self):
self.send_request('list "date" "date" "2001"')
self.assertInResponse("OK")
def test_list_date_by_genre(self):
self.send_request('list "date" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_date_by_artist_and_album(self):
self.send_request('list "date" "artist" "anartist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_date_without_filter_value(self):
self.send_request('list "date" "artist" ""')
self.assertInResponse("OK")
def test_list_date_should_not_return_blank_dates(self):
self.backend.library.dummy_find_exact_result = SearchResult(
tracks=[Track(date="")]
)
self.send_request('list "date"')
self.assertNotInResponse("Date: ")
self.assertInResponse("OK")
# Genre
def test_list_genre_with_quotes(self):
self.send_request('list "genre"')
self.assertInResponse("OK")
def test_list_genre_without_quotes(self):
self.send_request("list genre")
self.assertInResponse("OK")
def test_list_genre_without_quotes_and_capitalized(self):
self.send_request("list Genre")
self.assertInResponse("OK")
def test_list_genre_with_query_of_one_token(self):
self.send_request('list "genre" "anartist"')
self.assertEqualResponse(
'ACK [2@0] {list} should be "Album" for 3 arguments'
)
def test_list_genre_by_artist(self):
self.send_request('list "genre" "artist" "anartist"')
self.assertInResponse("OK")
def test_list_genre_by_album(self):
self.send_request('list "genre" "album" "analbum"')
self.assertInResponse("OK")
def test_list_genre_by_full_date(self):
self.send_request('list "genre" "date" "2001-01-01"')
self.assertInResponse("OK")
def test_list_genre_by_year(self):
self.send_request('list "genre" "date" "2001"')
self.assertInResponse("OK")
def test_list_genre_by_genre(self):
self.send_request('list "genre" "genre" "agenre"')
self.assertInResponse("OK")
def test_list_genre_by_artist_and_album(self):
self.send_request('list "genre" "artist" "anartist" "album" "analbum"')
self.assertInResponse("OK")
def test_list_genre_without_filter_value(self):
self.send_request('list "genre" "artist" ""')
self.assertInResponse("OK")
class MusicDatabaseSearchTest(protocol.BaseTestCase):
def test_search(self):
self.backend.library.dummy_search_result = SearchResult(
albums=[Album(uri="dummy:album:a", name="A")],
artists=[Artist(uri="dummy:artist:b", name="B")],
tracks=[Track(uri="dummy:track:c", name="C")],
)
self.send_request('search "any" "foo"')
self.assertInResponse("file: dummy:album:a")
self.assertInResponse("Title: Album: A")
self.assertInResponse("file: dummy:artist:b")
self.assertInResponse("Title: Artist: B")
self.assertInResponse("file: dummy:track:c")
self.assertInResponse("Title: C")
self.assertInResponse("OK")
def test_search_album(self):
self.send_request('search "album" "analbum"')
self.assertInResponse("OK")
def test_search_album_without_quotes(self):
self.send_request('search album "analbum"')
self.assertInResponse("OK")
def test_search_album_without_filter_value(self):
self.send_request('search "album" ""')
self.assertInResponse("OK")
def test_search_artist(self):
self.send_request('search "artist" "anartist"')
self.assertInResponse("OK")
def test_search_artist_without_quotes(self):
self.send_request('search artist "anartist"')
self.assertInResponse("OK")
def test_search_artist_without_filter_value(self):
self.send_request('search "artist" ""')
self.assertInResponse("OK")
def test_search_albumartist(self):
self.send_request('search "albumartist" "analbumartist"')
self.assertInResponse("OK")
def test_search_albumartist_without_quotes(self):
self.send_request('search albumartist "analbumartist"')
self.assertInResponse("OK")
def test_search_albumartist_without_filter_value(self):
self.send_request('search "albumartist" ""')
self.assertInResponse("OK")
def test_search_composer(self):
self.send_request('search "composer" "acomposer"')
self.assertInResponse("OK")
def test_search_composer_without_quotes(self):
self.send_request('search composer "acomposer"')
self.assertInResponse("OK")
def test_search_composer_without_filter_value(self):
self.send_request('search "composer" ""')
self.assertInResponse("OK")
def test_search_performer(self):
self.send_request('search "performer" "aperformer"')
self.assertInResponse("OK")
def test_search_performer_without_quotes(self):
self.send_request('search performer "aperformer"')
self.assertInResponse("OK")
def test_search_performer_without_filter_value(self):
self.send_request('search "performer" ""')
self.assertInResponse("OK")
def test_search_filename(self):
self.send_request('search "filename" "afilename"')
self.assertInResponse("OK")
def test_search_filename_without_quotes(self):
self.send_request('search filename "afilename"')
self.assertInResponse("OK")
def test_search_filename_without_filter_value(self):
self.send_request('search "filename" ""')
self.assertInResponse("OK")
def test_search_file(self):
self.send_request('search "file" "afilename"')
self.assertInResponse("OK")
def test_search_file_without_quotes(self):
self.send_request('search file "afilename"')
self.assertInResponse("OK")
def test_search_file_without_filter_value(self):
self.send_request('search "file" ""')
self.assertInResponse("OK")
def test_search_title(self):
self.send_request('search "title" "atitle"')
self.assertInResponse("OK")
def test_search_title_without_quotes(self):
self.send_request('search title "atitle"')
self.assertInResponse("OK")
def test_search_title_without_filter_value(self):
self.send_request('search "title" ""')
self.assertInResponse("OK")
def test_search_any(self):
self.send_request('search "any" "anything"')
self.assertInResponse("OK")
def test_search_any_without_quotes(self):
self.send_request('search any "anything"')
self.assertInResponse("OK")
def test_search_any_without_filter_value(self):
self.send_request('search "any" ""')
self.assertInResponse("OK")
def test_search_track_no(self):
self.send_request('search "track" "10"')
self.assertInResponse("OK")
def test_search_track_no_without_quotes(self):
self.send_request('search track "10"')
self.assertInResponse("OK")
def test_search_track_no_without_filter_value(self):
self.send_request('search "track" ""')
self.assertInResponse("OK")
def test_search_genre(self):
self.send_request('search "genre" "agenre"')
self.assertInResponse("OK")
def test_search_genre_without_quotes(self):
self.send_request('search genre "agenre"')
self.assertInResponse("OK")
def test_search_genre_without_filter_value(self):
self.send_request('search "genre" ""')
self.assertInResponse("OK")
def test_search_date(self):
self.send_request('search "date" "2002-01-01"')
self.assertInResponse("OK")
def test_search_date_without_quotes(self):
self.send_request('search date "2002-01-01"')
self.assertInResponse("OK")
def test_search_date_with_capital_d_and_incomplete_date(self):
self.send_request('search Date "2005"')
self.assertInResponse("OK")
def test_search_date_without_filter_value(self):
self.send_request('search "date" ""')
self.assertInResponse("OK")
def test_search_comment(self):
self.send_request('search "comment" "acomment"')
self.assertInResponse("OK")
def test_search_comment_without_quotes(self):
self.send_request('search comment "acomment"')
self.assertInResponse("OK")
def test_search_comment_without_filter_value(self):
self.send_request('search "comment" ""')
self.assertInResponse("OK")
def test_search_else_should_fail(self):
self.send_request('search "sometype" "something"')
self.assertEqualResponse("ACK [2@0] {search} incorrect arguments")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_playback.py 0000644 0001751 0000171 00000043564 00000000000 021150 0 ustar 00runner docker import unittest
from mopidy.core import PlaybackState
from mopidy.models import Track
from tests import protocol
PAUSED = PlaybackState.PAUSED
PLAYING = PlaybackState.PLAYING
STOPPED = PlaybackState.STOPPED
class PlaybackOptionsHandlerTest(protocol.BaseTestCase):
def test_consume_off(self):
self.send_request('consume "0"')
assert not self.core.tracklist.get_consume().get()
self.assertInResponse("OK")
def test_consume_off_without_quotes(self):
self.send_request("consume 0")
assert not self.core.tracklist.get_consume().get()
self.assertInResponse("OK")
def test_consume_on(self):
self.send_request('consume "1"')
assert self.core.tracklist.get_consume().get()
self.assertInResponse("OK")
def test_consume_on_without_quotes(self):
self.send_request("consume 1")
assert self.core.tracklist.get_consume().get()
self.assertInResponse("OK")
def test_crossfade(self):
self.send_request('crossfade "10"')
self.assertInResponse("ACK [0@0] {crossfade} Not implemented")
def test_random_off(self):
self.send_request('random "0"')
assert not self.core.tracklist.get_random().get()
self.assertInResponse("OK")
def test_random_off_without_quotes(self):
self.send_request("random 0")
assert not self.core.tracklist.get_random().get()
self.assertInResponse("OK")
def test_random_on(self):
self.send_request('random "1"')
assert self.core.tracklist.get_random().get()
self.assertInResponse("OK")
def test_random_on_without_quotes(self):
self.send_request("random 1")
assert self.core.tracklist.get_random().get()
self.assertInResponse("OK")
def test_repeat_off(self):
self.send_request('repeat "0"')
assert not self.core.tracklist.get_repeat().get()
self.assertInResponse("OK")
def test_repeat_off_without_quotes(self):
self.send_request("repeat 0")
assert not self.core.tracklist.get_repeat().get()
self.assertInResponse("OK")
def test_repeat_on(self):
self.send_request('repeat "1"')
assert self.core.tracklist.get_repeat().get()
self.assertInResponse("OK")
def test_repeat_on_without_quotes(self):
self.send_request("repeat 1")
assert self.core.tracklist.get_repeat().get()
self.assertInResponse("OK")
def test_single_off(self):
self.send_request('single "0"')
assert not self.core.tracklist.get_single().get()
self.assertInResponse("OK")
def test_single_off_without_quotes(self):
self.send_request("single 0")
assert not self.core.tracklist.get_single().get()
self.assertInResponse("OK")
def test_single_on(self):
self.send_request('single "1"')
assert self.core.tracklist.get_single().get()
self.assertInResponse("OK")
def test_single_on_without_quotes(self):
self.send_request("single 1")
assert self.core.tracklist.get_single().get()
self.assertInResponse("OK")
def test_replay_gain_mode_off(self):
self.send_request('replay_gain_mode "off"')
self.assertInResponse("ACK [0@0] {replay_gain_mode} Not implemented")
def test_replay_gain_mode_track(self):
self.send_request('replay_gain_mode "track"')
self.assertInResponse("ACK [0@0] {replay_gain_mode} Not implemented")
def test_replay_gain_mode_album(self):
self.send_request('replay_gain_mode "album"')
self.assertInResponse("ACK [0@0] {replay_gain_mode} Not implemented")
def test_replay_gain_status_default(self):
self.send_request("replay_gain_status")
self.assertInResponse("OK")
self.assertInResponse("replay_gain_mode: off")
def test_mixrampdb(self):
self.send_request('mixrampdb "10"')
self.assertInResponse("ACK [0@0] {mixrampdb} Not implemented")
def test_mixrampdelay(self):
self.send_request('mixrampdelay "10"')
self.assertInResponse("ACK [0@0] {mixrampdelay} Not implemented")
@unittest.SkipTest
def test_replay_gain_status_off(self):
pass
@unittest.SkipTest
def test_replay_gain_status_track(self):
pass
@unittest.SkipTest
def test_replay_gain_status_album(self):
pass
class PlaybackControlHandlerTest(protocol.BaseTestCase):
def setUp(self): # noqa: N802
super().setUp()
self.tracks = [
Track(uri="dummy:a", length=40000),
Track(uri="dummy:b", length=40000),
]
self.backend.library.dummy_library = self.tracks
self.core.tracklist.add(uris=[t.uri for t in self.tracks]).get()
def test_next(self):
self.core.tracklist.clear().get()
self.send_request("next")
self.assertInResponse("OK")
def test_pause_off(self):
self.send_request('play "0"')
self.send_request('pause "1"')
self.send_request('pause "0"')
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_pause_on(self):
self.send_request('play "0"')
self.send_request('pause "1"')
assert PAUSED == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_pause_toggle(self):
self.send_request('play "0"')
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
# Deprecated version
self.send_request("pause")
assert PAUSED == self.core.playback.get_state().get()
self.assertInResponse("OK")
self.send_request("pause")
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_play_without_pos(self):
self.send_request("play")
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_play_with_pos(self):
self.send_request('play "0"')
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_play_with_pos_without_quotes(self):
self.send_request("play 0")
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_play_with_pos_out_of_bounds(self):
self.core.tracklist.clear().get()
self.send_request('play "0"')
assert STOPPED == self.core.playback.get_state().get()
self.assertInResponse("ACK [2@0] {play} Bad song index")
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
assert self.core.playback.get_current_track().get() is None
self.send_request('play "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert "dummy:a" == self.core.playback.get_current_track().get().uri
self.assertInResponse("OK")
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
assert self.core.playback.get_current_track().get() is None
self.core.playback.play()
self.core.playback.next()
self.core.playback.stop().get()
assert self.core.playback.get_current_track().get() is not None
self.send_request('play "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert "dummy:b" == self.core.playback.get_current_track().get().uri
self.assertInResponse("OK")
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
self.core.tracklist.clear()
self.send_request('play "-1"')
assert STOPPED == self.core.playback.get_state().get()
assert self.core.playback.get_current_track().get() is None
self.assertInResponse("OK")
def test_play_minus_is_ignored_if_playing(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
assert PLAYING == self.core.playback.get_state().get()
self.send_request('play "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_play_minus_one_resumes_if_paused(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
assert PLAYING == self.core.playback.get_state().get()
self.core.playback.pause()
assert PAUSED == self.core.playback.get_state().get()
self.send_request('play "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_playid(self):
self.send_request('playid "1"')
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_playid_without_quotes(self):
self.send_request("playid 1")
assert PLAYING == self.core.playback.get_state().get()
self.assertInResponse("OK")
def test_playid_minus_1_plays_first_in_playlist_if_no_current_track(self):
assert self.core.playback.get_current_track().get() is None
self.send_request('playid "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert "dummy:a" == self.core.playback.get_current_track().get().uri
self.assertInResponse("OK")
def test_playid_minus_1_plays_current_track_if_current_track_is_set(self):
assert self.core.playback.get_current_track().get() is None
self.core.playback.play().get()
self.core.playback.next().get()
self.core.playback.stop()
assert self.core.playback.get_current_track().get() is not None
self.send_request('playid "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert "dummy:b" == self.core.playback.get_current_track().get().uri
self.assertInResponse("OK")
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
self.core.tracklist.clear()
self.send_request('playid "-1"')
assert STOPPED == self.core.playback.get_state().get()
assert self.core.playback.get_current_track().get() is None
self.assertInResponse("OK")
def test_playid_minus_is_ignored_if_playing(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
assert PLAYING == self.core.playback.get_state().get()
self.send_request('playid "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_playid_minus_one_resumes_if_paused(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
assert PLAYING == self.core.playback.get_state().get()
self.core.playback.pause()
assert PAUSED == self.core.playback.get_state().get()
self.send_request('playid "-1"')
assert PLAYING == self.core.playback.get_state().get()
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_playid_which_does_not_exist(self):
self.send_request('playid "12345"')
self.assertInResponse("ACK [50@0] {playid} No such song")
def test_previous(self):
self.core.tracklist.clear().get()
self.send_request("previous")
self.assertInResponse("OK")
def test_seek_in_current_track(self):
self.core.playback.play()
self.send_request('seek "0" "30"')
current_track = self.core.playback.get_current_track().get()
assert current_track == self.tracks[0]
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_seek_in_another_track(self):
self.core.playback.play()
current_track = self.core.playback.get_current_track().get()
assert current_track != self.tracks[1]
self.send_request('seek "1" "30"')
current_track = self.core.playback.get_current_track().get()
assert current_track == self.tracks[1]
self.assertInResponse("OK")
def test_seek_without_quotes(self):
self.core.playback.play()
self.send_request("seek 0 30")
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_seek_with_float(self):
self.core.playback.play()
self.send_request('seek "0" "30.1"')
assert self.core.playback.get_time_position().get() >= 30100
self.assertInResponse("OK")
def test_seekid_in_current_track(self):
self.core.playback.play()
self.send_request('seekid "1" "30"')
current_track = self.core.playback.get_current_track().get()
assert current_track == self.tracks[0]
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_seekid_in_another_track(self):
self.core.playback.play()
self.send_request('seekid "2" "30"')
current_tl_track = self.core.playback.get_current_tl_track().get()
assert current_tl_track.tlid == 2
assert current_tl_track.track == self.tracks[1]
self.assertInResponse("OK")
def test_seekid_with_float(self):
self.core.playback.play()
self.send_request('seekid "1" "30.1"')
current_track = self.core.playback.get_current_track().get()
assert current_track == self.tracks[0]
assert self.core.playback.get_time_position().get() >= 30100
self.assertInResponse("OK")
def test_seekcur_absolute_value(self):
self.core.playback.play().get()
self.send_request('seekcur "30"')
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_seekcur_positive_diff(self):
self.core.playback.play().get()
self.core.playback.seek(10000)
assert self.core.playback.get_time_position().get() >= 10000
self.send_request('seekcur "+20"')
assert self.core.playback.get_time_position().get() >= 30000
self.assertInResponse("OK")
def test_seekcur_negative_diff(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
self.send_request('seekcur "-20"')
assert self.core.playback.get_time_position().get() <= 15000
self.assertInResponse("OK")
def test_seekcur_absolute_float(self):
self.core.playback.play().get()
self.send_request('seekcur "30.1"')
assert self.core.playback.get_time_position().get() >= 30100
self.assertInResponse("OK")
def test_seekcur_negative_float(self):
self.core.playback.play().get()
self.core.playback.seek(30000)
assert self.core.playback.get_time_position().get() >= 30000
self.send_request('seekcur "-20.1"')
assert self.core.playback.get_time_position().get() <= 10000
self.assertInResponse("OK")
def test_stop(self):
self.core.tracklist.clear().get()
self.send_request("stop")
assert STOPPED == self.core.playback.get_state().get()
self.assertInResponse("OK")
class VolumeTest(protocol.BaseTestCase):
def test_setvol_below_min(self):
self.send_request('setvol "-10"')
assert 0 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_min(self):
self.send_request('setvol "0"')
assert 0 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_middle(self):
self.send_request('setvol "50"')
assert 50 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_max(self):
self.send_request('setvol "100"')
assert 100 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_above_max(self):
self.send_request('setvol "110"')
assert 100 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_plus_is_ignored(self):
self.send_request('setvol "+10"')
assert 10 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_setvol_without_quotes(self):
self.send_request("setvol 50")
assert 50 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_volume_plus(self):
self.core.mixer.set_volume(50)
self.send_request("volume +20")
assert 70 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_volume_minus(self):
self.core.mixer.set_volume(50)
self.send_request("volume -20")
assert 30 == self.core.mixer.get_volume().get()
self.assertInResponse("OK")
def test_volume_less_than_minus_100(self):
self.core.mixer.set_volume(50)
self.send_request("volume -110")
assert 50 == self.core.mixer.get_volume().get()
self.assertInResponse("ACK [2@0] {volume} Invalid volume value")
def test_volume_more_than_plus_100(self):
self.core.mixer.set_volume(50)
self.send_request("volume +110")
assert 50 == self.core.mixer.get_volume().get()
self.assertInResponse("ACK [2@0] {volume} Invalid volume value")
class VolumeWithNoMixerTest(protocol.BaseTestCase):
enable_mixer = False
def test_setvol_without_mixer_fails(self):
self.send_request('setvol "100"')
self.assertInResponse("ACK [52@0] {setvol} problems setting volume")
def test_volume_without_mixer_failes(self):
self.send_request("volume +100")
self.assertInResponse("ACK [52@0] {volume} problems setting volume")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_reflection.py 0000644 0001751 0000171 00000007651 00000000000 021511 0 ustar 00runner docker from tests import protocol
class ReflectionHandlerTest(protocol.BaseTestCase):
def test_config_is_not_allowed_across_the_network(self):
self.send_request("config")
self.assertEqualResponse(
'ACK [4@0] {config} you don\'t have permission for "config"'
)
def test_commands_returns_list_of_all_commands(self):
self.send_request("commands")
# Check if some random commands are included
self.assertInResponse("command: commands")
self.assertInResponse("command: play")
self.assertInResponse("command: status")
# Check if commands you do not have access to are not present
self.assertNotInResponse("command: config")
self.assertNotInResponse("command: kill")
# Check if the blacklisted commands are not present
self.assertNotInResponse("command: command_list_begin")
self.assertNotInResponse("command: command_list_ok_begin")
self.assertNotInResponse("command: command_list_end")
self.assertInResponse("command: idle")
self.assertNotInResponse("command: noidle")
self.assertNotInResponse("command: sticker")
self.assertInResponse("OK")
def test_decoders(self):
self.send_request("decoders")
self.assertInResponse("OK")
def test_notcommands_returns_only_config_and_kill_and_ok(self):
response = self.send_request("notcommands")
assert 3 == len(response)
self.assertInResponse("command: config")
self.assertInResponse("command: kill")
self.assertInResponse("OK")
def test_tagtypes(self):
self.send_request("tagtypes")
self.assertInResponse("tagtype: Artist")
self.assertInResponse("tagtype: ArtistSort")
self.assertInResponse("tagtype: Album")
self.assertInResponse("tagtype: AlbumArtist")
self.assertInResponse("tagtype: AlbumArtistSort")
self.assertInResponse("tagtype: Title")
self.assertInResponse("tagtype: Track")
self.assertInResponse("tagtype: Name")
self.assertInResponse("tagtype: Genre")
self.assertInResponse("tagtype: Date")
self.assertInResponse("tagtype: Composer")
self.assertInResponse("tagtype: Performer")
self.assertInResponse("tagtype: Disc")
self.assertInResponse("tagtype: MUSICBRAINZ_ARTISTID")
self.assertInResponse("tagtype: MUSICBRAINZ_ALBUMID")
self.assertInResponse("tagtype: MUSICBRAINZ_ALBUMARTISTID")
self.assertInResponse("tagtype: MUSICBRAINZ_TRACKID")
self.assertInResponse("OK")
def test_urlhandlers(self):
self.send_request("urlhandlers")
self.assertInResponse("OK")
self.assertInResponse("handler: dummy")
class ReflectionWhenNotAuthedTest(protocol.BaseTestCase):
def get_config(self):
config = super().get_config()
config["mpd"]["password"] = "topsecret"
return config
def test_commands_show_less_if_auth_required_and_not_authed(self):
self.send_request("commands")
# Not requiring auth
self.assertInResponse("command: close")
self.assertInResponse("command: commands")
self.assertInResponse("command: notcommands")
self.assertInResponse("command: password")
self.assertInResponse("command: ping")
# Requiring auth
self.assertNotInResponse("command: play")
self.assertNotInResponse("command: status")
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
self.send_request("notcommands")
# Not requiring auth
self.assertNotInResponse("command: close")
self.assertNotInResponse("command: commands")
self.assertNotInResponse("command: notcommands")
self.assertNotInResponse("command: password")
self.assertNotInResponse("command: ping")
# Requiring auth
self.assertInResponse("command: play")
self.assertInResponse("command: status")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_regression.py 0000644 0001751 0000171 00000017647 00000000000 021545 0 ustar 00runner docker import random
from unittest import mock
from mopidy.models import Playlist, Ref, Track
from mopidy_mpd.protocol import stored_playlists
from tests import protocol
def mock_shuffle(foo):
foo[:] = [foo[1], foo[2], foo[5], foo[3], foo[4], foo[0]]
class IssueGH17RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/17
How to reproduce:
- Play a playlist where one track cannot be played
- Turn on random mode
- Press next until you get to the unplayable track
"""
@mock.patch.object(protocol.core.tracklist.random, "shuffle", mock_shuffle)
def test(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:error"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
Track(uri="dummy:f"),
]
self.audio.trigger_fake_playback_failure("dummy:error")
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks]).get()
# Playlist order: abcfde
self.send_request("play")
assert "dummy:a" == self.core.playback.get_current_track().get().uri
self.send_request('random "1"')
self.send_request("next")
assert "dummy:b" == self.core.playback.get_current_track().get().uri
self.send_request("next")
# Should now be at track 'c', but playback fails and it skips ahead
assert "dummy:f" == self.core.playback.get_current_track().get().uri
self.send_request("next")
assert "dummy:d" == self.core.playback.get_current_track().get().uri
self.send_request("next")
assert "dummy:e" == self.core.playback.get_current_track().get().uri
class IssueGH18RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/18
How to reproduce:
Play, random on, next, random off, next, next.
At this point it gives the same song over and over.
"""
def test(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
Track(uri="dummy:f"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks]).get()
random.seed(1)
self.send_request("play")
self.send_request('random "1"')
self.send_request("next")
self.send_request('random "0"')
self.send_request("next")
self.send_request("next")
tl_track_1 = self.core.playback.get_current_tl_track().get()
self.send_request("next")
tl_track_2 = self.core.playback.get_current_tl_track().get()
self.send_request("next")
tl_track_3 = self.core.playback.get_current_tl_track().get()
assert tl_track_1 != tl_track_2
assert tl_track_2 != tl_track_3
class IssueGH22RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/22
How to reproduce:
Play, random on, remove all tracks from the current playlist (as in
"delete" each one, not "clear").
Alternatively: Play, random on, remove a random track from the current
playlist, press next until it crashes.
"""
def test(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
Track(uri="dummy:f"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks]).get()
random.seed(1)
self.send_request("play")
self.send_request('random "1"')
self.send_request('deleteid "1"')
self.send_request('deleteid "2"')
self.send_request('deleteid "3"')
self.send_request('deleteid "4"')
self.send_request('deleteid "5"')
self.send_request('deleteid "6"')
self.send_request("status")
class IssueGH69RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/69
How to reproduce:
Play track, stop, clear current playlist, load a new playlist, status.
The status response now contains "song: None".
"""
def test(self):
self.core.playlists.create("foo")
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
Track(uri="dummy:f"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[t.uri for t in tracks]).get()
self.send_request("play")
self.send_request("stop")
self.send_request("clear")
self.send_request('load "foo"')
self.assertNotInResponse("song: None")
class IssueGH113RegressionTest(protocol.BaseTestCase):
r"""
The issue: https://github.com/mopidy/mopidy/issues/113
How to reproduce:
- Have a playlist with a name contining backslashes, like
"all lart spotify:track:\w\{22\} pastes".
- Try to load the playlist with the backslashes in the playlist name
escaped.
"""
def test(self):
self.core.playlists.create(r"all lart spotify:track:\w\{22\} pastes")
self.send_request('lsinfo "/"')
self.assertInResponse(
r"playlist: all lart spotify:track:\w\{22\} pastes"
)
self.send_request(
r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"'
)
self.assertInResponse("OK")
class IssueGH137RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/137
How to reproduce:
- Send "list" query with mismatching quotes
"""
def test(self):
self.send_request(
'list Date Artist "Anita Ward" '
'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"'
)
self.assertInResponse("ACK [2@0] {list} Invalid unquoted character")
class IssueGH1120RegressionTest(protocol.BaseTestCase):
"""
The issue: https://github.com/mopidy/mopidy/issues/1120
How to reproduce:
- A playlist must be in both browse results and playlists
- Call for instance ``lsinfo "/"`` to populate the cache with the
playlist name from the playlist backend.
- Call ``lsinfo "/dummy"`` to override the playlist name with the browse
name.
- Call ``lsinfo "/"`` and we now have an invalid name with ``/`` in it.
"""
@mock.patch.object(stored_playlists, "_get_last_modified")
def test(self, last_modified_mock):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.library.dummy_browse_result = {
"dummy:/": [Ref.playlist(name="Top 100 tracks", uri="dummy:/1")],
}
self.backend.playlists.set_dummy_playlists(
[Playlist(name="Top 100 tracks", uri="dummy:/1")]
)
response1 = self.send_request('lsinfo "/"')
self.send_request('lsinfo "/dummy"')
response2 = self.send_request('lsinfo "/"')
assert response1 == response2
class IssueGH1348RegressionTest(protocol.BaseTestCase):
"""
The issue: http://github.com/mopidy/mopidy/issues/1348
"""
def test(self):
self.backend.library.dummy_library = [Track(uri="dummy:a")]
# Create a dummy playlist and trigger population of mapping
self.send_request('playlistadd "testing1" "dummy:a"')
self.send_request("listplaylists")
# Create an other playlist which isn't in the map
self.send_request('playlistadd "testing2" "dummy:a"')
assert ["OK"] == self.send_request('rm "testing2"')
playlists = self.backend.playlists.as_list().get()
assert ["testing1"] == [ref.name for ref in playlists]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_status.py 0000644 0001751 0000171 00000003445 00000000000 020677 0 ustar 00runner docker from mopidy.models import Album, Track
from tests import protocol
class StatusHandlerTest(protocol.BaseTestCase):
def test_clearerror(self):
self.send_request("clearerror")
self.assertEqualResponse("ACK [0@0] {clearerror} Not implemented")
def test_currentsong(self):
track = Track(uri="dummy:/a")
self.backend.library.dummy_library = [track]
self.core.tracklist.add(uris=[track.uri]).get()
self.core.playback.play().get()
self.send_request("currentsong")
self.assertInResponse("file: dummy:/a")
self.assertInResponse("Time: 0")
self.assertNotInResponse("Artist: ")
self.assertNotInResponse("Title: ")
self.assertNotInResponse("Album: ")
self.assertNotInResponse("Track: 0")
self.assertNotInResponse("Date: ")
self.assertInResponse("Pos: 0")
self.assertInResponse("Id: 1")
self.assertInResponse("OK")
def test_currentsong_unicode(self):
track = Track(
uri="dummy:/à",
name="a nàme",
album=Album(uri="something:àlbum:12345"),
)
self.backend.library.dummy_library = [track]
self.core.tracklist.add(uris=[track.uri]).get()
self.core.playback.play().get()
self.send_request("currentsong")
self.assertInResponse("file: dummy:/à")
self.assertInResponse("Title: a nàme")
self.assertInResponse("X-AlbumUri: something:àlbum:12345")
def test_currentsong_without_song(self):
self.send_request("currentsong")
self.assertInResponse("OK")
def test_stats_command(self):
self.send_request("stats")
self.assertInResponse("OK")
def test_status_command(self):
self.send_request("status")
self.assertInResponse("OK")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_stickers.py 0000644 0001751 0000171 00000002351 00000000000 021176 0 ustar 00runner docker from tests import protocol
class StickersHandlerTest(protocol.BaseTestCase):
def test_sticker_get(self):
self.send_request('sticker get "song" "file:///dev/urandom" "a_name"')
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
def test_sticker_set(self):
self.send_request(
'sticker set "song" "file:///dev/urandom" "a_name" "a_value"'
)
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
def test_sticker_delete_with_name(self):
self.send_request(
'sticker delete "song" "file:///dev/urandom" "a_name"'
)
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
def test_sticker_delete_without_name(self):
self.send_request('sticker delete "song" "file:///dev/urandom"')
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
def test_sticker_list(self):
self.send_request('sticker list "song" "file:///dev/urandom"')
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
def test_sticker_find(self):
self.send_request('sticker find "song" "file:///dev/urandom" "a_name"')
self.assertEqualResponse("ACK [0@0] {sticker} Not implemented")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/protocol/test_stored_playlists.py 0000644 0001751 0000171 00000050034 00000000000 022754 0 ustar 00runner docker from unittest import mock
from mopidy.models import Playlist, Track
from mopidy_mpd.protocol import stored_playlists
from tests import protocol
class PlaylistsHandlerTest(protocol.BaseTestCase):
def test_listplaylist(self):
self.backend.playlists.set_dummy_playlists(
[
Playlist(
name="name", uri="dummy:name", tracks=[Track(uri="dummy:a")]
)
]
)
self.send_request('listplaylist "name"')
self.assertInResponse("file: dummy:a")
self.assertInResponse("OK")
def test_listplaylist_unicode(self):
self.backend.playlists.set_dummy_playlists(
[
Playlist(
name="nàmé", uri="dummy:nàmé", tracks=[Track(uri="dummy:à")]
)
]
)
self.send_request('listplaylist "nàmé"')
self.assertInResponse("file: dummy:à")
self.assertInResponse("OK")
def test_listplaylist_without_quotes(self):
self.backend.playlists.set_dummy_playlists(
[
Playlist(
name="name", uri="dummy:name", tracks=[Track(uri="dummy:a")]
)
]
)
self.send_request("listplaylist name")
self.assertInResponse("file: dummy:a")
self.assertInResponse("OK")
def test_listplaylist_fails_if_no_playlist_is_found(self):
self.send_request('listplaylist "name"')
self.assertEqualResponse("ACK [50@0] {listplaylist} No such playlist")
def test_listplaylist_duplicate(self):
playlist1 = Playlist(name="a", uri="dummy:a1", tracks=[Track(uri="b")])
playlist2 = Playlist(name="a", uri="dummy:a2", tracks=[Track(uri="c")])
self.backend.playlists.set_dummy_playlists([playlist1, playlist2])
self.send_request('listplaylist "a [2]"')
self.assertInResponse("file: c")
self.assertInResponse("OK")
def test_listplaylistinfo(self):
tracks = [
Track(uri="dummy:a", name="Track A", length=5000),
Track(uri="dummy:é", name="Track é", length=5000),
]
self.backend.library.dummy_library = tracks
self.backend.playlists.set_dummy_playlists(
[Playlist(name="name", uri="dummy:name", tracks=tracks)]
)
self.send_request('listplaylistinfo "name"')
self.assertInResponse("file: dummy:a")
self.assertInResponse("Title: Track A")
self.assertInResponse("Time: 5")
self.assertInResponse("file: dummy:é")
self.assertInResponse("Title: Track é")
self.assertNotInResponse("Track: 0")
self.assertNotInResponse("Pos: 0")
self.assertInResponse("OK")
def test_listplaylistinfo_without_quotes(self):
tracks = [
Track(uri="dummy:a"),
]
self.backend.library.dummy_library = tracks
self.backend.playlists.set_dummy_playlists(
[Playlist(name="name", uri="dummy:name", tracks=tracks)]
)
self.send_request("listplaylistinfo name")
self.assertInResponse("file: dummy:a")
self.assertNotInResponse("Track: 0")
self.assertNotInResponse("Pos: 0")
self.assertInResponse("OK")
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
self.send_request('listplaylistinfo "name"')
self.assertEqualResponse(
"ACK [50@0] {listplaylistinfo} No such playlist"
)
def test_listplaylistinfo_duplicate(self):
tracks = [
Track(uri="dummy:b"),
Track(uri="dummy:c"),
]
self.backend.library.dummy_library = tracks
playlist1 = Playlist(name="a", uri="dummy:a1", tracks=tracks[:1])
playlist2 = Playlist(name="a", uri="dummy:a2", tracks=tracks[1:])
self.backend.playlists.set_dummy_playlists([playlist1, playlist2])
self.send_request('listplaylistinfo "a [2]"')
self.assertInResponse("file: dummy:c")
self.assertNotInResponse("Track: 0")
self.assertNotInResponse("Pos: 0")
self.assertInResponse("OK")
@mock.patch.object(stored_playlists, "_get_last_modified")
def test_listplaylists(self, last_modified_mock):
last_modified_mock.return_value = "2015-08-05T22:51:06Z"
self.backend.playlists.set_dummy_playlists(
[
Playlist(name="a", uri="dummy:a"),
Playlist(name="é", uri="dummy:é"),
]
)
self.send_request("listplaylists")
self.assertInResponse("playlist: a")
self.assertInResponse("playlist: é")
# Date without milliseconds and with time zone information
self.assertInResponse("Last-Modified: 2015-08-05T22:51:06Z")
self.assertInResponse("OK")
def test_listplaylists_duplicate(self):
playlist1 = Playlist(name="a", uri="dummy:a1")
playlist2 = Playlist(name="a", uri="dummy:a2")
self.backend.playlists.set_dummy_playlists([playlist1, playlist2])
self.send_request("listplaylists")
self.assertInResponse("playlist: a")
self.assertInResponse("playlist: a [2]")
self.assertInResponse("OK")
def test_listplaylists_ignores_playlists_without_name(self):
last_modified = 1390942873222
self.backend.playlists.set_dummy_playlists(
[Playlist(name="", uri="dummy:", last_modified=last_modified)]
)
self.send_request("listplaylists")
self.assertNotInResponse("playlist: ")
self.assertInResponse("OK")
def test_listplaylists_replaces_newline_with_space(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a\n", uri="dummy:")]
)
self.send_request("listplaylists")
self.assertInResponse("playlist: a ")
self.assertNotInResponse("playlist: a\n")
self.assertInResponse("OK")
def test_listplaylists_replaces_carriage_return_with_space(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a\r", uri="dummy:")]
)
self.send_request("listplaylists")
self.assertInResponse("playlist: a ")
self.assertNotInResponse("playlist: a\r")
self.assertInResponse("OK")
def test_listplaylists_replaces_forward_slash_with_pipe(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="a/b", uri="dummy:")]
)
self.send_request("listplaylists")
self.assertInResponse("playlist: a|b")
self.assertNotInResponse("playlist: a/b")
self.assertInResponse("OK")
def test_load_appends_to_tracklist(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:ǫ"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=["dummy:a", "dummy:b"]).get()
assert len(self.core.tracklist.get_tracks().get()) == 2
self.backend.playlists.set_dummy_playlists(
[Playlist(name="A-list", uri="dummy:A-list", tracks=tracks[2:])]
)
self.send_request('load "A-list"')
tracks = self.core.tracklist.get_tracks().get()
assert 5 == len(tracks)
assert "dummy:a" == tracks[0].uri
assert "dummy:b" == tracks[1].uri
assert "dummy:c" == tracks[2].uri
assert "dummy:d" == tracks[3].uri
assert "dummy:ǫ" == tracks[4].uri
self.assertInResponse("OK")
def test_load_with_range_loads_part_of_playlist(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=["dummy:a", "dummy:b"]).get()
assert len(self.core.tracklist.get_tracks().get()) == 2
self.backend.playlists.set_dummy_playlists(
[Playlist(name="A-list", uri="dummy:A-list", tracks=tracks[2:])]
)
self.send_request('load "A-list" "1:2"')
tracks = self.core.tracklist.get_tracks().get()
assert 3 == len(tracks)
assert "dummy:a" == tracks[0].uri
assert "dummy:b" == tracks[1].uri
assert "dummy:d" == tracks[2].uri
self.assertInResponse("OK")
def test_load_with_range_without_end_loads_rest_of_playlist(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
Track(uri="dummy:d"),
Track(uri="dummy:e"),
]
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=["dummy:a", "dummy:b"]).get()
assert len(self.core.tracklist.get_tracks().get()) == 2
self.backend.playlists.set_dummy_playlists(
[Playlist(name="A-list", uri="dummy:A-list", tracks=tracks[2:])]
)
self.send_request('load "A-list" "1:"')
tracks = self.core.tracklist.get_tracks().get()
assert 4 == len(tracks)
assert "dummy:a" == tracks[0].uri
assert "dummy:b" == tracks[1].uri
assert "dummy:d" == tracks[2].uri
assert "dummy:e" == tracks[3].uri
self.assertInResponse("OK")
def test_load_unknown_playlist_acks(self):
self.send_request('load "unknown playlist"')
assert 0 == len(self.core.tracklist.get_tracks().get())
self.assertEqualResponse("ACK [50@0] {load} No such playlist")
# No invalid name check for load.
self.send_request('load "unknown/playlist"')
self.assertEqualResponse("ACK [50@0] {load} No such playlist")
def test_load_full_track_metadata(self):
tracks = [
Track(uri="dummy:a", name="Track A", length=5000),
]
self.backend.library.dummy_library = tracks
self.backend.playlists.set_dummy_playlists(
[
Playlist(
name="A-list", uri="dummy:a1", tracks=[Track(uri="dummy:a")]
)
]
)
self.send_request('load "A-list"')
tracks = self.core.tracklist.get_tracks().get()
assert len(tracks) == 1
assert tracks[0].uri == "dummy:a"
assert tracks[0].name == "Track A"
assert tracks[0].length == 5000
self.assertInResponse("OK")
def test_playlistadd(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
]
self.backend.library.dummy_library = tracks
self.backend.playlists.set_dummy_playlists(
[Playlist(name="name", uri="dummy:a1", tracks=[tracks[0]])]
)
self.send_request('playlistadd "name" "dummy:b"')
self.assertInResponse("OK")
assert 2 == len(self.backend.playlists.get_items("dummy:a1").get())
def test_playlistadd_creates_playlist(self):
tracks = [
Track(uri="dummy:é"),
]
self.backend.library.dummy_library = tracks
self.send_request('playlistadd "namé" "dummy:é"')
self.assertInResponse("OK")
assert self.backend.playlists.lookup("dummy:namé").get() is not None
def test_playlistadd_invalid_name_acks(self):
self.send_request('playlistadd "foo/bar" "dummy:a"')
self.assertInResponse(
"ACK [2@0] {playlistadd} playlist name is "
"invalid: playlist names may not contain "
"slashes, newlines or carriage returns"
)
def test_playlistclear(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="namé", uri="dummy:a1", tracks=[Track(uri="b")])]
)
self.send_request('playlistclear "namé"')
self.assertInResponse("OK")
assert 0 == len(self.backend.playlists.get_items("dummy:a1").get())
def test_playlistclear_creates_playlist(self):
self.send_request('playlistclear "name"')
self.assertInResponse("OK")
assert self.backend.playlists.lookup("dummy:name").get() is not None
def test_playlistclear_creates_playlist_save_fails(self):
self.backend.playlists.set_allow_save(False)
self.send_request('playlistclear "name"')
self.assertInResponse(
"ACK [0@0] {playlistclear} Backend with "
'scheme "dummy" failed to save playlist'
)
def test_playlistclear_invalid_name_acks(self):
self.send_request('playlistclear "foo/bar"')
self.assertInResponse(
"ACK [2@0] {playlistclear} playlist name is "
"invalid: playlist names may not contain "
"slashes, newlines or carriage returns"
)
def test_playlistdelete(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
] # len() == 3
self.backend.playlists.set_dummy_playlists(
[Playlist(name="namé", uri="dummy:a1", tracks=tracks)]
)
self.send_request('playlistdelete "namé" "2"')
self.assertInResponse("OK")
assert 2 == len(self.backend.playlists.get_items("dummy:a1").get())
def test_playlistdelete_save_fails(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"),
] # len() == 3
self.backend.playlists.set_dummy_playlists(
[Playlist(name="name", uri="dummy:a1", tracks=tracks)]
)
self.backend.playlists.set_allow_save(False)
self.send_request('playlistdelete "name" "2"')
self.assertInResponse(
"ACK [0@0] {playlistdelete} Backend with "
'scheme "dummy" failed to save playlist'
)
def test_playlistdelete_invalid_name_acks(self):
self.send_request('playlistdelete "foo/bar" "0"')
self.assertInResponse(
"ACK [2@0] {playlistdelete} playlist name is "
"invalid: playlist names may not contain "
"slashes, newlines or carriage returns"
)
def test_playlistdelete_unknown_playlist_acks(self):
self.send_request('playlistdelete "foobar" "0"')
self.assertInResponse("ACK [50@0] {playlistdelete} No such playlist")
def test_playlistdelete_unknown_index_acks(self):
self.send_request('save "foobar"')
self.send_request('playlistdelete "foobar" "0"')
self.assertInResponse("ACK [2@0] {playlistdelete} Bad song index")
def test_playlistmove(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"), # this one is getting moved to top
]
self.backend.playlists.set_dummy_playlists(
[Playlist(name="namé", uri="dummy:a1", tracks=tracks)]
)
self.send_request('playlistmove "namé" "2" "0"')
self.assertInResponse("OK")
assert (
"dummy:c"
== self.backend.playlists.get_items("dummy:a1").get()[0].uri
)
def test_playlistmove_save_fails(self):
tracks = [
Track(uri="dummy:a"),
Track(uri="dummy:b"),
Track(uri="dummy:c"), # this one is getting moved to top
]
self.backend.playlists.set_dummy_playlists(
[Playlist(name="name", uri="dummy:a1", tracks=tracks)]
)
self.backend.playlists.set_allow_save(False)
self.send_request('playlistmove "name" "2" "0"')
self.assertInResponse(
"ACK [0@0] {playlistmove} Backend with "
'scheme "dummy" failed to save playlist'
)
def test_playlistmove_invalid_name_acks(self):
self.send_request('playlistmove "foo/bar" "0" "1"')
self.assertInResponse(
"ACK [2@0] {playlistmove} playlist name is "
"invalid: playlist names may not contain "
"slashes, newlines or carriage returns"
)
def test_playlistmove_unknown_playlist_acks(self):
self.send_request('playlistmove "foobar" "0" "1"')
self.assertInResponse("ACK [50@0] {playlistmove} No such playlist")
def test_playlistmove_unknown_position_acks(self):
self.send_request('save "foobar"')
self.send_request('playlistmove "foobar" "0" "1"')
self.assertInResponse("ACK [2@0] {playlistmove} Bad song index")
def test_playlistmove_same_index_shortcircuits_everything(self):
# Bad indexes on unknown playlist:
self.send_request('playlistmove "foobar" "0" "0"')
self.assertInResponse("OK")
self.send_request('playlistmove "foobar" "100000" "100000"')
self.assertInResponse("OK")
# Bad indexes on known playlist:
self.send_request('save "foobar"')
self.send_request('playlistmove "foobar" "0" "0"')
self.assertInResponse("OK")
self.send_request('playlistmove "foobar" "10" "10"')
self.assertInResponse("OK")
# Invalid playlist name:
self.send_request('playlistmove "foo/bar" "0" "0"')
self.assertInResponse("OK")
def test_rename(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="old_name", uri="dummy:a1", tracks=[Track(uri="b")])]
)
self.send_request('rename "old_name" "new_namé"')
self.assertInResponse("OK")
assert self.backend.playlists.lookup("dummy:new_namé").get() is not None
def test_rename_save_fails(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="old_name", uri="dummy:a1", tracks=[Track(uri="b")])]
)
self.backend.playlists.set_allow_save(False)
self.send_request('rename "old_name" "new_name"')
self.assertInResponse(
"ACK [0@0] {rename} Backend with "
'scheme "dummy" failed to save playlist'
)
def test_rename_unknown_playlist_acks(self):
self.send_request('rename "foo" "bar"')
self.assertInResponse("ACK [50@0] {rename} No such playlist")
def test_rename_to_existing_acks(self):
self.send_request('save "foo"')
self.send_request('save "bar"')
self.send_request('rename "foo" "bar"')
self.assertInResponse("ACK [56@0] {rename} Playlist already exists")
def test_rename_invalid_name_acks(self):
expected = (
"ACK [2@0] {rename} playlist name is invalid: playlist "
"names may not contain slashes, newlines or carriage "
"returns"
)
self.send_request('rename "foo/bar" "bar"')
self.assertInResponse(expected)
self.send_request('rename "foo" "foo/bar"')
self.assertInResponse(expected)
self.send_request('rename "bar/foo" "foo/bar"')
self.assertInResponse(expected)
def test_rm(self):
self.backend.playlists.set_dummy_playlists(
[Playlist(name="namé", uri="dummy:à1", tracks=[Track(uri="b")])]
)
self.send_request('rm "namé"')
self.assertInResponse("OK")
assert self.backend.playlists.lookup("dummy:à1").get() is None
def test_rm_unknown_playlist_acks(self):
self.send_request('rm "name"')
self.assertInResponse("ACK [50@0] {rm} No such playlist")
def test_rm_invalid_name_acks(self):
self.send_request('rm "foo/bar"')
self.assertInResponse(
"ACK [2@0] {rm} playlist name is invalid: "
"playlist names may not contain slashes, "
"newlines or carriage returns"
)
def test_save(self):
self.send_request('save "namé"')
self.assertInResponse("OK")
assert self.backend.playlists.lookup("dummy:namé").get() is not None
def test_save_fails(self):
self.backend.playlists.set_allow_save(False)
self.send_request('save "name"')
self.assertInResponse(
"ACK [0@0] {save} Backend with "
'scheme "dummy" failed to save playlist'
)
def test_save_invalid_name_acks(self):
self.send_request('save "foo/bar"')
self.assertInResponse(
"ACK [2@0] {save} playlist name is invalid: "
"playlist names may not contain slashes, "
"newlines or carriage returns"
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_actor.py 0000644 0001751 0000171 00000003222 00000000000 016614 0 ustar 00runner docker from unittest import mock
import pytest
from mopidy_mpd import actor
# NOTE: Should be kept in sync with all events from mopidy.core.listener
@pytest.mark.parametrize(
"event,expected",
[
(["track_playback_paused", "tl_track", "time_position"], None),
(["track_playback_resumed", "tl_track", "time_position"], None),
(["track_playback_started", "tl_track"], None),
(["track_playback_ended", "tl_track", "time_position"], None),
(["playback_state_changed", "old_state", "new_state"], "player"),
(["tracklist_changed"], "playlist"),
(["playlists_loaded"], "stored_playlist"),
(["playlist_changed", "playlist"], "stored_playlist"),
(["playlist_deleted", "uri"], "stored_playlist"),
(["options_changed"], "options"),
(["volume_changed", "volume"], "mixer"),
(["mute_changed", "mute"], "output"),
(["seeked", "time_position"], "player"),
(["stream_title_changed", "title"], "playlist"),
],
)
def test_idle_hooked_up_correctly(event, expected):
config = {
"mpd": {
"hostname": "foobar",
"port": 1234,
"zeroconf": None,
"max_connections": None,
"connection_timeout": None,
}
}
with mock.patch.object(actor.MpdFrontend, "_setup_server"):
frontend = actor.MpdFrontend(core=mock.Mock(), config=config)
with mock.patch("mopidy.listener.send") as send_mock:
frontend.on_event(event[0], **{e: None for e in event[1:]})
if expected is None:
assert not send_mock.call_args
else:
send_mock.assert_called_once_with(mock.ANY, expected)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_commands.py 0000644 0001751 0000171 00000023365 00000000000 017317 0 ustar 00runner docker import unittest
from mopidy_mpd import exceptions, protocol
class TestConverts(unittest.TestCase):
def test_integer(self):
assert 123 == protocol.INT("123")
assert (-123) == protocol.INT("-123")
assert 123 == protocol.INT("+123")
self.assertRaises(ValueError, protocol.INT, "3.14")
self.assertRaises(ValueError, protocol.INT, "")
self.assertRaises(ValueError, protocol.INT, "abc")
self.assertRaises(ValueError, protocol.INT, "12 34")
def test_unsigned_integer(self):
assert 123 == protocol.UINT("123")
self.assertRaises(ValueError, protocol.UINT, "-123")
self.assertRaises(ValueError, protocol.UINT, "+123")
self.assertRaises(ValueError, protocol.UINT, "3.14")
self.assertRaises(ValueError, protocol.UINT, "")
self.assertRaises(ValueError, protocol.UINT, "abc")
self.assertRaises(ValueError, protocol.UINT, "12 34")
def test_boolean(self):
assert protocol.BOOL("1") is True
assert protocol.BOOL("0") is False
self.assertRaises(ValueError, protocol.BOOL, "3.14")
self.assertRaises(ValueError, protocol.BOOL, "")
self.assertRaises(ValueError, protocol.BOOL, "true")
self.assertRaises(ValueError, protocol.BOOL, "false")
self.assertRaises(ValueError, protocol.BOOL, "abc")
self.assertRaises(ValueError, protocol.BOOL, "12 34")
def test_range(self):
assert slice(1, 2) == protocol.RANGE("1")
assert slice(0, 1) == protocol.RANGE("0")
assert slice(0, None) == protocol.RANGE("0:")
assert slice(1, 3) == protocol.RANGE("1:3")
self.assertRaises(ValueError, protocol.RANGE, "3.14")
self.assertRaises(ValueError, protocol.RANGE, "1:abc")
self.assertRaises(ValueError, protocol.RANGE, "abc:1")
self.assertRaises(ValueError, protocol.RANGE, "2:1")
self.assertRaises(ValueError, protocol.RANGE, "-1:2")
self.assertRaises(ValueError, protocol.RANGE, "1 : 2")
self.assertRaises(ValueError, protocol.RANGE, "")
self.assertRaises(ValueError, protocol.RANGE, "true")
self.assertRaises(ValueError, protocol.RANGE, "false")
self.assertRaises(ValueError, protocol.RANGE, "abc")
self.assertRaises(ValueError, protocol.RANGE, "12 34")
class TestCommands(unittest.TestCase):
def setUp(self): # noqa: N802
self.commands = protocol.Commands()
def test_add_as_a_decorator(self):
@self.commands.add("test")
def test(context):
pass
def test_register_second_command_to_same_name_fails(self):
def func(context):
pass
self.commands.add("foo")(func)
with self.assertRaises(Exception):
self.commands.add("foo")(func)
def test_function_only_takes_context_succeeds(self):
sentinel = object()
self.commands.add("bar")(lambda context: sentinel)
assert sentinel == self.commands.call(["bar"])
def test_function_has_required_arg_succeeds(self):
sentinel = object()
self.commands.add("bar")(lambda context, required: sentinel)
assert sentinel == self.commands.call(["bar", "arg"])
def test_function_has_optional_args_succeeds(self):
sentinel = object()
self.commands.add("bar")(lambda context, optional=None: sentinel)
assert sentinel == self.commands.call(["bar"])
assert sentinel == self.commands.call(["bar", "arg"])
def test_function_has_required_and_optional_args_succeeds(self):
sentinel = object()
def func(context, required, optional=None):
return sentinel
self.commands.add("bar")(func)
assert sentinel == self.commands.call(["bar", "arg"])
assert sentinel == self.commands.call(["bar", "arg", "arg"])
def test_function_has_varargs_succeeds(self):
sentinel, args = object(), []
self.commands.add("bar")(lambda context, *args: sentinel)
for _ in range(10):
assert sentinel == self.commands.call((["bar"] + args))
args.append("test")
def test_function_has_only_varags_succeeds(self):
sentinel = object()
self.commands.add("baz")(lambda *args: sentinel)
assert sentinel == self.commands.call(["baz"])
def test_function_has_no_arguments_fails(self):
with self.assertRaises(TypeError):
self.commands.add("test")(lambda: True)
def test_function_has_required_and_varargs_fails(self):
with self.assertRaises(TypeError):
def func(context, required, *args):
pass
self.commands.add("test")(func)
def test_function_has_optional_and_varargs_fails(self):
with self.assertRaises(TypeError):
def func(context, optional=None, *args):
pass
self.commands.add("test")(func)
def test_function_hash_keywordargs_fails(self):
with self.assertRaises(TypeError):
self.commands.add("test")(lambda context, **kwargs: True)
def test_call_chooses_correct_handler(self):
sentinel1, sentinel2, sentinel3 = object(), object(), object()
self.commands.add("foo")(lambda context: sentinel1)
self.commands.add("bar")(lambda context: sentinel2)
self.commands.add("baz")(lambda context: sentinel3)
assert sentinel1 == self.commands.call(["foo"])
assert sentinel2 == self.commands.call(["bar"])
assert sentinel3 == self.commands.call(["baz"])
def test_call_with_nonexistent_handler(self):
with self.assertRaises(exceptions.MpdUnknownCommand):
self.commands.call(["bar"])
def test_call_passes_context(self):
sentinel = object()
self.commands.add("foo")(lambda context: context)
assert sentinel == self.commands.call(["foo"], context=sentinel)
def test_call_without_args_fails(self):
with self.assertRaises(exceptions.MpdNoCommand):
self.commands.call([])
def test_call_passes_required_argument(self):
self.commands.add("foo")(lambda context, required: required)
assert "test123" == self.commands.call(["foo", "test123"])
def test_call_passes_optional_argument(self):
sentinel = object()
self.commands.add("foo")(lambda context, optional=sentinel: optional)
assert sentinel == self.commands.call(["foo"])
assert "test" == self.commands.call(["foo", "test"])
def test_call_passes_required_and_optional_argument(self):
def func(context, required, optional=None):
return (required, optional)
self.commands.add("foo")(func)
assert ("arg", None) == self.commands.call(["foo", "arg"])
assert ("arg", "kwarg") == self.commands.call(["foo", "arg", "kwarg"])
def test_call_passes_varargs(self):
self.commands.add("foo")(lambda context, *args: args)
def test_call_incorrect_args(self):
self.commands.add("foo")(lambda context: context)
with self.assertRaises(exceptions.MpdArgError):
self.commands.call(["foo", "bar"])
self.commands.add("bar")(lambda context, required: context)
with self.assertRaises(exceptions.MpdArgError):
self.commands.call(["bar", "bar", "baz"])
self.commands.add("baz")(lambda context, optional=None: context)
with self.assertRaises(exceptions.MpdArgError):
self.commands.call(["baz", "bar", "baz"])
def test_validator_gets_applied_to_required_arg(self):
sentinel = object()
def func(context, required):
return required
self.commands.add("test", required=lambda v: sentinel)(func)
assert sentinel == self.commands.call(["test", "foo"])
def test_validator_gets_applied_to_optional_arg(self):
sentinel = object()
def func(context, optional=None):
return optional
self.commands.add("foo", optional=lambda v: sentinel)(func)
assert sentinel == self.commands.call(["foo", "123"])
def test_validator_skips_optional_default(self):
sentinel = object()
def func(context, optional=sentinel):
return optional
self.commands.add("foo", optional=lambda v: None)(func)
assert sentinel == self.commands.call(["foo"])
def test_validator_applied_to_non_existent_arg_fails(self):
self.commands.add("foo")(lambda context, arg: arg)
with self.assertRaises(TypeError):
def func(context, wrong_arg):
return wrong_arg
self.commands.add("bar", arg=lambda v: v)(func)
def test_validator_called_context_fails(self):
return # TODO: how to handle this
with self.assertRaises(TypeError):
def func(context):
pass
self.commands.add("bar", context=lambda v: v)(func)
def test_validator_value_error_is_converted(self):
def validdate(value):
raise ValueError
def func(context, arg):
pass
self.commands.add("bar", arg=validdate)(func)
with self.assertRaises(exceptions.MpdArgError):
self.commands.call(["bar", "test"])
def test_auth_required_gets_stored(self):
def func1(context):
pass
def func2(context):
pass
self.commands.add("foo")(func1)
self.commands.add("bar", auth_required=False)(func2)
assert self.commands.handlers["foo"].auth_required
assert not self.commands.handlers["bar"].auth_required
def test_list_command_gets_stored(self):
def func1(context):
pass
def func2(context):
pass
self.commands.add("foo")(func1)
self.commands.add("bar", list_command=False)(func2)
assert self.commands.handlers["foo"].list_command
assert not self.commands.handlers["bar"].list_command
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_dispatcher.py 0000644 0001751 0000171 00000006454 00000000000 017644 0 ustar 00runner docker import unittest
import pykka
import pytest
from mopidy import core
from mopidy.models import Ref
from mopidy_mpd.dispatcher import MpdContext, MpdDispatcher
from mopidy_mpd.exceptions import MpdAckError
from mopidy_mpd.uri_mapper import MpdUriMapper
from tests import dummy_backend
class MpdDispatcherTest(unittest.TestCase):
def setUp(self): # noqa: N802
config = {"mpd": {"password": None, "command_blacklist": ["disabled"]}}
self.backend = dummy_backend.create_proxy()
self.dispatcher = MpdDispatcher(config=config)
self.core = core.Core.start(backends=[self.backend]).proxy()
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
def test_call_handler_for_unknown_command_raises_exception(self):
with self.assertRaises(MpdAckError) as cm:
self.dispatcher._call_handler("an_unknown_command with args")
assert (
cm.exception.get_mpd_ack()
== 'ACK [5@0] {} unknown command "an_unknown_command"'
)
def test_handling_unknown_request_yields_error(self):
result = self.dispatcher.handle_request("an unhandled request")
assert result[0] == 'ACK [5@0] {} unknown command "an"'
def test_handling_blacklisted_command(self):
result = self.dispatcher.handle_request("disabled")
assert (
result[0]
== 'ACK [0@0] {disabled} "disabled" has been disabled in the server'
)
@pytest.fixture
def a_track():
return Ref.track(uri="dummy:/a", name="a")
@pytest.fixture
def b_track():
return Ref.track(uri="dummy:/foo/b", name="b")
@pytest.fixture
def backend_to_browse(a_track, b_track):
backend = dummy_backend.create_proxy()
backend.library.dummy_browse_result = {
"dummy:/": [a_track, Ref.directory(uri="dummy:/foo", name="foo")],
"dummy:/foo": [b_track],
}
return backend
@pytest.fixture
def mpd_context(backend_to_browse):
mopidy_core = core.Core.start(backends=[backend_to_browse]).proxy()
uri_map = MpdUriMapper(mopidy_core)
return MpdContext(None, core=mopidy_core, uri_map=uri_map)
class TestMpdContext:
@classmethod
def teardown_class(cls):
pykka.ActorRegistry.stop_all()
def test_browse_root(self, mpd_context, a_track):
results = mpd_context.browse("dummy", recursive=False, lookup=False)
assert [("/dummy/a", a_track), ("/dummy/foo", None)] == list(results)
def test_browse_root_recursive(self, mpd_context, a_track, b_track):
results = mpd_context.browse("dummy", recursive=True, lookup=False)
assert [
("/dummy", None),
("/dummy/a", a_track),
("/dummy/foo", None),
("/dummy/foo/b", b_track),
] == list(results)
@pytest.mark.parametrize(
"bad_ref",
[
Ref.track(uri="dummy:/x"),
Ref.track(name="x"),
Ref.directory(uri="dummy:/y"),
Ref.directory(name="y"),
],
)
def test_browse_skips_bad_refs(
self, backend_to_browse, a_track, bad_ref, mpd_context
):
backend_to_browse.library.dummy_browse_result = {
"dummy:/": [bad_ref, a_track],
}
results = mpd_context.browse("dummy", recursive=False, lookup=False)
assert [("/dummy/a", a_track)] == list(results)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_exceptions.py 0000644 0001751 0000171 00000003703 00000000000 017671 0 ustar 00runner docker import unittest
from mopidy_mpd.exceptions import (
MpdAckError,
MpdNoCommand,
MpdNoExistError,
MpdNotImplemented,
MpdPermissionError,
MpdSystemError,
MpdUnknownCommand,
)
class MpdExceptionsTest(unittest.TestCase):
def test_mpd_not_implemented_is_a_mpd_ack_error(self):
try:
raise MpdNotImplemented
except MpdAckError as exc:
assert (
exc.message # noqa: B306: Our own exception
== "Not implemented"
)
def test_get_mpd_ack_with_default_values(self):
e = MpdAckError("A description")
assert e.get_mpd_ack() == "ACK [0@0] {None} A description"
def test_get_mpd_ack_with_values(self):
try:
raise MpdAckError("A description", index=7, command="foo")
except MpdAckError as e:
assert e.get_mpd_ack() == "ACK [0@7] {foo} A description"
def test_mpd_unknown_command(self):
try:
raise MpdUnknownCommand(command="play")
except MpdAckError as e:
assert e.get_mpd_ack() == 'ACK [5@0] {} unknown command "play"'
def test_mpd_no_command(self):
try:
raise MpdNoCommand
except MpdAckError as e:
assert e.get_mpd_ack() == "ACK [5@0] {} No command given"
def test_mpd_system_error(self):
try:
raise MpdSystemError("foo")
except MpdSystemError as e:
assert e.get_mpd_ack() == "ACK [52@0] {None} foo"
def test_mpd_permission_error(self):
try:
raise MpdPermissionError(command="foo")
except MpdPermissionError as e:
assert (
e.get_mpd_ack()
== 'ACK [4@0] {foo} you don\'t have permission for "foo"'
)
def test_mpd_noexist_error(self):
try:
raise MpdNoExistError(command="foo")
except MpdNoExistError as e:
assert e.get_mpd_ack() == "ACK [50@0] {foo} "
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_extension.py 0000644 0001751 0000171 00000001105 00000000000 017516 0 ustar 00runner docker from mopidy_mpd import Extension
def test_get_default_config():
ext = Extension()
config = ext.get_default_config()
assert "[mpd]" in config
assert "enabled = true" in config
def test_get_config_schema():
ext = Extension()
schema = ext.get_config_schema()
assert "hostname" in schema
assert "port" in schema
assert "password" in schema
assert "max_connections" in schema
assert "connection_timeout" in schema
assert "zeroconf" in schema
assert "command_blacklist" in schema
assert "default_playlist_scheme" in schema
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_path_utils.py 0000644 0001751 0000171 00000000772 00000000000 017667 0 ustar 00runner docker import os
import unittest
from tests import path_utils
# TODO: kill this in favour of just os.path.getmtime + mocks
class MtimeTest(unittest.TestCase):
def tearDown(self): # noqa: N802
path_utils.mtime.undo_fake()
def test_mtime_of_current_dir(self):
mtime_dir = int(os.stat(".").st_mtime)
assert mtime_dir == path_utils.mtime(".")
def test_fake_time_is_returned(self):
path_utils.mtime.set_fake_time(123456)
assert path_utils.mtime(".") == 123456
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_session.py 0000644 0001751 0000171 00000001505 00000000000 017171 0 ustar 00runner docker import logging
from unittest.mock import Mock, sentinel
from mopidy_mpd import dispatcher, network, session
def test_on_start_logged(caplog):
caplog.set_level(logging.INFO)
connection = Mock(spec=network.Connection)
session.MpdSession(connection).on_start()
assert f"New MPD connection from {connection}" in caplog.text
def test_on_line_received_logged(caplog):
caplog.set_level(logging.DEBUG)
connection = Mock(spec=network.Connection)
mpd_session = session.MpdSession(connection)
mpd_session.dispatcher = Mock(spec=dispatcher.MpdDispatcher)
mpd_session.dispatcher.handle_request.return_value = [str(sentinel.resp)]
mpd_session.on_line_received(sentinel.line)
assert f"Request from {connection}: {sentinel.line}" in caplog.text
assert f"Response to {connection}:" in caplog.text
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_status.py 0000644 0001751 0000171 00000017755 00000000000 017047 0 ustar 00runner docker import unittest
import pykka
from mopidy import core
from mopidy.core import PlaybackState
from mopidy.models import Track
from mopidy_mpd import dispatcher
from mopidy_mpd.protocol import status
from tests import dummy_audio, dummy_backend, dummy_mixer
PAUSED = PlaybackState.PAUSED
PLAYING = PlaybackState.PLAYING
STOPPED = PlaybackState.STOPPED
# FIXME migrate to using protocol.BaseTestCase instead of status.stats
# directly?
class StatusHandlerTest(unittest.TestCase):
def setUp(self): # noqa: N802
config = {"core": {"max_tracklist_length": 10000}}
self.audio = dummy_audio.create_proxy()
self.mixer = dummy_mixer.create_proxy()
self.backend = dummy_backend.create_proxy(audio=self.audio)
self.core = core.Core.start(
config,
audio=self.audio,
mixer=self.mixer,
backends=[self.backend],
).proxy()
self.dispatcher = dispatcher.MpdDispatcher(core=self.core)
self.context = self.dispatcher.context
def tearDown(self): # noqa: N802
pykka.ActorRegistry.stop_all()
def set_tracklist(self, tracks):
self.backend.library.dummy_library = tracks
self.core.tracklist.add(uris=[track.uri for track in tracks]).get()
def test_stats_method(self):
result = status.stats(self.context)
assert "artists" in result
assert int(result["artists"]) >= 0
assert "albums" in result
assert int(result["albums"]) >= 0
assert "songs" in result
assert int(result["songs"]) >= 0
assert "uptime" in result
assert int(result["uptime"]) >= 0
assert "db_playtime" in result
assert int(result["db_playtime"]) >= 0
assert "db_update" in result
assert int(result["db_update"]) >= 0
assert "playtime" in result
assert int(result["playtime"]) >= 0
def test_status_method_contains_volume_with_na_value(self):
result = dict(status.status(self.context))
assert "volume" in result
assert int(result["volume"]) == (-1)
def test_status_method_contains_volume(self):
self.core.mixer.set_volume(17)
result = dict(status.status(self.context))
assert "volume" in result
assert int(result["volume"]) == 17
def test_status_method_contains_repeat_is_0(self):
result = dict(status.status(self.context))
assert "repeat" in result
assert int(result["repeat"]) == 0
def test_status_method_contains_repeat_is_1(self):
self.core.tracklist.set_repeat(True)
result = dict(status.status(self.context))
assert "repeat" in result
assert int(result["repeat"]) == 1
def test_status_method_contains_random_is_0(self):
result = dict(status.status(self.context))
assert "random" in result
assert int(result["random"]) == 0
def test_status_method_contains_random_is_1(self):
self.core.tracklist.set_random(True)
result = dict(status.status(self.context))
assert "random" in result
assert int(result["random"]) == 1
def test_status_method_contains_single(self):
result = dict(status.status(self.context))
assert "single" in result
assert int(result["single"]) in (0, 1)
def test_status_method_contains_consume_is_0(self):
result = dict(status.status(self.context))
assert "consume" in result
assert int(result["consume"]) == 0
def test_status_method_contains_consume_is_1(self):
self.core.tracklist.set_consume(True)
result = dict(status.status(self.context))
assert "consume" in result
assert int(result["consume"]) == 1
def test_status_method_contains_playlist(self):
result = dict(status.status(self.context))
assert "playlist" in result
assert int(result["playlist"]) >= 0
assert int(result["playlist"]) <= ((2 ** 31) - 1)
def test_status_method_contains_playlistlength(self):
result = dict(status.status(self.context))
assert "playlistlength" in result
assert int(result["playlistlength"]) >= 0
def test_status_method_contains_xfade(self):
result = dict(status.status(self.context))
assert "xfade" in result
assert int(result["xfade"]) >= 0
def test_status_method_contains_state_is_play(self):
self.core.playback.set_state(PLAYING)
result = dict(status.status(self.context))
assert "state" in result
assert result["state"] == "play"
def test_status_method_contains_state_is_stop(self):
self.core.playback.set_state(STOPPED)
result = dict(status.status(self.context))
assert "state" in result
assert result["state"] == "stop"
def test_status_method_contains_state_is_pause(self):
self.core.playback.set_state(PLAYING)
self.core.playback.set_state(PAUSED)
result = dict(status.status(self.context))
assert "state" in result
assert result["state"] == "pause"
def test_status_method_when_playlist_loaded_contains_song(self):
self.set_tracklist([Track(uri="dummy:/a")])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "song" in result
assert int(result["song"]) >= 0
def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self):
self.set_tracklist([Track(uri="dummy:/a")])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "songid" in result
assert int(result["songid"]) == 1
def test_status_method_when_playlist_loaded_contains_nextsong(self):
self.set_tracklist([Track(uri="dummy:/a"), Track(uri="dummy:/b")])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "nextsong" in result
assert int(result["nextsong"]) >= 0
def test_status_method_when_playlist_loaded_contains_nextsongid(self):
self.set_tracklist([Track(uri="dummy:/a"), Track(uri="dummy:/b")])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "nextsongid" in result
assert int(result["nextsongid"]) == 2
def test_status_method_when_playing_contains_time_with_no_length(self):
self.set_tracklist([Track(uri="dummy:/a", length=None)])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "time" in result
(position, total) = result["time"].split(":")
position = int(position)
total = int(total)
assert position <= total
def test_status_method_when_playing_contains_time_with_length(self):
self.set_tracklist([Track(uri="dummy:/a", length=10000)])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "time" in result
(position, total) = result["time"].split(":")
position = int(position)
total = int(total)
assert position <= total
def test_status_method_when_playing_contains_elapsed(self):
self.set_tracklist([Track(uri="dummy:/a", length=60000)])
self.core.playback.play().get()
self.core.playback.pause()
self.core.playback.seek(59123)
result = dict(status.status(self.context))
assert "elapsed" in result
assert result["elapsed"] == "59.123"
def test_status_method_when_starting_playing_contains_elapsed_zero(self):
self.set_tracklist([Track(uri="dummy:/a", length=10000)])
self.core.playback.play().get()
self.core.playback.pause()
result = dict(status.status(self.context))
assert "elapsed" in result
assert result["elapsed"] == "0.000"
def test_status_method_when_playing_contains_bitrate(self):
self.set_tracklist([Track(uri="dummy:/a", bitrate=3200)])
self.core.playback.play().get()
result = dict(status.status(self.context))
assert "bitrate" in result
assert int(result["bitrate"]) == 3200
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_tokenizer.py 0000644 0001751 0000171 00000014433 00000000000 017524 0 ustar 00runner docker import unittest
from mopidy_mpd import exceptions, tokenize
class TestTokenizer(unittest.TestCase):
def assertTokenizeEquals(self, expected, line): # noqa: N802
assert expected == tokenize.split(line)
def assertTokenizeRaises(self, exception, message, line): # noqa: N802
with self.assertRaises(exception) as cm:
tokenize.split(line)
assert cm.exception.message == message
def test_empty_string(self):
ex = exceptions.MpdNoCommand
msg = "No command given"
self.assertTokenizeRaises(ex, msg, "")
self.assertTokenizeRaises(ex, msg, " ")
self.assertTokenizeRaises(ex, msg, "\t\t\t")
def test_command(self):
self.assertTokenizeEquals(["test"], "test")
self.assertTokenizeEquals(["test123"], "test123")
self.assertTokenizeEquals(["foo_bar"], "foo_bar")
def test_command_trailing_whitespace(self):
self.assertTokenizeEquals(["test"], "test ")
self.assertTokenizeEquals(["test"], "test\t\t\t")
def test_command_leading_whitespace(self):
ex = exceptions.MpdUnknownError
msg = "Letter expected"
self.assertTokenizeRaises(ex, msg, " test")
self.assertTokenizeRaises(ex, msg, "\ttest")
def test_invalid_command(self):
ex = exceptions.MpdUnknownError
msg = "Invalid word character"
self.assertTokenizeRaises(ex, msg, "foo/bar")
self.assertTokenizeRaises(ex, msg, "æøå")
self.assertTokenizeRaises(ex, msg, "test?")
self.assertTokenizeRaises(ex, msg, 'te"st')
def test_unquoted_param(self):
self.assertTokenizeEquals(["test", "param"], "test param")
self.assertTokenizeEquals(["test", "param"], "test\tparam")
def test_unquoted_param_leading_whitespace(self):
self.assertTokenizeEquals(["test", "param"], "test param")
self.assertTokenizeEquals(["test", "param"], "test\t\tparam")
def test_unquoted_param_trailing_whitespace(self):
self.assertTokenizeEquals(["test", "param"], "test param ")
self.assertTokenizeEquals(["test", "param"], "test param\t\t")
def test_unquoted_param_invalid_chars(self):
ex = exceptions.MpdArgError
msg = "Invalid unquoted character"
self.assertTokenizeRaises(ex, msg, 'test par"m')
self.assertTokenizeRaises(ex, msg, "test foo\bbar")
self.assertTokenizeRaises(ex, msg, 'test foo"bar"baz')
self.assertTokenizeRaises(ex, msg, "test foo'bar")
def test_unquoted_param_numbers(self):
self.assertTokenizeEquals(["test", "123"], "test 123")
self.assertTokenizeEquals(["test", "+123"], "test +123")
self.assertTokenizeEquals(["test", "-123"], "test -123")
self.assertTokenizeEquals(["test", "3.14"], "test 3.14")
def test_unquoted_param_extended_chars(self):
self.assertTokenizeEquals(["test", "æøå"], "test æøå")
self.assertTokenizeEquals(["test", "?#$"], "test ?#$")
self.assertTokenizeEquals(["test", "/foo/bar/"], "test /foo/bar/")
self.assertTokenizeEquals(["test", "foo\\bar"], "test foo\\bar")
def test_unquoted_params(self):
self.assertTokenizeEquals(["test", "foo", "bar"], "test foo bar")
def test_quoted_param(self):
self.assertTokenizeEquals(["test", "param"], 'test "param"')
self.assertTokenizeEquals(["test", "param"], 'test\t"param"')
def test_quoted_param_leading_whitespace(self):
self.assertTokenizeEquals(["test", "param"], 'test "param"')
self.assertTokenizeEquals(["test", "param"], 'test\t\t"param"')
def test_quoted_param_trailing_whitespace(self):
self.assertTokenizeEquals(["test", "param"], 'test "param" ')
self.assertTokenizeEquals(["test", "param"], 'test "param"\t\t')
def test_quoted_param_invalid_chars(self):
ex = exceptions.MpdArgError
msg = "Space expected after closing '\"'"
self.assertTokenizeRaises(ex, msg, 'test "foo"bar"')
self.assertTokenizeRaises(ex, msg, 'test "foo"bar" ')
self.assertTokenizeRaises(ex, msg, 'test "foo"bar')
self.assertTokenizeRaises(ex, msg, 'test "foo"bar ')
def test_quoted_param_numbers(self):
self.assertTokenizeEquals(["test", "123"], 'test "123"')
self.assertTokenizeEquals(["test", "+123"], 'test "+123"')
self.assertTokenizeEquals(["test", "-123"], 'test "-123"')
self.assertTokenizeEquals(["test", "3.14"], 'test "3.14"')
def test_quoted_param_spaces(self):
self.assertTokenizeEquals(["test", "foo bar"], 'test "foo bar"')
self.assertTokenizeEquals(["test", "foo bar"], 'test "foo bar"')
self.assertTokenizeEquals(["test", " param\t"], 'test " param\t"')
def test_quoted_param_extended_chars(self):
self.assertTokenizeEquals(["test", "æøå"], 'test "æøå"')
self.assertTokenizeEquals(["test", "?#$"], 'test "?#$"')
self.assertTokenizeEquals(["test", "/foo/bar/"], 'test "/foo/bar/"')
def test_quoted_param_escaping(self):
self.assertTokenizeEquals(["test", "\\"], r'test "\\"')
self.assertTokenizeEquals(["test", '"'], r'test "\""')
self.assertTokenizeEquals(["test", " "], r'test "\ "')
self.assertTokenizeEquals(["test", "\\n"], r'test "\\\n"')
def test_quoted_params(self):
self.assertTokenizeEquals(["test", "foo", "bar"], 'test "foo" "bar"')
def test_mixed_params(self):
self.assertTokenizeEquals(["test", "foo", "bar"], 'test foo "bar"')
self.assertTokenizeEquals(["test", "foo", "bar"], 'test "foo" bar')
self.assertTokenizeEquals(["test", "1", "2"], 'test 1 "2"')
self.assertTokenizeEquals(["test", "1", "2"], 'test "1" 2')
self.assertTokenizeEquals(
["test", "foo bar", "baz", "123"], 'test "foo bar" baz 123'
)
self.assertTokenizeEquals(
["test", 'foo"bar', "baz", "123"], r'test "foo\"bar" baz 123'
)
def test_unbalanced_quotes(self):
ex = exceptions.MpdArgError
msg = "Invalid unquoted character"
self.assertTokenizeRaises(ex, msg, 'test "foo bar" baz"')
def test_missing_closing_quote(self):
ex = exceptions.MpdArgError
msg = "Missing closing '\"'"
self.assertTokenizeRaises(ex, msg, 'test "foo')
self.assertTokenizeRaises(ex, msg, 'test "foo a ')
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tests/test_translator.py 0000644 0001751 0000171 00000015216 00000000000 017703 0 ustar 00runner docker import unittest
from mopidy.models import Album, Artist, Playlist, TlTrack, Track
from mopidy_mpd import translator
from tests import path_utils
class TrackMpdFormatTest(unittest.TestCase):
track = Track(
uri="à uri",
artists=[Artist(name="an artist")],
name="a nàme",
album=Album(
name="an album",
num_tracks=13,
artists=[Artist(name="an other artist")],
uri="urischeme:àlbum:12345",
),
track_no=7,
composers=[Artist(name="a composer")],
performers=[Artist(name="a performer")],
genre="a genre",
date="1977-01-01",
disc_no=1,
comment="a comment",
length=137000,
)
def setUp(self): # noqa: N802
self.media_dir = "/dir/subdir"
path_utils.mtime.set_fake_time(1234567)
def tearDown(self): # noqa: N802
path_utils.mtime.undo_fake()
def test_track_to_mpd_format_for_empty_track(self):
result = translator.track_to_mpd_format(
Track(uri="a uri", length=137000)
)
assert ("file", "a uri") in result
assert ("Time", 137) in result
assert ("Artist", "") not in result
assert ("Title", "") not in result
assert ("Album", "") not in result
assert ("Track", 0) not in result
assert ("Date", "") not in result
assert len(result) == 2
def test_track_to_mpd_format_with_position(self):
result = translator.track_to_mpd_format(Track(), position=1)
assert ("Pos", 1) not in result
def test_track_to_mpd_format_with_tlid(self):
result = translator.track_to_mpd_format(TlTrack(1, Track()))
assert ("Id", 1) not in result
def test_track_to_mpd_format_with_position_and_tlid(self):
result = translator.track_to_mpd_format(
TlTrack(2, Track(uri="a uri")), position=1
)
assert ("Pos", 1) in result
assert ("Id", 2) in result
def test_track_to_mpd_format_for_nonempty_track(self):
result = translator.track_to_mpd_format(
TlTrack(122, self.track), position=9
)
assert ("file", "à uri") in result
assert ("Time", 137) in result
assert ("Artist", "an artist") in result
assert ("Title", "a nàme") in result
assert ("Album", "an album") in result
assert ("AlbumArtist", "an other artist") in result
assert ("Composer", "a composer") in result
assert ("Performer", "a performer") in result
assert ("Genre", "a genre") in result
assert ("Track", "7/13") in result
assert ("Date", "1977-01-01") in result
assert ("Disc", 1) in result
assert ("Pos", 9) in result
assert ("Id", 122) in result
assert ("X-AlbumUri", "urischeme:àlbum:12345") in result
assert ("Comment", "a comment") not in result
assert len(result) == 15
def test_track_to_mpd_format_with_last_modified(self):
track = self.track.replace(last_modified=995303899000)
result = translator.track_to_mpd_format(track)
assert ("Last-Modified", "2001-07-16T17:18:19Z") in result
def test_track_to_mpd_format_with_last_modified_of_zero(self):
track = self.track.replace(last_modified=0)
result = translator.track_to_mpd_format(track)
keys = [k for k, v in result]
assert "Last-Modified" not in keys
def test_track_to_mpd_format_musicbrainz_trackid(self):
track = self.track.replace(musicbrainz_id="foo")
result = translator.track_to_mpd_format(track)
assert ("MUSICBRAINZ_TRACKID", "foo") in result
def test_track_to_mpd_format_musicbrainz_albumid(self):
album = self.track.album.replace(musicbrainz_id="foo")
track = self.track.replace(album=album)
result = translator.track_to_mpd_format(track)
assert ("MUSICBRAINZ_ALBUMID", "foo") in result
def test_track_to_mpd_format_musicbrainz_albumartistid(self):
artist = list(self.track.artists)[0].replace(musicbrainz_id="foo")
album = self.track.album.replace(artists=[artist])
track = self.track.replace(album=album)
result = translator.track_to_mpd_format(track)
assert ("MUSICBRAINZ_ALBUMARTISTID", "foo") in result
def test_track_to_mpd_format_musicbrainz_artistid(self):
artist = list(self.track.artists)[0].replace(musicbrainz_id="foo")
track = self.track.replace(artists=[artist])
result = translator.track_to_mpd_format(track)
assert ("MUSICBRAINZ_ARTISTID", "foo") in result
def test_concat_multi_values(self):
artists = [Artist(name="ABBA"), Artist(name="Beatles")]
translated = translator.concat_multi_values(artists, "name")
assert translated == "ABBA;Beatles"
def test_concat_multi_values_artist_with_no_name(self):
artists = [Artist(name=None)]
translated = translator.concat_multi_values(artists, "name")
assert translated == ""
def test_concat_multi_values_artist_with_no_musicbrainz_id(self):
artists = [Artist(name="Jah Wobble")]
translated = translator.concat_multi_values(artists, "musicbrainz_id")
assert translated == ""
def test_track_to_mpd_format_with_stream_title(self):
result = translator.track_to_mpd_format(self.track, stream_title="foo")
assert ("Name", "a nàme") in result
assert ("Title", "foo") in result
def test_track_to_mpd_format_with_empty_stream_title(self):
result = translator.track_to_mpd_format(self.track, stream_title="")
assert ("Name", "a nàme") in result
assert ("Title", "") not in result
def test_track_to_mpd_format_with_stream_and_no_track_name(self):
track = self.track.replace(name=None)
result = translator.track_to_mpd_format(track, stream_title="foo")
assert ("Name", "") not in result
assert ("Title", "foo") in result
class PlaylistMpdFormatTest(unittest.TestCase):
def test_mpd_format(self):
playlist = Playlist(
tracks=[
Track(uri="foo", track_no=1),
Track(uri="bàr", track_no=2),
Track(uri="baz", track_no=3),
]
)
result = translator.playlist_to_mpd_format(playlist)
assert len(result) == 3
def test_mpd_format_with_range(self):
playlist = Playlist(
tracks=[
Track(uri="foo", track_no=1),
Track(uri="bàr", track_no=2),
Track(uri="baz", track_no=3),
]
)
result = translator.playlist_to_mpd_format(playlist, 1, 2)
assert len(result) == 1
assert dict(result[0])["Track"] == 2
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1607133598.0
Mopidy-MPD-3.1.0/tox.ini 0000644 0001751 0000171 00000000742 00000000000 014250 0 ustar 00runner docker [tox]
envlist = py37, py38, py39, black, check-manifest, flake8
[testenv]
sitepackages = true
deps = .[test]
commands =
python -m pytest \
--basetemp={envtmpdir} \
--cov=mopidy_mpd --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