Release v0.5.0
This commit is contained in:
commit
3785661420
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
*.pyc
|
||||
*.swp
|
||||
.coverage
|
||||
.idea
|
||||
.noseids
|
||||
.tox
|
||||
MANIFEST
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
include LICENSE pylintrc *.rst data/mopidy.desktop
|
||||
include *.ini
|
||||
include *.rst
|
||||
include LICENSE
|
||||
include MANIFEST.in
|
||||
include data/mopidy.desktop
|
||||
include mopidy/backends/spotify/spotify_appkey.key
|
||||
include pylintrc
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
recursive-include requirements *
|
||||
|
||||
@ -1,31 +1,38 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
|
||||
from mopidy import settings
|
||||
from mopidy.utils.log import setup_console_logging, setup_root_logger
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.frontends.mpd.translator import tracks_to_tag_cache_format
|
||||
|
||||
tracks = []
|
||||
setup_root_logger()
|
||||
setup_console_logging(2)
|
||||
|
||||
def store(data):
|
||||
track = translator(data)
|
||||
tracks.append(track)
|
||||
print >> sys.stderr, 'Added %s' % track.uri
|
||||
tracks = []
|
||||
|
||||
def debug(uri, error):
|
||||
print >> sys.stderr, 'Failed %s: %s' % (uri, error)
|
||||
def store(data):
|
||||
track = translator(data)
|
||||
tracks.append(track)
|
||||
logging.debug(u'Added %s', track.uri)
|
||||
|
||||
print >> sys.stderr, 'Scanning %s' % settings.LOCAL_MUSIC_PATH
|
||||
def debug(uri, error, debug):
|
||||
logging.error(u'Failed %s: %s - %s', uri, error, debug)
|
||||
|
||||
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
|
||||
logging.info(u'Scanning %s', settings.LOCAL_MUSIC_PATH)
|
||||
|
||||
scanner = Scanner(settings.LOCAL_MUSIC_PATH, store, debug)
|
||||
try:
|
||||
scanner.start()
|
||||
except KeyboardInterrupt:
|
||||
scanner.stop()
|
||||
|
||||
print >> sys.stderr, 'Done'
|
||||
logging.info(u'Done')
|
||||
|
||||
for a in tracks_to_tag_cache_format(tracks):
|
||||
if len(a) == 1:
|
||||
print (u'%s' % a).encode('utf-8')
|
||||
else:
|
||||
print (u'%s: %s' % a).encode('utf-8')
|
||||
for a in tracks_to_tag_cache_format(tracks):
|
||||
if len(a) == 1:
|
||||
print (u'%s' % a).encode('utf-8')
|
||||
else:
|
||||
print (u'%s: %s' % a).encode('utf-8')
|
||||
|
||||
BIN
docs/_static/thread_communication.png
vendored
BIN
docs/_static/thread_communication.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
37
docs/_static/thread_communication.txt
vendored
37
docs/_static/thread_communication.txt
vendored
@ -1,37 +0,0 @@
|
||||
Script for use with www.websequencediagrams.com
|
||||
===============================================
|
||||
|
||||
Main -> Core: create
|
||||
activate Core
|
||||
note over Core: create NadMixer
|
||||
Core -> NadTalker: create
|
||||
activate NadTalker
|
||||
note over NadTalker: calibrate device
|
||||
note over Core: create DespotifyBackend
|
||||
Core -> despotify: connect to Spotify
|
||||
activate despotify
|
||||
note over Core: create MpdFrontend
|
||||
Main -> Server: create
|
||||
activate Server
|
||||
note over Server: open port
|
||||
Client -> Server: connect
|
||||
note over Server: open session
|
||||
Client -> Server: play 1
|
||||
Server -> Core: play 1
|
||||
Core -> despotify: play first track
|
||||
Client -> Server: setvol 50
|
||||
Server -> Core: setvol 50
|
||||
Core -> NadTalker: volume = 50
|
||||
Client -> Server: status
|
||||
Server -> Core: status
|
||||
Core -> NadTalker: volume?
|
||||
NadTalker -> Core: volume = 50
|
||||
Core -> Server: status response
|
||||
Server -> Client: status response
|
||||
despotify -> Core: end of track callback
|
||||
Core -> despotify: play second track
|
||||
Client -> Server: stop
|
||||
Server -> Core: stop
|
||||
Core -> despotify: stop
|
||||
Client -> Server: disconnect
|
||||
note over Server: close session
|
||||
@ -1,20 +1,18 @@
|
||||
.. _output-api:
|
||||
|
||||
**********
|
||||
Output API
|
||||
**********
|
||||
|
||||
Outputs are responsible for playing audio.
|
||||
Outputs are used by :mod:`mopidy.gstreamer` to output audio in some way.
|
||||
|
||||
.. warning::
|
||||
|
||||
A stable output API is not available yet, as we've only implemented a
|
||||
single output module.
|
||||
|
||||
.. automodule:: mopidy.outputs.base
|
||||
:synopsis: Base class for outputs
|
||||
.. autoclass:: mopidy.outputs.BaseOutput
|
||||
:members:
|
||||
|
||||
|
||||
Output implementations
|
||||
======================
|
||||
|
||||
* :mod:`mopidy.outputs.gstreamer`
|
||||
* :class:`mopidy.outputs.custom.CustomOutput`
|
||||
* :class:`mopidy.outputs.local.LocalOutput`
|
||||
* :class:`mopidy.outputs.shoutcast.ShoutcastOutput`
|
||||
|
||||
@ -5,7 +5,7 @@ Authors
|
||||
Contributors to Mopidy in the order of appearance:
|
||||
|
||||
- Stein Magnus Jodal <stein.magnus@jodal.no>
|
||||
- Johannes Knutsen <johannes@knutseninfo.no>
|
||||
- Johannes Knutsen <johannes@knutsen.me>
|
||||
- Thomas Adamcik <adamcik@samfundet.no>
|
||||
- Kristian Klette <klette@klette.us>
|
||||
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
def setup(app):
|
||||
app.connect('autodoc-skip-member', autodoc_private_members_with_doc)
|
||||
|
||||
def autodoc_private_members_with_doc(app, what, name, obj, skip, options):
|
||||
if not skip:
|
||||
return skip
|
||||
if (name.startswith('_') and obj.__doc__ is not None
|
||||
and not (name.startswith('__') and name.endswith('__'))):
|
||||
return False
|
||||
return skip
|
||||
127
docs/changes.rst
127
docs/changes.rst
@ -5,8 +5,91 @@ Changes
|
||||
This change log is used to track all major changes to Mopidy.
|
||||
|
||||
|
||||
0.4.1 (2011-05-06)
|
||||
==================
|
||||
v0.5.0 (2011-06-15)
|
||||
===================
|
||||
|
||||
Since last time we've added support for audio streaming to SHOUTcast servers
|
||||
and fixed the longstanding playlist loading issue in the Spotify backend. As
|
||||
always the release has a bunch of bug fixes and minor improvements.
|
||||
|
||||
Please note that 0.5.0 requires some updated dependencies, as listed under
|
||||
*Important changes* below.
|
||||
|
||||
**Important changes**
|
||||
|
||||
- If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and
|
||||
pyspotify 1.3. If you install from APT, libspotify and pyspotify will
|
||||
automatically be upgraded. If you are not installing from APT, follow the
|
||||
instructions at :doc:`/installation/libspotify/`.
|
||||
|
||||
- If you have explicitly set the :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE`
|
||||
setting, you must update your settings file. The new setting is named
|
||||
:attr:`mopidy.settings.SPOTIFY_BITRATE` and accepts the integer values 96,
|
||||
160, and 320.
|
||||
|
||||
- Mopidy now supports running with 1 to N outputs at the same time. This
|
||||
feature was mainly added to facilitate SHOUTcast support, which Mopidy has
|
||||
also gained. In its current state outputs can not be toggled during runtime.
|
||||
|
||||
**Changes**
|
||||
|
||||
- Local backend:
|
||||
|
||||
- Fix local backend time query errors that where coming from stopped
|
||||
pipeline. (Fixes: :issue:`87`)
|
||||
|
||||
- Spotify backend:
|
||||
|
||||
- Thanks to Antoine Pierlot-Garcin's recent work on updating and improving
|
||||
pyspotify, stored playlists will again load when Mopidy starts. The
|
||||
workaround of searching and reconnecting to make the playlists appear are
|
||||
no longer necessary. (Fixes: :issue:`59`)
|
||||
|
||||
- Track's that are no longer available in Spotify's archives are now
|
||||
"autolinked" to corresponding tracks in other albums, just like the
|
||||
official Spotify clients do. (Fixes: :issue:`34`)
|
||||
|
||||
- MPD frontend:
|
||||
|
||||
- Refactoring and cleanup. Most notably, all request handlers now get an
|
||||
instance of :class:`mopidy.frontends.mpd.dispatcher.MpdContext` as the
|
||||
first argument. The new class contains reference to any object in Mopidy
|
||||
the MPD protocol implementation should need access to.
|
||||
|
||||
- Close the client connection when the command ``close`` is received.
|
||||
|
||||
- Do not allow access to the command ``kill``.
|
||||
|
||||
- ``commands`` and ``notcommands`` now have correct output if password
|
||||
authentication is turned on, but the connected user has not been
|
||||
authenticated yet.
|
||||
|
||||
- Command line usage:
|
||||
|
||||
- Support passing options to GStreamer. See :option:`--help-gst` for a list
|
||||
of available options. (Fixes: :issue:`95`)
|
||||
|
||||
- Improve :option:`--list-settings` output. (Fixes: :issue:`91`)
|
||||
|
||||
- Added :option:`--interactive` for reading missing local settings from
|
||||
``stdin``. (Fixes: :issue:`96`)
|
||||
|
||||
- Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``,
|
||||
which initiates the same shutdown procedure as CTRL+C does.
|
||||
|
||||
- Tag cache generator:
|
||||
|
||||
- Made it possible to abort :command:`mopidy-scan` with CTRL+C.
|
||||
|
||||
- Fixed bug regarding handling of bad dates.
|
||||
|
||||
- Use :mod:`logging` instead of ``print`` statements.
|
||||
|
||||
- Found and worked around strange WMA metadata behaviour.
|
||||
|
||||
|
||||
v0.4.1 (2011-05-06)
|
||||
===================
|
||||
|
||||
This is a bug fix release fixing audio problems on older GStreamer and some
|
||||
minor bugs.
|
||||
@ -35,8 +118,8 @@ minor bugs.
|
||||
configured. (Fixes: :issue:`84`)
|
||||
|
||||
|
||||
0.4.0 (2011-04-27)
|
||||
==================
|
||||
v0.4.0 (2011-04-27)
|
||||
===================
|
||||
|
||||
Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've
|
||||
fixed a bunch of issues and bugs, with the help of several new contributors
|
||||
@ -134,8 +217,8 @@ loading from Mopidy 0.3.0 is still present.
|
||||
the debug log, to ease debugging of issues with attached debug logs.
|
||||
|
||||
|
||||
0.3.1 (2010-01-22)
|
||||
==================
|
||||
v0.3.1 (2010-01-22)
|
||||
===================
|
||||
|
||||
A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
|
||||
|
||||
@ -148,8 +231,8 @@ A couple of fixes to the 0.3.0 release is needed to get a smooth installation.
|
||||
installed if the installation is executed as the root user.
|
||||
|
||||
|
||||
0.3.0 (2010-01-22)
|
||||
==================
|
||||
v0.3.0 (2010-01-22)
|
||||
===================
|
||||
|
||||
Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large
|
||||
changes. The main features are support for high bitrate audio from Spotify, and
|
||||
@ -303,8 +386,8 @@ to this problem.
|
||||
:class:`mopidy.outputs.base.BaseOutput`.
|
||||
|
||||
|
||||
0.2.1 (2011-01-07)
|
||||
==================
|
||||
v0.2.1 (2011-01-07)
|
||||
===================
|
||||
|
||||
This is a maintenance release without any new features.
|
||||
|
||||
@ -316,8 +399,8 @@ This is a maintenance release without any new features.
|
||||
failure.
|
||||
|
||||
|
||||
0.2.0 (2010-10-24)
|
||||
==================
|
||||
v0.2.0 (2010-10-24)
|
||||
===================
|
||||
|
||||
In Mopidy 0.2.0 we've added a `Last.fm <http://www.last.fm/>`_ scrobbling
|
||||
support, which means that Mopidy now can submit meta data about the tracks you
|
||||
@ -384,8 +467,8 @@ searching at the same time, thanks to Valentin David.
|
||||
should now exit immediately.
|
||||
|
||||
|
||||
0.1.0 (2010-08-23)
|
||||
==================
|
||||
v0.1.0 (2010-08-23)
|
||||
===================
|
||||
|
||||
After three weeks of long nights and sprints we're finally pleased enough with
|
||||
the state of Mopidy to remove the alpha label, and do a regular release.
|
||||
@ -516,8 +599,8 @@ fixing the OS X issues for a future release. You can track the progress at
|
||||
:meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead.
|
||||
|
||||
|
||||
0.1.0a3 (2010-08-03)
|
||||
====================
|
||||
v0.1.0a3 (2010-08-03)
|
||||
=====================
|
||||
|
||||
In the last two months, Mopidy's MPD frontend has gotten lots of stability
|
||||
fixes and error handling improvements, proper support for having the same track
|
||||
@ -594,8 +677,8 @@ Enjoy the best alpha relase of Mopidy ever :-)
|
||||
``cp_track``.
|
||||
|
||||
|
||||
0.1.0a2 (2010-06-02)
|
||||
====================
|
||||
v0.1.0a2 (2010-06-02)
|
||||
=====================
|
||||
|
||||
It has been a rather slow month for Mopidy, but we would like to keep up with
|
||||
the established pace of at least a release per month.
|
||||
@ -610,8 +693,8 @@ the established pace of at least a release per month.
|
||||
control :class:`mopidy.mixers.alsa.AlsaMixer` should use.
|
||||
|
||||
|
||||
0.1.0a1 (2010-05-04)
|
||||
====================
|
||||
v0.1.0a1 (2010-05-04)
|
||||
=====================
|
||||
|
||||
Since the previous release Mopidy has seen about 300 commits, more than 200 new
|
||||
tests, a libspotify release, and major feature additions to Spotify. The new
|
||||
@ -651,8 +734,8 @@ As always, report problems at our IRC channel or our issue tracker. Thanks!
|
||||
- And much more.
|
||||
|
||||
|
||||
0.1.0a0 (2010-03-27)
|
||||
====================
|
||||
v0.1.0a0 (2010-03-27)
|
||||
=====================
|
||||
|
||||
"*Release early. Release often. Listen to your customers.*" wrote Eric S.
|
||||
Raymond in *The Cathedral and the Bazaar*.
|
||||
|
||||
@ -25,7 +25,7 @@ import mopidy
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'autodoc_private_members',
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram',
|
||||
'sphinx.ext.extlinks', 'sphinx.ext.viewcode']
|
||||
|
||||
|
||||
@ -7,4 +7,3 @@ Development
|
||||
|
||||
roadmap
|
||||
contributing
|
||||
internals
|
||||
|
||||
@ -1,113 +0,0 @@
|
||||
*********
|
||||
Internals
|
||||
*********
|
||||
|
||||
Some of the following notes and details will hopefully be useful when you start
|
||||
developing on Mopidy, while some may only be useful when you get deeper into
|
||||
specific parts of Mopidy.
|
||||
|
||||
In addition to what you'll find here, don't forget the :doc:`/api/index`.
|
||||
|
||||
|
||||
Class instantiation and usage
|
||||
=============================
|
||||
|
||||
The following diagram shows how Mopidy is wired together with the MPD client,
|
||||
the Spotify service, and the speakers.
|
||||
|
||||
**Legend**
|
||||
|
||||
- Filled red boxes are the key external systems.
|
||||
- Gray boxes are external dependencies.
|
||||
- Blue circles lives in the ``main`` process, also known as ``CoreProcess``.
|
||||
It is processing messages put on the core queue.
|
||||
- Purple circles lives in a process named ``MpdProcess``, running an
|
||||
:mod:`asyncore` loop.
|
||||
- Green circles lives in a process named ``GStreamerProcess``.
|
||||
- Brown circle is a thread living in the ``CoreProcess``.
|
||||
|
||||
.. digraph:: class_instantiation_and_usage
|
||||
|
||||
"main" [ color="blue" ]
|
||||
"CoreProcess" [ color="blue" ]
|
||||
|
||||
# Frontend
|
||||
"MPD client" [ color="red", style="filled", shape="box" ]
|
||||
"MpdFrontend" [ color="blue" ]
|
||||
"MpdProcess" [ color="purple" ]
|
||||
"MpdServer" [ color="purple" ]
|
||||
"MpdSession" [ color="purple" ]
|
||||
"MpdDispatcher" [ color="blue" ]
|
||||
|
||||
# Backend
|
||||
"Libspotify\nBackend" [ color="blue" ]
|
||||
"Libspotify\nSessionManager" [ color="brown" ]
|
||||
"pyspotify" [ color="gray", shape="box" ]
|
||||
"libspotify" [ color="gray", shape="box" ]
|
||||
"Spotify" [ color="red", style="filled", shape="box" ]
|
||||
|
||||
# Output/mixer
|
||||
"GStreamer\nOutput" [ color="blue" ]
|
||||
"GStreamer\nSoftwareMixer" [ color="blue" ]
|
||||
"GStreamer\nProcess" [ color="green" ]
|
||||
"GStreamer" [ color="gray", shape="box" ]
|
||||
"Speakers" [ color="red", style="filled", shape="box" ]
|
||||
|
||||
"main" -> "CoreProcess" [ label="create" ]
|
||||
|
||||
# Frontend
|
||||
"CoreProcess" -> "MpdFrontend" [ label="create" ]
|
||||
"MpdFrontend" -> "MpdProcess" [ label="create" ]
|
||||
"MpdFrontend" -> "MpdDispatcher" [ label="create" ]
|
||||
"MpdProcess" -> "MpdServer" [ label="create" ]
|
||||
"MpdServer" -> "MpdSession" [ label="create one\nper client" ]
|
||||
"MpdSession" -> "MpdDispatcher" [
|
||||
label="pass requests\nvia core_queue" ]
|
||||
"MpdDispatcher" -> "MpdSession" [
|
||||
label="pass response\nvia reply_to pipe" ]
|
||||
"MpdDispatcher" -> "Libspotify\nBackend" [ label="use backend API" ]
|
||||
"MPD client" -> "MpdServer" [ label="connect" ]
|
||||
"MPD client" -> "MpdSession" [ label="request" ]
|
||||
"MpdSession" -> "MPD client" [ label="response" ]
|
||||
|
||||
# Backend
|
||||
"CoreProcess" -> "Libspotify\nBackend" [ label="create" ]
|
||||
"Libspotify\nBackend" -> "Libspotify\nSessionManager" [
|
||||
label="creates and uses" ]
|
||||
"Libspotify\nSessionManager" -> "Libspotify\nBackend" [
|
||||
label="pass commands\nvia core_queue" ]
|
||||
"Libspotify\nSessionManager" -> "pyspotify" [ label="use Python\nwrapper" ]
|
||||
"pyspotify" -> "Libspotify\nSessionManager" [ label="use callbacks" ]
|
||||
"pyspotify" -> "libspotify" [ label="use C library" ]
|
||||
"libspotify" -> "Spotify" [ label="use service" ]
|
||||
"Libspotify\nSessionManager" -> "GStreamer\nProcess" [
|
||||
label="pass commands\nand audio data\nvia output_queue" ]
|
||||
|
||||
# Output/mixer
|
||||
"Libspotify\nBackend" -> "GStreamer\nSoftwareMixer" [
|
||||
label="create and\nuse mixer API" ]
|
||||
"GStreamer\nSoftwareMixer" -> "GStreamer\nProcess" [
|
||||
label="pass commands\nvia output_queue" ]
|
||||
"CoreProcess" -> "GStreamer\nOutput" [ label="create" ]
|
||||
"GStreamer\nOutput" -> "GStreamer\nProcess" [ label="create" ]
|
||||
"GStreamer\nProcess" -> "GStreamer" [ label="use library" ]
|
||||
"GStreamer" -> "Speakers" [ label="play audio" ]
|
||||
|
||||
|
||||
Thread/process communication
|
||||
============================
|
||||
|
||||
.. warning::
|
||||
This section is currently outdated.
|
||||
|
||||
- Everything starts with ``Main``.
|
||||
- ``Main`` creates a ``Core`` process which runs the frontend, backend, and
|
||||
mixer.
|
||||
- Mixers *may* create an additional process for communication with external
|
||||
devices, like ``NadTalker`` in this example.
|
||||
- Backend libraries *may* have threads of their own, like ``despotify`` here
|
||||
which has additional threads in the ``Core`` process.
|
||||
- ``Server`` part currently runs in the same process and thread as ``Main``.
|
||||
- ``Client`` is some external client talking to ``Server`` over a socket.
|
||||
|
||||
.. image:: /_static/thread_communication.png
|
||||
@ -26,7 +26,7 @@ Feature wishlist
|
||||
We maintain our collection of sane or less sane ideas for future Mopidy
|
||||
features as `issues <https://github.com/mopidy/mopidy/issues>`_ at GitHub
|
||||
labeled with `the "wishlist" label
|
||||
<https://github.com/mopidy/mopidy/issues/labels/wishlist>`_. Feel free to vote
|
||||
<https://github.com/mopidy/mopidy/issues?labels=wishlist>`_. Feel free to vote
|
||||
up any feature you would love to see in Mopidy, but please refrain from adding
|
||||
a comment just to say "I want this too!". You are of course free to add
|
||||
comments if you have suggestions for how the feature should work or be
|
||||
|
||||
@ -33,13 +33,13 @@ User documentation
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
changes
|
||||
installation/index
|
||||
settings
|
||||
running
|
||||
clients/index
|
||||
authors
|
||||
licenses
|
||||
changes
|
||||
|
||||
Reference documentation
|
||||
=======================
|
||||
|
||||
@ -73,5 +73,12 @@ Using a custom audio sink
|
||||
=========================
|
||||
|
||||
If you for some reason want to use some other GStreamer audio sink than
|
||||
``autoaudiosink``, you can change :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||
in your ``settings.py`` file.
|
||||
``autoaudiosink``, you can add ``mopidy.outputs.custom.CustomOutput`` to the
|
||||
:attr:`mopidy.settings.OUTPUTS` setting, and set the
|
||||
:attr:`mopidy.settings.CUSTOM_OUTPUT` setting to a partial GStreamer pipeline
|
||||
description describing the GStreamer sink you want to use.
|
||||
|
||||
Example of ``settings.py`` for OSS4::
|
||||
|
||||
OUTPUTS = (u'mopidy.outputs.custom.CustomOutput',)
|
||||
CUSTOM_OUTPUT = u'oss4sink'
|
||||
|
||||
@ -9,8 +9,8 @@ setup and whether you want to use stable releases or less stable development
|
||||
versions.
|
||||
|
||||
|
||||
Install dependencies
|
||||
====================
|
||||
Requirements
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
@ -98,7 +98,7 @@ install Mopidy from PyPI using Pip.
|
||||
#. Then, you need to install Pip::
|
||||
|
||||
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo brew install pip # On OS X
|
||||
sudo easy_install pip # On OS X
|
||||
|
||||
#. To install the currently latest stable release of Mopidy::
|
||||
|
||||
@ -132,7 +132,7 @@ Mopidy's ``develop`` branch.
|
||||
#. Then, you need to install Pip::
|
||||
|
||||
sudo aptitude install python-setuptools python-pip # On Ubuntu/Debian
|
||||
sudo brew install pip # On OS X
|
||||
sudo easy_install pip # On OS X
|
||||
|
||||
#. To install the latest snapshot of Mopidy, run::
|
||||
|
||||
@ -155,7 +155,7 @@ If you want to contribute to Mopidy, you should install Mopidy using Git.
|
||||
#. Then install Git, if haven't already::
|
||||
|
||||
sudo aptitude install git-core # On Ubuntu/Debian
|
||||
sudo brew install git # On OS X
|
||||
sudo brew install git # On OS X using Homebrew
|
||||
|
||||
#. Clone the official Mopidy repository, or your own fork of it::
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ libspotify installation
|
||||
|
||||
Mopidy uses `libspotify
|
||||
<http://developer.spotify.com/en/libspotify/overview/>`_ for playing music from
|
||||
the Spotify music service. To use :mod:`mopidy.backends.libspotify` you must
|
||||
install libspotify and `pyspotify <http://github.com/mopidy/pyspotify>`_.
|
||||
the Spotify music service. To use :mod:`mopidy.backends.spotify` you must
|
||||
install libspotify and `pyspotify <http://pyspotify.mopidy.com/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
@ -30,7 +30,7 @@ If you run a Debian based Linux distribution, like Ubuntu, see
|
||||
http://apt.mopidy.com/ for how to the Mopidy APT archive as a software source
|
||||
on your installation. Then, simply run::
|
||||
|
||||
sudo apt-get install libspotify7
|
||||
sudo apt-get install libspotify8
|
||||
|
||||
When libspotify has been installed, continue with
|
||||
:ref:`pyspotify_installation`.
|
||||
@ -39,14 +39,14 @@ When libspotify has been installed, continue with
|
||||
On Linux from source
|
||||
--------------------
|
||||
|
||||
Download and install libspotify 0.0.7 for your OS and CPU architecture from
|
||||
Download and install libspotify 0.0.8 for your OS and CPU architecture from
|
||||
https://developer.spotify.com/en/libspotify/.
|
||||
|
||||
For 64-bit Linux the process is as follows::
|
||||
|
||||
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.7-linux6-x86_64.tar.gz
|
||||
tar zxfv libspotify-0.0.7-linux6-x86_64.tar.gz
|
||||
cd libspotify-0.0.7-linux6-x86_64/
|
||||
wget http://developer.spotify.com/download/libspotify/libspotify-0.0.8-linux6-x86_64.tar.gz
|
||||
tar zxfv libspotify-0.0.8-linux6-x86_64.tar.gz
|
||||
cd libspotify-0.0.8-linux6-x86_64/
|
||||
sudo make install prefix=/usr/local
|
||||
sudo ldconfig
|
||||
|
||||
@ -103,14 +103,10 @@ Debian/Ubuntu systems run::
|
||||
|
||||
On OS X no additional dependencies are needed.
|
||||
|
||||
Get the pyspotify code, and install it::
|
||||
Then get, build, and install the latest releast of pyspotify using ``pip``::
|
||||
|
||||
wget --no-check-certificate -O pyspotify.tar.gz https://github.com/mopidy/pyspotify/tarball/mopidy
|
||||
tar zxfv pyspotify.tar.gz
|
||||
cd pyspotify/
|
||||
sudo python setup.py install
|
||||
sudo pip install -U pyspotify
|
||||
|
||||
It is important that you install pyspotify from the ``mopidy`` branch of the
|
||||
``mopidy/pyspotify`` repository, as the upstream repository at
|
||||
``winjer/pyspotify`` is not updated with changes needed to support e.g.
|
||||
libspotify 0.0.7 and high bitrate audio.
|
||||
Or using the older ``easy_install``::
|
||||
|
||||
sudo easy_install pyspotify
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
:mod:`mopidy.frontends.mpd` -- MPD server
|
||||
*****************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd
|
||||
:synopsis: MPD frontend
|
||||
:members:
|
||||
@ -11,28 +13,30 @@
|
||||
MPD server
|
||||
==========
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.server
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.server
|
||||
:synopsis: MPD server
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.server
|
||||
|
||||
|
||||
MPD session
|
||||
===========
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.session
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.session
|
||||
:synopsis: MPD client session
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.session
|
||||
|
||||
|
||||
MPD dispatcher
|
||||
==============
|
||||
|
||||
.. inheritance-diagram:: mopidy.frontends.mpd.dispatcher
|
||||
|
||||
.. automodule:: mopidy.frontends.mpd.dispatcher
|
||||
:synopsis: MPD request dispatcher
|
||||
:members:
|
||||
|
||||
9
docs/modules/gstreamer.rst
Normal file
9
docs/modules/gstreamer.rst
Normal file
@ -0,0 +1,9 @@
|
||||
********************************************
|
||||
:mod:`mopidy.gstreamer` -- GStreamer adapter
|
||||
********************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.gstreamer
|
||||
|
||||
.. automodule:: mopidy.gstreamer
|
||||
:synopsis: GStreamer adapter
|
||||
:members:
|
||||
14
docs/modules/outputs.rst
Normal file
14
docs/modules/outputs.rst
Normal file
@ -0,0 +1,14 @@
|
||||
************************************************
|
||||
:mod:`mopidy.outputs` -- GStreamer audio outputs
|
||||
************************************************
|
||||
|
||||
The following GStreamer audio outputs implements the :ref:`output-api`.
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.custom
|
||||
.. autoclass:: mopidy.outputs.custom.CustomOutput
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.local
|
||||
.. autoclass:: mopidy.outputs.local.LocalOutput
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.shoutcast
|
||||
.. autoclass:: mopidy.outputs.shoutcast.ShoutcastOutput
|
||||
@ -1,9 +0,0 @@
|
||||
*********************************************************************
|
||||
:mod:`mopidy.outputs.gstreamer` -- GStreamer output for all platforms
|
||||
*********************************************************************
|
||||
|
||||
.. inheritance-diagram:: mopidy.outputs.gstreamer
|
||||
|
||||
.. automodule:: mopidy.outputs.gstreamer
|
||||
:synopsis: GStreamer output for all platforms
|
||||
:members:
|
||||
@ -56,7 +56,7 @@ You may also want to change some of the ``LOCAL_*`` settings. See
|
||||
|
||||
Currently, Mopidy supports using Spotify *or* local storage as a music
|
||||
source. We're working on using both sources simultaneously, and will
|
||||
hopefully have support for this in the 0.3 release.
|
||||
hopefully have support for this in the 0.6 release.
|
||||
|
||||
|
||||
.. _generating_a_tag_cache:
|
||||
@ -92,6 +92,7 @@ To make a ``tag_cache`` of your local music available for Mopidy:
|
||||
|
||||
.. _use_mpd_on_a_network:
|
||||
|
||||
|
||||
Connecting from other machines on the network
|
||||
=============================================
|
||||
|
||||
@ -119,6 +120,31 @@ file::
|
||||
LASTFM_PASSWORD = u'mysecret'
|
||||
|
||||
|
||||
Streaming audio through a SHOUTcast/Icecast server
|
||||
==================================================
|
||||
|
||||
If you want to play the audio on another computer than the one running Mopidy,
|
||||
you can stream the audio from Mopidy through an SHOUTcast or Icecast audio
|
||||
streaming server. Multiple media players can then be connected to the streaming
|
||||
server simultaneously. To use the SHOUTcast output, do the following:
|
||||
|
||||
#. Install, configure and start the Icecast server. It can be found in the
|
||||
``icecast2`` package in Debian/Ubuntu.
|
||||
|
||||
#. Add ``mopidy.outputs.shoutcast.ShoutcastOutput`` output to the
|
||||
:attr:`mopidy.settings.OUTPUTS` setting.
|
||||
|
||||
#. Check the default values for the following settings, and alter them to match
|
||||
your Icecast setup if needed:
|
||||
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
|
||||
|
||||
|
||||
Available settings
|
||||
==================
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ if not (2, 6) <= sys.version_info < (3,):
|
||||
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
VERSION = (0, 4, 1)
|
||||
VERSION = (0, 5, 0)
|
||||
|
||||
def get_version():
|
||||
try:
|
||||
|
||||
@ -2,6 +2,8 @@ from copy import copy
|
||||
import logging
|
||||
import random
|
||||
|
||||
from mopidy.models import CpTrack
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.base')
|
||||
|
||||
class CurrentPlaylistController(object):
|
||||
@ -66,7 +68,7 @@ class CurrentPlaylistController(object):
|
||||
"""
|
||||
assert at_position <= len(self._cp_tracks), \
|
||||
u'at_position can not be greater than playlist length'
|
||||
cp_track = (self.version, track)
|
||||
cp_track = CpTrack(self.version, track)
|
||||
if at_position is not None:
|
||||
self._cp_tracks.insert(at_position, cp_track)
|
||||
else:
|
||||
|
||||
@ -80,12 +80,12 @@ class PlaybackController(object):
|
||||
def _get_cpid(self, cp_track):
|
||||
if cp_track is None:
|
||||
return None
|
||||
return cp_track[0]
|
||||
return cp_track.cpid
|
||||
|
||||
def _get_track(self, cp_track):
|
||||
if cp_track is None:
|
||||
return None
|
||||
return cp_track[1]
|
||||
return cp_track.track
|
||||
|
||||
@property
|
||||
def current_cpid(self):
|
||||
@ -263,6 +263,7 @@ class PlaybackController(object):
|
||||
.. digraph:: state_transitions
|
||||
|
||||
"STOPPED" -> "PLAYING" [ label="play" ]
|
||||
"STOPPED" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "STOPPED" [ label="stop" ]
|
||||
"PLAYING" -> "PAUSED" [ label="pause" ]
|
||||
"PLAYING" -> "PLAYING" [ label="play" ]
|
||||
@ -331,7 +332,7 @@ class PlaybackController(object):
|
||||
self.stop(clear_current_track=True)
|
||||
|
||||
if self.consume:
|
||||
self.backend.current_playlist.remove(cpid=original_cp_track[0])
|
||||
self.backend.current_playlist.remove(cpid=original_cp_track.cpid)
|
||||
|
||||
def on_current_playlist_change(self):
|
||||
"""
|
||||
@ -389,7 +390,7 @@ class PlaybackController(object):
|
||||
self.state = self.STOPPED
|
||||
self.current_cp_track = cp_track
|
||||
self.state = self.PLAYING
|
||||
if not self.provider.play(cp_track[1]):
|
||||
if not self.provider.play(cp_track.track):
|
||||
# Track is not playable
|
||||
if self.random and self._shuffled:
|
||||
self._shuffled.remove(cp_track)
|
||||
|
||||
@ -12,7 +12,7 @@ from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
BasePlaybackProvider, StoredPlaylistsController,
|
||||
BaseStoredPlaylistsProvider)
|
||||
from mopidy.models import Playlist, Track, Album
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
from .translator import parse_m3u, parse_mpd_tag_cache
|
||||
|
||||
@ -22,7 +22,11 @@ class LocalBackend(ThreadingActor, Backend):
|
||||
"""
|
||||
A backend for playing music from a local music archive.
|
||||
|
||||
**Issues:** http://github.com/mopidy/mopidy/issues/labels/backend-local
|
||||
**Issues:** https://github.com/mopidy/mopidy/issues?labels=backend-local
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
@ -50,12 +54,12 @@ class LocalBackend(ThreadingActor, Backend):
|
||||
|
||||
self.uri_handlers = [u'file://']
|
||||
|
||||
self.output = None
|
||||
self.gstreamer = None
|
||||
|
||||
def on_start(self):
|
||||
output_refs = ActorRegistry.get_by_class(BaseOutput)
|
||||
assert len(output_refs) == 1, 'Expected exactly one running output.'
|
||||
self.output = output_refs[0].proxy()
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
|
||||
|
||||
class LocalPlaybackController(PlaybackController):
|
||||
@ -67,24 +71,26 @@ class LocalPlaybackController(PlaybackController):
|
||||
|
||||
@property
|
||||
def time_position(self):
|
||||
return self.backend.output.get_position().get()
|
||||
return self.backend.gstreamer.get_position().get()
|
||||
|
||||
|
||||
class LocalPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.output.set_state('PAUSED').get()
|
||||
return self.backend.gstreamer.pause_playback().get()
|
||||
|
||||
def play(self, track):
|
||||
return self.backend.output.play_uri(track.uri).get()
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.gstreamer.set_uri(track.uri).get()
|
||||
return self.backend.gstreamer.start_playback().get()
|
||||
|
||||
def resume(self):
|
||||
return self.backend.output.set_state('PLAYING').get()
|
||||
return self.backend.gstreamer.start_playback().get()
|
||||
|
||||
def seek(self, time_position):
|
||||
return self.backend.output.set_position(time_position).get()
|
||||
return self.backend.gstreamer.set_position(time_position).get()
|
||||
|
||||
def stop(self):
|
||||
return self.backend.output.set_state('READY').get()
|
||||
return self.backend.gstreamer.stop_playback().get()
|
||||
|
||||
|
||||
class LocalStoredPlaylistsProvider(BaseStoredPlaylistsProvider):
|
||||
@ -172,7 +178,7 @@ class LocalLibraryProvider(BaseLibraryProvider):
|
||||
|
||||
tracks = parse_mpd_tag_cache(tag_cache, music_folder)
|
||||
|
||||
logger.info('Loading songs in %s from %s', music_folder, tag_cache)
|
||||
logger.info('Loading tracks in %s from %s', music_folder, tag_cache)
|
||||
|
||||
for track in tracks:
|
||||
self._uri_mapping[track.uri] = track
|
||||
|
||||
@ -6,11 +6,12 @@ from pykka.registry import ActorRegistry
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import (Backend, CurrentPlaylistController,
|
||||
LibraryController, PlaybackController, StoredPlaylistsController)
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify')
|
||||
|
||||
ENCODING = 'utf-8'
|
||||
BITRATES = {96: 2, 160: 0, 320: 1}
|
||||
|
||||
class SpotifyBackend(ThreadingActor, Backend):
|
||||
"""
|
||||
@ -27,7 +28,12 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
trade mark of the Spotify Group.
|
||||
|
||||
**Issues:**
|
||||
http://github.com/mopidy/mopidy/issues/labels/backend-spotify
|
||||
https://github.com/mopidy/mopidy/issues?labels=backend-spotify
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- libspotify == 0.0.8 (libspotify8 package from apt.mopidy.com)
|
||||
- pyspotify == 1.3 (python-spotify package from apt.mopidy.com)
|
||||
|
||||
**Settings:**
|
||||
|
||||
@ -63,22 +69,25 @@ class SpotifyBackend(ThreadingActor, Backend):
|
||||
|
||||
self.uri_handlers = [u'spotify:', u'http://open.spotify.com/']
|
||||
|
||||
self.output = None
|
||||
self.gstreamer = None
|
||||
self.spotify = None
|
||||
|
||||
def on_start(self):
|
||||
output_refs = ActorRegistry.get_by_class(BaseOutput)
|
||||
assert len(output_refs) == 1, 'Expected exactly one running output.'
|
||||
self.output = output_refs[0].proxy()
|
||||
# Fail early if settings are not present
|
||||
self.username = settings.SPOTIFY_USERNAME
|
||||
self.password = settings.SPOTIFY_PASSWORD
|
||||
|
||||
def on_start(self):
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, 'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
self.spotify = self._connect()
|
||||
|
||||
def _connect(self):
|
||||
from .session_manager import SpotifySessionManager
|
||||
|
||||
logger.info(u'Mopidy uses SPOTIFY(R) CORE')
|
||||
logger.debug(u'Connecting to Spotify')
|
||||
spotify = SpotifySessionManager(
|
||||
settings.SPOTIFY_USERNAME, settings.SPOTIFY_PASSWORD)
|
||||
spotify = SpotifySessionManager(self.username, self.password)
|
||||
spotify.start()
|
||||
return spotify
|
||||
|
||||
46
mopidy/backends/spotify/container_manager.py
Normal file
46
mopidy/backends/spotify/container_manager.py
Normal file
@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from spotify.manager import SpotifyContainerManager as \
|
||||
PyspotifyContainerManager
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.container_manager')
|
||||
|
||||
class SpotifyContainerManager(PyspotifyContainerManager):
|
||||
def __init__(self, session_manager):
|
||||
PyspotifyContainerManager.__init__(self)
|
||||
self.session_manager = session_manager
|
||||
|
||||
def container_loaded(self, container, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: playlist container loaded')
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
playlist_container = self.session_manager.session.playlist_container()
|
||||
for playlist in playlist_container:
|
||||
self.session_manager.playlist_manager.watch(playlist)
|
||||
logger.debug(u'Watching %d playlist(s) for changes',
|
||||
len(playlist_container))
|
||||
|
||||
def playlist_added(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: playlist added at position %d',
|
||||
position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
def playlist_moved(self, container, playlist, old_position, new_position,
|
||||
userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" moved from position %d to %d',
|
||||
playlist.name(), old_position, new_position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
|
||||
def playlist_removed(self, container, playlist, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: playlist "%s" removed from position %d',
|
||||
playlist.name(), position)
|
||||
# container_loaded() is called after this callback, so we do not need
|
||||
# to handle this callback.
|
||||
@ -8,10 +8,9 @@ logger = logging.getLogger('mopidy.backends.spotify.playback')
|
||||
|
||||
class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
def pause(self):
|
||||
return self.backend.output.set_state('PAUSED')
|
||||
return self.backend.gstreamer.pause_playback()
|
||||
|
||||
def play(self, track):
|
||||
self.backend.output.set_state('READY')
|
||||
if self.backend.playback.state == self.backend.playback.PLAYING:
|
||||
self.backend.spotify.session.play(0)
|
||||
if track.uri is None:
|
||||
@ -20,7 +19,10 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
self.backend.spotify.session.load(
|
||||
Link.from_string(track.uri).as_track())
|
||||
self.backend.spotify.session.play(1)
|
||||
self.backend.output.play_uri('appsrc://')
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.gstreamer.set_uri('appsrc://')
|
||||
self.backend.gstreamer.start_playback()
|
||||
self.backend.gstreamer.set_metadata(track)
|
||||
return True
|
||||
except SpotifyError as e:
|
||||
logger.info('Playback of %s failed: %s', track.uri, e)
|
||||
@ -30,12 +32,12 @@ class SpotifyPlaybackProvider(BasePlaybackProvider):
|
||||
return self.seek(self.backend.playback.time_position)
|
||||
|
||||
def seek(self, time_position):
|
||||
self.backend.output.set_state('READY')
|
||||
self.backend.gstreamer.prepare_change()
|
||||
self.backend.spotify.session.seek(time_position)
|
||||
self.backend.output.set_state('PLAYING')
|
||||
self.backend.gstreamer.start_playback()
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
result = self.backend.output.set_state('READY')
|
||||
result = self.backend.gstreamer.stop_playback()
|
||||
self.backend.spotify.session.play(0)
|
||||
return result
|
||||
|
||||
93
mopidy/backends/spotify/playlist_manager.py
Normal file
93
mopidy/backends/spotify/playlist_manager.py
Normal file
@ -0,0 +1,93 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from spotify.manager import SpotifyPlaylistManager as PyspotifyPlaylistManager
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.playlist_manager')
|
||||
|
||||
class SpotifyPlaylistManager(PyspotifyPlaylistManager):
|
||||
def __init__(self, session_manager):
|
||||
PyspotifyPlaylistManager.__init__(self)
|
||||
self.session_manager = session_manager
|
||||
|
||||
def tracks_added(self, playlist, tracks, position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) added to position %d in playlist "%s"',
|
||||
len(tracks), position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def tracks_moved(self, playlist, tracks, new_position, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) moved to position %d in playlist "%s"',
|
||||
len(tracks), new_position, playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def tracks_removed(self, playlist, tracks, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: '
|
||||
u'%d track(s) removed from playlist "%s"', len(tracks), playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def playlist_renamed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Playlist renamed to "%s"',
|
||||
playlist.name())
|
||||
self.session_manager.refresh_stored_playlists()
|
||||
|
||||
def playlist_state_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: The state of playlist "%s" changed',
|
||||
playlist.name())
|
||||
|
||||
def playlist_update_in_progress(self, playlist, done, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
if done:
|
||||
logger.debug(u'Callback called: '
|
||||
u'Update of playlist "%s" done', playlist.name())
|
||||
else:
|
||||
logger.debug(u'Callback called: '
|
||||
u'Update of playlist "%s" in progress', playlist.name())
|
||||
|
||||
def playlist_metadata_updated(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Metadata updated for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def track_created_changed(self, playlist, position, user, when, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
when = datetime.datetime.fromtimestamp(when)
|
||||
logger.debug(
|
||||
u'Callback called: Created by/when for track %d in playlist '
|
||||
u'"%s" changed to user "N/A" and time "%s"',
|
||||
position, playlist.name(), when)
|
||||
|
||||
def track_message_changed(self, playlist, position, message, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Message for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), message)
|
||||
|
||||
def track_seen_changed(self, playlist, position, seen, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Seen attribute for track %d in playlist '
|
||||
u'"%s" changed to "%s"', position, playlist.name(), seen)
|
||||
|
||||
def description_changed(self, playlist, description, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Description changed for playlist "%s" to "%s"',
|
||||
playlist.name(), description)
|
||||
|
||||
def subscribers_changed(self, playlist, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(
|
||||
u'Callback called: Subscribers changed for playlist "%s"',
|
||||
playlist.name())
|
||||
|
||||
def image_changed(self, playlist, image, userdata):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Callback called: Image changed for playlist "%s"',
|
||||
playlist.name())
|
||||
@ -8,9 +8,12 @@ from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import get_version, settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.backends.spotify import BITRATES
|
||||
from mopidy.backends.spotify.container_manager import SpotifyContainerManager
|
||||
from mopidy.backends.spotify.playlist_manager import SpotifyPlaylistManager
|
||||
from mopidy.backends.spotify.translator import SpotifyTranslator
|
||||
from mopidy.models import Playlist
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils.process import BaseThread
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.session_manager')
|
||||
@ -27,22 +30,26 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
def __init__(self, username, password):
|
||||
PyspotifySessionManager.__init__(self, username, password)
|
||||
BaseThread.__init__(self)
|
||||
self.name = 'SpotifySMThread'
|
||||
self.name = 'SpotifyThread'
|
||||
|
||||
self.output = None
|
||||
self.gstreamer = None
|
||||
self.backend = None
|
||||
|
||||
self.connected = threading.Event()
|
||||
self.session = None
|
||||
|
||||
self.container_manager = None
|
||||
self.playlist_manager = None
|
||||
|
||||
def run_inside_try(self):
|
||||
self.setup()
|
||||
self.connect()
|
||||
|
||||
def setup(self):
|
||||
output_refs = ActorRegistry.get_by_class(BaseOutput)
|
||||
assert len(output_refs) == 1, 'Expected exactly one running output.'
|
||||
self.output = output_refs[0].proxy()
|
||||
gstreamer_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(gstreamer_refs) == 1, \
|
||||
'Expected exactly one running gstreamer.'
|
||||
self.gstreamer = gstreamer_refs[0].proxy()
|
||||
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
@ -53,14 +60,19 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
if error:
|
||||
logger.error(u'Spotify login error: %s', error)
|
||||
return
|
||||
|
||||
logger.info(u'Connected to Spotify')
|
||||
self.session = session
|
||||
if settings.SPOTIFY_HIGH_BITRATE:
|
||||
logger.debug(u'Preferring high bitrate from Spotify')
|
||||
self.session.set_preferred_bitrate(1)
|
||||
else:
|
||||
logger.debug(u'Preferring normal bitrate from Spotify')
|
||||
self.session.set_preferred_bitrate(0)
|
||||
|
||||
logger.debug(u'Preferred Spotify bitrate is %s kbps',
|
||||
settings.SPOTIFY_BITRATE)
|
||||
self.session.set_preferred_bitrate(BITRATES[settings.SPOTIFY_BITRATE])
|
||||
|
||||
self.container_manager = SpotifyContainerManager(self)
|
||||
self.playlist_manager = SpotifyPlaylistManager(self)
|
||||
|
||||
self.container_manager.watch(self.session.playlist_container())
|
||||
|
||||
self.connected.set()
|
||||
|
||||
def logged_out(self, session):
|
||||
@ -69,13 +81,12 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
|
||||
def metadata_updated(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'Metadata updated')
|
||||
self.refresh_stored_playlists()
|
||||
logger.debug(u'Callback called: Metadata updated')
|
||||
|
||||
def connection_error(self, session, error):
|
||||
"""Callback used by pyspotify"""
|
||||
if error is None:
|
||||
logger.info(u'Spotify connection error resolved')
|
||||
logger.info(u'Spotify connection OK')
|
||||
else:
|
||||
logger.error(u'Spotify connection error: %s', error)
|
||||
self.backend.playback.pause()
|
||||
@ -106,7 +117,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
'sample_rate': sample_rate,
|
||||
'channels': channels,
|
||||
}
|
||||
self.output.deliver_data(capabilites, bytes(frames))
|
||||
self.gstreamer.emit_data(capabilites, bytes(frames))
|
||||
|
||||
def play_token_lost(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
@ -120,7 +131,7 @@ class SpotifySessionManager(BaseThread, PyspotifySessionManager):
|
||||
def end_of_track(self, session):
|
||||
"""Callback used by pyspotify"""
|
||||
logger.debug(u'End of data stream reached')
|
||||
self.output.end_of_data_stream()
|
||||
self.gstreamer.emit_end_of_stream()
|
||||
|
||||
def refresh_stored_playlists(self):
|
||||
"""Refresh the stored playlists in the backend with fresh meta data
|
||||
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from spotify import Link, SpotifyError
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.spotify import ENCODING
|
||||
from mopidy.backends.spotify import ENCODING, BITRATES
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
|
||||
logger = logging.getLogger('mopidy.backends.spotify.translator')
|
||||
@ -16,34 +16,35 @@ class SpotifyTranslator(object):
|
||||
return Artist(name=u'[loading...]')
|
||||
return Artist(
|
||||
uri=str(Link.from_artist(spotify_artist)),
|
||||
name=spotify_artist.name().decode(ENCODING),
|
||||
name=spotify_artist.name()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def to_mopidy_album(cls, spotify_album):
|
||||
if not spotify_album.is_loaded():
|
||||
if spotify_album is None or not spotify_album.is_loaded():
|
||||
return Album(name=u'[loading...]')
|
||||
# TODO pyspotify got much more data on albums than this
|
||||
return Album(name=spotify_album.name().decode(ENCODING))
|
||||
return Album(name=spotify_album.name())
|
||||
|
||||
@classmethod
|
||||
def to_mopidy_track(cls, spotify_track):
|
||||
uri = str(Link.from_track(spotify_track, 0))
|
||||
if not spotify_track.is_loaded():
|
||||
return Track(uri=uri, name=u'[loading...]')
|
||||
if dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR:
|
||||
if (spotify_track.album() is not None and
|
||||
dt.MINYEAR <= int(spotify_track.album().year()) <= dt.MAXYEAR):
|
||||
date = dt.date(spotify_track.album().year(), 1, 1)
|
||||
else:
|
||||
date = None
|
||||
return Track(
|
||||
uri=uri,
|
||||
name=spotify_track.name().decode(ENCODING),
|
||||
name=spotify_track.name(),
|
||||
artists=[cls.to_mopidy_artist(a) for a in spotify_track.artists()],
|
||||
album=cls.to_mopidy_album(spotify_track.album()),
|
||||
track_no=spotify_track.index(),
|
||||
date=date,
|
||||
length=spotify_track.duration(),
|
||||
bitrate=(settings.SPOTIFY_HIGH_BITRATE and 320 or 160),
|
||||
bitrate=BITRATES[settings.SPOTIFY_BITRATE],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -56,7 +57,7 @@ class SpotifyTranslator(object):
|
||||
try:
|
||||
return Playlist(
|
||||
uri=str(Link.from_playlist(spotify_playlist)),
|
||||
name=spotify_playlist.name().decode(ENCODING),
|
||||
name=spotify_playlist.name(),
|
||||
# FIXME if check on link is a hackish workaround for is_local
|
||||
tracks=[cls.to_mopidy_track(t) for t in spotify_playlist
|
||||
if str(Link.from_track(t, 0))],
|
||||
|
||||
@ -1,37 +1,64 @@
|
||||
import logging
|
||||
import optparse
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Extract any non-GStreamer arguments, and leave the GStreamer arguments for
|
||||
# processing by GStreamer. This needs to be done before GStreamer is imported,
|
||||
# so that GStreamer doesn't hijack e.g. ``--help``.
|
||||
# NOTE This naive fix does not support values like ``bar`` in
|
||||
# ``--gst-foo bar``. Use equals to pass values, like ``--gst-foo=bar``.
|
||||
def is_gst_arg(arg):
|
||||
return arg.startswith('--gst') or arg == '--help-gst'
|
||||
gstreamer_args = [arg for arg in sys.argv[1:] if is_gst_arg(arg)]
|
||||
mopidy_args = [arg for arg in sys.argv[1:] if not is_gst_arg(arg)]
|
||||
sys.argv[1:] = gstreamer_args
|
||||
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import get_version, settings, OptionalDependencyError
|
||||
from mopidy import (get_version, settings, OptionalDependencyError,
|
||||
SettingsError)
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils import get_class
|
||||
from mopidy.utils.log import setup_logging
|
||||
from mopidy.utils.path import get_or_create_folder, get_or_create_file
|
||||
from mopidy.utils.process import GObjectEventThread
|
||||
from mopidy.utils.process import (GObjectEventThread, exit_handler,
|
||||
stop_all_actors)
|
||||
from mopidy.utils.settings import list_settings_optparse_callback
|
||||
|
||||
logger = logging.getLogger('mopidy.core')
|
||||
|
||||
def main():
|
||||
options = parse_options()
|
||||
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
setup_settings()
|
||||
setup_gobject_loop()
|
||||
setup_output()
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
signal.signal(signal.SIGTERM, exit_handler)
|
||||
try:
|
||||
while ActorRegistry.get_all():
|
||||
options = parse_options()
|
||||
setup_logging(options.verbosity_level, options.save_debug_log)
|
||||
setup_settings(options.interactive)
|
||||
setup_gobject_loop()
|
||||
setup_gstreamer()
|
||||
setup_mixer()
|
||||
setup_backend()
|
||||
setup_frontends()
|
||||
while True:
|
||||
time.sleep(1)
|
||||
logger.info(u'No actors left. Exiting...')
|
||||
except SettingsError as e:
|
||||
logger.error(e.message)
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'User interrupt. Exiting...')
|
||||
ActorRegistry.stop_all()
|
||||
logger.info(u'Interrupted. Exiting...')
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
finally:
|
||||
stop_all_actors()
|
||||
|
||||
def parse_options():
|
||||
parser = optparse.OptionParser(version=u'Mopidy %s' % get_version())
|
||||
parser.add_option('--help-gst',
|
||||
action='store_true', dest='help_gst',
|
||||
help='show GStreamer help options')
|
||||
parser.add_option('-i', '--interactive',
|
||||
action='store_true', dest='interactive',
|
||||
help='ask interactively for required settings which is missing')
|
||||
parser.add_option('-q', '--quiet',
|
||||
action='store_const', const=0, dest='verbosity_level',
|
||||
help='less output (warning level)')
|
||||
@ -44,18 +71,22 @@ def parse_options():
|
||||
parser.add_option('--list-settings',
|
||||
action='callback', callback=list_settings_optparse_callback,
|
||||
help='list current settings')
|
||||
return parser.parse_args()[0]
|
||||
return parser.parse_args(args=mopidy_args)[0]
|
||||
|
||||
def setup_settings():
|
||||
def setup_settings(interactive):
|
||||
get_or_create_folder('~/.mopidy/')
|
||||
get_or_create_file('~/.mopidy/settings.py')
|
||||
settings.validate()
|
||||
try:
|
||||
settings.validate(interactive)
|
||||
except SettingsError, e:
|
||||
logger.error(e.message)
|
||||
sys.exit(1)
|
||||
|
||||
def setup_gobject_loop():
|
||||
GObjectEventThread().start()
|
||||
|
||||
def setup_output():
|
||||
get_class(settings.OUTPUT).start()
|
||||
def setup_gstreamer():
|
||||
GStreamer.start()
|
||||
|
||||
def setup_mixer():
|
||||
get_class(settings.MIXER).start()
|
||||
|
||||
@ -13,11 +13,15 @@ class MpdFrontend(ThreadingActor, BaseFrontend):
|
||||
"""
|
||||
The MPD frontend.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.MPD_SERVER_HOSTNAME`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PORT`
|
||||
- :attr:`mopidy.settings.MPD_SERVER_PASSWORD`
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from pykka import ActorDeadError
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdArgError,
|
||||
MpdUnknownCommand)
|
||||
from mopidy.frontends.mpd import exceptions
|
||||
from mopidy.frontends.mpd.protocol import mpd_commands, request_handlers
|
||||
# Do not remove the following import. The protocol modules must be imported to
|
||||
# get them registered as request handlers.
|
||||
@ -16,6 +18,8 @@ from mopidy.frontends.mpd.protocol import (audio_output, command_list,
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
from mopidy.utils import flatten
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.dispatcher')
|
||||
|
||||
class MpdDispatcher(object):
|
||||
"""
|
||||
The MPD session feeds the MPD dispatcher with requests. The dispatcher
|
||||
@ -23,67 +27,186 @@ class MpdDispatcher(object):
|
||||
back to the MPD session.
|
||||
"""
|
||||
|
||||
# XXX Consider merging MpdDispatcher into MpdSession
|
||||
|
||||
def __init__(self):
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
self.backend = backend_refs[0].proxy()
|
||||
|
||||
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
|
||||
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
|
||||
self.mixer = mixer_refs[0].proxy()
|
||||
|
||||
def __init__(self, session=None):
|
||||
self.authenticated = False
|
||||
self.command_list = False
|
||||
self.command_list_ok = False
|
||||
self.command_list_index = None
|
||||
self.context = MpdContext(self, session=session)
|
||||
|
||||
def handle_request(self, request, command_list_index=None):
|
||||
def handle_request(self, request, current_command_list_index=None):
|
||||
"""Dispatch incoming requests to the correct handler."""
|
||||
if self.command_list is not False and request != u'command_list_end':
|
||||
self.command_list.append(request)
|
||||
return None
|
||||
try:
|
||||
(handler, kwargs) = self.find_handler(request)
|
||||
result = handler(self, **kwargs)
|
||||
except MpdAckError as e:
|
||||
if command_list_index is not None:
|
||||
e.index = command_list_index
|
||||
return self.handle_response(e.get_mpd_ack(), add_ok=False)
|
||||
if request in (u'command_list_begin', u'command_list_ok_begin'):
|
||||
return None
|
||||
if command_list_index is not None:
|
||||
return self.handle_response(result, add_ok=False)
|
||||
return self.handle_response(result)
|
||||
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._add_ok_filter,
|
||||
self._call_handler_filter,
|
||||
]
|
||||
return self._call_next_filter(request, response, filter_chain)
|
||||
|
||||
def find_handler(self, request):
|
||||
"""Find the correct handler for a request."""
|
||||
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 settings.MPD_SERVER_PASSWORD is None:
|
||||
self.authenticated = True
|
||||
return self._call_next_filter(request, response, filter_chain)
|
||||
else:
|
||||
command_name = request.split(' ')[0]
|
||||
command_names_not_requiring_auth = [
|
||||
command.name for command in mpd_commands
|
||||
if not command.auth_required]
|
||||
if command_name in command_names_not_requiring_auth:
|
||||
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] == u'OK':
|
||||
response = response[:-1]
|
||||
return response
|
||||
|
||||
def _is_receiving_command_list(self, request):
|
||||
return (self.command_list is not False
|
||||
and request != u'command_list_end')
|
||||
|
||||
def _is_processing_command_list(self, request):
|
||||
return (self.command_list_index is not None
|
||||
and request != u'command_list_end')
|
||||
|
||||
|
||||
### 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(u'OK')
|
||||
return response
|
||||
|
||||
def _has_error(self, response):
|
||||
return response and response[-1].startswith(u'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 ActorDeadError as e:
|
||||
logger.warning(u'Tried to communicate with dead actor.')
|
||||
raise exceptions.MpdSystemError(e.message)
|
||||
|
||||
def _call_handler(self, request):
|
||||
(handler, kwargs) = self._find_handler(request)
|
||||
return handler(self.context, **kwargs)
|
||||
|
||||
def _find_handler(self, request):
|
||||
for pattern in request_handlers:
|
||||
matches = re.match(pattern, request)
|
||||
if matches is not None:
|
||||
return (request_handlers[pattern], matches.groupdict())
|
||||
command = request.split(' ')[0]
|
||||
if command in mpd_commands:
|
||||
raise MpdArgError(u'incorrect arguments', command=command)
|
||||
raise MpdUnknownCommand(command=command)
|
||||
command_name = request.split(' ')[0]
|
||||
if command_name in [command.name for command in mpd_commands]:
|
||||
raise exceptions.MpdArgError(u'incorrect arguments',
|
||||
command=command_name)
|
||||
raise exceptions.MpdUnknownCommand(command=command_name)
|
||||
|
||||
def handle_response(self, result, add_ok=True):
|
||||
"""Format the response from a request handler."""
|
||||
response = []
|
||||
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:
|
||||
result = []
|
||||
elif isinstance(result, set):
|
||||
result = list(result)
|
||||
elif not isinstance(result, list):
|
||||
result = [result]
|
||||
for line in flatten(result):
|
||||
if isinstance(line, dict):
|
||||
for (key, value) in line.items():
|
||||
response.append(u'%s: %s' % (key, value))
|
||||
elif isinstance(line, tuple):
|
||||
(key, value) = line
|
||||
response.append(u'%s: %s' % (key, value))
|
||||
else:
|
||||
response.append(line)
|
||||
if add_ok and (not response or not response[-1].startswith(u'ACK')):
|
||||
response.append(u'OK')
|
||||
return response
|
||||
return []
|
||||
if isinstance(result, set):
|
||||
return flatten(list(result))
|
||||
if not isinstance(result, list):
|
||||
return [result]
|
||||
return flatten(result)
|
||||
|
||||
def _format_lines(self, line):
|
||||
if isinstance(line, dict):
|
||||
return [u'%s: %s' % (key, value) for (key, value) in line.items()]
|
||||
if isinstance(line, tuple):
|
||||
(key, value) = line
|
||||
return [u'%s: %s' % (key, value)]
|
||||
return [line]
|
||||
|
||||
|
||||
class MpdContext(object):
|
||||
"""
|
||||
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.frontends.mpd.session.MpdSession`.
|
||||
session = None
|
||||
|
||||
def __init__(self, dispatcher, session=None):
|
||||
self.dispatcher = dispatcher
|
||||
self.session = session
|
||||
self._backend = None
|
||||
self._mixer = None
|
||||
|
||||
@property
|
||||
def backend(self):
|
||||
"""
|
||||
The backend. An instance of :class:`mopidy.backends.base.Backend`.
|
||||
"""
|
||||
if self._backend is not None:
|
||||
return self._backend
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
self._backend = backend_refs[0].proxy()
|
||||
return self._backend
|
||||
|
||||
@property
|
||||
def mixer(self):
|
||||
"""
|
||||
The mixer. An instance of :class:`mopidy.mixers.base.BaseMixer`.
|
||||
"""
|
||||
if self._mixer is not None:
|
||||
return self._mixer
|
||||
mixer_refs = ActorRegistry.get_by_class(BaseMixer)
|
||||
assert len(mixer_refs) == 1, 'Expected exactly one running mixer.'
|
||||
self._mixer = mixer_refs[0].proxy()
|
||||
return self._mixer
|
||||
|
||||
@ -16,10 +16,11 @@ class MpdAckError(MopidyException):
|
||||
ACK_ERROR_PLAYER_SYNC = 55
|
||||
ACK_ERROR_EXIST = 56
|
||||
|
||||
def __init__(self, message=u'', error_code=0, index=0, command=u''):
|
||||
super(MpdAckError, self).__init__(message, error_code, index, command)
|
||||
error_code = 0
|
||||
|
||||
def __init__(self, message=u'', index=0, command=u''):
|
||||
super(MpdAckError, self).__init__(message, index, command)
|
||||
self.message = message
|
||||
self.error_code = error_code
|
||||
self.index = index
|
||||
self.command = command
|
||||
|
||||
@ -30,31 +31,38 @@ class MpdAckError(MopidyException):
|
||||
ACK [%(error_code)i@%(index)i] {%(command)s} description
|
||||
"""
|
||||
return u'ACK [%i@%i] {%s} %s' % (
|
||||
self.error_code, self.index, self.command, self.message)
|
||||
self.__class__.error_code, self.index, self.command, self.message)
|
||||
|
||||
class MpdArgError(MpdAckError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdArgError, self).__init__(*args, **kwargs)
|
||||
self.error_code = MpdAckError.ACK_ERROR_ARG
|
||||
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(MpdPasswordError, self).__init__(*args, **kwargs)
|
||||
self.error_code = MpdAckError.ACK_ERROR_PASSWORD
|
||||
super(MpdPermissionError, self).__init__(*args, **kwargs)
|
||||
self.message = u'you don\'t have permission for "%s"' % self.command
|
||||
|
||||
class MpdUnknownCommand(MpdAckError):
|
||||
error_code = MpdAckError.ACK_ERROR_UNKNOWN
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdUnknownCommand, self).__init__(*args, **kwargs)
|
||||
self.message = u'unknown command "%s"' % self.command
|
||||
self.command = u''
|
||||
self.error_code = MpdAckError.ACK_ERROR_UNKNOWN
|
||||
|
||||
class MpdNoExistError(MpdAckError):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdNoExistError, self).__init__(*args, **kwargs)
|
||||
self.error_code = MpdAckError.ACK_ERROR_NO_EXIST
|
||||
error_code = MpdAckError.ACK_ERROR_NO_EXIST
|
||||
|
||||
class MpdSystemError(MpdAckError):
|
||||
error_code = MpdAckError.ACK_ERROR_SYSTEM
|
||||
|
||||
class MpdNotImplemented(MpdAckError):
|
||||
error_code = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MpdNotImplemented, self).__init__(*args, **kwargs)
|
||||
self.message = u'Not implemented'
|
||||
|
||||
@ -10,6 +10,7 @@ implement our own MPD server which is compatible with the numerous existing
|
||||
`MPD clients <http://mpd.wikia.com/wiki/Clients>`_.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
import re
|
||||
|
||||
#: The MPD protocol uses UTF-8 for encoding all data.
|
||||
@ -21,12 +22,16 @@ LINE_TERMINATOR = u'\n'
|
||||
#: The MPD protocol version is 0.16.0.
|
||||
VERSION = u'0.16.0'
|
||||
|
||||
MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required'])
|
||||
|
||||
#: List of all available commands, represented as :class:`MpdCommand` objects.
|
||||
mpd_commands = set()
|
||||
|
||||
request_handlers = {}
|
||||
|
||||
def handle_pattern(pattern):
|
||||
def handle_request(pattern, auth_required=True):
|
||||
"""
|
||||
Decorator for connecting command handlers to command patterns.
|
||||
Decorator for connecting command handlers to command requests.
|
||||
|
||||
If you use named groups in the pattern, the decorated method will get the
|
||||
groups as keyword arguments. If the group is optional, remember to give the
|
||||
@ -35,7 +40,7 @@ def handle_pattern(pattern):
|
||||
For example, if the command is ``do that thing`` the ``what`` argument will
|
||||
be ``this thing``::
|
||||
|
||||
@handle_pattern('^do (?P<what>.+)$')
|
||||
@handle_request('^do (?P<what>.+)$')
|
||||
def do(what):
|
||||
...
|
||||
|
||||
@ -45,7 +50,8 @@ def handle_pattern(pattern):
|
||||
def decorator(func):
|
||||
match = re.search('([a-z_]+)', pattern)
|
||||
if match is not None:
|
||||
mpd_commands.add(match.group())
|
||||
mpd_commands.add(
|
||||
MpdCommand(name=match.group(), auth_required=auth_required))
|
||||
if pattern in request_handlers:
|
||||
raise ValueError(u'Tried to redefine handler for %s with %s' % (
|
||||
pattern, func))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^disableoutput "(?P<outputid>\d+)"$')
|
||||
def disableoutput(frontend, outputid):
|
||||
@handle_request(r'^disableoutput "(?P<outputid>\d+)"$')
|
||||
def disableoutput(context, outputid):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
@ -12,8 +12,8 @@ def disableoutput(frontend, outputid):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^enableoutput "(?P<outputid>\d+)"$')
|
||||
def enableoutput(frontend, outputid):
|
||||
@handle_request(r'^enableoutput "(?P<outputid>\d+)"$')
|
||||
def enableoutput(context, outputid):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
@ -23,8 +23,8 @@ def enableoutput(frontend, outputid):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^outputs$')
|
||||
def outputs(frontend):
|
||||
@handle_request(r'^outputs$')
|
||||
def outputs(context):
|
||||
"""
|
||||
*musicpd.org, audio output section:*
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdUnknownCommand
|
||||
|
||||
@handle_pattern(r'^command_list_begin$')
|
||||
def command_list_begin(frontend):
|
||||
@handle_request(r'^command_list_begin$')
|
||||
def command_list_begin(context):
|
||||
"""
|
||||
*musicpd.org, command list section:*
|
||||
|
||||
@ -18,31 +18,33 @@ def command_list_begin(frontend):
|
||||
returned. If ``command_list_ok_begin`` is used, ``list_OK`` is
|
||||
returned for each successful command executed in the command list.
|
||||
"""
|
||||
frontend.command_list = []
|
||||
frontend.command_list_ok = False
|
||||
context.dispatcher.command_list = []
|
||||
context.dispatcher.command_list_ok = False
|
||||
|
||||
@handle_pattern(r'^command_list_end$')
|
||||
def command_list_end(frontend):
|
||||
@handle_request(r'^command_list_end$')
|
||||
def command_list_end(context):
|
||||
"""See :meth:`command_list_begin()`."""
|
||||
if frontend.command_list is False:
|
||||
if context.dispatcher.command_list is False:
|
||||
# Test for False exactly, and not e.g. empty list
|
||||
raise MpdUnknownCommand(command='command_list_end')
|
||||
(command_list, frontend.command_list) = (frontend.command_list, False)
|
||||
(command_list_ok, frontend.command_list_ok) = (
|
||||
frontend.command_list_ok, False)
|
||||
result = []
|
||||
for i, command in enumerate(command_list):
|
||||
response = frontend.handle_request(command, command_list_index=i)
|
||||
if response is not None:
|
||||
result.append(response)
|
||||
if response and response[-1].startswith(u'ACK'):
|
||||
return result
|
||||
(command_list, context.dispatcher.command_list) = (
|
||||
context.dispatcher.command_list, False)
|
||||
(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(u'ACK')):
|
||||
return command_list_response
|
||||
if command_list_ok:
|
||||
response.append(u'list_OK')
|
||||
return result
|
||||
command_list_response.append(u'list_OK')
|
||||
return command_list_response
|
||||
|
||||
@handle_pattern(r'^command_list_ok_begin$')
|
||||
def command_list_ok_begin(frontend):
|
||||
@handle_request(r'^command_list_ok_begin$')
|
||||
def command_list_ok_begin(context):
|
||||
"""See :meth:`command_list_begin()`."""
|
||||
frontend.command_list = []
|
||||
frontend.command_list_ok = True
|
||||
context.dispatcher.command_list = []
|
||||
context.dispatcher.command_list_ok = True
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.exceptions import MpdPasswordError
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import (MpdPasswordError,
|
||||
MpdPermissionError)
|
||||
|
||||
@handle_pattern(r'^close$')
|
||||
def close(frontend):
|
||||
@handle_request(r'^close$', auth_required=False)
|
||||
def close(context):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
@ -11,10 +12,10 @@ def close(frontend):
|
||||
|
||||
Closes the connection to MPD.
|
||||
"""
|
||||
pass # TODO
|
||||
context.session.close()
|
||||
|
||||
@handle_pattern(r'^kill$')
|
||||
def kill(frontend):
|
||||
@handle_request(r'^kill$')
|
||||
def kill(context):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
@ -22,10 +23,10 @@ def kill(frontend):
|
||||
|
||||
Kills MPD.
|
||||
"""
|
||||
pass # TODO
|
||||
raise MpdPermissionError(command=u'kill')
|
||||
|
||||
@handle_pattern(r'^password "(?P<password>[^"]+)"$')
|
||||
def password_(frontend, password):
|
||||
@handle_request(r'^password "(?P<password>[^"]+)"$', auth_required=False)
|
||||
def password_(context, password):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
@ -34,14 +35,13 @@ def password_(frontend, password):
|
||||
This is used for authentication with the server. ``PASSWORD`` is
|
||||
simply the plaintext password.
|
||||
"""
|
||||
# You will not get to this code without being authenticated. This is for
|
||||
# when you are already authenticated, and are sending additional 'password'
|
||||
# requests.
|
||||
if settings.MPD_SERVER_PASSWORD != password:
|
||||
if password == settings.MPD_SERVER_PASSWORD:
|
||||
context.dispatcher.authenticated = True
|
||||
else:
|
||||
raise MpdPasswordError(u'incorrect password', command=u'password')
|
||||
|
||||
@handle_pattern(r'^ping$')
|
||||
def ping(frontend):
|
||||
@handle_request(r'^ping$', auth_required=False)
|
||||
def ping(context):
|
||||
"""
|
||||
*musicpd.org, connection section:*
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.translator import tracks_to_mpd_format
|
||||
|
||||
@handle_pattern(r'^add "(?P<uri>[^"]*)"$')
|
||||
def add(frontend, uri):
|
||||
@handle_request(r'^add "(?P<uri>[^"]*)"$')
|
||||
def add(context, uri):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -19,17 +19,17 @@ def add(frontend, uri):
|
||||
"""
|
||||
if not uri:
|
||||
return
|
||||
for handler_prefix in frontend.backend.uri_handlers.get():
|
||||
for handler_prefix in context.backend.uri_handlers.get():
|
||||
if uri.startswith(handler_prefix):
|
||||
track = frontend.backend.library.lookup(uri).get()
|
||||
track = context.backend.library.lookup(uri).get()
|
||||
if track is not None:
|
||||
frontend.backend.current_playlist.add(track)
|
||||
context.backend.current_playlist.add(track)
|
||||
return
|
||||
raise MpdNoExistError(
|
||||
u'directory or file not found', command=u'add')
|
||||
|
||||
@handle_pattern(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
def addid(frontend, uri, songpos=None):
|
||||
@handle_request(r'^addid "(?P<uri>[^"]*)"( "(?P<songpos>\d+)")*$')
|
||||
def addid(context, uri, songpos=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -51,18 +51,18 @@ def addid(frontend, uri, songpos=None):
|
||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||
if songpos is not None:
|
||||
songpos = int(songpos)
|
||||
track = frontend.backend.library.lookup(uri).get()
|
||||
track = context.backend.library.lookup(uri).get()
|
||||
if track is None:
|
||||
raise MpdNoExistError(u'No such song', command=u'addid')
|
||||
if songpos and songpos > len(
|
||||
frontend.backend.current_playlist.tracks.get()):
|
||||
context.backend.current_playlist.tracks.get()):
|
||||
raise MpdArgError(u'Bad song index', command=u'addid')
|
||||
cp_track = frontend.backend.current_playlist.add(track,
|
||||
cp_track = context.backend.current_playlist.add(track,
|
||||
at_position=songpos).get()
|
||||
return ('Id', cp_track[0])
|
||||
return ('Id', cp_track.cpid)
|
||||
|
||||
@handle_pattern(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def delete_range(frontend, start, end=None):
|
||||
@handle_request(r'^delete "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def delete_range(context, start, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -74,25 +74,25 @@ def delete_range(frontend, start, end=None):
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
else:
|
||||
end = len(frontend.backend.current_playlist.tracks.get())
|
||||
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()[start:end]
|
||||
end = len(context.backend.current_playlist.tracks.get())
|
||||
cp_tracks = context.backend.current_playlist.cp_tracks.get()[start:end]
|
||||
if not cp_tracks:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
for (cpid, _) in cp_tracks:
|
||||
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
context.backend.current_playlist.remove(cpid=cpid)
|
||||
|
||||
@handle_pattern(r'^delete "(?P<songpos>\d+)"$')
|
||||
def delete_songpos(frontend, songpos):
|
||||
@handle_request(r'^delete "(?P<songpos>\d+)"$')
|
||||
def delete_songpos(context, songpos):
|
||||
"""See :meth:`delete_range`"""
|
||||
try:
|
||||
songpos = int(songpos)
|
||||
(cpid, _) = frontend.backend.current_playlist.cp_tracks.get()[songpos]
|
||||
frontend.backend.current_playlist.remove(cpid=cpid)
|
||||
(cpid, _) = context.backend.current_playlist.cp_tracks.get()[songpos]
|
||||
context.backend.current_playlist.remove(cpid=cpid)
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'delete')
|
||||
|
||||
@handle_pattern(r'^deleteid "(?P<cpid>\d+)"$')
|
||||
def deleteid(frontend, cpid):
|
||||
@handle_request(r'^deleteid "(?P<cpid>\d+)"$')
|
||||
def deleteid(context, cpid):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -102,14 +102,14 @@ def deleteid(frontend, cpid):
|
||||
"""
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
if frontend.backend.playback.current_cpid.get() == cpid:
|
||||
frontend.backend.playback.next()
|
||||
return frontend.backend.current_playlist.remove(cpid=cpid).get()
|
||||
if context.backend.playback.current_cpid.get() == cpid:
|
||||
context.backend.playback.next()
|
||||
return context.backend.current_playlist.remove(cpid=cpid).get()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'deleteid')
|
||||
|
||||
@handle_pattern(r'^clear$')
|
||||
def clear(frontend):
|
||||
@handle_request(r'^clear$')
|
||||
def clear(context):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -117,10 +117,10 @@ def clear(frontend):
|
||||
|
||||
Clears the current playlist.
|
||||
"""
|
||||
frontend.backend.current_playlist.clear()
|
||||
context.backend.current_playlist.clear()
|
||||
|
||||
@handle_pattern(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
||||
def move_range(frontend, start, to, end=None):
|
||||
@handle_request(r'^move "(?P<start>\d+):(?P<end>\d+)*" "(?P<to>\d+)"$')
|
||||
def move_range(context, start, to, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -130,21 +130,21 @@ def move_range(frontend, start, to, end=None):
|
||||
``TO`` in the playlist.
|
||||
"""
|
||||
if end is None:
|
||||
end = len(frontend.backend.current_playlist.tracks.get())
|
||||
end = len(context.backend.current_playlist.tracks.get())
|
||||
start = int(start)
|
||||
end = int(end)
|
||||
to = int(to)
|
||||
frontend.backend.current_playlist.move(start, end, to)
|
||||
context.backend.current_playlist.move(start, end, to)
|
||||
|
||||
@handle_pattern(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
||||
def move_songpos(frontend, songpos, to):
|
||||
@handle_request(r'^move "(?P<songpos>\d+)" "(?P<to>\d+)"$')
|
||||
def move_songpos(context, songpos, to):
|
||||
"""See :meth:`move_range`."""
|
||||
songpos = int(songpos)
|
||||
to = int(to)
|
||||
frontend.backend.current_playlist.move(songpos, songpos + 1, to)
|
||||
context.backend.current_playlist.move(songpos, songpos + 1, to)
|
||||
|
||||
@handle_pattern(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
|
||||
def moveid(frontend, cpid, to):
|
||||
@handle_request(r'^moveid "(?P<cpid>\d+)" "(?P<to>\d+)"$')
|
||||
def moveid(context, cpid, to):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -156,13 +156,13 @@ def moveid(frontend, cpid, to):
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
to = int(to)
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
frontend.backend.current_playlist.move(position, position + 1, to)
|
||||
context.backend.current_playlist.move(position, position + 1, to)
|
||||
|
||||
@handle_pattern(r'^playlist$')
|
||||
def playlist(frontend):
|
||||
@handle_request(r'^playlist$')
|
||||
def playlist(context):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -174,11 +174,11 @@ def playlist(frontend):
|
||||
|
||||
Do not use this, instead use ``playlistinfo``.
|
||||
"""
|
||||
return playlistinfo(frontend)
|
||||
return playlistinfo(context)
|
||||
|
||||
@handle_pattern(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
|
||||
@handle_pattern(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
def playlistfind(frontend, tag, needle):
|
||||
@handle_request(r'^playlistfind (?P<tag>[^"]+) "(?P<needle>[^"]+)"$')
|
||||
@handle_request(r'^playlistfind "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
def playlistfind(context, tag, needle):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -192,17 +192,17 @@ def playlistfind(frontend, tag, needle):
|
||||
"""
|
||||
if tag == 'filename':
|
||||
try:
|
||||
cp_track = frontend.backend.current_playlist.get(uri=needle).get()
|
||||
cp_track = context.backend.current_playlist.get(uri=needle).get()
|
||||
(cpid, track) = cp_track
|
||||
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
return track.mpd_format(cpid=cpid, position=position)
|
||||
except LookupError:
|
||||
return None
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistid( "(?P<cpid>\d+)")*$')
|
||||
def playlistid(frontend, cpid=None):
|
||||
@handle_request(r'^playlistid( "(?P<cpid>\d+)")*$')
|
||||
def playlistid(context, cpid=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -214,22 +214,22 @@ def playlistid(frontend, cpid=None):
|
||||
if cpid is not None:
|
||||
try:
|
||||
cpid = int(cpid)
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = frontend.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
|
||||
position = context.backend.current_playlist.cp_tracks.get().index(
|
||||
cp_track)
|
||||
return cp_track[1].mpd_format(position=position, cpid=cpid)
|
||||
return cp_track.track.mpd_format(position=position, cpid=cpid)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playlistid')
|
||||
else:
|
||||
cpids = [ct[0] for ct in
|
||||
frontend.backend.current_playlist.cp_tracks.get()]
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
frontend.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
context.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
|
||||
@handle_pattern(r'^playlistinfo$')
|
||||
@handle_pattern(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||
@handle_pattern(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def playlistinfo(frontend, songpos=None,
|
||||
@handle_request(r'^playlistinfo$')
|
||||
@handle_request(r'^playlistinfo "(?P<songpos>-?\d+)"$')
|
||||
@handle_request(r'^playlistinfo "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def playlistinfo(context, songpos=None,
|
||||
start=None, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
@ -255,30 +255,30 @@ def playlistinfo(frontend, songpos=None,
|
||||
if start == -1:
|
||||
end = None
|
||||
cpids = [ct[0] for ct in
|
||||
frontend.backend.current_playlist.cp_tracks.get()]
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
frontend.backend.current_playlist.tracks.get(),
|
||||
context.backend.current_playlist.tracks.get(),
|
||||
start, end, cpids=cpids)
|
||||
else:
|
||||
if start is None:
|
||||
start = 0
|
||||
start = int(start)
|
||||
if not (0 <= start <= len(
|
||||
frontend.backend.current_playlist.tracks.get())):
|
||||
context.backend.current_playlist.tracks.get())):
|
||||
raise MpdArgError(u'Bad song index', command=u'playlistinfo')
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
if end > len(frontend.backend.current_playlist.tracks.get()):
|
||||
if end > len(context.backend.current_playlist.tracks.get()):
|
||||
end = None
|
||||
cpids = [ct[0] for ct in
|
||||
frontend.backend.current_playlist.cp_tracks.get()]
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
frontend.backend.current_playlist.tracks.get(),
|
||||
context.backend.current_playlist.tracks.get(),
|
||||
start, end, cpids=cpids)
|
||||
|
||||
@handle_pattern(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
@handle_pattern(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
|
||||
def playlistsearch(frontend, tag, needle):
|
||||
@handle_request(r'^playlistsearch "(?P<tag>[^"]+)" "(?P<needle>[^"]+)"$')
|
||||
@handle_request(r'^playlistsearch (?P<tag>\S+) "(?P<needle>[^"]+)"$')
|
||||
def playlistsearch(context, tag, needle):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -294,9 +294,9 @@ def playlistsearch(frontend, tag, needle):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^plchanges (?P<version>-?\d+)$')
|
||||
@handle_pattern(r'^plchanges "(?P<version>-?\d+)"$')
|
||||
def plchanges(frontend, version):
|
||||
@handle_request(r'^plchanges (?P<version>-?\d+)$')
|
||||
@handle_request(r'^plchanges "(?P<version>-?\d+)"$')
|
||||
def plchanges(context, version):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -312,14 +312,14 @@ def plchanges(frontend, version):
|
||||
- Calls ``plchanges "-1"`` two times per second to get the entire playlist.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) < frontend.backend.current_playlist.version:
|
||||
if int(version) < context.backend.current_playlist.version:
|
||||
cpids = [ct[0] for ct in
|
||||
frontend.backend.current_playlist.cp_tracks.get()]
|
||||
context.backend.current_playlist.cp_tracks.get()]
|
||||
return tracks_to_mpd_format(
|
||||
frontend.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
context.backend.current_playlist.tracks.get(), cpids=cpids)
|
||||
|
||||
@handle_pattern(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
def plchangesposid(frontend, version):
|
||||
@handle_request(r'^plchangesposid "(?P<version>\d+)"$')
|
||||
def plchangesposid(context, version):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -333,17 +333,17 @@ def plchangesposid(frontend, version):
|
||||
``playlistlength`` returned by status command.
|
||||
"""
|
||||
# XXX Naive implementation that returns all tracks as changed
|
||||
if int(version) != frontend.backend.current_playlist.version.get():
|
||||
if int(version) != context.backend.current_playlist.version.get():
|
||||
result = []
|
||||
for (position, (cpid, _)) in enumerate(
|
||||
frontend.backend.current_playlist.cp_tracks.get()):
|
||||
context.backend.current_playlist.cp_tracks.get()):
|
||||
result.append((u'cpos', position))
|
||||
result.append((u'Id', cpid))
|
||||
return result
|
||||
|
||||
@handle_pattern(r'^shuffle$')
|
||||
@handle_pattern(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def shuffle(frontend, start=None, end=None):
|
||||
@handle_request(r'^shuffle$')
|
||||
@handle_request(r'^shuffle "(?P<start>\d+):(?P<end>\d+)*"$')
|
||||
def shuffle(context, start=None, end=None):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -356,10 +356,10 @@ def shuffle(frontend, start=None, end=None):
|
||||
start = int(start)
|
||||
if end is not None:
|
||||
end = int(end)
|
||||
frontend.backend.current_playlist.shuffle(start, end)
|
||||
context.backend.current_playlist.shuffle(start, end)
|
||||
|
||||
@handle_pattern(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||
def swap(frontend, songpos1, songpos2):
|
||||
@handle_request(r'^swap "(?P<songpos1>\d+)" "(?P<songpos2>\d+)"$')
|
||||
def swap(context, songpos1, songpos2):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -369,18 +369,18 @@ def swap(frontend, songpos1, songpos2):
|
||||
"""
|
||||
songpos1 = int(songpos1)
|
||||
songpos2 = int(songpos2)
|
||||
tracks = frontend.backend.current_playlist.tracks.get()
|
||||
tracks = context.backend.current_playlist.tracks.get()
|
||||
song1 = tracks[songpos1]
|
||||
song2 = tracks[songpos2]
|
||||
del tracks[songpos1]
|
||||
tracks.insert(songpos1, song2)
|
||||
del tracks[songpos2]
|
||||
tracks.insert(songpos2, song1)
|
||||
frontend.backend.current_playlist.clear()
|
||||
frontend.backend.current_playlist.append(tracks)
|
||||
context.backend.current_playlist.clear()
|
||||
context.backend.current_playlist.append(tracks)
|
||||
|
||||
@handle_pattern(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||
def swapid(frontend, cpid1, cpid2):
|
||||
@handle_request(r'^swapid "(?P<cpid1>\d+)" "(?P<cpid2>\d+)"$')
|
||||
def swapid(context, cpid1, cpid2):
|
||||
"""
|
||||
*musicpd.org, current playlist section:*
|
||||
|
||||
@ -390,9 +390,9 @@ def swapid(frontend, cpid1, cpid2):
|
||||
"""
|
||||
cpid1 = int(cpid1)
|
||||
cpid2 = int(cpid2)
|
||||
cp_track1 = frontend.backend.current_playlist.get(cpid=cpid1).get()
|
||||
cp_track2 = frontend.backend.current_playlist.get(cpid=cpid2).get()
|
||||
cp_tracks = frontend.backend.current_playlist.cp_tracks.get()
|
||||
cp_track1 = context.backend.current_playlist.get(cpid=cpid1).get()
|
||||
cp_track2 = context.backend.current_playlist.get(cpid=cpid2).get()
|
||||
cp_tracks = context.backend.current_playlist.cp_tracks.get()
|
||||
position1 = cp_tracks.index(cp_track1)
|
||||
position2 = cp_tracks.index(cp_track2)
|
||||
swap(frontend, position1, position2)
|
||||
swap(context, position1, position2)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
|
||||
@handle_pattern(r'^$')
|
||||
def empty(frontend):
|
||||
@handle_request(r'^$')
|
||||
def empty(context):
|
||||
"""The original MPD server returns ``OK`` on an empty request."""
|
||||
pass
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import re
|
||||
import shlex
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern, stored_playlists
|
||||
from mopidy.frontends.mpd.protocol import handle_request, stored_playlists
|
||||
from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented
|
||||
|
||||
def _build_query(mpd_query):
|
||||
@ -28,8 +28,8 @@ def _build_query(mpd_query):
|
||||
query[field] = [what]
|
||||
return query
|
||||
|
||||
@handle_pattern(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||
def count(frontend, tag, needle):
|
||||
@handle_request(r'^count "(?P<tag>[^"]+)" "(?P<needle>[^"]*)"$')
|
||||
def count(context, tag, needle):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -40,10 +40,10 @@ def count(frontend, tag, needle):
|
||||
"""
|
||||
return [('songs', 0), ('playtime', 0)] # TODO
|
||||
|
||||
@handle_pattern(r'^find '
|
||||
@handle_request(r'^find '
|
||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||
def find(frontend, mpd_query):
|
||||
def find(context, mpd_query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -68,12 +68,12 @@ def find(frontend, mpd_query):
|
||||
- also uses the search type "date".
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return frontend.backend.library.find_exact(**query).get().mpd_format()
|
||||
return context.backend.library.find_exact(**query).get().mpd_format()
|
||||
|
||||
@handle_pattern(r'^findadd '
|
||||
@handle_request(r'^findadd '
|
||||
r'(?P<query>("?([Aa]lbum|[Aa]rtist|[Ff]ilename|[Tt]itle|[Aa]ny)"? '
|
||||
'"[^"]+"\s?)+)$')
|
||||
def findadd(frontend, query):
|
||||
def findadd(context, query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -84,11 +84,11 @@ def findadd(frontend, query):
|
||||
``WHAT`` is what to find.
|
||||
"""
|
||||
# TODO Add result to current playlist
|
||||
#result = frontend.find(query)
|
||||
#result = context.find(query)
|
||||
|
||||
@handle_pattern(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
|
||||
@handle_request(r'^list "?(?P<field>([Aa]rtist|[Aa]lbum|[Dd]ate|[Gg]enre))"?'
|
||||
'( (?P<mpd_query>.*))?$')
|
||||
def list_(frontend, field, mpd_query=None):
|
||||
def list_(context, field, mpd_query=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -175,11 +175,11 @@ def list_(frontend, field, mpd_query=None):
|
||||
field = field.lower()
|
||||
query = _list_build_query(field, mpd_query)
|
||||
if field == u'artist':
|
||||
return _list_artist(frontend, query)
|
||||
return _list_artist(context, query)
|
||||
elif field == u'album':
|
||||
return _list_album(frontend, query)
|
||||
return _list_album(context, query)
|
||||
elif field == u'date':
|
||||
return _list_date(frontend, query)
|
||||
return _list_date(context, query)
|
||||
elif field == u'genre':
|
||||
pass # TODO We don't have genre in our internal data structures yet
|
||||
|
||||
@ -213,32 +213,32 @@ def _list_build_query(field, mpd_query):
|
||||
else:
|
||||
raise MpdArgError(u'not able to parse args', command=u'list')
|
||||
|
||||
def _list_artist(frontend, query):
|
||||
def _list_artist(context, query):
|
||||
artists = set()
|
||||
playlist = frontend.backend.library.find_exact(**query).get()
|
||||
playlist = context.backend.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
for artist in track.artists:
|
||||
artists.add((u'Artist', artist.name))
|
||||
return artists
|
||||
|
||||
def _list_album(frontend, query):
|
||||
def _list_album(context, query):
|
||||
albums = set()
|
||||
playlist = frontend.backend.library.find_exact(**query).get()
|
||||
playlist = context.backend.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
if track.album is not None:
|
||||
albums.add((u'Album', track.album.name))
|
||||
return albums
|
||||
|
||||
def _list_date(frontend, query):
|
||||
def _list_date(context, query):
|
||||
dates = set()
|
||||
playlist = frontend.backend.library.find_exact(**query).get()
|
||||
playlist = context.backend.library.find_exact(**query).get()
|
||||
for track in playlist.tracks:
|
||||
if track.date is not None:
|
||||
dates.add((u'Date', track.date.strftime('%Y-%m-%d')))
|
||||
return dates
|
||||
|
||||
@handle_pattern(r'^listall "(?P<uri>[^"]+)"')
|
||||
def listall(frontend, uri):
|
||||
@handle_request(r'^listall "(?P<uri>[^"]+)"')
|
||||
def listall(context, uri):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -248,8 +248,8 @@ def listall(frontend, uri):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^listallinfo "(?P<uri>[^"]+)"')
|
||||
def listallinfo(frontend, uri):
|
||||
@handle_request(r'^listallinfo "(?P<uri>[^"]+)"')
|
||||
def listallinfo(context, uri):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -260,9 +260,9 @@ def listallinfo(frontend, uri):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^lsinfo$')
|
||||
@handle_pattern(r'^lsinfo "(?P<uri>[^"]*)"$')
|
||||
def lsinfo(frontend, uri=None):
|
||||
@handle_request(r'^lsinfo$')
|
||||
@handle_request(r'^lsinfo "(?P<uri>[^"]*)"$')
|
||||
def lsinfo(context, uri=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -279,11 +279,11 @@ def lsinfo(frontend, uri=None):
|
||||
""``, and ``lsinfo "/"``.
|
||||
"""
|
||||
if uri is None or uri == u'/' or uri == u'':
|
||||
return stored_playlists.listplaylists(frontend)
|
||||
return stored_playlists.listplaylists(context)
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rescan( "(?P<uri>[^"]+)")*$')
|
||||
def rescan(frontend, uri=None):
|
||||
@handle_request(r'^rescan( "(?P<uri>[^"]+)")*$')
|
||||
def rescan(context, uri=None):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -291,12 +291,12 @@ def rescan(frontend, uri=None):
|
||||
|
||||
Same as ``update``, but also rescans unmodified files.
|
||||
"""
|
||||
return update(frontend, uri, rescan_unmodified_files=True)
|
||||
return update(context, uri, rescan_unmodified_files=True)
|
||||
|
||||
@handle_pattern(r'^search '
|
||||
@handle_request(r'^search '
|
||||
r'(?P<mpd_query>("?([Aa]lbum|[Aa]rtist|[Dd]ate|[Ff]ilename|'
|
||||
r'[Tt]itle|[Aa]ny)"? "[^"]+"\s?)+)$')
|
||||
def search(frontend, mpd_query):
|
||||
def search(context, mpd_query):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
@ -324,10 +324,10 @@ def search(frontend, mpd_query):
|
||||
- also uses the search type "date".
|
||||
"""
|
||||
query = _build_query(mpd_query)
|
||||
return frontend.backend.library.search(**query).get().mpd_format()
|
||||
return context.backend.library.search(**query).get().mpd_format()
|
||||
|
||||
@handle_pattern(r'^update( "(?P<uri>[^"]+)")*$')
|
||||
def update(frontend, uri=None, rescan_unmodified_files=False):
|
||||
@handle_request(r'^update( "(?P<uri>[^"]+)")*$')
|
||||
def update(context, uri=None, rescan_unmodified_files=False):
|
||||
"""
|
||||
*musicpd.org, music database section:*
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
from mopidy.backends.base import PlaybackController
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import (MpdArgError, MpdNoExistError,
|
||||
MpdNotImplemented)
|
||||
|
||||
@handle_pattern(r'^consume (?P<state>[01])$')
|
||||
@handle_pattern(r'^consume "(?P<state>[01])"$')
|
||||
def consume(frontend, state):
|
||||
@handle_request(r'^consume (?P<state>[01])$')
|
||||
@handle_request(r'^consume "(?P<state>[01])"$')
|
||||
def consume(context, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -16,12 +16,12 @@ def consume(frontend, state):
|
||||
playlist.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.consume = True
|
||||
context.backend.playback.consume = True
|
||||
else:
|
||||
frontend.backend.playback.consume = False
|
||||
context.backend.playback.consume = False
|
||||
|
||||
@handle_pattern(r'^crossfade "(?P<seconds>\d+)"$')
|
||||
def crossfade(frontend, seconds):
|
||||
@handle_request(r'^crossfade "(?P<seconds>\d+)"$')
|
||||
def crossfade(context, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -32,8 +32,8 @@ def crossfade(frontend, seconds):
|
||||
seconds = int(seconds)
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^next$')
|
||||
def next_(frontend):
|
||||
@handle_request(r'^next$')
|
||||
def next_(context):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -87,11 +87,11 @@ def next_(frontend):
|
||||
order as the first time.
|
||||
|
||||
"""
|
||||
return frontend.backend.playback.next().get()
|
||||
return context.backend.playback.next().get()
|
||||
|
||||
@handle_pattern(r'^pause$')
|
||||
@handle_pattern(r'^pause "(?P<state>[01])"$')
|
||||
def pause(frontend, state=None):
|
||||
@handle_request(r'^pause$')
|
||||
@handle_request(r'^pause "(?P<state>[01])"$')
|
||||
def pause(context, state=None):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -104,28 +104,28 @@ def pause(frontend, state=None):
|
||||
- Calls ``pause`` without any arguments to toogle pause.
|
||||
"""
|
||||
if state is None:
|
||||
if (frontend.backend.playback.state.get() ==
|
||||
if (context.backend.playback.state.get() ==
|
||||
PlaybackController.PLAYING):
|
||||
frontend.backend.playback.pause()
|
||||
elif (frontend.backend.playback.state.get() ==
|
||||
context.backend.playback.pause()
|
||||
elif (context.backend.playback.state.get() ==
|
||||
PlaybackController.PAUSED):
|
||||
frontend.backend.playback.resume()
|
||||
context.backend.playback.resume()
|
||||
elif int(state):
|
||||
frontend.backend.playback.pause()
|
||||
context.backend.playback.pause()
|
||||
else:
|
||||
frontend.backend.playback.resume()
|
||||
context.backend.playback.resume()
|
||||
|
||||
@handle_pattern(r'^play$')
|
||||
def play(frontend):
|
||||
@handle_request(r'^play$')
|
||||
def play(context):
|
||||
"""
|
||||
The original MPD server resumes from the paused state on ``play``
|
||||
without arguments.
|
||||
"""
|
||||
return frontend.backend.playback.play().get()
|
||||
return context.backend.playback.play().get()
|
||||
|
||||
@handle_pattern(r'^playid "(?P<cpid>\d+)"$')
|
||||
@handle_pattern(r'^playid "(?P<cpid>-1)"$')
|
||||
def playid(frontend, cpid):
|
||||
@handle_request(r'^playid "(?P<cpid>\d+)"$')
|
||||
@handle_request(r'^playid "(?P<cpid>-1)"$')
|
||||
def playid(context, cpid):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -144,16 +144,16 @@ def playid(frontend, cpid):
|
||||
"""
|
||||
cpid = int(cpid)
|
||||
if cpid == -1:
|
||||
return _play_minus_one(frontend)
|
||||
return _play_minus_one(context)
|
||||
try:
|
||||
cp_track = frontend.backend.current_playlist.get(cpid=cpid).get()
|
||||
return frontend.backend.playback.play(cp_track).get()
|
||||
cp_track = context.backend.current_playlist.get(cpid=cpid).get()
|
||||
return context.backend.playback.play(cp_track).get()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such song', command=u'playid')
|
||||
|
||||
@handle_pattern(r'^play (?P<songpos>-?\d+)$')
|
||||
@handle_pattern(r'^play "(?P<songpos>-?\d+)"$')
|
||||
def playpos(frontend, songpos):
|
||||
@handle_request(r'^play (?P<songpos>-?\d+)$')
|
||||
@handle_request(r'^play "(?P<songpos>-?\d+)"$')
|
||||
def playpos(context, songpos):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -176,29 +176,29 @@ def playpos(frontend, songpos):
|
||||
"""
|
||||
songpos = int(songpos)
|
||||
if songpos == -1:
|
||||
return _play_minus_one(frontend)
|
||||
return _play_minus_one(context)
|
||||
try:
|
||||
cp_track = frontend.backend.current_playlist.cp_tracks.get()[songpos]
|
||||
return frontend.backend.playback.play(cp_track).get()
|
||||
cp_track = context.backend.current_playlist.cp_tracks.get()[songpos]
|
||||
return context.backend.playback.play(cp_track).get()
|
||||
except IndexError:
|
||||
raise MpdArgError(u'Bad song index', command=u'play')
|
||||
|
||||
def _play_minus_one(frontend):
|
||||
if (frontend.backend.playback.state.get() == PlaybackController.PLAYING):
|
||||
def _play_minus_one(context):
|
||||
if (context.backend.playback.state.get() == PlaybackController.PLAYING):
|
||||
return # Nothing to do
|
||||
elif (frontend.backend.playback.state.get() == PlaybackController.PAUSED):
|
||||
return frontend.backend.playback.resume().get()
|
||||
elif frontend.backend.playback.current_cp_track.get() is not None:
|
||||
cp_track = frontend.backend.playback.current_cp_track.get()
|
||||
return frontend.backend.playback.play(cp_track).get()
|
||||
elif frontend.backend.current_playlist.cp_tracks.get():
|
||||
cp_track = frontend.backend.current_playlist.cp_tracks.get()[0]
|
||||
return frontend.backend.playback.play(cp_track).get()
|
||||
elif (context.backend.playback.state.get() == PlaybackController.PAUSED):
|
||||
return context.backend.playback.resume().get()
|
||||
elif context.backend.playback.current_cp_track.get() is not None:
|
||||
cp_track = context.backend.playback.current_cp_track.get()
|
||||
return context.backend.playback.play(cp_track).get()
|
||||
elif context.backend.current_playlist.cp_tracks.get():
|
||||
cp_track = context.backend.current_playlist.cp_tracks.get()[0]
|
||||
return context.backend.playback.play(cp_track).get()
|
||||
else:
|
||||
return # Fail silently
|
||||
|
||||
@handle_pattern(r'^previous$')
|
||||
def previous(frontend):
|
||||
@handle_request(r'^previous$')
|
||||
def previous(context):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -241,11 +241,11 @@ def previous(frontend):
|
||||
``previous`` should do a seek to time position 0.
|
||||
|
||||
"""
|
||||
return frontend.backend.playback.previous().get()
|
||||
return context.backend.playback.previous().get()
|
||||
|
||||
@handle_pattern(r'^random (?P<state>[01])$')
|
||||
@handle_pattern(r'^random "(?P<state>[01])"$')
|
||||
def random(frontend, state):
|
||||
@handle_request(r'^random (?P<state>[01])$')
|
||||
@handle_request(r'^random "(?P<state>[01])"$')
|
||||
def random(context, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -254,13 +254,13 @@ def random(frontend, state):
|
||||
Sets random state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.random = True
|
||||
context.backend.playback.random = True
|
||||
else:
|
||||
frontend.backend.playback.random = False
|
||||
context.backend.playback.random = False
|
||||
|
||||
@handle_pattern(r'^repeat (?P<state>[01])$')
|
||||
@handle_pattern(r'^repeat "(?P<state>[01])"$')
|
||||
def repeat(frontend, state):
|
||||
@handle_request(r'^repeat (?P<state>[01])$')
|
||||
@handle_request(r'^repeat "(?P<state>[01])"$')
|
||||
def repeat(context, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -269,12 +269,12 @@ def repeat(frontend, state):
|
||||
Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.repeat = True
|
||||
context.backend.playback.repeat = True
|
||||
else:
|
||||
frontend.backend.playback.repeat = False
|
||||
context.backend.playback.repeat = False
|
||||
|
||||
@handle_pattern(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
|
||||
def replay_gain_mode(frontend, mode):
|
||||
@handle_request(r'^replay_gain_mode "(?P<mode>(off|track|album))"$')
|
||||
def replay_gain_mode(context, mode):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -289,8 +289,8 @@ def replay_gain_mode(frontend, mode):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^replay_gain_status$')
|
||||
def replay_gain_status(frontend):
|
||||
@handle_request(r'^replay_gain_status$')
|
||||
def replay_gain_status(context):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -301,9 +301,9 @@ def replay_gain_status(frontend):
|
||||
"""
|
||||
return u'off' # TODO
|
||||
|
||||
@handle_pattern(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$')
|
||||
@handle_pattern(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seek(frontend, songpos, seconds):
|
||||
@handle_request(r'^seek (?P<songpos>\d+) (?P<seconds>\d+)$')
|
||||
@handle_request(r'^seek "(?P<songpos>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seek(context, songpos, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -316,12 +316,12 @@ def seek(frontend, songpos, seconds):
|
||||
|
||||
- issues ``seek 1 120`` without quotes around the arguments.
|
||||
"""
|
||||
if frontend.backend.playback.current_playlist_position != songpos:
|
||||
playpos(frontend, songpos)
|
||||
frontend.backend.playback.seek(int(seconds) * 1000)
|
||||
if context.backend.playback.current_playlist_position != songpos:
|
||||
playpos(context, songpos)
|
||||
context.backend.playback.seek(int(seconds) * 1000)
|
||||
|
||||
@handle_pattern(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seekid(frontend, cpid, seconds):
|
||||
@handle_request(r'^seekid "(?P<cpid>\d+)" "(?P<seconds>\d+)"$')
|
||||
def seekid(context, cpid, seconds):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -329,13 +329,13 @@ def seekid(frontend, cpid, seconds):
|
||||
|
||||
Seeks to the position ``TIME`` (in seconds) of song ``SONGID``.
|
||||
"""
|
||||
if frontend.backend.playback.current_cpid != cpid:
|
||||
playid(frontend, cpid)
|
||||
frontend.backend.playback.seek(int(seconds) * 1000)
|
||||
if context.backend.playback.current_cpid != cpid:
|
||||
playid(context, cpid)
|
||||
context.backend.playback.seek(int(seconds) * 1000)
|
||||
|
||||
@handle_pattern(r'^setvol (?P<volume>[-+]*\d+)$')
|
||||
@handle_pattern(r'^setvol "(?P<volume>[-+]*\d+)"$')
|
||||
def setvol(frontend, volume):
|
||||
@handle_request(r'^setvol (?P<volume>[-+]*\d+)$')
|
||||
@handle_request(r'^setvol "(?P<volume>[-+]*\d+)"$')
|
||||
def setvol(context, volume):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -352,11 +352,11 @@ def setvol(frontend, volume):
|
||||
volume = 0
|
||||
if volume > 100:
|
||||
volume = 100
|
||||
frontend.mixer.volume = volume
|
||||
context.mixer.volume = volume
|
||||
|
||||
@handle_pattern(r'^single (?P<state>[01])$')
|
||||
@handle_pattern(r'^single "(?P<state>[01])"$')
|
||||
def single(frontend, state):
|
||||
@handle_request(r'^single (?P<state>[01])$')
|
||||
@handle_request(r'^single "(?P<state>[01])"$')
|
||||
def single(context, state):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -367,12 +367,12 @@ def single(frontend, state):
|
||||
song is repeated if the ``repeat`` mode is enabled.
|
||||
"""
|
||||
if int(state):
|
||||
frontend.backend.playback.single = True
|
||||
context.backend.playback.single = True
|
||||
else:
|
||||
frontend.backend.playback.single = False
|
||||
context.backend.playback.single = False
|
||||
|
||||
@handle_pattern(r'^stop$')
|
||||
def stop(frontend):
|
||||
@handle_request(r'^stop$')
|
||||
def stop(context):
|
||||
"""
|
||||
*musicpd.org, playback section:*
|
||||
|
||||
@ -380,4 +380,4 @@ def stop(frontend):
|
||||
|
||||
Stops playing.
|
||||
"""
|
||||
frontend.backend.playback.stop()
|
||||
context.backend.playback.stop()
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern, mpd_commands
|
||||
from mopidy.frontends.mpd.protocol import handle_request, mpd_commands
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^commands$')
|
||||
def commands(frontend):
|
||||
@handle_request(r'^commands$', auth_required=False)
|
||||
def commands(context):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
@ -10,25 +10,34 @@ def commands(frontend):
|
||||
|
||||
Shows which commands the current user has access to.
|
||||
"""
|
||||
# FIXME When password auth is turned on and the client is not
|
||||
# authenticated, 'commands' should list only the commands the client does
|
||||
# have access to. To implement this we need access to the session object to
|
||||
# check if the client is authenticated or not.
|
||||
if context.dispatcher.authenticated:
|
||||
command_names = [command.name for command in mpd_commands]
|
||||
else:
|
||||
command_names = [command.name for command in mpd_commands
|
||||
if not command.auth_required]
|
||||
|
||||
sorted_commands = sorted(list(mpd_commands))
|
||||
# No permission to use
|
||||
if 'kill' in command_names:
|
||||
command_names.remove('kill')
|
||||
|
||||
# Not shown by MPD in its command list
|
||||
sorted_commands.remove('command_list_begin')
|
||||
sorted_commands.remove('command_list_ok_begin')
|
||||
sorted_commands.remove('command_list_end')
|
||||
sorted_commands.remove('idle')
|
||||
sorted_commands.remove('noidle')
|
||||
sorted_commands.remove('sticker')
|
||||
if 'command_list_begin' in command_names:
|
||||
command_names.remove('command_list_begin')
|
||||
if 'command_list_ok_begin' in command_names:
|
||||
command_names.remove('command_list_ok_begin')
|
||||
if 'command_list_end' in command_names:
|
||||
command_names.remove('command_list_end')
|
||||
if 'idle' in command_names:
|
||||
command_names.remove('idle')
|
||||
if 'noidle' in command_names:
|
||||
command_names.remove('noidle')
|
||||
if 'sticker' in command_names:
|
||||
command_names.remove('sticker')
|
||||
|
||||
return [('command', c) for c in sorted_commands]
|
||||
return [('command', command_name) for command_name in sorted(command_names)]
|
||||
|
||||
@handle_pattern(r'^decoders$')
|
||||
def decoders(frontend):
|
||||
@handle_request(r'^decoders$')
|
||||
def decoders(context):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
@ -46,8 +55,8 @@ def decoders(frontend):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^notcommands$')
|
||||
def notcommands(frontend):
|
||||
@handle_request(r'^notcommands$', auth_required=False)
|
||||
def notcommands(context):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
@ -55,14 +64,19 @@ def notcommands(frontend):
|
||||
|
||||
Shows which commands the current user does not have access to.
|
||||
"""
|
||||
# FIXME When password auth is turned on and the client is not
|
||||
# authenticated, 'notcommands' should list all the commands the client does
|
||||
# not have access to. To implement this we need access to the session
|
||||
# object to check if the client is authenticated or not.
|
||||
pass
|
||||
if context.dispatcher.authenticated:
|
||||
command_names = []
|
||||
else:
|
||||
command_names = [command.name for command in mpd_commands
|
||||
if command.auth_required]
|
||||
|
||||
@handle_pattern(r'^tagtypes$')
|
||||
def tagtypes(frontend):
|
||||
# No permission to use
|
||||
command_names.append('kill')
|
||||
|
||||
return [('command', command_name) for command_name in sorted(command_names)]
|
||||
|
||||
@handle_request(r'^tagtypes$')
|
||||
def tagtypes(context):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
@ -72,8 +86,8 @@ def tagtypes(frontend):
|
||||
"""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^urlhandlers$')
|
||||
def urlhandlers(frontend):
|
||||
@handle_request(r'^urlhandlers$')
|
||||
def urlhandlers(context):
|
||||
"""
|
||||
*musicpd.org, reflection section:*
|
||||
|
||||
@ -81,4 +95,4 @@ def urlhandlers(frontend):
|
||||
|
||||
Gets a list of available URL handlers.
|
||||
"""
|
||||
return [(u'handler', uri) for uri in frontend.backend.uri_handlers.get()]
|
||||
return [(u'handler', uri) for uri in context.backend.uri_handlers.get()]
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import pykka.future
|
||||
|
||||
from mopidy.backends.base import PlaybackController
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^clearerror$')
|
||||
def clearerror(frontend):
|
||||
@handle_request(r'^clearerror$')
|
||||
def clearerror(context):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
@ -14,8 +16,8 @@ def clearerror(frontend):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^currentsong$')
|
||||
def currentsong(frontend):
|
||||
@handle_request(r'^currentsong$')
|
||||
def currentsong(context):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
@ -24,15 +26,15 @@ def currentsong(frontend):
|
||||
Displays the song info of the current song (same song that is
|
||||
identified in status).
|
||||
"""
|
||||
current_cp_track = frontend.backend.playback.current_cp_track.get()
|
||||
current_cp_track = context.backend.playback.current_cp_track.get()
|
||||
if current_cp_track is not None:
|
||||
return current_cp_track[1].mpd_format(
|
||||
position=frontend.backend.playback.current_playlist_position.get(),
|
||||
cpid=current_cp_track[0])
|
||||
return current_cp_track.track.mpd_format(
|
||||
position=context.backend.playback.current_playlist_position.get(),
|
||||
cpid=current_cp_track.cpid)
|
||||
|
||||
@handle_pattern(r'^idle$')
|
||||
@handle_pattern(r'^idle (?P<subsystems>.+)$')
|
||||
def idle(frontend, subsystems=None):
|
||||
@handle_request(r'^idle$')
|
||||
@handle_request(r'^idle (?P<subsystems>.+)$')
|
||||
def idle(context, subsystems=None):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
@ -67,13 +69,13 @@ def idle(frontend, subsystems=None):
|
||||
"""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^noidle$')
|
||||
def noidle(frontend):
|
||||
@handle_request(r'^noidle$')
|
||||
def noidle(context):
|
||||
"""See :meth:`_status_idle`."""
|
||||
pass # TODO
|
||||
|
||||
@handle_pattern(r'^stats$')
|
||||
def stats(frontend):
|
||||
@handle_request(r'^stats$')
|
||||
def stats(context):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
@ -98,8 +100,8 @@ def stats(frontend):
|
||||
'playtime': 0, # TODO
|
||||
}
|
||||
|
||||
@handle_pattern(r'^status$')
|
||||
def status(frontend):
|
||||
@handle_request(r'^status$')
|
||||
def status(context):
|
||||
"""
|
||||
*musicpd.org, status section:*
|
||||
|
||||
@ -130,65 +132,80 @@ def status(frontend):
|
||||
- ``updatings_db``: job id
|
||||
- ``error``: if there is an error, returns message here
|
||||
"""
|
||||
futures = {
|
||||
'current_playlist.tracks': context.backend.current_playlist.tracks,
|
||||
'current_playlist.version': context.backend.current_playlist.version,
|
||||
'mixer.volume': context.mixer.volume,
|
||||
'playback.consume': context.backend.playback.consume,
|
||||
'playback.random': context.backend.playback.random,
|
||||
'playback.repeat': context.backend.playback.repeat,
|
||||
'playback.single': context.backend.playback.single,
|
||||
'playback.state': context.backend.playback.state,
|
||||
'playback.current_cp_track': context.backend.playback.current_cp_track,
|
||||
'playback.current_playlist_position':
|
||||
context.backend.playback.current_playlist_position,
|
||||
'playback.time_position': context.backend.playback.time_position,
|
||||
}
|
||||
pykka.future.get_all(futures.values())
|
||||
result = [
|
||||
('volume', _status_volume(frontend)),
|
||||
('repeat', _status_repeat(frontend)),
|
||||
('random', _status_random(frontend)),
|
||||
('single', _status_single(frontend)),
|
||||
('consume', _status_consume(frontend)),
|
||||
('playlist', _status_playlist_version(frontend)),
|
||||
('playlistlength', _status_playlist_length(frontend)),
|
||||
('xfade', _status_xfade(frontend)),
|
||||
('state', _status_state(frontend)),
|
||||
('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 frontend.backend.playback.current_track.get() is not None:
|
||||
result.append(('song', _status_songpos(frontend)))
|
||||
result.append(('songid', _status_songid(frontend)))
|
||||
if frontend.backend.playback.state.get() in (PlaybackController.PLAYING,
|
||||
if futures['playback.current_cp_track'].get() is not None:
|
||||
result.append(('song', _status_songpos(futures)))
|
||||
result.append(('songid', _status_songid(futures)))
|
||||
if futures['playback.state'].get() in (PlaybackController.PLAYING,
|
||||
PlaybackController.PAUSED):
|
||||
result.append(('time', _status_time(frontend)))
|
||||
result.append(('elapsed', _status_time_elapsed(frontend)))
|
||||
result.append(('bitrate', _status_bitrate(frontend)))
|
||||
result.append(('time', _status_time(futures)))
|
||||
result.append(('elapsed', _status_time_elapsed(futures)))
|
||||
result.append(('bitrate', _status_bitrate(futures)))
|
||||
return result
|
||||
|
||||
def _status_bitrate(frontend):
|
||||
current_track = frontend.backend.playback.current_track.get()
|
||||
if current_track is not None:
|
||||
return current_track.bitrate
|
||||
def _status_bitrate(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is not None:
|
||||
return current_cp_track.track.bitrate
|
||||
|
||||
def _status_consume(frontend):
|
||||
if frontend.backend.playback.consume.get():
|
||||
def _status_consume(futures):
|
||||
if futures['playback.consume'].get():
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_playlist_length(frontend):
|
||||
return len(frontend.backend.current_playlist.tracks.get())
|
||||
def _status_playlist_length(futures):
|
||||
return len(futures['current_playlist.tracks'].get())
|
||||
|
||||
def _status_playlist_version(frontend):
|
||||
return frontend.backend.current_playlist.version.get()
|
||||
def _status_playlist_version(futures):
|
||||
return futures['current_playlist.version'].get()
|
||||
|
||||
def _status_random(frontend):
|
||||
return int(frontend.backend.playback.random.get())
|
||||
def _status_random(futures):
|
||||
return int(futures['playback.random'].get())
|
||||
|
||||
def _status_repeat(frontend):
|
||||
return int(frontend.backend.playback.repeat.get())
|
||||
def _status_repeat(futures):
|
||||
return int(futures['playback.repeat'].get())
|
||||
|
||||
def _status_single(frontend):
|
||||
return int(frontend.backend.playback.single.get())
|
||||
def _status_single(futures):
|
||||
return int(futures['playback.single'].get())
|
||||
|
||||
def _status_songid(frontend):
|
||||
current_cpid = frontend.backend.playback.current_cpid.get()
|
||||
if current_cpid is not None:
|
||||
return current_cpid
|
||||
def _status_songid(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is not None:
|
||||
return current_cp_track.cpid
|
||||
else:
|
||||
return _status_songpos(frontend)
|
||||
return _status_songpos(futures)
|
||||
|
||||
def _status_songpos(frontend):
|
||||
return frontend.backend.playback.current_playlist_position.get()
|
||||
def _status_songpos(futures):
|
||||
return futures['playback.current_playlist_position'].get()
|
||||
|
||||
def _status_state(frontend):
|
||||
state = frontend.backend.playback.state.get()
|
||||
def _status_state(futures):
|
||||
state = futures['playback.state'].get()
|
||||
if state == PlaybackController.PLAYING:
|
||||
return u'play'
|
||||
elif state == PlaybackController.STOPPED:
|
||||
@ -196,28 +213,28 @@ def _status_state(frontend):
|
||||
elif state == PlaybackController.PAUSED:
|
||||
return u'pause'
|
||||
|
||||
def _status_time(frontend):
|
||||
return u'%s:%s' % (_status_time_elapsed(frontend) // 1000,
|
||||
_status_time_total(frontend) // 1000)
|
||||
def _status_time(futures):
|
||||
return u'%s:%s' % (_status_time_elapsed(futures) // 1000,
|
||||
_status_time_total(futures) // 1000)
|
||||
|
||||
def _status_time_elapsed(frontend):
|
||||
return frontend.backend.playback.time_position.get()
|
||||
def _status_time_elapsed(futures):
|
||||
return futures['playback.time_position'].get()
|
||||
|
||||
def _status_time_total(frontend):
|
||||
current_track = frontend.backend.playback.current_track.get()
|
||||
if current_track is None:
|
||||
def _status_time_total(futures):
|
||||
current_cp_track = futures['playback.current_cp_track'].get()
|
||||
if current_cp_track is None:
|
||||
return 0
|
||||
elif current_track.length is None:
|
||||
elif current_cp_track.track.length is None:
|
||||
return 0
|
||||
else:
|
||||
return current_track.length
|
||||
return current_cp_track.track.length
|
||||
|
||||
def _status_volume(frontend):
|
||||
volume = frontend.mixer.volume.get()
|
||||
def _status_volume(futures):
|
||||
volume = futures['mixer.volume'].get()
|
||||
if volume is not None:
|
||||
return volume
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _status_xfade(frontend):
|
||||
return 0 # TODO
|
||||
def _status_xfade(futures):
|
||||
return 0 # Not supported
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^sticker delete "(?P<field>[^"]+)" '
|
||||
@handle_request(r'^sticker delete "(?P<field>[^"]+)" '
|
||||
r'"(?P<uri>[^"]+)"( "(?P<name>[^"]+)")*$')
|
||||
def sticker_delete(frontend, field, uri, name=None):
|
||||
def sticker_delete(context, field, uri, name=None):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
@ -14,9 +14,9 @@ def sticker_delete(frontend, field, uri, name=None):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
@handle_request(r'^sticker find "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)"$')
|
||||
def sticker_find(frontend, field, uri, name):
|
||||
def sticker_find(context, field, uri, name):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
@ -28,9 +28,9 @@ def sticker_find(frontend, field, uri, name):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
@handle_request(r'^sticker get "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)"$')
|
||||
def sticker_get(frontend, field, uri, name):
|
||||
def sticker_get(context, field, uri, name):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
@ -40,8 +40,8 @@ def sticker_get(frontend, field, uri, name):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def sticker_list(frontend, field, uri):
|
||||
@handle_request(r'^sticker list "(?P<field>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def sticker_list(context, field, uri):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
@ -51,9 +51,9 @@ def sticker_list(frontend, field, uri):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
@handle_request(r'^sticker set "(?P<field>[^"]+)" "(?P<uri>[^"]+)" '
|
||||
r'"(?P<name>[^"]+)" "(?P<value>[^"]+)"$')
|
||||
def sticker_set(frontend, field, uri, name, value):
|
||||
def sticker_set(context, field, uri, name, value):
|
||||
"""
|
||||
*musicpd.org, sticker section:*
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import datetime as dt
|
||||
|
||||
from mopidy.frontends.mpd.protocol import handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import handle_request
|
||||
from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented
|
||||
|
||||
@handle_pattern(r'^listplaylist "(?P<name>[^"]+)"$')
|
||||
def listplaylist(frontend, name):
|
||||
@handle_request(r'^listplaylist "(?P<name>[^"]+)"$')
|
||||
def listplaylist(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -19,13 +19,13 @@ def listplaylist(frontend, name):
|
||||
file: relative/path/to/file3.mp3
|
||||
"""
|
||||
try:
|
||||
playlist = frontend.backend.stored_playlists.get(name=name).get()
|
||||
playlist = context.backend.stored_playlists.get(name=name).get()
|
||||
return ['file: %s' % t.uri for t in playlist.tracks]
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'listplaylist')
|
||||
|
||||
@handle_pattern(r'^listplaylistinfo "(?P<name>[^"]+)"$')
|
||||
def listplaylistinfo(frontend, name):
|
||||
@handle_request(r'^listplaylistinfo "(?P<name>[^"]+)"$')
|
||||
def listplaylistinfo(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -39,14 +39,14 @@ def listplaylistinfo(frontend, name):
|
||||
Album, Artist, Track
|
||||
"""
|
||||
try:
|
||||
playlist = frontend.backend.stored_playlists.get(name=name).get()
|
||||
playlist = context.backend.stored_playlists.get(name=name).get()
|
||||
return playlist.mpd_format()
|
||||
except LookupError:
|
||||
raise MpdNoExistError(
|
||||
u'No such playlist', command=u'listplaylistinfo')
|
||||
|
||||
@handle_pattern(r'^listplaylists$')
|
||||
def listplaylists(frontend):
|
||||
@handle_request(r'^listplaylists$')
|
||||
def listplaylists(context):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -67,7 +67,7 @@ def listplaylists(frontend):
|
||||
Last-Modified: 2010-02-06T02:11:08Z
|
||||
"""
|
||||
result = []
|
||||
for playlist in frontend.backend.stored_playlists.playlists.get():
|
||||
for playlist in context.backend.stored_playlists.playlists.get():
|
||||
result.append((u'playlist', playlist.name))
|
||||
last_modified = (playlist.last_modified or
|
||||
dt.datetime.now()).isoformat()
|
||||
@ -79,8 +79,8 @@ def listplaylists(frontend):
|
||||
result.append((u'Last-Modified', last_modified))
|
||||
return result
|
||||
|
||||
@handle_pattern(r'^load "(?P<name>[^"]+)"$')
|
||||
def load(frontend, name):
|
||||
@handle_request(r'^load "(?P<name>[^"]+)"$')
|
||||
def load(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -93,13 +93,13 @@ def load(frontend, name):
|
||||
- ``load`` appends the given playlist to the current playlist.
|
||||
"""
|
||||
try:
|
||||
playlist = frontend.backend.stored_playlists.get(name=name).get()
|
||||
frontend.backend.current_playlist.append(playlist.tracks)
|
||||
playlist = context.backend.stored_playlists.get(name=name).get()
|
||||
context.backend.current_playlist.append(playlist.tracks)
|
||||
except LookupError:
|
||||
raise MpdNoExistError(u'No such playlist', command=u'load')
|
||||
|
||||
@handle_pattern(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def playlistadd(frontend, name, uri):
|
||||
@handle_request(r'^playlistadd "(?P<name>[^"]+)" "(?P<uri>[^"]+)"$')
|
||||
def playlistadd(context, name, uri):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -111,8 +111,8 @@ def playlistadd(frontend, name, uri):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistclear "(?P<name>[^"]+)"$')
|
||||
def playlistclear(frontend, name):
|
||||
@handle_request(r'^playlistclear "(?P<name>[^"]+)"$')
|
||||
def playlistclear(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -122,8 +122,8 @@ def playlistclear(frontend, name):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistdelete "(?P<name>[^"]+)" "(?P<songpos>\d+)"$')
|
||||
def playlistdelete(frontend, name, songpos):
|
||||
@handle_request(r'^playlistdelete "(?P<name>[^"]+)" "(?P<songpos>\d+)"$')
|
||||
def playlistdelete(context, name, songpos):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -133,9 +133,9 @@ def playlistdelete(frontend, name, songpos):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^playlistmove "(?P<name>[^"]+)" '
|
||||
@handle_request(r'^playlistmove "(?P<name>[^"]+)" '
|
||||
r'"(?P<from_pos>\d+)" "(?P<to_pos>\d+)"$')
|
||||
def playlistmove(frontend, name, from_pos, to_pos):
|
||||
def playlistmove(context, name, from_pos, to_pos):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -152,8 +152,8 @@ def playlistmove(frontend, name, from_pos, to_pos):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
|
||||
def rename(frontend, old_name, new_name):
|
||||
@handle_request(r'^rename "(?P<old_name>[^"]+)" "(?P<new_name>[^"]+)"$')
|
||||
def rename(context, old_name, new_name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -163,8 +163,8 @@ def rename(frontend, old_name, new_name):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^rm "(?P<name>[^"]+)"$')
|
||||
def rm(frontend, name):
|
||||
@handle_request(r'^rm "(?P<name>[^"]+)"$')
|
||||
def rm(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
@ -174,8 +174,8 @@ def rm(frontend, name):
|
||||
"""
|
||||
raise MpdNotImplemented # TODO
|
||||
|
||||
@handle_pattern(r'^save "(?P<name>[^"]+)"$')
|
||||
def save(frontend, name):
|
||||
@handle_request(r'^save "(?P<name>[^"]+)"$')
|
||||
def save(context, name):
|
||||
"""
|
||||
*musicpd.org, stored playlists section:*
|
||||
|
||||
|
||||
@ -1,73 +1,38 @@
|
||||
import asyncore
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.utils import network
|
||||
from .session import MpdSession
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.mpd.server')
|
||||
|
||||
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 IOError, e:
|
||||
logger.debug(u'Platform supports IPv6, but socket '
|
||||
'creation failed, disabling: %s', e)
|
||||
return False
|
||||
|
||||
has_ipv6 = _try_ipv6_socket()
|
||||
|
||||
class MpdServer(asyncore.dispatcher):
|
||||
"""
|
||||
The MPD server. Creates a :class:`mopidy.frontends.mpd.session.MpdSession`
|
||||
for each client connection.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
asyncore.dispatcher.__init__(self)
|
||||
|
||||
def start(self):
|
||||
"""Start MPD server."""
|
||||
try:
|
||||
if has_ipv6:
|
||||
self.create_socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
# Explicitly configure socket to work for both IPv4 and IPv6
|
||||
self.socket.setsockopt(
|
||||
socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
else:
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.set_socket(network.create_socket())
|
||||
self.set_reuse_addr()
|
||||
hostname = self._format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
hostname = network.format_hostname(settings.MPD_SERVER_HOSTNAME)
|
||||
port = settings.MPD_SERVER_PORT
|
||||
logger.debug(u'MPD server is binding to [%s]:%s', hostname, port)
|
||||
self.bind((hostname, port))
|
||||
self.listen(1)
|
||||
logger.info(u'MPD server running at [%s]:%s',
|
||||
self._format_hostname(settings.MPD_SERVER_HOSTNAME),
|
||||
settings.MPD_SERVER_PORT)
|
||||
logger.info(u'MPD server running at [%s]:%s', hostname, port)
|
||||
except IOError, e:
|
||||
logger.error(u'MPD server startup failed: %s' % str(e).decode('utf-8'))
|
||||
logger.error(u'MPD server startup failed: %s' %
|
||||
str(e).decode('utf-8'))
|
||||
sys.exit(1)
|
||||
|
||||
def handle_accept(self):
|
||||
"""Handle new client connection."""
|
||||
"""Called by asyncore when a new client connects."""
|
||||
(client_socket, client_socket_address) = self.accept()
|
||||
logger.info(u'MPD client connection from [%s]:%s',
|
||||
client_socket_address[0], client_socket_address[1])
|
||||
MpdSession(self, client_socket, client_socket_address).start()
|
||||
|
||||
def handle_close(self):
|
||||
"""Handle end of client connection."""
|
||||
self.close()
|
||||
|
||||
def _format_hostname(self, hostname):
|
||||
if (has_ipv6
|
||||
and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
|
||||
hostname = '::ffff:%s' % hostname
|
||||
return hostname
|
||||
MpdSession(self, client_socket, client_socket_address)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import asynchat
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.protocol import ENCODING, LINE_TERMINATOR, VERSION
|
||||
from mopidy.utils.log import indent
|
||||
@ -22,70 +21,38 @@ class MpdSession(asynchat.async_chat):
|
||||
self.input_buffer = []
|
||||
self.authenticated = False
|
||||
self.set_terminator(LINE_TERMINATOR.encode(ENCODING))
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def start(self):
|
||||
"""Start a new client session."""
|
||||
self.send_response(u'OK MPD %s' % VERSION)
|
||||
self.dispatcher = MpdDispatcher(session=self)
|
||||
self.send_response([u'OK MPD %s' % VERSION])
|
||||
|
||||
def collect_incoming_data(self, data):
|
||||
"""Collect incoming data into buffer until a terminator is found."""
|
||||
"""Called by asynchat when new data arrives."""
|
||||
self.input_buffer.append(data)
|
||||
|
||||
def found_terminator(self):
|
||||
"""Handle request when a terminator is found."""
|
||||
"""Called by asynchat when a terminator is found in incoming data."""
|
||||
data = ''.join(self.input_buffer).strip()
|
||||
self.input_buffer = []
|
||||
try:
|
||||
request = data.decode(ENCODING)
|
||||
logger.debug(u'Input from [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(request))
|
||||
self.handle_request(request)
|
||||
self.send_response(self.handle_request(data))
|
||||
except UnicodeDecodeError as e:
|
||||
logger.warning(u'Received invalid data: %s', e)
|
||||
|
||||
def handle_request(self, request):
|
||||
"""Handle request by sending it to the MPD frontend."""
|
||||
if not self.authenticated:
|
||||
(self.authenticated, response) = self.check_password(request)
|
||||
if response is not None:
|
||||
self.send_response(response)
|
||||
return
|
||||
response = self.dispatcher.handle_request(request)
|
||||
if response is not None:
|
||||
self.handle_response(response)
|
||||
"""Handle the request using the MPD command handlers."""
|
||||
request = request.decode(ENCODING)
|
||||
logger.debug(u'Request from [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(request))
|
||||
return self.dispatcher.handle_request(request)
|
||||
|
||||
def handle_response(self, response):
|
||||
"""Handle response from the MPD frontend."""
|
||||
self.send_response(LINE_TERMINATOR.join(response))
|
||||
|
||||
def send_response(self, output):
|
||||
"""Send a response to the client."""
|
||||
logger.debug(u'Output to [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(output))
|
||||
output = u'%s%s' % (output, LINE_TERMINATOR)
|
||||
data = output.encode(ENCODING)
|
||||
self.push(data)
|
||||
|
||||
def check_password(self, request):
|
||||
def send_response(self, response):
|
||||
"""
|
||||
Takes any request and tries to authenticate the client using it.
|
||||
|
||||
:rtype: a two-tuple containing (is_authenticated, response_message). If
|
||||
the response_message is :class:`None`, normal processing should
|
||||
continue, even though the client may not be authenticated.
|
||||
Format a response from the MPD command handlers and send it to the
|
||||
client.
|
||||
"""
|
||||
if settings.MPD_SERVER_PASSWORD is None:
|
||||
return (True, None)
|
||||
command = request.split(' ')[0]
|
||||
if command == 'password':
|
||||
if request == 'password "%s"' % settings.MPD_SERVER_PASSWORD:
|
||||
return (True, u'OK')
|
||||
else:
|
||||
return (False, u'ACK [3@0] {password} incorrect password')
|
||||
if command in ('close', 'commands', 'notcommands', 'ping'):
|
||||
return (False, None)
|
||||
else:
|
||||
return (False,
|
||||
u'ACK [4@0] {%(c)s} you don\'t have permission for "%(c)s"' %
|
||||
{'c': command})
|
||||
if response:
|
||||
response = LINE_TERMINATOR.join(response)
|
||||
logger.debug(u'Response to [%s]:%s: %s', self.client_address,
|
||||
self.client_port, indent(response))
|
||||
response = u'%s%s' % (response, LINE_TERMINATOR)
|
||||
data = response.encode(ENCODING)
|
||||
self.push(data)
|
||||
|
||||
388
mopidy/gstreamer.py
Normal file
388
mopidy/gstreamer.py
Normal file
@ -0,0 +1,388 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.utils import get_class
|
||||
from mopidy.backends.base import Backend
|
||||
|
||||
logger = logging.getLogger('mopidy.gstreamer')
|
||||
|
||||
default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
channels=(int)2,
|
||||
width=(int)16,
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
|
||||
class GStreamer(ThreadingActor):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.OUTPUTS`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._pipeline = None
|
||||
self._source = None
|
||||
self._tee = None
|
||||
self._uridecodebin = None
|
||||
self._volume = None
|
||||
self._outputs = []
|
||||
self._handlers = {}
|
||||
|
||||
def on_start(self):
|
||||
# **Warning:** :class:`GStreamer` requires
|
||||
# :class:`mopidy.utils.process.GObjectEventThread` to be running. This
|
||||
# is not enforced by :class:`GStreamer` itself.
|
||||
self._setup_pipeline()
|
||||
self._setup_outputs()
|
||||
self._setup_message_processor()
|
||||
|
||||
def _setup_pipeline(self):
|
||||
description = ' ! '.join([
|
||||
'uridecodebin name=uri',
|
||||
'audioconvert name=convert',
|
||||
'volume name=volume',
|
||||
'tee name=tee'])
|
||||
|
||||
logger.debug(u'Setting up base GStreamer pipeline: %s', description)
|
||||
|
||||
self._pipeline = gst.parse_launch(description)
|
||||
self._tee = self._pipeline.get_by_name('tee')
|
||||
self._volume = self._pipeline.get_by_name('volume')
|
||||
self._uridecodebin = self._pipeline.get_by_name('uri')
|
||||
|
||||
self._uridecodebin.connect('notify::source', self._on_new_source)
|
||||
self._uridecodebin.connect('pad-added', self._on_new_pad,
|
||||
self._pipeline.get_by_name('convert').get_pad('sink'))
|
||||
|
||||
def _setup_outputs(self):
|
||||
for output in settings.OUTPUTS:
|
||||
get_class(output)(self).connect()
|
||||
|
||||
def _setup_message_processor(self):
|
||||
bus = self._pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect('message', self._on_message)
|
||||
|
||||
def _on_new_source(self, element, pad):
|
||||
self._source = element.get_property('source')
|
||||
try:
|
||||
self._source.set_property('caps', default_caps)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def _on_new_pad(self, source, pad, target_pad):
|
||||
if not pad.is_linked():
|
||||
pad.link(target_pad)
|
||||
|
||||
def _on_message(self, bus, message):
|
||||
if message.src in self._handlers:
|
||||
if self._handlers[message.src](message):
|
||||
return # Message was handeled by output
|
||||
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||
'Telling backend ...')
|
||||
self._get_backend().playback.on_end_of_track()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
error, debug = message.parse_error()
|
||||
logger.error(u'%s %s', error, debug)
|
||||
self.stop_playback()
|
||||
elif message.type == gst.MESSAGE_WARNING:
|
||||
error, debug = message.parse_warning()
|
||||
logger.warning(u'%s %s', error, debug)
|
||||
|
||||
def _get_backend(self):
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
return backend_refs[0].proxy()
|
||||
|
||||
def set_uri(self, uri):
|
||||
"""
|
||||
Set URI of audio to be played.
|
||||
|
||||
You *MUST* call :meth:`prepare_change` before calling this method.
|
||||
|
||||
:param uri: the URI to play
|
||||
:type uri: string
|
||||
"""
|
||||
self._uridecodebin.set_property('uri', uri)
|
||||
|
||||
def emit_data(self, capabilities, data):
|
||||
"""
|
||||
Call this to deliver raw audio data to be played.
|
||||
|
||||
:param capabilities: a GStreamer capabilities string
|
||||
:type capabilities: string
|
||||
:param data: raw audio data to be played
|
||||
"""
|
||||
caps = gst.caps_from_string(capabilities)
|
||||
buffer_ = gst.Buffer(buffer(data))
|
||||
buffer_.set_caps(caps)
|
||||
self._source.set_property('caps', caps)
|
||||
self._source.emit('push-buffer', buffer_)
|
||||
|
||||
def emit_end_of_stream(self):
|
||||
"""
|
||||
Put an end-of-stream token on the pipeline. This is typically used in
|
||||
combination with :meth:`emit_data`.
|
||||
|
||||
We will get a GStreamer message when the stream playback reaches the
|
||||
token, and can then do any end-of-stream related tasks.
|
||||
"""
|
||||
self._source.emit('end-of-stream')
|
||||
|
||||
def get_position(self):
|
||||
"""
|
||||
Get position in milliseconds.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
if self._pipeline.get_state()[1] == gst.STATE_NULL:
|
||||
return 0
|
||||
try:
|
||||
position = self._pipeline.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
except gst.QueryError, e:
|
||||
logger.error('time_position failed: %s', e)
|
||||
return 0
|
||||
|
||||
def set_position(self, position):
|
||||
"""
|
||||
Set position in milliseconds.
|
||||
|
||||
:param position: the position in milliseconds
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self._pipeline.get_state() # block until state changes are done
|
||||
handeled = self._pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
|
||||
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
|
||||
self._pipeline.get_state() # block until seek is done
|
||||
return handeled
|
||||
|
||||
def start_playback(self):
|
||||
"""
|
||||
Notify GStreamer that it should start playback.
|
||||
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
return self._set_state(gst.STATE_PLAYING)
|
||||
|
||||
def pause_playback(self):
|
||||
"""
|
||||
Notify GStreamer that it should pause playback.
|
||||
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
return self._set_state(gst.STATE_PAUSED)
|
||||
|
||||
def prepare_change(self):
|
||||
"""
|
||||
Notify GStreamer that we are about to change state of playback.
|
||||
|
||||
This function *MUST* be called before changing URIs or doing
|
||||
changes like updating data that is being pushed. The reason for this
|
||||
is that GStreamer will reset all its state when it changes to
|
||||
:attr:`gst.STATE_READY`.
|
||||
"""
|
||||
return self._set_state(gst.STATE_READY)
|
||||
|
||||
def stop_playback(self):
|
||||
"""
|
||||
Notify GStreamer that is should stop playback.
|
||||
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
return self._set_state(gst.STATE_NULL)
|
||||
|
||||
def _set_state(self, state):
|
||||
"""
|
||||
Internal method for setting the raw GStreamer state.
|
||||
|
||||
.. digraph:: gst_state_transitions
|
||||
|
||||
graph [rankdir="LR"];
|
||||
node [fontsize=10];
|
||||
|
||||
"NULL" -> "READY"
|
||||
"PAUSED" -> "PLAYING"
|
||||
"PAUSED" -> "READY"
|
||||
"PLAYING" -> "PAUSED"
|
||||
"READY" -> "NULL"
|
||||
"READY" -> "PAUSED"
|
||||
|
||||
:param state: State to set pipeline to. One of: `gst.STATE_NULL`,
|
||||
`gst.STATE_READY`, `gst.STATE_PAUSED` and `gst.STATE_PLAYING`.
|
||||
:type state: :class:`gst.State`
|
||||
:rtype: :class:`True` if successfull, else :class:`False`
|
||||
"""
|
||||
result = self._pipeline.set_state(state)
|
||||
if result == gst.STATE_CHANGE_FAILURE:
|
||||
logger.warning('Setting GStreamer state to %s: failed',
|
||||
state.value_name)
|
||||
return False
|
||||
elif result == gst.STATE_CHANGE_ASYNC:
|
||||
logger.debug('Setting GStreamer state to %s: async',
|
||||
state.value_name)
|
||||
return True
|
||||
else:
|
||||
logger.debug('Setting GStreamer state to %s: OK',
|
||||
state.value_name)
|
||||
return True
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Get volume level of the GStreamer software mixer.
|
||||
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
return int(self._volume.get_property('volume') * 100)
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set volume level of the GStreamer software mixer.
|
||||
|
||||
:param volume: the volume in the range [0..100]
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
self._volume.set_property('volume', volume / 100.0)
|
||||
return True
|
||||
|
||||
def set_metadata(self, track):
|
||||
"""
|
||||
Set track metadata for currently playing song.
|
||||
|
||||
Only needs to be called by sources such as `appsrc` which do not
|
||||
already inject tags in pipeline, e.g. when using :meth:`emit_data` to
|
||||
deliver raw audio data to GStreamer.
|
||||
|
||||
:param track: the current track
|
||||
:type track: :class:`mopidy.modes.Track`
|
||||
"""
|
||||
taglist = gst.TagList()
|
||||
artists = [a for a in (track.artists or []) if a.name]
|
||||
|
||||
if artists:
|
||||
taglist[gst.TAG_ARTIST] = u', '.join([a.name for a in artists])
|
||||
if track.name:
|
||||
taglist[gst.TAG_TITLE] = track.name
|
||||
if track.album and track.album.name:
|
||||
taglist[gst.TAG_ALBUM] = track.album.name
|
||||
|
||||
event = gst.event_new_tag(taglist)
|
||||
self._pipeline.send_event(event)
|
||||
|
||||
def connect_output(self, output):
|
||||
"""
|
||||
Connect output to pipeline.
|
||||
|
||||
:param output: output to connect to the pipeline
|
||||
:type output: :class:`gst.Bin`
|
||||
"""
|
||||
self._pipeline.add(output)
|
||||
output.sync_state_with_parent() # Required to add to running pipe
|
||||
gst.element_link_many(self._tee, output)
|
||||
self._outputs.append(output)
|
||||
logger.debug('GStreamer added %s', output.get_name())
|
||||
|
||||
def list_outputs(self):
|
||||
"""
|
||||
Get list with the name of all active outputs.
|
||||
|
||||
:rtype: list of strings
|
||||
"""
|
||||
return [output.get_name() for output in self._outputs]
|
||||
|
||||
def remove_output(self, output):
|
||||
"""
|
||||
Remove output from our pipeline.
|
||||
|
||||
:param output: output to remove from the pipeline
|
||||
:type output: :class:`gst.Bin`
|
||||
"""
|
||||
if output not in self._outputs:
|
||||
raise LookupError('Ouput %s not present in pipeline'
|
||||
% output.get_name)
|
||||
teesrc = output.get_pad('sink').get_peer()
|
||||
handler = teesrc.add_event_probe(self._handle_event_probe)
|
||||
|
||||
struct = gst.Structure('mopidy-unlink-tee')
|
||||
struct.set_value('handler', handler)
|
||||
|
||||
event = gst.event_new_custom(gst.EVENT_CUSTOM_DOWNSTREAM, struct)
|
||||
self._tee.send_event(event)
|
||||
|
||||
def _handle_event_probe(self, teesrc, event):
|
||||
if event.type == gst.EVENT_CUSTOM_DOWNSTREAM and event.has_name('mopidy-unlink-tee'):
|
||||
data = self._get_structure_data(event.get_structure())
|
||||
|
||||
output = teesrc.get_peer().get_parent()
|
||||
|
||||
teesrc.unlink(teesrc.get_peer())
|
||||
teesrc.remove_event_probe(data['handler'])
|
||||
|
||||
output.set_state(gst.STATE_NULL)
|
||||
self._pipeline.remove(output)
|
||||
|
||||
logger.warning('Removed %s', output.get_name())
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_structure_data(self, struct):
|
||||
# Ugly hack to get around missing get_value in pygst bindings :/
|
||||
data = {}
|
||||
def get_data(key, value):
|
||||
data[key] = value
|
||||
struct.foreach(get_data)
|
||||
return data
|
||||
|
||||
def connect_message_handler(self, element, handler):
|
||||
"""
|
||||
Attach custom message handler for given element.
|
||||
|
||||
Hook to allow outputs (or other code) to register custom message
|
||||
handlers for all messages coming from the element in question.
|
||||
|
||||
In the case of outputs, :meth:`mopidy.outputs.BaseOutput.on_connect`
|
||||
should be used to attach such handlers and care should be taken to
|
||||
remove them in :meth:`mopidy.outputs.BaseOutput.on_remove` using
|
||||
:meth:`remove_message_handler`.
|
||||
|
||||
The handler callback will only be given the message in question, and
|
||||
is free to ignore the message. However, if the handler wants to prevent
|
||||
the default handling of the message it should return :class:`True`
|
||||
indicating that the message has been handled.
|
||||
|
||||
Note that there can only be one handler per element.
|
||||
|
||||
:param element: element to watch messages from
|
||||
:type element: :class:`gst.Element`
|
||||
:param handler: callable that takes :class:`gst.Message` and returns
|
||||
:class:`True` if the message has been handeled
|
||||
:type handler: callable
|
||||
"""
|
||||
self._handlers[element] = handler
|
||||
|
||||
def remove_message_handler(self, element):
|
||||
"""
|
||||
Remove custom message handler.
|
||||
|
||||
:param element: element to remove message handling from.
|
||||
:type element: :class:`gst.Element`
|
||||
"""
|
||||
self._handlers.pop(element, None)
|
||||
@ -51,9 +51,9 @@ class AlsaMixer(ThreadingActor, BaseMixer):
|
||||
return [settings.MIXER_ALSA_CONTROL]
|
||||
return [u'Master', u'PCM']
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
# FIXME does not seem to see external volume changes.
|
||||
return self._mixer.getvolume()[0]
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
self._mixer.setvolume(volume)
|
||||
|
||||
@ -17,9 +17,10 @@ class BaseMixer(object):
|
||||
Integer in range [0, 100]. :class:`None` if unknown. Values below 0 is
|
||||
equal to 0. Values above 100 is equal to 100.
|
||||
"""
|
||||
if self._get_volume() is None:
|
||||
volume = self.get_volume()
|
||||
if volume is None:
|
||||
return None
|
||||
return int(self._get_volume() / self.amplification_factor)
|
||||
return int(volume / self.amplification_factor)
|
||||
|
||||
@volume.setter
|
||||
def volume(self, volume):
|
||||
@ -28,9 +29,9 @@ class BaseMixer(object):
|
||||
volume = 0
|
||||
elif volume > 100:
|
||||
volume = 100
|
||||
self._set_volume(volume)
|
||||
self.set_volume(volume)
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
"""
|
||||
Return volume as integer in range [0, 100]. :class:`None` if unknown.
|
||||
|
||||
@ -38,7 +39,7 @@ class BaseMixer(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set volume as integer in range [0, 100].
|
||||
|
||||
|
||||
@ -35,14 +35,14 @@ class DenonMixer(ThreadingActor, BaseMixer):
|
||||
from serial import Serial
|
||||
self._device = Serial(port=settings.MIXER_EXT_PORT, timeout=0.2)
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
self._ensure_open_device()
|
||||
self._device.write('MV?\r')
|
||||
vol = str(self._device.readline()[2:4])
|
||||
logger.debug(u'_get_volume() = %s' % vol)
|
||||
return self._levels.index(vol)
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
# Clamp according to Denon-spec
|
||||
if volume > 99:
|
||||
volume = 99
|
||||
|
||||
@ -8,8 +8,8 @@ class DummyMixer(ThreadingActor, BaseMixer):
|
||||
def __init__(self):
|
||||
self._volume = None
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
return self._volume
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
self._volume = volume
|
||||
|
||||
@ -2,7 +2,7 @@ from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy.mixers.base import BaseMixer
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
|
||||
"""Mixer which uses GStreamer to control volume in software."""
|
||||
@ -11,12 +11,12 @@ class GStreamerSoftwareMixer(ThreadingActor, BaseMixer):
|
||||
self.output = None
|
||||
|
||||
def on_start(self):
|
||||
output_refs = ActorRegistry.get_by_class(BaseOutput)
|
||||
output_refs = ActorRegistry.get_by_class(GStreamer)
|
||||
assert len(output_refs) == 1, 'Expected exactly one running output.'
|
||||
self.output = output_refs[0].proxy()
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
return self.output.get_volume().get()
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
self.output.set_volume(volume).get()
|
||||
|
||||
@ -40,10 +40,10 @@ class NadMixer(ThreadingActor, BaseMixer):
|
||||
self._volume_cache = None
|
||||
self._nad_talker = NadTalker.start().proxy()
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
return self._volume_cache
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
self._volume_cache = volume
|
||||
self._nad_talker.set_volume(volume)
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
|
||||
and self._last_update is not None
|
||||
and (int(time.time() - self._last_update) < self.CACHE_TTL))
|
||||
|
||||
def _get_volume(self):
|
||||
def get_volume(self):
|
||||
if not self._valid_cache():
|
||||
try:
|
||||
self._cache = int(Popen(
|
||||
@ -40,7 +40,7 @@ class OsaMixer(ThreadingActor, BaseMixer):
|
||||
self._last_update = int(time.time())
|
||||
return self._cache
|
||||
|
||||
def _set_volume(self, volume):
|
||||
def set_volume(self, volume):
|
||||
Popen(['osascript', '-e', 'set volume output volume %d' % volume])
|
||||
self._cache = volume
|
||||
self._last_update = int(time.time())
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from mopidy.frontends.mpd import translator
|
||||
from collections import namedtuple
|
||||
|
||||
class ImmutableObject(object):
|
||||
"""
|
||||
@ -129,6 +129,9 @@ class Album(ImmutableObject):
|
||||
super(Album, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
CpTrack = namedtuple('CpTrack', ['cpid', 'track'])
|
||||
|
||||
|
||||
class Track(ImmutableObject):
|
||||
"""
|
||||
:param uri: track URI
|
||||
@ -183,6 +186,7 @@ class Track(ImmutableObject):
|
||||
super(Track, self).__init__(*args, **kwargs)
|
||||
|
||||
def mpd_format(self, *args, **kwargs):
|
||||
from mopidy.frontends.mpd import translator
|
||||
return translator.track_to_mpd_format(self, *args, **kwargs)
|
||||
|
||||
|
||||
@ -222,4 +226,5 @@ class Playlist(ImmutableObject):
|
||||
return len(self.tracks)
|
||||
|
||||
def mpd_format(self, *args, **kwargs):
|
||||
from mopidy.frontends.mpd import translator
|
||||
return translator.playlist_to_mpd_format(self, *args, **kwargs)
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs')
|
||||
|
||||
class BaseOutput(object):
|
||||
"""Base class for pluggable audio outputs."""
|
||||
|
||||
MESSAGE_EOS = gst.MESSAGE_EOS
|
||||
MESSAGE_ERROR = gst.MESSAGE_ERROR
|
||||
MESSAGE_WARNING = gst.MESSAGE_WARNING
|
||||
|
||||
def __init__(self, gstreamer):
|
||||
self.gstreamer = gstreamer
|
||||
self.bin = self._build_bin()
|
||||
self.bin.set_name(self.get_name())
|
||||
|
||||
self.modify_bin()
|
||||
|
||||
def _build_bin(self):
|
||||
description = 'queue ! %s' % self.describe_bin()
|
||||
logger.debug('Creating new output: %s', description)
|
||||
return gst.parse_bin_from_description(description, True)
|
||||
|
||||
def connect(self):
|
||||
"""Attach output to GStreamer pipeline."""
|
||||
self.gstreamer.connect_output(self.bin)
|
||||
self.on_connect()
|
||||
|
||||
def on_connect(self):
|
||||
"""
|
||||
Called after output has been connected to GStreamer pipeline.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def remove(self):
|
||||
"""Remove output from GStreamer pipeline."""
|
||||
self.gstreamer.remove_output(self.bin)
|
||||
self.on_remove()
|
||||
|
||||
def on_remove(self):
|
||||
"""
|
||||
Called after output has been removed from GStreamer pipeline.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
Get name of the output. Defaults to the output's class name.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
|
||||
:rtype: string
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
def modify_bin(self):
|
||||
"""
|
||||
Modifies ``self.bin`` before it is installed if needed.
|
||||
|
||||
Overriding this method allows for outputs to modify the constructed bin
|
||||
before it is installed. This can for instance be a good place to call
|
||||
`set_properties` on elements that need to be configured.
|
||||
|
||||
*MAY be implemented by subclass.*
|
||||
"""
|
||||
pass
|
||||
|
||||
def describe_bin(self):
|
||||
"""
|
||||
Return string describing the output bin in :command:`gst-launch`
|
||||
format.
|
||||
|
||||
For simple cases this can just be a sink such as ``autoaudiosink``,
|
||||
or it can be a chain like ``element1 ! element2 ! sink``. See the
|
||||
manpage of :command:`gst-launch` for details on the format.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_properties(self, element, properties):
|
||||
"""
|
||||
Helper method for setting of properties on elements.
|
||||
|
||||
Will call :meth:`gst.Element.set_property` on ``element`` for each key
|
||||
in ``properties`` that has a value that is not :class:`None`.
|
||||
|
||||
:param element: element to set properties on
|
||||
:type element: :class:`gst.Element`
|
||||
:param properties: properties to set on element
|
||||
:type properties: dict
|
||||
"""
|
||||
for key, value in properties.items():
|
||||
if value is not None:
|
||||
element.set_property(key, value)
|
||||
@ -1,91 +0,0 @@
|
||||
class BaseOutput(object):
|
||||
"""
|
||||
Base class for audio outputs.
|
||||
"""
|
||||
|
||||
def play_uri(self, uri):
|
||||
"""
|
||||
Play URI.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param uri: the URI to play
|
||||
:type uri: string
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def deliver_data(self, capabilities, data):
|
||||
"""
|
||||
Deliver audio data to be played.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param capabilities: a GStreamer capabilities string
|
||||
:type capabilities: string
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def end_of_data_stream(self):
|
||||
"""
|
||||
Signal that the last audio data has been delivered.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_position(self):
|
||||
"""
|
||||
Get position in milliseconds.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_position(self, position):
|
||||
"""
|
||||
Set position in milliseconds.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param position: the position in milliseconds
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_state(self, state):
|
||||
"""
|
||||
Set playback state.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param state: the state
|
||||
:type state: string
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_volume(self):
|
||||
"""
|
||||
Get volume level for software mixer.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:rtype: int in range [0..100]
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""
|
||||
Set volume level for software mixer.
|
||||
|
||||
*MUST be implemented by subclass.*
|
||||
|
||||
:param volume: the volume in the range [0..100]
|
||||
:type volume: int
|
||||
:rtype: :class:`True` if successful, else :class:`False`
|
||||
"""
|
||||
raise NotImplementedError
|
||||
34
mopidy/outputs/custom.py
Normal file
34
mopidy/outputs/custom.py
Normal file
@ -0,0 +1,34 @@
|
||||
from mopidy import settings
|
||||
from mopidy.outputs import BaseOutput
|
||||
|
||||
class CustomOutput(BaseOutput):
|
||||
"""
|
||||
Custom output for using alternate setups.
|
||||
|
||||
This output is intended to handle two main cases:
|
||||
|
||||
1. Simple things like switching which sink to use. Say :class:`LocalOutput`
|
||||
doesn't work for you and you want to switch to ALSA, simple. Set
|
||||
:attr:`mopidy.settings.CUSTOM_OUTPUT` to ``alsasink`` and you are good
|
||||
to go. Some possible sinks include:
|
||||
|
||||
- alsasink
|
||||
- osssink
|
||||
- pulsesink
|
||||
- ...and many more
|
||||
|
||||
2. Advanced setups that require complete control of the output bin. For
|
||||
these cases setup :attr:`mopidy.settings.CUSTOM_OUTPUT` with a
|
||||
:command:`gst-launch` compatible string describing the target setup.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.CUSTOM_OUTPUT`
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return settings.CUSTOM_OUTPUT
|
||||
@ -1,63 +0,0 @@
|
||||
from pykka.actor import ThreadingActor
|
||||
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
|
||||
class DummyOutput(ThreadingActor, BaseOutput):
|
||||
"""
|
||||
Audio output used for testing.
|
||||
"""
|
||||
|
||||
# pylint: disable = R0902
|
||||
# Too many instance attributes (9/7)
|
||||
|
||||
#: For testing. Contains the last URI passed to :meth:`play_uri`.
|
||||
uri = None
|
||||
|
||||
#: For testing. Contains the last capabilities passed to
|
||||
#: :meth:`deliver_data`.
|
||||
capabilities = None
|
||||
|
||||
#: For testing. Contains the last data passed to :meth:`deliver_data`.
|
||||
data = None
|
||||
|
||||
#: For testing. :class:`True` if :meth:`end_of_data_stream` has been
|
||||
#: called.
|
||||
end_of_data_stream_called = False
|
||||
|
||||
#: For testing. Contains the current position.
|
||||
position = 0
|
||||
|
||||
#: For testing. Contains the current state.
|
||||
state = 'NULL'
|
||||
|
||||
#: For testing. Contains the current volume.
|
||||
volume = 100
|
||||
|
||||
def play_uri(self, uri):
|
||||
self.uri = uri
|
||||
return True
|
||||
|
||||
def deliver_data(self, capabilities, data):
|
||||
self.capabilities = capabilities
|
||||
self.data = data
|
||||
|
||||
def end_of_data_stream(self):
|
||||
self.end_of_data_stream_called = True
|
||||
|
||||
def get_position(self):
|
||||
return self.position
|
||||
|
||||
def set_position(self, position):
|
||||
self.position = position
|
||||
return True
|
||||
|
||||
def set_state(self, state):
|
||||
self.state = state
|
||||
return True
|
||||
|
||||
def get_volume(self):
|
||||
return self.volume
|
||||
|
||||
def set_volume(self, volume):
|
||||
self.volume = volume
|
||||
return True
|
||||
@ -1,170 +0,0 @@
|
||||
import pygst
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
|
||||
import logging
|
||||
|
||||
from pykka.actor import ThreadingActor
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.base import Backend
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs.gstreamer')
|
||||
|
||||
default_caps = gst.Caps("""
|
||||
audio/x-raw-int,
|
||||
endianness=(int)1234,
|
||||
channels=(int)2,
|
||||
width=(int)16,
|
||||
depth=(int)16,
|
||||
signed=(boolean)true,
|
||||
rate=(int)44100""")
|
||||
|
||||
class GStreamerOutput(ThreadingActor, BaseOutput):
|
||||
"""
|
||||
Audio output through `GStreamer <http://gstreamer.freedesktop.org/>`_.
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.GSTREAMER_AUDIO_SINK`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.gst_pipeline = None
|
||||
|
||||
def on_start(self):
|
||||
self._setup_gstreamer()
|
||||
|
||||
def _setup_gstreamer(self):
|
||||
"""
|
||||
**Warning:** :class:`GStreamerOutput` requires
|
||||
:class:`mopidy.utils.process.GObjectEventThread` to be running. This is
|
||||
not enforced by :class:`GStreamerOutput` itself.
|
||||
"""
|
||||
|
||||
logger.debug(u'Setting up GStreamer pipeline')
|
||||
|
||||
self.gst_pipeline = gst.parse_launch(' ! '.join([
|
||||
'audioconvert name=convert',
|
||||
'volume name=volume',
|
||||
settings.GSTREAMER_AUDIO_SINK,
|
||||
]))
|
||||
|
||||
pad = self.gst_pipeline.get_by_name('convert').get_pad('sink')
|
||||
|
||||
uridecodebin = gst.element_factory_make('uridecodebin', 'uri')
|
||||
uridecodebin.connect('pad-added', self._process_new_pad, pad)
|
||||
uridecodebin.connect('notify::source', self._process_new_source)
|
||||
self.gst_pipeline.add(uridecodebin)
|
||||
|
||||
# Setup bus and message processor
|
||||
gst_bus = self.gst_pipeline.get_bus()
|
||||
gst_bus.add_signal_watch()
|
||||
gst_bus.connect('message', self._process_gstreamer_message)
|
||||
|
||||
def _process_new_source(self, element, pad):
|
||||
source = element.get_by_name('source')
|
||||
try:
|
||||
source.set_property('caps', default_caps)
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def _process_new_pad(self, source, pad, target_pad):
|
||||
pad.link(target_pad)
|
||||
|
||||
def _process_gstreamer_message(self, bus, message):
|
||||
"""Process messages from GStreamer."""
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
logger.debug(u'GStreamer signalled end-of-stream. '
|
||||
'Telling backend ...')
|
||||
self._get_backend().playback.on_end_of_track()
|
||||
elif message.type == gst.MESSAGE_ERROR:
|
||||
self.set_state('NULL')
|
||||
error, debug = message.parse_error()
|
||||
logger.error(u'%s %s', error, debug)
|
||||
# FIXME Should we send 'stop_playback' to the backend here? Can we
|
||||
# differentiate on how serious the error is?
|
||||
|
||||
def _get_backend(self):
|
||||
backend_refs = ActorRegistry.get_by_class(Backend)
|
||||
assert len(backend_refs) == 1, 'Expected exactly one running backend.'
|
||||
return backend_refs[0].proxy()
|
||||
|
||||
def play_uri(self, uri):
|
||||
"""Play audio at URI"""
|
||||
self.set_state('READY')
|
||||
self.gst_pipeline.get_by_name('uri').set_property('uri', uri)
|
||||
return self.set_state('PLAYING')
|
||||
|
||||
def deliver_data(self, caps_string, data):
|
||||
"""Deliver audio data to be played"""
|
||||
source = self.gst_pipeline.get_by_name('source')
|
||||
caps = gst.caps_from_string(caps_string)
|
||||
buffer_ = gst.Buffer(buffer(data))
|
||||
buffer_.set_caps(caps)
|
||||
source.set_property('caps', caps)
|
||||
source.emit('push-buffer', buffer_)
|
||||
|
||||
def end_of_data_stream(self):
|
||||
"""
|
||||
Add end-of-stream token to source.
|
||||
|
||||
We will get a GStreamer message when the stream playback reaches the
|
||||
token, and can then do any end-of-stream related tasks.
|
||||
"""
|
||||
self.gst_pipeline.get_by_name('source').emit('end-of-stream')
|
||||
|
||||
def get_position(self):
|
||||
try:
|
||||
position = self.gst_pipeline.query_position(gst.FORMAT_TIME)[0]
|
||||
return position // gst.MSECOND
|
||||
except gst.QueryError, e:
|
||||
logger.error('time_position failed: %s', e)
|
||||
return 0
|
||||
|
||||
def set_position(self, position):
|
||||
self.gst_pipeline.get_state() # block until state changes are done
|
||||
handeled = self.gst_pipeline.seek_simple(gst.Format(gst.FORMAT_TIME),
|
||||
gst.SEEK_FLAG_FLUSH, position * gst.MSECOND)
|
||||
self.gst_pipeline.get_state() # block until seek is done
|
||||
return handeled
|
||||
|
||||
def set_state(self, state_name):
|
||||
"""
|
||||
Set the GStreamer state. Returns :class:`True` if successful.
|
||||
|
||||
.. digraph:: gst_state_transitions
|
||||
|
||||
"NULL" -> "READY"
|
||||
"PAUSED" -> "PLAYING"
|
||||
"PAUSED" -> "READY"
|
||||
"PLAYING" -> "PAUSED"
|
||||
"READY" -> "NULL"
|
||||
"READY" -> "PAUSED"
|
||||
|
||||
:param state_name: NULL, READY, PAUSED, or PLAYING
|
||||
:type state_name: string
|
||||
:rtype: :class:`True` or :class:`False`
|
||||
"""
|
||||
result = self.gst_pipeline.set_state(
|
||||
getattr(gst, 'STATE_' + state_name))
|
||||
if result == gst.STATE_CHANGE_FAILURE:
|
||||
logger.warning('Setting GStreamer state to %s: failed', state_name)
|
||||
return False
|
||||
else:
|
||||
logger.debug('Setting GStreamer state to %s: OK', state_name)
|
||||
return True
|
||||
|
||||
def get_volume(self):
|
||||
"""Get volume in range [0..100]"""
|
||||
gst_volume = self.gst_pipeline.get_by_name('volume')
|
||||
return int(gst_volume.get_property('volume') * 100)
|
||||
|
||||
def set_volume(self, volume):
|
||||
"""Set volume in range [0..100]"""
|
||||
gst_volume = self.gst_pipeline.get_by_name('volume')
|
||||
gst_volume.set_property('volume', volume / 100.0)
|
||||
return True
|
||||
20
mopidy/outputs/local.py
Normal file
20
mopidy/outputs/local.py
Normal file
@ -0,0 +1,20 @@
|
||||
from mopidy.outputs import BaseOutput
|
||||
|
||||
class LocalOutput(BaseOutput):
|
||||
"""
|
||||
Basic output to local audio sink.
|
||||
|
||||
This output will normally tell GStreamer to choose whatever it thinks is
|
||||
best for your system. In other words this is usually a sane choice.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- None
|
||||
|
||||
**Settings:**
|
||||
|
||||
- None
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return 'autoaudiosink'
|
||||
58
mopidy/outputs/shoutcast.py
Normal file
58
mopidy/outputs/shoutcast.py
Normal file
@ -0,0 +1,58 @@
|
||||
import logging
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.outputs import BaseOutput
|
||||
|
||||
logger = logging.getLogger('mopidy.outputs.shoutcast')
|
||||
|
||||
class ShoutcastOutput(BaseOutput):
|
||||
"""
|
||||
Shoutcast streaming output.
|
||||
|
||||
This output allows for streaming to an icecast server or anything else that
|
||||
supports Shoutcast. The output supports setting for: server address, port,
|
||||
mount point, user, password and encoder to use. Please see
|
||||
:class:`mopidy.settings` for details about settings.
|
||||
|
||||
**Dependencies:**
|
||||
|
||||
- A SHOUTcast/Icecast server
|
||||
|
||||
**Settings:**
|
||||
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_HOSTNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PORT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_USERNAME`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_PASSWORD`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_MOUNT`
|
||||
- :attr:`mopidy.settings.SHOUTCAST_OUTPUT_ENCODER`
|
||||
"""
|
||||
|
||||
def describe_bin(self):
|
||||
return 'audioconvert ! %s ! shout2send name=shoutcast' \
|
||||
% settings.SHOUTCAST_OUTPUT_ENCODER
|
||||
|
||||
def modify_bin(self):
|
||||
self.set_properties(self.bin.get_by_name('shoutcast'), {
|
||||
u'ip': settings.SHOUTCAST_OUTPUT_HOSTNAME,
|
||||
u'port': settings.SHOUTCAST_OUTPUT_PORT,
|
||||
u'mount': settings.SHOUTCAST_OUTPUT_MOUNT,
|
||||
u'username': settings.SHOUTCAST_OUTPUT_USERNAME,
|
||||
u'password': settings.SHOUTCAST_OUTPUT_PASSWORD,
|
||||
})
|
||||
|
||||
def on_connect(self):
|
||||
self.gstreamer.connect_message_handler(
|
||||
self.bin.get_by_name('shoutcast'), self.message_handler)
|
||||
|
||||
def on_remove(self):
|
||||
self.gstreamer.remove_message_handler(
|
||||
self.bin.get_by_name('shoutcast'))
|
||||
|
||||
def message_handler(self, message):
|
||||
if message.type != self.MESSAGE_ERROR:
|
||||
return False
|
||||
error, debug = message.parse_error()
|
||||
logger.warning('%s (%s)', error, debug)
|
||||
self.remove()
|
||||
return True
|
||||
@ -16,48 +16,34 @@ def translator(data):
|
||||
artist_kwargs = {}
|
||||
track_kwargs = {}
|
||||
|
||||
# FIXME replace with data.get('foo', None) ?
|
||||
def _retrieve(source_key, target_key, target):
|
||||
if source_key in data:
|
||||
target[target_key] = data[source_key]
|
||||
|
||||
if 'album' in data:
|
||||
album_kwargs['name'] = data['album']
|
||||
_retrieve(gst.TAG_ALBUM, 'name', album_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_COUNT, 'num_tracks', album_kwargs)
|
||||
_retrieve(gst.TAG_ARTIST, 'name', artist_kwargs)
|
||||
|
||||
if 'track-count' in data:
|
||||
album_kwargs['num_tracks'] = data['track-count']
|
||||
|
||||
if 'artist' in data:
|
||||
artist_kwargs['name'] = data['artist']
|
||||
|
||||
if 'date' in data:
|
||||
date = data['date']
|
||||
if gst.TAG_DATE in data and data[gst.TAG_DATE]:
|
||||
date = data[gst.TAG_DATE]
|
||||
date = datetime.date(date.year, date.month, date.day)
|
||||
track_kwargs['date'] = date
|
||||
|
||||
if 'title' in data:
|
||||
track_kwargs['name'] = data['title']
|
||||
_retrieve(gst.TAG_TITLE, 'name', track_kwargs)
|
||||
_retrieve(gst.TAG_TRACK_NUMBER, 'track_no', track_kwargs)
|
||||
|
||||
if 'track-number' in data:
|
||||
track_kwargs['track_no'] = data['track-number']
|
||||
|
||||
if 'album-artist' in data:
|
||||
albumartist_kwargs['name'] = data['album-artist']
|
||||
|
||||
if 'musicbrainz-trackid' in data:
|
||||
track_kwargs['musicbrainz_id'] = data['musicbrainz-trackid']
|
||||
|
||||
if 'musicbrainz-artistid' in data:
|
||||
artist_kwargs['musicbrainz_id'] = data['musicbrainz-artistid']
|
||||
|
||||
if 'musicbrainz-albumid' in data:
|
||||
album_kwargs['musicbrainz_id'] = data['musicbrainz-albumid']
|
||||
|
||||
if 'musicbrainz-albumartistid' in data:
|
||||
albumartist_kwargs['musicbrainz_id'] = data['musicbrainz-albumartistid']
|
||||
# Following keys don't seem to have TAG_* constant.
|
||||
_retrieve('album-artist', 'name', albumartist_kwargs)
|
||||
_retrieve('musicbrainz-trackid', 'musicbrainz_id', track_kwargs)
|
||||
_retrieve('musicbrainz-artistid', 'musicbrainz_id', artist_kwargs)
|
||||
_retrieve('musicbrainz-albumid', 'musicbrainz_id', album_kwargs)
|
||||
_retrieve('musicbrainz-albumartistid', 'musicbrainz_id', albumartist_kwargs)
|
||||
|
||||
if albumartist_kwargs:
|
||||
album_kwargs['artists'] = [Artist(**albumartist_kwargs)]
|
||||
|
||||
track_kwargs['uri'] = data['uri']
|
||||
track_kwargs['length'] = data['duration']
|
||||
track_kwargs['length'] = data[gst.TAG_DURATION]
|
||||
track_kwargs['album'] = Album(**album_kwargs)
|
||||
track_kwargs['artists'] = [Artist(**artist_kwargs)]
|
||||
|
||||
@ -71,17 +57,16 @@ class Scanner(object):
|
||||
self.error_callback = error_callback
|
||||
self.loop = gobject.MainLoop()
|
||||
|
||||
caps = gst.Caps('audio/x-raw-int')
|
||||
fakesink = gst.element_factory_make('fakesink')
|
||||
pad = fakesink.get_pad('sink')
|
||||
|
||||
self.uribin = gst.element_factory_make('uridecodebin')
|
||||
self.uribin.connect('pad-added', self.process_new_pad, pad)
|
||||
self.uribin.set_property('caps', caps)
|
||||
self.uribin.set_property('caps', gst.Caps('audio/x-raw-int'))
|
||||
self.uribin.connect('pad-added', self.process_new_pad,
|
||||
fakesink.get_pad('sink'))
|
||||
|
||||
self.pipe = gst.element_factory_make('pipeline')
|
||||
self.pipe.add(fakesink)
|
||||
self.pipe.add(self.uribin)
|
||||
self.pipe.add(fakesink)
|
||||
|
||||
bus = self.pipe.get_bus()
|
||||
bus.add_signal_watch()
|
||||
@ -92,22 +77,36 @@ class Scanner(object):
|
||||
pad.link(target_pad)
|
||||
|
||||
def process_tags(self, bus, message):
|
||||
data = message.parse_tag()
|
||||
data = dict([(k, data[k]) for k in data.keys()])
|
||||
data['uri'] = unicode(self.uribin.get_property('uri'))
|
||||
data['duration'] = self.get_duration()
|
||||
self.data_callback(data)
|
||||
self.next_uri()
|
||||
taglist = message.parse_tag()
|
||||
data = {
|
||||
'uri': unicode(self.uribin.get_property('uri')),
|
||||
gst.TAG_DURATION: self.get_duration(),
|
||||
}
|
||||
|
||||
for key in taglist.keys():
|
||||
# XXX: For some crazy reason some wma files spit out lists here,
|
||||
# not sure if this is due to better data in headers or wma being
|
||||
# stupid. So ugly hack for now :/
|
||||
if type(taglist[key]) is list:
|
||||
data[key] = taglist[key][0]
|
||||
else:
|
||||
data[key] = taglist[key]
|
||||
|
||||
try:
|
||||
self.data_callback(data)
|
||||
self.next_uri()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
|
||||
def process_error(self, bus, message):
|
||||
if self.error_callback:
|
||||
uri = self.uribin.get_property('uri')
|
||||
errors = message.parse_error()
|
||||
self.error_callback(uri, errors)
|
||||
error, debug = message.parse_error()
|
||||
self.error_callback(uri, error, debug)
|
||||
self.next_uri()
|
||||
|
||||
def get_duration(self):
|
||||
self.pipe.get_state()
|
||||
self.pipe.get_state() # Block until state change is done.
|
||||
try:
|
||||
return self.pipe.query_duration(
|
||||
gst.FORMAT_TIME, None)[0] // gst.MSECOND
|
||||
|
||||
@ -26,6 +26,13 @@ BACKENDS = (
|
||||
#: details on the format.
|
||||
CONSOLE_LOG_FORMAT = u'%(levelname)-8s %(message)s'
|
||||
|
||||
#: Which GStreamer bin description to use in :class:`mopidy.outputs.CustomOutput`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: CUSTOM_OUTPUT = u'fakesink'
|
||||
CUSTOM_OUTPUT = u'fakesink'
|
||||
|
||||
#: The log format used for debug logging.
|
||||
#:
|
||||
#: See http://docs.python.org/library/logging.html#formatter-objects for
|
||||
@ -54,13 +61,6 @@ FRONTENDS = (
|
||||
u'mopidy.frontends.lastfm.LastfmFrontend',
|
||||
)
|
||||
|
||||
#: Which GStreamer audio sink to use in :mod:`mopidy.outputs.gstreamer`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: GSTREAMER_AUDIO_SINK = u'autoaudiosink'
|
||||
GSTREAMER_AUDIO_SINK = u'autoaudiosink'
|
||||
|
||||
#: Your `Last.fm <http://www.last.fm/>`_ username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
@ -143,13 +143,6 @@ MIXER_EXT_SPEAKERS_B = None
|
||||
#: MIXER_MAX_VOLUME = 100
|
||||
MIXER_MAX_VOLUME = 100
|
||||
|
||||
#: Audio output handler to use.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
||||
OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
||||
|
||||
#: Which address Mopidy's MPD server should bind to.
|
||||
#:
|
||||
#:Examples:
|
||||
@ -164,15 +157,81 @@ OUTPUT = u'mopidy.outputs.gstreamer.GStreamerOutput'
|
||||
#: Listens on all interfaces, both IPv4 and IPv6.
|
||||
MPD_SERVER_HOSTNAME = u'127.0.0.1'
|
||||
|
||||
#: Which TCP port Mopidy's MPD server should listen to.
|
||||
#:
|
||||
#: Default: 6600
|
||||
MPD_SERVER_PORT = 6600
|
||||
|
||||
#: The password required for connecting to the MPD server.
|
||||
#:
|
||||
#: Default: :class:`None`, which means no password required.
|
||||
MPD_SERVER_PASSWORD = None
|
||||
|
||||
#: Which TCP port Mopidy's MPD server should listen to.
|
||||
#: List of outputs to use. See :mod:`mopidy.outputs` for all available
|
||||
#: backends
|
||||
#:
|
||||
#: Default: 6600
|
||||
MPD_SERVER_PORT = 6600
|
||||
#: Default::
|
||||
#:
|
||||
#: OUTPUTS = (
|
||||
#: u'mopidy.outputs.local.LocalOutput',
|
||||
#: )
|
||||
OUTPUTS = (
|
||||
u'mopidy.outputs.local.LocalOutput',
|
||||
)
|
||||
|
||||
#: Hostname of the SHOUTcast server which Mopidy should stream audio to.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
|
||||
SHOUTCAST_OUTPUT_HOSTNAME = u'127.0.0.1'
|
||||
|
||||
#: Port of the SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_PORT = 8000
|
||||
SHOUTCAST_OUTPUT_PORT = 8000
|
||||
|
||||
#: User to authenticate as against SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
SHOUTCAST_OUTPUT_USERNAME = u'source'
|
||||
|
||||
#: Password to authenticate with against SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
SHOUTCAST_OUTPUT_PASSWORD = u'hackme'
|
||||
|
||||
#: Mountpoint to use for the stream on the SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_MOUNT = u'/stream'
|
||||
SHOUTCAST_OUTPUT_MOUNT = u'/stream'
|
||||
|
||||
#: Encoder to use to process audio data before streaming to SHOUTcast server.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.outputs.shoutcast`.
|
||||
#:
|
||||
#: Default::
|
||||
#:
|
||||
#: SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
|
||||
SHOUTCAST_OUTPUT_ENCODER = u'lame mode=stereo bitrate=320'
|
||||
|
||||
#: Path to the Spotify cache.
|
||||
#:
|
||||
@ -189,11 +248,13 @@ SPOTIFY_USERNAME = u''
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
SPOTIFY_PASSWORD = u''
|
||||
|
||||
#: Do you prefer high bitrate (320k)?
|
||||
#: Spotify preferred bitrate.
|
||||
#:
|
||||
#: Available values are 96, 160, and 320.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.backends.spotify`.
|
||||
#
|
||||
#: Default::
|
||||
#:
|
||||
#: SPOTIFY_HIGH_BITRATE = False # 160k
|
||||
SPOTIFY_HIGH_BITRATE = False
|
||||
#: SPOTIFY_BITRATE = 160
|
||||
SPOTIFY_BITRATE = 160
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import logging.handlers
|
||||
import platform
|
||||
|
||||
from mopidy import get_version, get_platform, get_python, settings
|
||||
|
||||
|
||||
36
mopidy/utils/network.py
Normal file
36
mopidy/utils/network.py
Normal file
@ -0,0 +1,36 @@
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.server')
|
||||
|
||||
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 IOError, e:
|
||||
logger.debug(u'Platform supports IPv6, but socket '
|
||||
'creation failed, disabling: %s', e)
|
||||
return False
|
||||
|
||||
#: Boolean value that indicates if creating an IPv6 socket will succeed.
|
||||
has_ipv6 = _try_ipv6_socket()
|
||||
|
||||
def create_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
|
||||
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
return sock
|
||||
|
||||
def format_hostname(hostname):
|
||||
"""Format hostname for display."""
|
||||
if (has_ipv6 and re.match('\d+.\d+.\d+.\d+', hostname) is not None):
|
||||
hostname = '::ffff:%s' % hostname
|
||||
return hostname
|
||||
@ -1,13 +1,40 @@
|
||||
import logging
|
||||
import signal
|
||||
import thread
|
||||
import threading
|
||||
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
|
||||
from pykka import ActorDeadError
|
||||
from pykka.registry import ActorRegistry
|
||||
|
||||
from mopidy import SettingsError
|
||||
|
||||
logger = logging.getLogger('mopidy.utils.process')
|
||||
|
||||
def exit_process():
|
||||
logger.debug(u'Interrupting main...')
|
||||
thread.interrupt_main()
|
||||
logger.debug(u'Interrupted main')
|
||||
|
||||
def exit_handler(signum, frame):
|
||||
"""A :mod:`signal` handler which will exit the program on signal."""
|
||||
signals = dict((k, v) for v, k in signal.__dict__.iteritems()
|
||||
if v.startswith('SIG') and not v.startswith('SIG_'))
|
||||
logger.info(u'Got %s signal', signals[signum])
|
||||
exit_process()
|
||||
|
||||
def stop_all_actors():
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
while num_actors:
|
||||
logger.debug(u'Seeing %d actor and %d non-actor thread(s): %s',
|
||||
num_actors, threading.active_count() - num_actors,
|
||||
', '.join([t.name for t in threading.enumerate()]))
|
||||
logger.debug(u'Stopping %d actor(s)...', num_actors)
|
||||
ActorRegistry.stop_all()
|
||||
num_actors = len(ActorRegistry.get_all())
|
||||
logger.debug(u'All actors stopped.')
|
||||
|
||||
class BaseThread(threading.Thread):
|
||||
def __init__(self):
|
||||
@ -21,26 +48,19 @@ class BaseThread(threading.Thread):
|
||||
self.run_inside_try()
|
||||
except KeyboardInterrupt:
|
||||
logger.info(u'Interrupted by user')
|
||||
self.exit(0, u'Interrupted by user')
|
||||
except SettingsError as e:
|
||||
logger.error(e.message)
|
||||
self.exit(1, u'Settings error')
|
||||
except ImportError as e:
|
||||
logger.error(e)
|
||||
self.exit(2, u'Import error')
|
||||
except ActorDeadError as e:
|
||||
logger.warning(e)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
self.exit(3, u'Unknown error')
|
||||
logger.debug(u'%s: Exiting thread', self.name)
|
||||
|
||||
def run_inside_try(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def exit(self, status=0, reason=None):
|
||||
self.destroy()
|
||||
|
||||
|
||||
class GObjectEventThread(BaseThread):
|
||||
"""
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
# Absolute import needed to import ~/.mopidy/settings.py and not ourselves
|
||||
from __future__ import absolute_import
|
||||
from copy import copy
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
from pprint import pformat
|
||||
import sys
|
||||
|
||||
from mopidy import SettingsError
|
||||
@ -62,12 +64,28 @@ class SettingsProxy(object):
|
||||
else:
|
||||
super(SettingsProxy, self).__setattr__(attr, value)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, interactive):
|
||||
if interactive:
|
||||
self._read_missing_settings_from_stdin(self.current, self.runtime)
|
||||
if self.get_errors():
|
||||
logger.error(u'Settings validation errors: %s',
|
||||
indent(self.get_errors_as_string()))
|
||||
raise SettingsError(u'Settings validation failed.')
|
||||
|
||||
def _read_missing_settings_from_stdin(self, current, runtime):
|
||||
for setting, value in sorted(current.iteritems()):
|
||||
if isinstance(value, basestring) and len(value) == 0:
|
||||
runtime[setting] = self._read_from_stdin(setting + u': ')
|
||||
|
||||
def _read_from_stdin(self, prompt):
|
||||
if u'_PASSWORD' in prompt:
|
||||
return (getpass.getpass(prompt)
|
||||
.decode(sys.stdin.encoding, 'ignore'))
|
||||
else:
|
||||
sys.stdout.write(prompt)
|
||||
return (sys.stdin.readline().strip()
|
||||
.decode(sys.stdin.encoding, 'ignore'))
|
||||
|
||||
def get_errors(self):
|
||||
return validate_settings(self.default, self.local)
|
||||
|
||||
@ -97,12 +115,16 @@ def validate_settings(defaults, settings):
|
||||
'DUMP_LOG_FILENAME': 'DEBUG_LOG_FILENAME',
|
||||
'DUMP_LOG_FORMAT': 'DEBUG_LOG_FORMAT',
|
||||
'FRONTEND': 'FRONTENDS',
|
||||
'GSTREAMER_AUDIO_SINK': 'CUSTOM_OUTPUT',
|
||||
'LOCAL_MUSIC_FOLDER': 'LOCAL_MUSIC_PATH',
|
||||
'LOCAL_OUTPUT_OVERRIDE': 'CUSTOM_OUTPUT',
|
||||
'LOCAL_PLAYLIST_FOLDER': 'LOCAL_PLAYLIST_PATH',
|
||||
'LOCAL_TAG_CACHE': 'LOCAL_TAG_CACHE_FILE',
|
||||
'OUTPUT': None,
|
||||
'SERVER': None,
|
||||
'SERVER_HOSTNAME': 'MPD_SERVER_HOSTNAME',
|
||||
'SERVER_PORT': 'MPD_SERVER_PORT',
|
||||
'SPOTIFY_HIGH_BITRATE': 'SPOTIFY_BITRATE',
|
||||
'SPOTIFY_LIB_APPKEY': None,
|
||||
'SPOTIFY_LIB_CACHE': 'SPOTIFY_CACHE_PATH',
|
||||
}
|
||||
@ -123,6 +145,11 @@ def validate_settings(defaults, settings):
|
||||
'longer available.')
|
||||
continue
|
||||
|
||||
if setting == 'SPOTIFY_BITRATE':
|
||||
if value not in (96, 160, 320):
|
||||
errors[setting] = (u'Unavailable Spotify bitrate. ' +
|
||||
u'Available bitrates are 96, 160, and 320.')
|
||||
|
||||
if setting not in defaults:
|
||||
errors[setting] = u'Unknown setting. Is it misspelled?'
|
||||
continue
|
||||
@ -137,19 +164,22 @@ def list_settings_optparse_callback(*args):
|
||||
option.
|
||||
"""
|
||||
from mopidy import settings
|
||||
print format_settings_list(settings)
|
||||
sys.exit(0)
|
||||
|
||||
def format_settings_list(settings):
|
||||
errors = settings.get_errors()
|
||||
lines = []
|
||||
for (key, value) in sorted(settings.current.iteritems()):
|
||||
default_value = settings.default.get(key)
|
||||
value = mask_value_if_secret(key, value)
|
||||
lines.append(u'%s:' % key)
|
||||
lines.append(u' Value: %s' % repr(value))
|
||||
masked_value = mask_value_if_secret(key, value)
|
||||
lines.append(u'%s: %s' % (key, indent(pformat(masked_value), places=2)))
|
||||
if value != default_value and default_value is not None:
|
||||
lines.append(u' Default: %s' % repr(default_value))
|
||||
lines.append(u' Default: %s' %
|
||||
indent(pformat(default_value), places=4))
|
||||
if errors.get(key) is not None:
|
||||
lines.append(u' Error: %s' % errors[key])
|
||||
print u'Settings: %s' % indent('\n'.join(lines), places=2)
|
||||
sys.exit(0)
|
||||
return '\n'.join(lines)
|
||||
|
||||
def mask_value_if_secret(key, value):
|
||||
if key.endswith('PASSWORD') and value:
|
||||
|
||||
5
setup.py
5
setup.py
@ -69,11 +69,6 @@ for dirpath, dirnames, filenames in os.walk(project_dir):
|
||||
data_files.append([dirpath,
|
||||
[os.path.join(dirpath, f) for f in filenames]])
|
||||
|
||||
if os.geteuid() == 0:
|
||||
# Only try to install this file if we are root
|
||||
data_files.append(
|
||||
('/usr/local/share/applications', ['data/mopidy.desktop']))
|
||||
|
||||
setup(
|
||||
name='Mopidy',
|
||||
version=get_version(),
|
||||
|
||||
@ -3,7 +3,7 @@ import multiprocessing
|
||||
import random
|
||||
|
||||
from mopidy.models import Playlist, Track
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
from tests.backends.base import populate_playlist
|
||||
|
||||
@ -12,7 +12,7 @@ class CurrentPlaylistControllerTest(object):
|
||||
|
||||
def setUp(self):
|
||||
self.backend = self.backend_class()
|
||||
self.backend.output = mock.Mock(spec=BaseOutput)
|
||||
self.backend.gstreamer = mock.Mock(spec=GStreamer)
|
||||
self.controller = self.backend.current_playlist
|
||||
self.playback = self.backend.playback
|
||||
|
||||
@ -23,14 +23,14 @@ class CurrentPlaylistControllerTest(object):
|
||||
cp_track = self.controller.add(track)
|
||||
self.assertEqual(track, self.controller.tracks[-1])
|
||||
self.assertEqual(cp_track, self.controller.cp_tracks[-1])
|
||||
self.assertEqual(track, cp_track[1])
|
||||
self.assertEqual(track, cp_track.track)
|
||||
|
||||
def test_add_at_position(self):
|
||||
for track in self.tracks[:-1]:
|
||||
cp_track = self.controller.add(track, 0)
|
||||
self.assertEqual(track, self.controller.tracks[0])
|
||||
self.assertEqual(cp_track, self.controller.cp_tracks[0])
|
||||
self.assertEqual(track, cp_track[1])
|
||||
self.assertEqual(track, cp_track.track)
|
||||
|
||||
@populate_playlist
|
||||
def test_add_at_position_outside_of_playlist(self):
|
||||
@ -40,12 +40,12 @@ class CurrentPlaylistControllerTest(object):
|
||||
@populate_playlist
|
||||
def test_get_by_cpid(self):
|
||||
cp_track = self.controller.cp_tracks[1]
|
||||
self.assertEqual(cp_track, self.controller.get(cpid=cp_track[0]))
|
||||
self.assertEqual(cp_track, self.controller.get(cpid=cp_track.cpid))
|
||||
|
||||
@populate_playlist
|
||||
def test_get_by_uri(self):
|
||||
cp_track = self.controller.cp_tracks[1]
|
||||
self.assertEqual(cp_track, self.controller.get(uri=cp_track[1].uri))
|
||||
self.assertEqual(cp_track, self.controller.get(uri=cp_track.track.uri))
|
||||
|
||||
@populate_playlist
|
||||
def test_get_by_uri_raises_error_for_invalid_uri(self):
|
||||
|
||||
@ -4,7 +4,7 @@ import random
|
||||
import time
|
||||
|
||||
from mopidy.models import Track
|
||||
from mopidy.outputs.base import BaseOutput
|
||||
from mopidy.gstreamer import GStreamer
|
||||
|
||||
from tests import SkipTest
|
||||
from tests.backends.base import populate_playlist
|
||||
@ -16,7 +16,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
def setUp(self):
|
||||
self.backend = self.backend_class()
|
||||
self.backend.output = mock.Mock(spec=BaseOutput)
|
||||
self.backend.gstreamer = mock.Mock(spec=GStreamer)
|
||||
self.playback = self.backend.playback
|
||||
self.current_playlist = self.backend.current_playlist
|
||||
|
||||
@ -520,7 +520,7 @@ class PlaybackControllerTest(object):
|
||||
|
||||
self.assert_(wrapper.called)
|
||||
|
||||
@SkipTest # Blocks for 10ms and does not work with DummyOutput
|
||||
@SkipTest # Blocks for 10ms
|
||||
@populate_playlist
|
||||
def test_end_of_track_callback_gets_called(self):
|
||||
self.playback.play()
|
||||
@ -599,7 +599,7 @@ class PlaybackControllerTest(object):
|
||||
self.playback.pause()
|
||||
self.assertEqual(self.playback.resume(), None)
|
||||
|
||||
@SkipTest # Uses sleep and does not work with DummyOutput+LocalBackend
|
||||
@SkipTest # Uses sleep and might not work with LocalBackend
|
||||
@populate_playlist
|
||||
def test_resume_continues_from_right_position(self):
|
||||
self.playback.play()
|
||||
@ -729,7 +729,7 @@ class PlaybackControllerTest(object):
|
||||
def test_time_position_when_stopped(self):
|
||||
future = mock.Mock()
|
||||
future.get = mock.Mock(return_value=0)
|
||||
self.backend.output.get_position = mock.Mock(return_value=future)
|
||||
self.backend.gstreamer.get_position = mock.Mock(return_value=future)
|
||||
|
||||
self.assertEqual(self.playback.time_position, 0)
|
||||
|
||||
@ -737,11 +737,11 @@ class PlaybackControllerTest(object):
|
||||
def test_time_position_when_stopped_with_playlist(self):
|
||||
future = mock.Mock()
|
||||
future.get = mock.Mock(return_value=0)
|
||||
self.backend.output.get_position = mock.Mock(return_value=future)
|
||||
self.backend.gstreamer.get_position = mock.Mock(return_value=future)
|
||||
|
||||
self.assertEqual(self.playback.time_position, 0)
|
||||
|
||||
@SkipTest # Uses sleep and does not work with LocalBackend+DummyOutput
|
||||
@SkipTest # Uses sleep and does might not work with LocalBackend
|
||||
@populate_playlist
|
||||
def test_time_position_when_playing(self):
|
||||
self.playback.play()
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class AudioOutputHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_enableoutput(self):
|
||||
result = self.h.handle_request(u'enableoutput "0"')
|
||||
result = self.dispatcher.handle_request(u'enableoutput "0"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_disableoutput(self):
|
||||
result = self.h.handle_request(u'disableoutput "0"')
|
||||
result = self.dispatcher.handle_request(u'disableoutput "0"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_outputs(self):
|
||||
result = self.h.handle_request(u'outputs')
|
||||
result = self.dispatcher.handle_request(u'outputs')
|
||||
self.assert_(u'outputid: 0' in result)
|
||||
self.assert_(u'outputname: None' in result)
|
||||
self.assert_(u'outputenabled: 1' in result)
|
||||
|
||||
63
tests/frontends/mpd/authentication_test.py
Normal file
63
tests/frontends/mpd/authentication_test.py
Normal file
@ -0,0 +1,63 @@
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.session import MpdSession
|
||||
|
||||
class AuthenticationTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.session = mock.Mock(spec=MpdSession)
|
||||
self.dispatcher = MpdDispatcher(session=self.session)
|
||||
|
||||
def tearDown(self):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_authentication_with_valid_password_is_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'password "topsecret"')
|
||||
self.assertTrue(self.dispatcher.authenticated)
|
||||
self.assert_(u'OK' in response)
|
||||
|
||||
def test_authentication_with_invalid_password_is_not_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'password "secret"')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(u'ACK [3@0] {password} incorrect password' in response)
|
||||
|
||||
def test_authentication_with_anything_when_password_check_turned_off(self):
|
||||
settings.MPD_SERVER_PASSWORD = None
|
||||
response = self.dispatcher.handle_request(u'any request at all')
|
||||
self.assertTrue(self.dispatcher.authenticated)
|
||||
self.assert_('ACK [5@0] {} unknown command "any"' in response)
|
||||
|
||||
def test_anything_when_not_authenticated_should_fail(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'any request at all')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(
|
||||
u'ACK [4@0] {any} you don\'t have permission for "any"' in response)
|
||||
|
||||
def test_close_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'close')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(u'OK' in response)
|
||||
|
||||
def test_commands_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'commands')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(u'OK' in response)
|
||||
|
||||
def test_notcommands_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'notcommands')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(u'OK' in response)
|
||||
|
||||
def test_ping_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
response = self.dispatcher.handle_request(u'ping')
|
||||
self.assertFalse(self.dispatcher.authenticated)
|
||||
self.assert_(u'OK' in response)
|
||||
@ -8,55 +8,56 @@ class CommandListsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = dispatcher.MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_command_list_begin(self):
|
||||
result = self.h.handle_request(u'command_list_begin')
|
||||
self.assert_(result is None)
|
||||
result = self.dispatcher.handle_request(u'command_list_begin')
|
||||
self.assertEquals(result, [])
|
||||
|
||||
def test_command_list_end(self):
|
||||
self.h.handle_request(u'command_list_begin')
|
||||
result = self.h.handle_request(u'command_list_end')
|
||||
self.dispatcher.handle_request(u'command_list_begin')
|
||||
result = self.dispatcher.handle_request(u'command_list_end')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_command_list_end_without_start_first_is_an_unknown_command(self):
|
||||
result = self.h.handle_request(u'command_list_end')
|
||||
result = self.dispatcher.handle_request(u'command_list_end')
|
||||
self.assertEquals(result[0],
|
||||
u'ACK [5@0] {} unknown command "command_list_end"')
|
||||
|
||||
def test_command_list_with_ping(self):
|
||||
self.h.handle_request(u'command_list_begin')
|
||||
self.assertEqual([], self.h.command_list)
|
||||
self.assertEqual(False, self.h.command_list_ok)
|
||||
self.h.handle_request(u'ping')
|
||||
self.assert_(u'ping' in self.h.command_list)
|
||||
result = self.h.handle_request(u'command_list_end')
|
||||
self.dispatcher.handle_request(u'command_list_begin')
|
||||
self.assertEqual([], self.dispatcher.command_list)
|
||||
self.assertEqual(False, self.dispatcher.command_list_ok)
|
||||
self.dispatcher.handle_request(u'ping')
|
||||
self.assert_(u'ping' in self.dispatcher.command_list)
|
||||
result = self.dispatcher.handle_request(u'command_list_end')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(False, self.h.command_list)
|
||||
self.assertEqual(False, self.dispatcher.command_list)
|
||||
|
||||
def test_command_list_with_error_returns_ack_with_correct_index(self):
|
||||
self.h.handle_request(u'command_list_begin')
|
||||
self.h.handle_request(u'play') # Known command
|
||||
self.h.handle_request(u'paly') # Unknown command
|
||||
result = self.h.handle_request(u'command_list_end')
|
||||
self.dispatcher.handle_request(u'command_list_begin')
|
||||
self.dispatcher.handle_request(u'play') # Known command
|
||||
self.dispatcher.handle_request(u'paly') # Unknown command
|
||||
result = self.dispatcher.handle_request(u'command_list_end')
|
||||
self.assertEqual(len(result), 1, result)
|
||||
self.assertEqual(result[0], u'ACK [5@1] {} unknown command "paly"')
|
||||
|
||||
def test_command_list_ok_begin(self):
|
||||
result = self.h.handle_request(u'command_list_ok_begin')
|
||||
self.assert_(result is None)
|
||||
result = self.dispatcher.handle_request(u'command_list_ok_begin')
|
||||
self.assertEquals(result, [])
|
||||
|
||||
def test_command_list_ok_with_ping(self):
|
||||
self.h.handle_request(u'command_list_ok_begin')
|
||||
self.assertEqual([], self.h.command_list)
|
||||
self.assertEqual(True, self.h.command_list_ok)
|
||||
self.h.handle_request(u'ping')
|
||||
self.assert_(u'ping' in self.h.command_list)
|
||||
result = self.h.handle_request(u'command_list_end')
|
||||
self.dispatcher.handle_request(u'command_list_ok_begin')
|
||||
self.assertEqual([], self.dispatcher.command_list)
|
||||
self.assertEqual(True, self.dispatcher.command_list_ok)
|
||||
self.dispatcher.handle_request(u'ping')
|
||||
self.assert_(u'ping' in self.dispatcher.command_list)
|
||||
result = self.dispatcher.handle_request(u'command_list_end')
|
||||
self.assert_(u'list_OK' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(False, self.h.command_list)
|
||||
self.assertEqual(False, self.h.command_list_ok)
|
||||
self.assertEqual(False, self.dispatcher.command_list)
|
||||
self.assertEqual(False, self.dispatcher.command_list_ok)
|
||||
|
||||
@ -1,48 +1,53 @@
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.session import MpdSession
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class ConnectionHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.session = mock.Mock(spec=MpdSession)
|
||||
self.dispatcher = MpdDispatcher(session=self.session)
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_close(self):
|
||||
result = self.h.handle_request(u'close')
|
||||
def test_close_closes_the_client_connection(self):
|
||||
result = self.dispatcher.handle_request(u'close')
|
||||
self.assert_(self.session.close.called,
|
||||
u'Should call close() on MpdSession')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_empty_request(self):
|
||||
result = self.h.handle_request(u'')
|
||||
result = self.dispatcher.handle_request(u'')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_kill(self):
|
||||
result = self.h.handle_request(u'kill')
|
||||
self.assert_(u'OK' in result)
|
||||
result = self.dispatcher.handle_request(u'kill')
|
||||
self.assert_(u'ACK [4@0] {kill} you don\'t have permission for "kill"' in result)
|
||||
|
||||
def test_valid_password_is_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
result = self.h.handle_request(u'password "topsecret"')
|
||||
result = self.dispatcher.handle_request(u'password "topsecret"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_invalid_password_is_not_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
result = self.h.handle_request(u'password "secret"')
|
||||
result = self.dispatcher.handle_request(u'password "secret"')
|
||||
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
|
||||
|
||||
def test_any_password_is_not_accepted_when_password_check_turned_off(self):
|
||||
settings.MPD_SERVER_PASSWORD = None
|
||||
result = self.h.handle_request(u'password "secret"')
|
||||
result = self.dispatcher.handle_request(u'password "secret"')
|
||||
self.assert_(u'ACK [3@0] {password} incorrect password' in result)
|
||||
|
||||
def test_ping(self):
|
||||
result = self.h.handle_request(u'ping')
|
||||
result = self.dispatcher.handle_request(u'ping')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
@ -1,160 +1,160 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
|
||||
class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_add(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library.provider.dummy_library = [
|
||||
self.backend.library.provider.dummy_library = [
|
||||
Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'add "dummy://foo"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'add "dummy://foo"')
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0], u'OK')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
|
||||
|
||||
def test_add_with_uri_not_found_in_library_should_ack(self):
|
||||
result = self.h.handle_request(u'add "dummy://foo"')
|
||||
result = self.dispatcher.handle_request(u'add "dummy://foo"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [50@0] {add} directory or file not found')
|
||||
|
||||
def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self):
|
||||
result = self.h.handle_request(u'add ""')
|
||||
result = self.dispatcher.handle_request(u'add ""')
|
||||
# TODO check that we add all tracks (we currently don't)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_addid_without_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library.provider.dummy_library = [
|
||||
self.backend.library.provider.dummy_library = [
|
||||
Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.b.current_playlist.tracks.get()[5], needle)
|
||||
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[5][0]
|
||||
in result)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'addid "dummy://foo"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.backend.current_playlist.tracks.get()[5], needle)
|
||||
self.assert_(u'Id: %d' %
|
||||
self.backend.current_playlist.cp_tracks.get()[5][0] in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_addid_with_empty_uri_acks(self):
|
||||
result = self.h.handle_request(u'addid ""')
|
||||
result = self.dispatcher.handle_request(u'addid ""')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
|
||||
|
||||
def test_addid_with_songpos(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library.provider.dummy_library = [
|
||||
self.backend.library.provider.dummy_library = [
|
||||
Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo" "3"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.b.current_playlist.tracks.get()[3], needle)
|
||||
self.assert_(u'Id: %d' % self.b.current_playlist.cp_tracks.get()[3][0]
|
||||
in result)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'addid "dummy://foo" "3"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 6)
|
||||
self.assertEqual(self.backend.current_playlist.tracks.get()[3], needle)
|
||||
self.assert_(u'Id: %d' %
|
||||
self.backend.current_playlist.cp_tracks.get()[3][0] in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_addid_with_songpos_out_of_bounds_should_ack(self):
|
||||
needle = Track(uri='dummy://foo')
|
||||
self.b.library.provider.dummy_library = [
|
||||
self.backend.library.provider.dummy_library = [
|
||||
Track(), Track(), needle, Track()]
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'addid "dummy://foo" "6"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'addid "dummy://foo" "6"')
|
||||
self.assertEqual(result[0], u'ACK [2@0] {addid} Bad song index')
|
||||
|
||||
def test_addid_with_uri_not_found_in_library_should_ack(self):
|
||||
result = self.h.handle_request(u'addid "dummy://foo"')
|
||||
result = self.dispatcher.handle_request(u'addid "dummy://foo"')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {addid} No such song')
|
||||
|
||||
def test_clear(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'clear')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'clear')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_songpos(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'delete "%d"' %
|
||||
self.b.current_playlist.cp_tracks.get()[2][0])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 4)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'delete "%d"' %
|
||||
self.backend.current_playlist.cp_tracks.get()[2][0])
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 4)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_songpos_out_of_bounds(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'delete "5"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'delete "5"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
|
||||
|
||||
def test_delete_open_range(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'delete "1:"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'delete "1:"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_closed_range(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'delete "1:3"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 3)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'delete "1:3"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 3)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_delete_range_out_of_bounds(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(), Track(), Track(), Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
result = self.h.handle_request(u'delete "5:7"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 5)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
result = self.dispatcher.handle_request(u'delete "5:7"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 5)
|
||||
self.assertEqual(result[0], u'ACK [2@0] {delete} Bad song index')
|
||||
|
||||
def test_deleteid(self):
|
||||
self.b.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
|
||||
result = self.h.handle_request(u'deleteid "1"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 1)
|
||||
self.backend.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
|
||||
result = self.dispatcher.handle_request(u'deleteid "1"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_deleteid_does_not_exist(self):
|
||||
self.b.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
|
||||
result = self.h.handle_request(u'deleteid "12345"')
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
|
||||
self.backend.current_playlist.append([Track(), Track()])
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
|
||||
result = self.dispatcher.handle_request(u'deleteid "12345"')
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
|
||||
self.assertEqual(result[0], u'ACK [50@0] {deleteid} No such song')
|
||||
|
||||
def test_move_songpos(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'move "1" "0"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'move "1" "0"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'b')
|
||||
self.assertEqual(tracks[1].name, 'a')
|
||||
self.assertEqual(tracks[2].name, 'c')
|
||||
@ -164,12 +164,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_move_open_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'move "2:" "0"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'move "2:" "0"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'c')
|
||||
self.assertEqual(tracks[1].name, 'd')
|
||||
self.assertEqual(tracks[2].name, 'e')
|
||||
@ -179,12 +179,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_move_closed_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'move "1:3" "0"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'move "1:3" "0"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'b')
|
||||
self.assertEqual(tracks[1].name, 'c')
|
||||
self.assertEqual(tracks[2].name, 'a')
|
||||
@ -194,12 +194,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_moveid(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'moveid "4" "2"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'moveid "4" "2"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'a')
|
||||
self.assertEqual(tracks[1].name, 'b')
|
||||
self.assertEqual(tracks[2].name, 'e')
|
||||
@ -209,30 +209,30 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlist_returns_same_as_playlistinfo(self):
|
||||
playlist_result = self.h.handle_request(u'playlist')
|
||||
playlistinfo_result = self.h.handle_request(u'playlistinfo')
|
||||
playlist_result = self.dispatcher.handle_request(u'playlist')
|
||||
playlistinfo_result = self.dispatcher.handle_request(u'playlistinfo')
|
||||
self.assertEqual(playlist_result, playlistinfo_result)
|
||||
|
||||
def test_playlistfind(self):
|
||||
result = self.h.handle_request(u'playlistfind "tag" "needle"')
|
||||
result = self.dispatcher.handle_request(u'playlistfind "tag" "needle"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_playlistfind_by_filename_not_in_current_playlist(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'playlistfind "filename" "file:///dev/null"')
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistfind_by_filename_without_quotes(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'playlistfind filename "file:///dev/null"')
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistfind_by_filename_in_current_playlist(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(uri='file:///exists')])
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'playlistfind filename "file:///exists"')
|
||||
self.assert_(u'file: file:///exists' in result)
|
||||
self.assert_(u'Id: 0' in result)
|
||||
@ -240,15 +240,15 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_without_songid(self):
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid')
|
||||
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.dispatcher.handle_request(u'playlistid')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_with_songid(self):
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid "1"')
|
||||
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.dispatcher.handle_request(u'playlistid "1"')
|
||||
self.assert_(u'Title: a' not in result)
|
||||
self.assert_(u'Id: 0' not in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
@ -256,16 +256,16 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistid_with_not_existing_songid_fails(self):
|
||||
self.b.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.h.handle_request(u'playlistid "25"')
|
||||
self.backend.current_playlist.append([Track(name='a'), Track(name='b')])
|
||||
result = self.dispatcher.handle_request(u'playlistid "25"')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {playlistid} No such song')
|
||||
|
||||
def test_playlistinfo_without_songpos_or_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'playlistinfo')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
@ -275,11 +275,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_songpos(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'playlistinfo "4"')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo "4"')
|
||||
self.assert_(u'Title: a' not in result)
|
||||
self.assert_(u'Title: b' not in result)
|
||||
self.assert_(u'Title: c' not in result)
|
||||
@ -289,16 +289,16 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self):
|
||||
result1 = self.h.handle_request(u'playlistinfo "-1"')
|
||||
result2 = self.h.handle_request(u'playlistinfo')
|
||||
result1 = self.dispatcher.handle_request(u'playlistinfo "-1"')
|
||||
result2 = self.dispatcher.handle_request(u'playlistinfo')
|
||||
self.assertEqual(result1, result2)
|
||||
|
||||
def test_playlistinfo_with_open_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'playlistinfo "2:"')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo "2:"')
|
||||
self.assert_(u'Title: a' not in result)
|
||||
self.assert_(u'Title: b' not in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
@ -308,11 +308,11 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_closed_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'playlistinfo "2:4"')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo "2:4"')
|
||||
self.assert_(u'Title: a' not in result)
|
||||
self.assert_(u'Title: b' not in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
@ -322,52 +322,53 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self):
|
||||
result = self.h.handle_request(u'playlistinfo "10:20"')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo "10:20"')
|
||||
self.assert_(u'ACK [2@0] {playlistinfo} Bad song index' in result)
|
||||
|
||||
def test_playlistinfo_with_too_high_end_of_range_returns_ok(self):
|
||||
result = self.h.handle_request(u'playlistinfo "0:20"')
|
||||
result = self.dispatcher.handle_request(u'playlistinfo "0:20"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_playlistsearch(self):
|
||||
result = self.h.handle_request(u'playlistsearch "any" "needle"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'playlistsearch "any" "needle"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_playlistsearch_without_quotes(self):
|
||||
result = self.h.handle_request(u'playlistsearch any "needle"')
|
||||
result = self.dispatcher.handle_request(u'playlistsearch any "needle"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_plchanges(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges "0"')
|
||||
result = self.dispatcher.handle_request(u'plchanges "0"')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchanges_with_minus_one_returns_entire_playlist(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges "-1"')
|
||||
result = self.dispatcher.handle_request(u'plchanges "-1"')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchanges_without_quotes_works(self):
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(name='a'), Track(name='b'), Track(name='c')])
|
||||
result = self.h.handle_request(u'plchanges 0')
|
||||
result = self.dispatcher.handle_request(u'plchanges 0')
|
||||
self.assert_(u'Title: a' in result)
|
||||
self.assert_(u'Title: b' in result)
|
||||
self.assert_(u'Title: c' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_plchangesposid(self):
|
||||
self.b.current_playlist.append([Track(), Track(), Track()])
|
||||
result = self.h.handle_request(u'plchangesposid "0"')
|
||||
cp_tracks = self.b.current_playlist.cp_tracks.get()
|
||||
self.backend.current_playlist.append([Track(), Track(), Track()])
|
||||
result = self.dispatcher.handle_request(u'plchangesposid "0"')
|
||||
cp_tracks = self.backend.current_playlist.cp_tracks.get()
|
||||
self.assert_(u'cpos: 0' in result)
|
||||
self.assert_(u'Id: %d' % cp_tracks[0][0]
|
||||
in result)
|
||||
@ -380,24 +381,24 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_without_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
version = self.b.current_playlist.version.get()
|
||||
result = self.h.handle_request(u'shuffle')
|
||||
self.assert_(version < self.b.current_playlist.version.get())
|
||||
version = self.backend.current_playlist.version.get()
|
||||
result = self.dispatcher.handle_request(u'shuffle')
|
||||
self.assert_(version < self.backend.current_playlist.version.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_with_open_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
version = self.b.current_playlist.version.get()
|
||||
result = self.h.handle_request(u'shuffle "4:"')
|
||||
self.assert_(version < self.b.current_playlist.version.get())
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
version = self.backend.current_playlist.version.get()
|
||||
result = self.dispatcher.handle_request(u'shuffle "4:"')
|
||||
self.assert_(version < self.backend.current_playlist.version.get())
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'a')
|
||||
self.assertEqual(tracks[1].name, 'b')
|
||||
self.assertEqual(tracks[2].name, 'c')
|
||||
@ -405,14 +406,14 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_shuffle_with_closed_range(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
version = self.b.current_playlist.version.get()
|
||||
result = self.h.handle_request(u'shuffle "1:3"')
|
||||
self.assert_(version < self.b.current_playlist.version.get())
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
version = self.backend.current_playlist.version.get()
|
||||
result = self.dispatcher.handle_request(u'shuffle "1:3"')
|
||||
self.assert_(version < self.backend.current_playlist.version.get())
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'a')
|
||||
self.assertEqual(tracks[3].name, 'd')
|
||||
self.assertEqual(tracks[4].name, 'e')
|
||||
@ -420,12 +421,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_swap(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'swap "1" "4"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'swap "1" "4"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'a')
|
||||
self.assertEqual(tracks[1].name, 'e')
|
||||
self.assertEqual(tracks[2].name, 'c')
|
||||
@ -435,12 +436,12 @@ class CurrentPlaylistHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_swapid(self):
|
||||
self.b.current_playlist.append([
|
||||
self.backend.current_playlist.append([
|
||||
Track(name='a'), Track(name='b'), Track(name='c'),
|
||||
Track(name='d'), Track(name='e'), Track(name='f'),
|
||||
])
|
||||
result = self.h.handle_request(u'swapid "1" "4"')
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
result = self.dispatcher.handle_request(u'swapid "1" "4"')
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(tracks[0].name, 'a')
|
||||
self.assertEqual(tracks[1].name, 'e')
|
||||
self.assertEqual(tracks[2].name, 'c')
|
||||
|
||||
@ -1,33 +1,33 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.exceptions import MpdAckError
|
||||
from mopidy.frontends.mpd.protocol import request_handlers, handle_pattern
|
||||
from mopidy.frontends.mpd.protocol import request_handlers, handle_request
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class MpdDispatcherTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_register_same_pattern_twice_fails(self):
|
||||
func = lambda: None
|
||||
try:
|
||||
handle_pattern('a pattern')(func)
|
||||
handle_pattern('a pattern')(func)
|
||||
handle_request('a pattern')(func)
|
||||
handle_request('a pattern')(func)
|
||||
self.fail('Registering a pattern twice shoulde raise ValueError')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def test_finding_handler_for_unknown_command_raises_exception(self):
|
||||
try:
|
||||
self.h.find_handler('an_unknown_command with args')
|
||||
self.dispatcher._find_handler('an_unknown_command with args')
|
||||
self.fail('Should raise exception')
|
||||
except MpdAckError as e:
|
||||
self.assertEqual(e.get_mpd_ack(),
|
||||
@ -37,18 +37,18 @@ class MpdDispatcherTest(unittest.TestCase):
|
||||
expected_handler = lambda x: None
|
||||
request_handlers['known_command (?P<arg1>.+)'] = \
|
||||
expected_handler
|
||||
(handler, kwargs) = self.h.find_handler('known_command an_arg')
|
||||
(handler, kwargs) = self.dispatcher._find_handler('known_command an_arg')
|
||||
self.assertEqual(handler, expected_handler)
|
||||
self.assert_('arg1' in kwargs)
|
||||
self.assertEqual(kwargs['arg1'], 'an_arg')
|
||||
|
||||
def test_handling_unknown_request_yields_error(self):
|
||||
result = self.h.handle_request('an unhandled request')
|
||||
result = self.dispatcher.handle_request('an unhandled request')
|
||||
self.assertEqual(result[0], u'ACK [5@0] {} unknown command "an"')
|
||||
|
||||
def test_handling_known_request(self):
|
||||
expected = 'magic'
|
||||
request_handlers['known request'] = lambda x: expected
|
||||
result = self.h.handle_request('known request')
|
||||
result = self.dispatcher.handle_request('known request')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdUnknownCommand,
|
||||
MpdNotImplemented)
|
||||
from mopidy.frontends.mpd.exceptions import (MpdAckError, MpdPermissionError,
|
||||
MpdUnknownCommand, MpdSystemError, MpdNotImplemented)
|
||||
|
||||
class MpdExceptionsTest(unittest.TestCase):
|
||||
def test_key_error_wrapped_in_mpd_ack_error(self):
|
||||
@ -25,10 +25,9 @@ class MpdExceptionsTest(unittest.TestCase):
|
||||
|
||||
def test_get_mpd_ack_with_values(self):
|
||||
try:
|
||||
raise MpdAckError('A description', error_code=6, index=7,
|
||||
command='foo')
|
||||
raise MpdAckError('A description', index=7, command='foo')
|
||||
except MpdAckError as e:
|
||||
self.assertEqual(e.get_mpd_ack(), u'ACK [6@7] {foo} A description')
|
||||
self.assertEqual(e.get_mpd_ack(), u'ACK [0@7] {foo} A description')
|
||||
|
||||
def test_mpd_unknown_command(self):
|
||||
try:
|
||||
@ -36,3 +35,17 @@ class MpdExceptionsTest(unittest.TestCase):
|
||||
except MpdAckError as e:
|
||||
self.assertEqual(e.get_mpd_ack(),
|
||||
u'ACK [5@0] {} unknown command "play"')
|
||||
|
||||
def test_mpd_system_error(self):
|
||||
try:
|
||||
raise MpdSystemError('foo')
|
||||
except MpdSystemError as e:
|
||||
self.assertEqual(e.get_mpd_ack(),
|
||||
u'ACK [52@0] {} foo')
|
||||
|
||||
def test_mpd_permission_error(self):
|
||||
try:
|
||||
raise MpdPermissionError(command='foo')
|
||||
except MpdPermissionError as e:
|
||||
self.assertEqual(e.get_mpd_ack(),
|
||||
u'ACK [4@0] {foo} you don\'t have permission for "foo"')
|
||||
|
||||
@ -1,390 +1,412 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class MusicDatabaseHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_count(self):
|
||||
result = self.h.handle_request(u'count "tag" "needle"')
|
||||
result = self.dispatcher.handle_request(u'count "tag" "needle"')
|
||||
self.assert_(u'songs: 0' in result)
|
||||
self.assert_(u'playtime: 0' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_findadd(self):
|
||||
result = self.h.handle_request(u'findadd "album" "what"')
|
||||
result = self.dispatcher.handle_request(u'findadd "album" "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_listall(self):
|
||||
result = self.h.handle_request(u'listall "file:///dev/urandom"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'listall "file:///dev/urandom"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_listallinfo(self):
|
||||
result = self.h.handle_request(u'listallinfo "file:///dev/urandom"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'listallinfo "file:///dev/urandom"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_lsinfo_without_path_returns_same_as_listplaylists(self):
|
||||
lsinfo_result = self.h.handle_request(u'lsinfo')
|
||||
listplaylists_result = self.h.handle_request(u'listplaylists')
|
||||
lsinfo_result = self.dispatcher.handle_request(u'lsinfo')
|
||||
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
|
||||
self.assertEqual(lsinfo_result, listplaylists_result)
|
||||
|
||||
def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self):
|
||||
lsinfo_result = self.h.handle_request(u'lsinfo ""')
|
||||
listplaylists_result = self.h.handle_request(u'listplaylists')
|
||||
lsinfo_result = self.dispatcher.handle_request(u'lsinfo ""')
|
||||
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
|
||||
self.assertEqual(lsinfo_result, listplaylists_result)
|
||||
|
||||
def test_lsinfo_for_root_returns_same_as_listplaylists(self):
|
||||
lsinfo_result = self.h.handle_request(u'lsinfo "/"')
|
||||
listplaylists_result = self.h.handle_request(u'listplaylists')
|
||||
lsinfo_result = self.dispatcher.handle_request(u'lsinfo "/"')
|
||||
listplaylists_result = self.dispatcher.handle_request(u'listplaylists')
|
||||
self.assertEqual(lsinfo_result, listplaylists_result)
|
||||
|
||||
def test_update_without_uri(self):
|
||||
result = self.h.handle_request(u'update')
|
||||
result = self.dispatcher.handle_request(u'update')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(u'updating_db: 0' in result)
|
||||
|
||||
def test_update_with_uri(self):
|
||||
result = self.h.handle_request(u'update "file:///dev/urandom"')
|
||||
result = self.dispatcher.handle_request(u'update "file:///dev/urandom"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(u'updating_db: 0' in result)
|
||||
|
||||
def test_rescan_without_uri(self):
|
||||
result = self.h.handle_request(u'rescan')
|
||||
result = self.dispatcher.handle_request(u'rescan')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(u'updating_db: 0' in result)
|
||||
|
||||
def test_rescan_with_uri(self):
|
||||
result = self.h.handle_request(u'rescan "file:///dev/urandom"')
|
||||
result = self.dispatcher.handle_request(u'rescan "file:///dev/urandom"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(u'updating_db: 0' in result)
|
||||
|
||||
|
||||
class MusicDatabaseFindTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_find_album(self):
|
||||
result = self.h.handle_request(u'find "album" "what"')
|
||||
result = self.dispatcher.handle_request(u'find "album" "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_album_without_quotes(self):
|
||||
result = self.h.handle_request(u'find album "what"')
|
||||
result = self.dispatcher.handle_request(u'find album "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_artist(self):
|
||||
result = self.h.handle_request(u'find "artist" "what"')
|
||||
result = self.dispatcher.handle_request(u'find "artist" "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_artist_without_quotes(self):
|
||||
result = self.h.handle_request(u'find artist "what"')
|
||||
result = self.dispatcher.handle_request(u'find artist "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_title(self):
|
||||
result = self.h.handle_request(u'find "title" "what"')
|
||||
result = self.dispatcher.handle_request(u'find "title" "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_title_without_quotes(self):
|
||||
result = self.h.handle_request(u'find title "what"')
|
||||
result = self.dispatcher.handle_request(u'find title "what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_date(self):
|
||||
result = self.h.handle_request(u'find "date" "2002-01-01"')
|
||||
result = self.dispatcher.handle_request(u'find "date" "2002-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_date_without_quotes(self):
|
||||
result = self.h.handle_request(u'find date "2002-01-01"')
|
||||
result = self.dispatcher.handle_request(u'find date "2002-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_date_with_capital_d_and_incomplete_date(self):
|
||||
result = self.h.handle_request(u'find Date "2005"')
|
||||
result = self.dispatcher.handle_request(u'find Date "2005"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_find_else_should_fail(self):
|
||||
|
||||
result = self.h.handle_request(u'find "somethingelse" "what"')
|
||||
result = self.dispatcher.handle_request(u'find "somethingelse" "what"')
|
||||
self.assertEqual(result[0], u'ACK [2@0] {find} incorrect arguments')
|
||||
|
||||
def test_find_album_and_artist(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'find album "album_what" artist "artist_what"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
|
||||
class MusicDatabaseListTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_list_foo_returns_ack(self):
|
||||
result = self.h.handle_request(u'list "foo"')
|
||||
result = self.dispatcher.handle_request(u'list "foo"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [2@0] {list} incorrect arguments')
|
||||
|
||||
### Artist
|
||||
|
||||
def test_list_artist_with_quotes(self):
|
||||
result = self.h.handle_request(u'list "artist"')
|
||||
result = self.dispatcher.handle_request(u'list "artist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_without_quotes(self):
|
||||
result = self.h.handle_request(u'list artist')
|
||||
result = self.dispatcher.handle_request(u'list artist')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_without_quotes_and_capitalized(self):
|
||||
result = self.h.handle_request(u'list Artist')
|
||||
result = self.dispatcher.handle_request(u'list Artist')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_with_query_of_one_token(self):
|
||||
result = self.h.handle_request(u'list "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(u'list "artist" "anartist"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [2@0] {list} should be "Album" for 3 arguments')
|
||||
|
||||
def test_list_artist_with_unknown_field_in_query_returns_ack(self):
|
||||
result = self.h.handle_request(u'list "artist" "foo" "bar"')
|
||||
result = self.dispatcher.handle_request(u'list "artist" "foo" "bar"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [2@0] {list} not able to parse args')
|
||||
|
||||
def test_list_artist_by_artist(self):
|
||||
result = self.h.handle_request(u'list "artist" "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "artist" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_by_album(self):
|
||||
result = self.h.handle_request(u'list "artist" "album" "analbum"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_by_full_date(self):
|
||||
result = self.h.handle_request(u'list "artist" "date" "2001-01-01"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "date" "2001-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_by_year(self):
|
||||
result = self.h.handle_request(u'list "artist" "date" "2001"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "date" "2001"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_by_genre(self):
|
||||
result = self.h.handle_request(u'list "artist" "genre" "agenre"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "genre" "agenre"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_artist_by_artist_and_album(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "artist" "artist" "anartist" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
### Album
|
||||
|
||||
def test_list_album_with_quotes(self):
|
||||
result = self.h.handle_request(u'list "album"')
|
||||
result = self.dispatcher.handle_request(u'list "album"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_without_quotes(self):
|
||||
result = self.h.handle_request(u'list album')
|
||||
result = self.dispatcher.handle_request(u'list album')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_without_quotes_and_capitalized(self):
|
||||
result = self.h.handle_request(u'list Album')
|
||||
result = self.dispatcher.handle_request(u'list Album')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_with_artist_name(self):
|
||||
result = self.h.handle_request(u'list "album" "anartist"')
|
||||
result = self.dispatcher.handle_request(u'list "album" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_artist(self):
|
||||
result = self.h.handle_request(u'list "album" "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "artist" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_album(self):
|
||||
result = self.h.handle_request(u'list "album" "album" "analbum"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_full_date(self):
|
||||
result = self.h.handle_request(u'list "album" "date" "2001-01-01"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "date" "2001-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_year(self):
|
||||
result = self.h.handle_request(u'list "album" "date" "2001"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "date" "2001"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_genre(self):
|
||||
result = self.h.handle_request(u'list "album" "genre" "agenre"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "genre" "agenre"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_album_by_artist_and_album(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "album" "artist" "anartist" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
### Date
|
||||
|
||||
def test_list_date_with_quotes(self):
|
||||
result = self.h.handle_request(u'list "date"')
|
||||
result = self.dispatcher.handle_request(u'list "date"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_without_quotes(self):
|
||||
result = self.h.handle_request(u'list date')
|
||||
result = self.dispatcher.handle_request(u'list date')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_without_quotes_and_capitalized(self):
|
||||
result = self.h.handle_request(u'list Date')
|
||||
result = self.dispatcher.handle_request(u'list Date')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_with_query_of_one_token(self):
|
||||
result = self.h.handle_request(u'list "date" "anartist"')
|
||||
result = self.dispatcher.handle_request(u'list "date" "anartist"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [2@0] {list} should be "Album" for 3 arguments')
|
||||
|
||||
def test_list_date_by_artist(self):
|
||||
result = self.h.handle_request(u'list "date" "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "date" "artist" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_by_album(self):
|
||||
result = self.h.handle_request(u'list "date" "album" "analbum"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "date" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_by_full_date(self):
|
||||
result = self.h.handle_request(u'list "date" "date" "2001-01-01"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "date" "date" "2001-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_by_year(self):
|
||||
result = self.h.handle_request(u'list "date" "date" "2001"')
|
||||
result = self.dispatcher.handle_request(u'list "date" "date" "2001"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_by_genre(self):
|
||||
result = self.h.handle_request(u'list "date" "genre" "agenre"')
|
||||
result = self.dispatcher.handle_request(u'list "date" "genre" "agenre"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_date_by_artist_and_album(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "date" "artist" "anartist" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
### Genre
|
||||
|
||||
def test_list_genre_with_quotes(self):
|
||||
result = self.h.handle_request(u'list "genre"')
|
||||
result = self.dispatcher.handle_request(u'list "genre"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_without_quotes(self):
|
||||
result = self.h.handle_request(u'list genre')
|
||||
result = self.dispatcher.handle_request(u'list genre')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_without_quotes_and_capitalized(self):
|
||||
result = self.h.handle_request(u'list Genre')
|
||||
result = self.dispatcher.handle_request(u'list Genre')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_with_query_of_one_token(self):
|
||||
result = self.h.handle_request(u'list "genre" "anartist"')
|
||||
result = self.dispatcher.handle_request(u'list "genre" "anartist"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [2@0] {list} should be "Album" for 3 arguments')
|
||||
|
||||
def test_list_genre_by_artist(self):
|
||||
result = self.h.handle_request(u'list "genre" "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "artist" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_by_album(self):
|
||||
result = self.h.handle_request(u'list "genre" "album" "analbum"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_by_full_date(self):
|
||||
result = self.h.handle_request(u'list "genre" "date" "2001-01-01"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "date" "2001-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_by_year(self):
|
||||
result = self.h.handle_request(u'list "genre" "date" "2001"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "date" "2001"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_by_genre(self):
|
||||
result = self.h.handle_request(u'list "genre" "genre" "agenre"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "genre" "agenre"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_list_genre_by_artist_and_album(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'list "genre" "artist" "anartist" "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
|
||||
class MusicDatabaseSearchTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_search_album(self):
|
||||
result = self.h.handle_request(u'search "album" "analbum"')
|
||||
result = self.dispatcher.handle_request(u'search "album" "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_album_without_quotes(self):
|
||||
result = self.h.handle_request(u'search album "analbum"')
|
||||
result = self.dispatcher.handle_request(u'search album "analbum"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_artist(self):
|
||||
result = self.h.handle_request(u'search "artist" "anartist"')
|
||||
result = self.dispatcher.handle_request(u'search "artist" "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_artist_without_quotes(self):
|
||||
result = self.h.handle_request(u'search artist "anartist"')
|
||||
result = self.dispatcher.handle_request(u'search artist "anartist"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_filename(self):
|
||||
result = self.h.handle_request(u'search "filename" "afilename"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'search "filename" "afilename"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_filename_without_quotes(self):
|
||||
result = self.h.handle_request(u'search filename "afilename"')
|
||||
result = self.dispatcher.handle_request(u'search filename "afilename"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_title(self):
|
||||
result = self.h.handle_request(u'search "title" "atitle"')
|
||||
result = self.dispatcher.handle_request(u'search "title" "atitle"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_title_without_quotes(self):
|
||||
result = self.h.handle_request(u'search title "atitle"')
|
||||
result = self.dispatcher.handle_request(u'search title "atitle"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_any(self):
|
||||
result = self.h.handle_request(u'search "any" "anything"')
|
||||
result = self.dispatcher.handle_request(u'search "any" "anything"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_any_without_quotes(self):
|
||||
result = self.h.handle_request(u'search any "anything"')
|
||||
result = self.dispatcher.handle_request(u'search any "anything"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_date(self):
|
||||
result = self.h.handle_request(u'search "date" "2002-01-01"')
|
||||
result = self.dispatcher.handle_request(u'search "date" "2002-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_date_without_quotes(self):
|
||||
result = self.h.handle_request(u'search date "2002-01-01"')
|
||||
result = self.dispatcher.handle_request(u'search date "2002-01-01"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_date_with_capital_d_and_incomplete_date(self):
|
||||
result = self.h.handle_request(u'search Date "2005"')
|
||||
result = self.dispatcher.handle_request(u'search Date "2005"')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_search_else_should_fail(self):
|
||||
result = self.h.handle_request(u'search "sometype" "something"')
|
||||
result = self.dispatcher.handle_request(
|
||||
u'search "sometype" "something"')
|
||||
self.assertEqual(result[0], u'ACK [2@0] {search} incorrect arguments')
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
from mopidy.backends.base import PlaybackController
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
|
||||
@ -14,393 +14,378 @@ STOPPED = PlaybackController.STOPPED
|
||||
|
||||
class PlaybackOptionsHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_consume_off(self):
|
||||
result = self.h.handle_request(u'consume "0"')
|
||||
self.assertFalse(self.b.playback.consume.get())
|
||||
result = self.dispatcher.handle_request(u'consume "0"')
|
||||
self.assertFalse(self.backend.playback.consume.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_consume_off_without_quotes(self):
|
||||
result = self.h.handle_request(u'consume 0')
|
||||
self.assertFalse(self.b.playback.consume.get())
|
||||
result = self.dispatcher.handle_request(u'consume 0')
|
||||
self.assertFalse(self.backend.playback.consume.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_consume_on(self):
|
||||
result = self.h.handle_request(u'consume "1"')
|
||||
self.assertTrue(self.b.playback.consume.get())
|
||||
result = self.dispatcher.handle_request(u'consume "1"')
|
||||
self.assertTrue(self.backend.playback.consume.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_consume_on_without_quotes(self):
|
||||
result = self.h.handle_request(u'consume 1')
|
||||
self.assertTrue(self.b.playback.consume.get())
|
||||
result = self.dispatcher.handle_request(u'consume 1')
|
||||
self.assertTrue(self.backend.playback.consume.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_crossfade(self):
|
||||
result = self.h.handle_request(u'crossfade "10"')
|
||||
result = self.dispatcher.handle_request(u'crossfade "10"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_random_off(self):
|
||||
result = self.h.handle_request(u'random "0"')
|
||||
self.assertFalse(self.b.playback.random.get())
|
||||
result = self.dispatcher.handle_request(u'random "0"')
|
||||
self.assertFalse(self.backend.playback.random.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_random_off_without_quotes(self):
|
||||
result = self.h.handle_request(u'random 0')
|
||||
self.assertFalse(self.b.playback.random.get())
|
||||
result = self.dispatcher.handle_request(u'random 0')
|
||||
self.assertFalse(self.backend.playback.random.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_random_on(self):
|
||||
result = self.h.handle_request(u'random "1"')
|
||||
self.assertTrue(self.b.playback.random.get())
|
||||
result = self.dispatcher.handle_request(u'random "1"')
|
||||
self.assertTrue(self.backend.playback.random.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_random_on_without_quotes(self):
|
||||
result = self.h.handle_request(u'random 1')
|
||||
self.assertTrue(self.b.playback.random.get())
|
||||
result = self.dispatcher.handle_request(u'random 1')
|
||||
self.assertTrue(self.backend.playback.random.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_repeat_off(self):
|
||||
result = self.h.handle_request(u'repeat "0"')
|
||||
self.assertFalse(self.b.playback.repeat.get())
|
||||
result = self.dispatcher.handle_request(u'repeat "0"')
|
||||
self.assertFalse(self.backend.playback.repeat.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_repeat_off_without_quotes(self):
|
||||
result = self.h.handle_request(u'repeat 0')
|
||||
self.assertFalse(self.b.playback.repeat.get())
|
||||
result = self.dispatcher.handle_request(u'repeat 0')
|
||||
self.assertFalse(self.backend.playback.repeat.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_repeat_on(self):
|
||||
result = self.h.handle_request(u'repeat "1"')
|
||||
self.assertTrue(self.b.playback.repeat.get())
|
||||
result = self.dispatcher.handle_request(u'repeat "1"')
|
||||
self.assertTrue(self.backend.playback.repeat.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_repeat_on_without_quotes(self):
|
||||
result = self.h.handle_request(u'repeat 1')
|
||||
self.assertTrue(self.b.playback.repeat.get())
|
||||
result = self.dispatcher.handle_request(u'repeat 1')
|
||||
self.assertTrue(self.backend.playback.repeat.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_setvol_below_min(self):
|
||||
result = self.h.handle_request(u'setvol "-10"')
|
||||
result = self.dispatcher.handle_request(u'setvol "-10"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(0, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_min(self):
|
||||
result = self.h.handle_request(u'setvol "0"')
|
||||
result = self.dispatcher.handle_request(u'setvol "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(0, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_middle(self):
|
||||
result = self.h.handle_request(u'setvol "50"')
|
||||
result = self.dispatcher.handle_request(u'setvol "50"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(50, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_max(self):
|
||||
result = self.h.handle_request(u'setvol "100"')
|
||||
result = self.dispatcher.handle_request(u'setvol "100"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(100, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_above_max(self):
|
||||
result = self.h.handle_request(u'setvol "110"')
|
||||
result = self.dispatcher.handle_request(u'setvol "110"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(100, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_plus_is_ignored(self):
|
||||
result = self.h.handle_request(u'setvol "+10"')
|
||||
result = self.dispatcher.handle_request(u'setvol "+10"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(10, self.mixer.volume.get())
|
||||
|
||||
def test_setvol_without_quotes(self):
|
||||
result = self.h.handle_request(u'setvol 50')
|
||||
result = self.dispatcher.handle_request(u'setvol 50')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(50, self.mixer.volume.get())
|
||||
|
||||
def test_single_off(self):
|
||||
result = self.h.handle_request(u'single "0"')
|
||||
self.assertFalse(self.b.playback.single.get())
|
||||
result = self.dispatcher.handle_request(u'single "0"')
|
||||
self.assertFalse(self.backend.playback.single.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_single_off_without_quotes(self):
|
||||
result = self.h.handle_request(u'single 0')
|
||||
self.assertFalse(self.b.playback.single.get())
|
||||
result = self.dispatcher.handle_request(u'single 0')
|
||||
self.assertFalse(self.backend.playback.single.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_single_on(self):
|
||||
result = self.h.handle_request(u'single "1"')
|
||||
self.assertTrue(self.b.playback.single.get())
|
||||
result = self.dispatcher.handle_request(u'single "1"')
|
||||
self.assertTrue(self.backend.playback.single.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_single_on_without_quotes(self):
|
||||
result = self.h.handle_request(u'single 1')
|
||||
self.assertTrue(self.b.playback.single.get())
|
||||
result = self.dispatcher.handle_request(u'single 1')
|
||||
self.assertTrue(self.backend.playback.single.get())
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_replay_gain_mode_off(self):
|
||||
result = self.h.handle_request(u'replay_gain_mode "off"')
|
||||
result = self.dispatcher.handle_request(u'replay_gain_mode "off"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_replay_gain_mode_track(self):
|
||||
result = self.h.handle_request(u'replay_gain_mode "track"')
|
||||
result = self.dispatcher.handle_request(u'replay_gain_mode "track"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_replay_gain_mode_album(self):
|
||||
result = self.h.handle_request(u'replay_gain_mode "album"')
|
||||
result = self.dispatcher.handle_request(u'replay_gain_mode "album"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_replay_gain_status_default(self):
|
||||
expected = u'off'
|
||||
result = self.h.handle_request(u'replay_gain_status')
|
||||
result = self.dispatcher.handle_request(u'replay_gain_status')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
|
||||
def test_replay_gain_status_off(self):
|
||||
raise SkipTest
|
||||
expected = u'off'
|
||||
self.h._replay_gain_mode(expected)
|
||||
result = self.h.handle_request(u'replay_gain_status')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
raise SkipTest # TODO
|
||||
|
||||
def test_replay_gain_status_track(self):
|
||||
raise SkipTest
|
||||
expected = u'track'
|
||||
self.h._replay_gain_mode(expected)
|
||||
result = self.h.handle_request(u'replay_gain_status')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
raise SkipTest # TODO
|
||||
|
||||
def test_replay_gain_status_album(self):
|
||||
raise SkipTest
|
||||
expected = u'album'
|
||||
self.h._replay_gain_mode(expected)
|
||||
result = self.h.handle_request(u'replay_gain_status')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(expected in result)
|
||||
raise SkipTest # TODO
|
||||
|
||||
|
||||
class PlaybackControlHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_next(self):
|
||||
result = self.h.handle_request(u'next')
|
||||
result = self.dispatcher.handle_request(u'next')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_pause_off(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.h.handle_request(u'play "0"')
|
||||
self.h.handle_request(u'pause "1"')
|
||||
result = self.h.handle_request(u'pause "0"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.dispatcher.handle_request(u'play "0"')
|
||||
self.dispatcher.handle_request(u'pause "1"')
|
||||
result = self.dispatcher.handle_request(u'pause "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_pause_on(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.h.handle_request(u'play "0"')
|
||||
result = self.h.handle_request(u'pause "1"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.dispatcher.handle_request(u'play "0"')
|
||||
result = self.dispatcher.handle_request(u'pause "1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PAUSED, self.b.playback.state.get())
|
||||
self.assertEqual(PAUSED, self.backend.playback.state.get())
|
||||
|
||||
def test_pause_toggle(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
result = self.dispatcher.handle_request(u'play "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'pause')
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'pause')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PAUSED, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'pause')
|
||||
self.assertEqual(PAUSED, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'pause')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_play_without_pos(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.state = PAUSED
|
||||
result = self.h.handle_request(u'play')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.backend.playback.state = PAUSED
|
||||
result = self.dispatcher.handle_request(u'play')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_play_with_pos(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
result = self.dispatcher.handle_request(u'play "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_play_with_pos_without_quotes(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'play 0')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
result = self.dispatcher.handle_request(u'play 0')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_play_with_pos_out_of_bounds(self):
|
||||
self.b.current_playlist.append([])
|
||||
result = self.h.handle_request(u'play "0"')
|
||||
self.backend.current_playlist.append([])
|
||||
result = self.dispatcher.handle_request(u'play "0"')
|
||||
self.assertEqual(result[0], u'ACK [2@0] {play} Bad song index')
|
||||
self.assertEqual(STOPPED, self.b.playback.state.get())
|
||||
self.assertEqual(STOPPED, self.backend.playback.state.get())
|
||||
|
||||
def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self):
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
result = self.dispatcher.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get().uri, 'a')
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
|
||||
|
||||
def test_play_minus_one_plays_current_track_if_current_track_is_set(self):
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.b.playback.play()
|
||||
self.b.playback.next()
|
||||
self.b.playback.stop()
|
||||
self.assertNotEqual(self.b.playback.current_track.get(), None)
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
self.backend.playback.play()
|
||||
self.backend.playback.next()
|
||||
self.backend.playback.stop()
|
||||
self.assertNotEqual(self.backend.playback.current_track.get(), None)
|
||||
result = self.dispatcher.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get().uri, 'b')
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
|
||||
|
||||
def test_play_minus_one_on_empty_playlist_does_not_ack(self):
|
||||
self.b.current_playlist.clear()
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.backend.current_playlist.clear()
|
||||
result = self.dispatcher.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(STOPPED, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.assertEqual(STOPPED, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
|
||||
def test_play_minus_is_ignored_if_playing(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.b.playback.seek(30000)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.backend.playback.seek(30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_play_minus_one_resumes_if_paused(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.b.playback.seek(30000)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.b.playback.state.get())
|
||||
self.b.playback.pause()
|
||||
self.assertEquals(PAUSED, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'play "-1"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.backend.playback.seek(30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.backend.playback.state.get())
|
||||
self.backend.playback.pause()
|
||||
self.assertEquals(PAUSED, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'play "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_playid(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'playid "0"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
result = self.dispatcher.handle_request(u'playid "0"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
|
||||
def test_playid_minus_one_plays_first_in_playlist_if_no_current_track(self):
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
result = self.dispatcher.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get().uri, 'a')
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get().uri, 'a')
|
||||
|
||||
def test_playid_minus_one_plays_current_track_if_current_track_is_set(self):
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.b.playback.play()
|
||||
self.b.playback.next()
|
||||
self.b.playback.stop()
|
||||
self.assertNotEqual(self.b.playback.current_track.get(), None)
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
self.backend.playback.play()
|
||||
self.backend.playback.next()
|
||||
self.backend.playback.stop()
|
||||
self.assertNotEqual(self.backend.playback.current_track.get(), None)
|
||||
result = self.dispatcher.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get().uri, 'b')
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get().uri, 'b')
|
||||
|
||||
def test_playid_minus_one_on_empty_playlist_does_not_ack(self):
|
||||
self.b.current_playlist.clear()
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.backend.current_playlist.clear()
|
||||
result = self.dispatcher.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(STOPPED, self.b.playback.state.get())
|
||||
self.assertEqual(self.b.playback.current_track.get(), None)
|
||||
self.assertEqual(STOPPED, self.backend.playback.state.get())
|
||||
self.assertEqual(self.backend.playback.current_track.get(), None)
|
||||
|
||||
def test_playid_minus_is_ignored_if_playing(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.b.playback.seek(30000)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.backend.playback.seek(30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_playid_minus_one_resumes_if_paused(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.b.playback.seek(30000)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.b.playback.state.get())
|
||||
self.b.playback.pause()
|
||||
self.assertEquals(PAUSED, self.b.playback.state.get())
|
||||
result = self.h.handle_request(u'playid "-1"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.backend.playback.seek(30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
self.assertEquals(PLAYING, self.backend.playback.state.get())
|
||||
self.backend.playback.pause()
|
||||
self.assertEquals(PAUSED, self.backend.playback.state.get())
|
||||
result = self.dispatcher.handle_request(u'playid "-1"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(PLAYING, self.b.playback.state.get())
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assertEqual(PLAYING, self.backend.playback.state.get())
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_playid_which_does_not_exist(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
result = self.h.handle_request(u'playid "12345"')
|
||||
self.backend.current_playlist.append([Track()])
|
||||
result = self.dispatcher.handle_request(u'playid "12345"')
|
||||
self.assertEqual(result[0], u'ACK [50@0] {playid} No such song')
|
||||
|
||||
def test_previous(self):
|
||||
result = self.h.handle_request(u'previous')
|
||||
result = self.dispatcher.handle_request(u'previous')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_seek(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.h.handle_request(u'seek "0"')
|
||||
result = self.h.handle_request(u'seek "0" "30"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.dispatcher.handle_request(u'seek "0"')
|
||||
result = self.dispatcher.handle_request(u'seek "0" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(self.b.playback.time_position >= 30000)
|
||||
self.assert_(self.backend.playback.time_position >= 30000)
|
||||
|
||||
def test_seek_with_songpos(self):
|
||||
seek_track = Track(uri='2', length=40000)
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(uri='1', length=40000), seek_track])
|
||||
result = self.h.handle_request(u'seek "1" "30"')
|
||||
result = self.dispatcher.handle_request(u'seek "1" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.current_track.get(), seek_track)
|
||||
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
|
||||
|
||||
def test_seek_without_quotes(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
self.h.handle_request(u'seek 0')
|
||||
result = self.h.handle_request(u'seek 0 30')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
self.dispatcher.handle_request(u'seek 0')
|
||||
result = self.dispatcher.handle_request(u'seek 0 30')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_seekid(self):
|
||||
self.b.current_playlist.append([Track(length=40000)])
|
||||
result = self.h.handle_request(u'seekid "0" "30"')
|
||||
self.backend.current_playlist.append([Track(length=40000)])
|
||||
result = self.dispatcher.handle_request(u'seekid "0" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(self.b.playback.time_position.get() >= 30000)
|
||||
self.assert_(self.backend.playback.time_position.get() >= 30000)
|
||||
|
||||
def test_seekid_with_cpid(self):
|
||||
seek_track = Track(uri='2', length=40000)
|
||||
self.b.current_playlist.append(
|
||||
self.backend.current_playlist.append(
|
||||
[Track(length=40000), seek_track])
|
||||
result = self.h.handle_request(u'seekid "1" "30"')
|
||||
result = self.dispatcher.handle_request(u'seekid "1" "30"')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(self.b.playback.current_cpid.get(), 1)
|
||||
self.assertEqual(self.b.playback.current_track.get(), seek_track)
|
||||
self.assertEqual(self.backend.playback.current_cpid.get(), 1)
|
||||
self.assertEqual(self.backend.playback.current_track.get(), seek_track)
|
||||
|
||||
def test_stop(self):
|
||||
result = self.h.handle_request(u'stop')
|
||||
result = self.dispatcher.handle_request(u'stop')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assertEqual(STOPPED, self.b.playback.state.get())
|
||||
self.assertEqual(STOPPED, self.backend.playback.state.get())
|
||||
|
||||
@ -1,25 +1,29 @@
|
||||
import unittest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class ReflectionHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
settings.runtime.clear()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_commands_returns_list_of_all_commands(self):
|
||||
result = self.h.handle_request(u'commands')
|
||||
result = self.dispatcher.handle_request(u'commands')
|
||||
# Check if some random commands are included
|
||||
self.assert_(u'command: commands' in result)
|
||||
self.assert_(u'command: play' in result)
|
||||
self.assert_(u'command: status' in result)
|
||||
# Check if commands you do not have access to are not present
|
||||
self.assert_(u'command: kill' not in result)
|
||||
# Check if the blacklisted commands are not present
|
||||
self.assert_(u'command: command_list_begin' not in result)
|
||||
self.assert_(u'command: command_list_ok_begin' not in result)
|
||||
@ -29,20 +33,47 @@ class ReflectionHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'command: sticker' not in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_commands_show_less_if_auth_required_and_not_authed(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'secret'
|
||||
result = self.dispatcher.handle_request(u'commands')
|
||||
# Not requiring auth
|
||||
self.assert_(u'command: close' in result, result)
|
||||
self.assert_(u'command: commands' in result, result)
|
||||
self.assert_(u'command: notcommands' in result, result)
|
||||
self.assert_(u'command: password' in result, result)
|
||||
self.assert_(u'command: ping' in result, result)
|
||||
# Requiring auth
|
||||
self.assert_(u'command: play' not in result, result)
|
||||
self.assert_(u'command: status' not in result, result)
|
||||
|
||||
def test_decoders(self):
|
||||
result = self.h.handle_request(u'decoders')
|
||||
result = self.dispatcher.handle_request(u'decoders')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_notcommands_returns_only_ok(self):
|
||||
result = self.h.handle_request(u'notcommands')
|
||||
self.assertEqual(1, len(result))
|
||||
def test_notcommands_returns_only_kill_and_ok(self):
|
||||
result = self.dispatcher.handle_request(u'notcommands')
|
||||
self.assertEqual(2, len(result))
|
||||
self.assert_(u'command: kill' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_notcommands_returns_more_if_auth_required_and_not_authed(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'secret'
|
||||
result = self.dispatcher.handle_request(u'notcommands')
|
||||
# Not requiring auth
|
||||
self.assert_(u'command: close' not in result, result)
|
||||
self.assert_(u'command: commands' not in result, result)
|
||||
self.assert_(u'command: notcommands' not in result, result)
|
||||
self.assert_(u'command: password' not in result, result)
|
||||
self.assert_(u'command: ping' not in result, result)
|
||||
# Requiring auth
|
||||
self.assert_(u'command: play' in result, result)
|
||||
self.assert_(u'command: status' in result, result)
|
||||
|
||||
def test_tagtypes(self):
|
||||
result = self.h.handle_request(u'tagtypes')
|
||||
result = self.dispatcher.handle_request(u'tagtypes')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_urlhandlers(self):
|
||||
result = self.h.handle_request(u'urlhandlers')
|
||||
result = self.dispatcher.handle_request(u'urlhandlers')
|
||||
self.assert_(u'OK' in result)
|
||||
self.assert_(u'handler: dummy:' in result)
|
||||
|
||||
@ -5,29 +5,6 @@ from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import server
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class MpdServerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.server = server.MpdServer()
|
||||
self.has_ipv6 = server.has_ipv6
|
||||
|
||||
def tearDown(self):
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
server.has_ipv6 = self.has_ipv6
|
||||
|
||||
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
|
||||
server.has_ipv6 = True
|
||||
self.assertEqual(self.server._format_hostname('0.0.0.0'),
|
||||
'::ffff:0.0.0.0')
|
||||
self.assertEqual(self.server._format_hostname('127.0.0.1'),
|
||||
'::ffff:127.0.0.1')
|
||||
|
||||
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
|
||||
server.has_ipv6 = False
|
||||
self.assertEquals(self.server._format_hostname('0.0.0.0'), '0.0.0.0')
|
||||
|
||||
class MpdSessionTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
@ -44,52 +21,3 @@ class MpdSessionTest(unittest.TestCase):
|
||||
self.session.input_buffer = ['\xff']
|
||||
self.session.found_terminator()
|
||||
self.assertEqual(len(self.session.input_buffer), 0)
|
||||
|
||||
def test_authentication_with_valid_password_is_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'password "topsecret"')
|
||||
self.assertTrue(authed)
|
||||
self.assertEqual(u'OK', response)
|
||||
|
||||
def test_authentication_with_invalid_password_is_not_accepted(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'password "secret"')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(u'ACK [3@0] {password} incorrect password', response)
|
||||
|
||||
def test_authentication_with_anything_when_password_check_turned_off(self):
|
||||
settings.MPD_SERVER_PASSWORD = None
|
||||
authed, response = self.session.check_password(u'any request at all')
|
||||
self.assertTrue(authed)
|
||||
self.assertEqual(None, response)
|
||||
|
||||
def test_anything_when_not_authenticated_should_fail(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'any request at all')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(
|
||||
u'ACK [4@0] {any} you don\'t have permission for "any"', response)
|
||||
|
||||
def test_close_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'close')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(None, response)
|
||||
|
||||
def test_commands_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'commands')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(None, response)
|
||||
|
||||
def test_notcommands_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'notcommands')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(None, response)
|
||||
|
||||
def test_ping_is_allowed_without_authentication(self):
|
||||
settings.MPD_SERVER_PASSWORD = u'topsecret'
|
||||
authed, response = self.session.check_password(u'ping')
|
||||
self.assertFalse(authed)
|
||||
self.assertEqual(None, response)
|
||||
|
||||
@ -2,7 +2,8 @@ import unittest
|
||||
|
||||
from mopidy.backends.base import PlaybackController
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.frontends.mpd.protocol import status
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track
|
||||
|
||||
@ -12,23 +13,24 @@ STOPPED = PlaybackController.STOPPED
|
||||
|
||||
class StatusHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
self.context = self.dispatcher.context
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_clearerror(self):
|
||||
result = self.h.handle_request(u'clearerror')
|
||||
result = self.dispatcher.handle_request(u'clearerror')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_currentsong(self):
|
||||
track = Track()
|
||||
self.b.current_playlist.append([track])
|
||||
self.b.playback.play()
|
||||
result = self.h.handle_request(u'currentsong')
|
||||
self.backend.current_playlist.append([track])
|
||||
self.backend.playback.play()
|
||||
result = self.dispatcher.handle_request(u'currentsong')
|
||||
self.assert_(u'file: ' in result)
|
||||
self.assert_(u'Time: 0' in result)
|
||||
self.assert_(u'Artist: ' in result)
|
||||
@ -41,27 +43,27 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_currentsong_without_song(self):
|
||||
result = self.h.handle_request(u'currentsong')
|
||||
result = self.dispatcher.handle_request(u'currentsong')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_idle_without_subsystems(self):
|
||||
result = self.h.handle_request(u'idle')
|
||||
result = self.dispatcher.handle_request(u'idle')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_idle_with_subsystems(self):
|
||||
result = self.h.handle_request(u'idle database playlist')
|
||||
result = self.dispatcher.handle_request(u'idle database playlist')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_noidle(self):
|
||||
result = self.h.handle_request(u'noidle')
|
||||
result = self.dispatcher.handle_request(u'noidle')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_stats_command(self):
|
||||
result = self.h.handle_request(u'stats')
|
||||
result = self.dispatcher.handle_request(u'stats')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_stats_method(self):
|
||||
result = dispatcher.status.stats(self.h)
|
||||
result = status.stats(self.context)
|
||||
self.assert_('artists' in result)
|
||||
self.assert_(int(result['artists']) >= 0)
|
||||
self.assert_('albums' in result)
|
||||
@ -78,110 +80,110 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(int(result['playtime']) >= 0)
|
||||
|
||||
def test_status_command(self):
|
||||
result = self.h.handle_request(u'status')
|
||||
result = self.dispatcher.handle_request(u'status')
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_status_method_contains_volume_which_defaults_to_0(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('volume' in result)
|
||||
self.assertEqual(int(result['volume']), 0)
|
||||
|
||||
def test_status_method_contains_volume(self):
|
||||
self.mixer.volume = 17
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('volume' in result)
|
||||
self.assertEqual(int(result['volume']), 17)
|
||||
|
||||
def test_status_method_contains_repeat_is_0(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertEqual(int(result['repeat']), 0)
|
||||
|
||||
def test_status_method_contains_repeat_is_1(self):
|
||||
self.b.playback.repeat = 1
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.repeat = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('repeat' in result)
|
||||
self.assertEqual(int(result['repeat']), 1)
|
||||
|
||||
def test_status_method_contains_random_is_0(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('random' in result)
|
||||
self.assertEqual(int(result['random']), 0)
|
||||
|
||||
def test_status_method_contains_random_is_1(self):
|
||||
self.b.playback.random = 1
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.random = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('random' in result)
|
||||
self.assertEqual(int(result['random']), 1)
|
||||
|
||||
def test_status_method_contains_single(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('single' in result)
|
||||
self.assert_(int(result['single']) in (0, 1))
|
||||
|
||||
def test_status_method_contains_consume_is_0(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('consume' in result)
|
||||
self.assertEqual(int(result['consume']), 0)
|
||||
|
||||
def test_status_method_contains_consume_is_1(self):
|
||||
self.b.playback.consume = 1
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.consume = 1
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('consume' in result)
|
||||
self.assertEqual(int(result['consume']), 1)
|
||||
|
||||
def test_status_method_contains_playlist(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('playlist' in result)
|
||||
self.assert_(int(result['playlist']) in xrange(0, 2**31 - 1))
|
||||
|
||||
def test_status_method_contains_playlistlength(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('playlistlength' in result)
|
||||
self.assert_(int(result['playlistlength']) >= 0)
|
||||
|
||||
def test_status_method_contains_xfade(self):
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('xfade' in result)
|
||||
self.assert_(int(result['xfade']) >= 0)
|
||||
|
||||
def test_status_method_contains_state_is_play(self):
|
||||
self.b.playback.state = PLAYING
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.state = PLAYING
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'play')
|
||||
|
||||
def test_status_method_contains_state_is_stop(self):
|
||||
self.b.playback.state = STOPPED
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.state = STOPPED
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'stop')
|
||||
|
||||
def test_status_method_contains_state_is_pause(self):
|
||||
self.b.playback.state = PLAYING
|
||||
self.b.playback.state = PAUSED
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.state = PLAYING
|
||||
self.backend.playback.state = PAUSED
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('state' in result)
|
||||
self.assertEqual(result['state'], 'pause')
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_song(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('song' in result)
|
||||
self.assert_(int(result['song']) >= 0)
|
||||
|
||||
def test_status_method_when_playlist_loaded_contains_cpid_as_songid(self):
|
||||
self.b.current_playlist.append([Track()])
|
||||
self.b.playback.play()
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.current_playlist.append([Track()])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('songid' in result)
|
||||
self.assertEqual(int(result['songid']), 0)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_no_length(self):
|
||||
self.b.current_playlist.append([Track(length=None)])
|
||||
self.b.playback.play()
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.current_playlist.append([Track(length=None)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
@ -189,9 +191,9 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(position <= total)
|
||||
|
||||
def test_status_method_when_playing_contains_time_with_length(self):
|
||||
self.b.current_playlist.append([Track(length=10000)])
|
||||
self.b.playback.play()
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.current_playlist.append([Track(length=10000)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('time' in result)
|
||||
(position, total) = result['time'].split(':')
|
||||
position = int(position)
|
||||
@ -199,15 +201,15 @@ class StatusHandlerTest(unittest.TestCase):
|
||||
self.assert_(position <= total)
|
||||
|
||||
def test_status_method_when_playing_contains_elapsed(self):
|
||||
self.b.playback.state = PAUSED
|
||||
self.b.playback.play_time_accumulated = 59123
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.playback.state = PAUSED
|
||||
self.backend.playback.play_time_accumulated = 59123
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('elapsed' in result)
|
||||
self.assertEqual(int(result['elapsed']), 59123)
|
||||
|
||||
def test_status_method_when_playing_contains_bitrate(self):
|
||||
self.b.current_playlist.append([Track(bitrate=320)])
|
||||
self.b.playback.play()
|
||||
result = dict(dispatcher.status.status(self.h))
|
||||
self.backend.current_playlist.append([Track(bitrate=320)])
|
||||
self.backend.playback.play()
|
||||
result = dict(status.status(self.context))
|
||||
self.assert_('bitrate' in result)
|
||||
self.assertEqual(int(result['bitrate']), 320)
|
||||
|
||||
@ -1,45 +1,45 @@
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
|
||||
class StickersHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_sticker_get(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker get "song" "file:///dev/urandom" "a_name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_sticker_set(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker set "song" "file:///dev/urandom" "a_name" "a_value"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_sticker_delete_with_name(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker delete "song" "file:///dev/urandom" "a_name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_sticker_delete_without_name(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker delete "song" "file:///dev/urandom"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_sticker_list(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker list "song" "file:///dev/urandom"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_sticker_find(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'sticker find "song" "file:///dev/urandom" "a_name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
@ -2,64 +2,64 @@ import datetime as dt
|
||||
import unittest
|
||||
|
||||
from mopidy.backends.dummy import DummyBackend
|
||||
from mopidy.frontends.mpd import dispatcher
|
||||
from mopidy.frontends.mpd.dispatcher import MpdDispatcher
|
||||
from mopidy.mixers.dummy import DummyMixer
|
||||
from mopidy.models import Track, Playlist
|
||||
|
||||
class StoredPlaylistsHandlerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b = DummyBackend.start().proxy()
|
||||
self.backend = DummyBackend.start().proxy()
|
||||
self.mixer = DummyMixer.start().proxy()
|
||||
self.h = dispatcher.MpdDispatcher()
|
||||
self.dispatcher = MpdDispatcher()
|
||||
|
||||
def tearDown(self):
|
||||
self.b.stop().get()
|
||||
self.backend.stop().get()
|
||||
self.mixer.stop().get()
|
||||
|
||||
def test_listplaylist(self):
|
||||
self.b.stored_playlists.playlists = [
|
||||
self.backend.stored_playlists.playlists = [
|
||||
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
|
||||
result = self.h.handle_request(u'listplaylist "name"')
|
||||
result = self.dispatcher.handle_request(u'listplaylist "name"')
|
||||
self.assert_(u'file: file:///dev/urandom' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_listplaylist_fails_if_no_playlist_is_found(self):
|
||||
result = self.h.handle_request(u'listplaylist "name"')
|
||||
result = self.dispatcher.handle_request(u'listplaylist "name"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [50@0] {listplaylist} No such playlist')
|
||||
|
||||
def test_listplaylistinfo(self):
|
||||
self.b.stored_playlists.playlists = [
|
||||
self.backend.stored_playlists.playlists = [
|
||||
Playlist(name='name', tracks=[Track(uri='file:///dev/urandom')])]
|
||||
result = self.h.handle_request(u'listplaylistinfo "name"')
|
||||
result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
|
||||
self.assert_(u'file: file:///dev/urandom' in result)
|
||||
self.assert_(u'Track: 0' in result)
|
||||
self.assert_(u'Pos: 0' not in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_listplaylistinfo_fails_if_no_playlist_is_found(self):
|
||||
result = self.h.handle_request(u'listplaylistinfo "name"')
|
||||
result = self.dispatcher.handle_request(u'listplaylistinfo "name"')
|
||||
self.assertEqual(result[0],
|
||||
u'ACK [50@0] {listplaylistinfo} No such playlist')
|
||||
|
||||
def test_listplaylists(self):
|
||||
last_modified = dt.datetime(2001, 3, 17, 13, 41, 17, 12345)
|
||||
self.b.stored_playlists.playlists = [Playlist(name='a',
|
||||
self.backend.stored_playlists.playlists = [Playlist(name='a',
|
||||
last_modified=last_modified)]
|
||||
result = self.h.handle_request(u'listplaylists')
|
||||
result = self.dispatcher.handle_request(u'listplaylists')
|
||||
self.assert_(u'playlist: a' in result)
|
||||
# Date without microseconds and with time zone information
|
||||
self.assert_(u'Last-Modified: 2001-03-17T13:41:17Z' in result)
|
||||
self.assert_(u'OK' in result)
|
||||
|
||||
def test_load_known_playlist_appends_to_current_playlist(self):
|
||||
self.b.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 2)
|
||||
self.b.stored_playlists.playlists = [Playlist(name='A-list',
|
||||
self.backend.current_playlist.append([Track(uri='a'), Track(uri='b')])
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 2)
|
||||
self.backend.stored_playlists.playlists = [Playlist(name='A-list',
|
||||
tracks=[Track(uri='c'), Track(uri='d'), Track(uri='e')])]
|
||||
result = self.h.handle_request(u'load "A-list"')
|
||||
result = self.dispatcher.handle_request(u'load "A-list"')
|
||||
self.assert_(u'OK' in result)
|
||||
tracks = self.b.current_playlist.tracks.get()
|
||||
tracks = self.backend.current_playlist.tracks.get()
|
||||
self.assertEqual(len(tracks), 5)
|
||||
self.assertEqual(tracks[0].uri, 'a')
|
||||
self.assertEqual(tracks[1].uri, 'b')
|
||||
@ -68,35 +68,35 @@ class StoredPlaylistsHandlerTest(unittest.TestCase):
|
||||
self.assertEqual(tracks[4].uri, 'e')
|
||||
|
||||
def test_load_unknown_playlist_acks(self):
|
||||
result = self.h.handle_request(u'load "unknown playlist"')
|
||||
result = self.dispatcher.handle_request(u'load "unknown playlist"')
|
||||
self.assert_(u'ACK [50@0] {load} No such playlist' in result)
|
||||
self.assertEqual(len(self.b.current_playlist.tracks.get()), 0)
|
||||
self.assertEqual(len(self.backend.current_playlist.tracks.get()), 0)
|
||||
|
||||
def test_playlistadd(self):
|
||||
result = self.h.handle_request(
|
||||
result = self.dispatcher.handle_request(
|
||||
u'playlistadd "name" "file:///dev/urandom"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_playlistclear(self):
|
||||
result = self.h.handle_request(u'playlistclear "name"')
|
||||
result = self.dispatcher.handle_request(u'playlistclear "name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_playlistdelete(self):
|
||||
result = self.h.handle_request(u'playlistdelete "name" "5"')
|
||||
result = self.dispatcher.handle_request(u'playlistdelete "name" "5"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_playlistmove(self):
|
||||
result = self.h.handle_request(u'playlistmove "name" "5" "10"')
|
||||
result = self.dispatcher.handle_request(u'playlistmove "name" "5" "10"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_rename(self):
|
||||
result = self.h.handle_request(u'rename "old_name" "new_name"')
|
||||
result = self.dispatcher.handle_request(u'rename "old_name" "new_name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_rm(self):
|
||||
result = self.h.handle_request(u'rm "name"')
|
||||
result = self.dispatcher.handle_request(u'rm "name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
def test_save(self):
|
||||
result = self.h.handle_request(u'save "name"')
|
||||
result = self.dispatcher.handle_request(u'save "name"')
|
||||
self.assert_(u'ACK [0@0] {} Not implemented' in result)
|
||||
|
||||
80
tests/gstreamer_test.py
Normal file
80
tests/gstreamer_test.py
Normal file
@ -0,0 +1,80 @@
|
||||
import multiprocessing
|
||||
import unittest
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
# FIXME Our Windows build server does not support GStreamer yet
|
||||
import sys
|
||||
if sys.platform == 'win32':
|
||||
raise SkipTest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.gstreamer import GStreamer
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
# TODO BaseOutputTest?
|
||||
|
||||
class GStreamerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
|
||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||
self.gstreamer = GStreamer()
|
||||
self.gstreamer.on_start()
|
||||
|
||||
def prepare_uri(self, uri):
|
||||
self.gstreamer.prepare_change()
|
||||
self.gstreamer.set_uri(uri)
|
||||
|
||||
def tearDown(self):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_start_playback_existing_file(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.assertTrue(self.gstreamer.start_playback())
|
||||
|
||||
def test_start_playback_non_existing_file(self):
|
||||
self.prepare_uri(self.song_uri + 'bogus')
|
||||
self.assertFalse(self.gstreamer.start_playback())
|
||||
|
||||
def test_pause_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.pause_playback())
|
||||
|
||||
def test_stop_playback_while_playing(self):
|
||||
self.prepare_uri(self.song_uri)
|
||||
self.gstreamer.start_playback()
|
||||
self.assertTrue(self.gstreamer.stop_playback())
|
||||
|
||||
@SkipTest
|
||||
def test_deliver_data(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_end_of_data_stream(self):
|
||||
pass # TODO
|
||||
|
||||
def test_default_get_volume_result(self):
|
||||
self.assertEqual(100, self.gstreamer.get_volume())
|
||||
|
||||
def test_set_volume(self):
|
||||
self.assertTrue(self.gstreamer.set_volume(50))
|
||||
self.assertEqual(50, self.gstreamer.get_volume())
|
||||
|
||||
def test_set_volume_to_zero(self):
|
||||
self.assertTrue(self.gstreamer.set_volume(0))
|
||||
self.assertEqual(0, self.gstreamer.get_volume())
|
||||
|
||||
def test_set_volume_to_one_hundred(self):
|
||||
self.assertTrue(self.gstreamer.set_volume(100))
|
||||
self.assertEqual(100, self.gstreamer.get_volume())
|
||||
|
||||
@SkipTest
|
||||
def test_set_state_encapsulation(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_set_position(self):
|
||||
pass # TODO
|
||||
28
tests/help_test.py
Normal file
28
tests/help_test.py
Normal file
@ -0,0 +1,28 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mopidy
|
||||
|
||||
class HelpTest(unittest.TestCase):
|
||||
def test_help_has_mopidy_options(self):
|
||||
mopidy_dir = os.path.dirname(mopidy.__file__)
|
||||
args = [sys.executable, mopidy_dir, '--help']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--version' in output)
|
||||
self.assert_('--help' in output)
|
||||
self.assert_('--help-gst' in output)
|
||||
self.assert_('--interactive' in output)
|
||||
self.assert_('--quiet' in output)
|
||||
self.assert_('--verbose' in output)
|
||||
self.assert_('--save-debug-log' in output)
|
||||
self.assert_('--list-settings' in output)
|
||||
|
||||
def test_help_gst_has_gstreamer_options(self):
|
||||
mopidy_dir = os.path.dirname(mopidy.__file__)
|
||||
args = [sys.executable, mopidy_dir, '--help-gst']
|
||||
process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
self.assert_('--gst-version' in output)
|
||||
@ -1,7 +1,7 @@
|
||||
import datetime as dt
|
||||
import unittest
|
||||
|
||||
from mopidy.models import Artist, Album, Track, Playlist
|
||||
from mopidy.models import Artist, Album, CpTrack, Track, Playlist
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
@ -274,6 +274,21 @@ class AlbumTest(unittest.TestCase):
|
||||
self.assertNotEqual(hash(album1), hash(album2))
|
||||
|
||||
|
||||
class CpTrackTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cpid = 123
|
||||
self.track = Track()
|
||||
self.cp_track = CpTrack(self.cpid, self.track)
|
||||
|
||||
def test_cp_track_can_be_accessed_as_a_tuple(self):
|
||||
self.assertEqual(self.cpid, self.cp_track[0])
|
||||
self.assertEqual(self.track, self.cp_track[1])
|
||||
|
||||
def test_cp_track_can_be_accessed_by_attribute_names(self):
|
||||
self.assertEqual(self.cpid, self.cp_track.cpid)
|
||||
self.assertEqual(self.track, self.cp_track.track)
|
||||
|
||||
|
||||
class TrackTest(unittest.TestCase):
|
||||
def test_uri(self):
|
||||
uri = u'an_uri'
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
import multiprocessing
|
||||
import unittest
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
# FIXME Our Windows build server does not support GStreamer yet
|
||||
import sys
|
||||
if sys.platform == 'win32':
|
||||
raise SkipTest
|
||||
|
||||
from mopidy import settings
|
||||
from mopidy.outputs.gstreamer import GStreamerOutput
|
||||
from mopidy.utils.path import path_to_uri
|
||||
|
||||
from tests import path_to_data_dir
|
||||
|
||||
class GStreamerOutputTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
settings.BACKENDS = ('mopidy.backends.local.LocalBackend',)
|
||||
self.song_uri = path_to_uri(path_to_data_dir('song1.wav'))
|
||||
self.output = GStreamerOutput()
|
||||
self.output.on_start()
|
||||
|
||||
def tearDown(self):
|
||||
settings.runtime.clear()
|
||||
|
||||
def test_play_uri_existing_file(self):
|
||||
self.assertTrue(self.output.play_uri(self.song_uri))
|
||||
|
||||
def test_play_uri_non_existing_file(self):
|
||||
self.assertFalse(self.output.play_uri(self.song_uri + 'bogus'))
|
||||
|
||||
@SkipTest
|
||||
def test_deliver_data(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_end_of_data_stream(self):
|
||||
pass # TODO
|
||||
|
||||
def test_default_get_volume_result(self):
|
||||
self.assertEqual(100, self.output.get_volume())
|
||||
|
||||
def test_set_volume(self):
|
||||
self.assertTrue(self.output.set_volume(50))
|
||||
self.assertEqual(50, self.output.get_volume())
|
||||
|
||||
def test_set_volume_to_zero(self):
|
||||
self.assertTrue(self.output.set_volume(0))
|
||||
self.assertEqual(0, self.output.get_volume())
|
||||
|
||||
def test_set_volume_to_one_hundred(self):
|
||||
self.assertTrue(self.output.set_volume(100))
|
||||
self.assertEqual(100, self.output.get_volume())
|
||||
|
||||
@SkipTest
|
||||
def test_set_state(self):
|
||||
pass # TODO
|
||||
|
||||
@SkipTest
|
||||
def test_set_position(self):
|
||||
pass # TODO
|
||||
@ -4,7 +4,7 @@ from datetime import date
|
||||
from mopidy.scanner import Scanner, translator
|
||||
from mopidy.models import Track, Artist, Album
|
||||
|
||||
from tests import path_to_data_dir
|
||||
from tests import path_to_data_dir, SkipTest
|
||||
|
||||
class FakeGstDate(object):
|
||||
def __init__(self, year, month, day):
|
||||
@ -144,9 +144,9 @@ class ScannerTest(unittest.TestCase):
|
||||
uri = data['uri'][len('file://'):]
|
||||
self.data[uri] = data
|
||||
|
||||
def error_callback(self, uri, errors):
|
||||
def error_callback(self, uri, error, debug):
|
||||
uri = uri[len('file://'):]
|
||||
self.errors[uri] = errors
|
||||
self.errors[uri] = (error, debug)
|
||||
|
||||
def test_data_is_set(self):
|
||||
self.scan('scanner/simple')
|
||||
@ -184,3 +184,7 @@ class ScannerTest(unittest.TestCase):
|
||||
def test_other_media_is_ignored(self):
|
||||
self.scan('scanner/image')
|
||||
self.assert_(self.errors)
|
||||
|
||||
@SkipTest
|
||||
def test_song_without_time_is_handeled(self):
|
||||
pass
|
||||
|
||||
57
tests/utils/network_test.py
Normal file
57
tests/utils/network_test.py
Normal file
@ -0,0 +1,57 @@
|
||||
import mock
|
||||
import socket
|
||||
import unittest
|
||||
|
||||
from mopidy.utils import network
|
||||
|
||||
from tests import SkipTest
|
||||
|
||||
class FormatHostnameTest(unittest.TestCase):
|
||||
@mock.patch('mopidy.utils.network.has_ipv6', True)
|
||||
def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self):
|
||||
network.has_ipv6 = True
|
||||
self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0')
|
||||
self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1')
|
||||
|
||||
@mock.patch('mopidy.utils.network.has_ipv6', False)
|
||||
def test_format_hostname_does_nothing_when_only_ipv4_available(self):
|
||||
network.has_ipv6 = False
|
||||
self.assertEquals(network.format_hostname('0.0.0.0'), '0.0.0.0')
|
||||
|
||||
|
||||
class TryIPv6SocketTest(unittest.TestCase):
|
||||
@mock.patch('socket.has_ipv6', False)
|
||||
def test_system_that_claims_no_ipv6_support(self):
|
||||
self.assertFalse(network._try_ipv6_socket())
|
||||
|
||||
@mock.patch('socket.has_ipv6', True)
|
||||
@mock.patch('socket.socket')
|
||||
def test_system_with_broken_ipv6(self, socket_mock):
|
||||
socket_mock.side_effect = IOError()
|
||||
self.assertFalse(network._try_ipv6_socket())
|
||||
|
||||
@mock.patch('socket.has_ipv6', True)
|
||||
@mock.patch('socket.socket')
|
||||
def test_with_working_ipv6(self, socket_mock):
|
||||
socket_mock.return_value = mock.Mock()
|
||||
self.assertTrue(network._try_ipv6_socket())
|
||||
|
||||
|
||||
class CreateSocketTest(unittest.TestCase):
|
||||
@mock.patch('mopidy.utils.network.has_ipv6', False)
|
||||
@mock.patch('socket.socket')
|
||||
def test_ipv4_socket(self, socket_mock):
|
||||
network.create_socket()
|
||||
self.assertEqual(socket_mock.call_args[0],
|
||||
(socket.AF_INET, socket.SOCK_STREAM))
|
||||
|
||||
@mock.patch('mopidy.utils.network.has_ipv6', True)
|
||||
@mock.patch('socket.socket')
|
||||
def test_ipv6_socket(self, socket_mock):
|
||||
network.create_socket()
|
||||
self.assertEqual(socket_mock.call_args[0],
|
||||
(socket.AF_INET6, socket.SOCK_STREAM))
|
||||
|
||||
@SkipTest
|
||||
def test_ipv6_only_is_set(self):
|
||||
pass
|
||||
@ -2,14 +2,15 @@ import os
|
||||
import unittest
|
||||
|
||||
from mopidy import settings as default_settings_module, SettingsError
|
||||
from mopidy.utils.settings import validate_settings, SettingsProxy
|
||||
from mopidy.utils.settings import mask_value_if_secret
|
||||
from mopidy.utils.settings import (format_settings_list, mask_value_if_secret,
|
||||
SettingsProxy, validate_settings)
|
||||
|
||||
class ValidateSettingsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.defaults = {
|
||||
'MPD_SERVER_HOSTNAME': '::',
|
||||
'MPD_SERVER_PORT': 6600,
|
||||
'SPOTIFY_BITRATE': 160,
|
||||
}
|
||||
|
||||
def test_no_errors_yields_empty_dict(self):
|
||||
@ -42,6 +43,13 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
'"mopidy.backends.despotify.DespotifyBackend" is no longer ' +
|
||||
'available.')
|
||||
|
||||
def test_unavailable_bitrate_setting_returns_error(self):
|
||||
result = validate_settings(self.defaults,
|
||||
{'SPOTIFY_BITRATE': 50})
|
||||
self.assertEqual(result['SPOTIFY_BITRATE'],
|
||||
u'Unavailable Spotify bitrate. ' +
|
||||
u'Available bitrates are 96, 160, and 320.')
|
||||
|
||||
def test_two_errors_are_both_reported(self):
|
||||
result = validate_settings(self.defaults,
|
||||
{'FOO': '', 'BAR': ''})
|
||||
@ -63,6 +71,7 @@ class ValidateSettingsTest(unittest.TestCase):
|
||||
class SettingsProxyTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.settings = SettingsProxy(default_settings_module)
|
||||
self.settings.local.clear()
|
||||
|
||||
def test_set_and_get_attr(self):
|
||||
self.settings.TEST = 'test'
|
||||
@ -140,3 +149,62 @@ class SettingsProxyTest(unittest.TestCase):
|
||||
self.settings.TEST = './test'
|
||||
actual = self.settings.TEST
|
||||
self.assertEqual(actual, './test')
|
||||
|
||||
def test_interactive_input_of_missing_defaults(self):
|
||||
self.settings.default['TEST'] = ''
|
||||
interactive_input = 'input'
|
||||
self.settings._read_from_stdin = lambda _: interactive_input
|
||||
self.settings.validate(interactive=True)
|
||||
self.assertEqual(interactive_input, self.settings.TEST)
|
||||
|
||||
def test_interactive_input_not_needed_when_setting_is_set_locally(self):
|
||||
self.settings.default['TEST'] = ''
|
||||
self.settings.local['TEST'] = 'test'
|
||||
self.settings._read_from_stdin = lambda _: self.fail(
|
||||
'Should not read from stdin')
|
||||
self.settings.validate(interactive=True)
|
||||
|
||||
|
||||
class FormatSettingListTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.settings = SettingsProxy(default_settings_module)
|
||||
|
||||
def test_contains_the_setting_name(self):
|
||||
self.settings.TEST = u'test'
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_('TEST:' in result, result)
|
||||
|
||||
def test_repr_of_a_string_value(self):
|
||||
self.settings.TEST = u'test'
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("TEST: u'test'" in result, result)
|
||||
|
||||
def test_repr_of_an_int_value(self):
|
||||
self.settings.TEST = 123
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("TEST: 123" in result, result)
|
||||
|
||||
def test_repr_of_a_tuple_value(self):
|
||||
self.settings.TEST = (123, u'abc')
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("TEST: (123, u'abc')" in result, result)
|
||||
|
||||
def test_passwords_are_masked(self):
|
||||
self.settings.TEST_PASSWORD = u'secret'
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("TEST_PASSWORD: u'secret'" not in result, result)
|
||||
self.assert_("TEST_PASSWORD: u'********'" in result, result)
|
||||
|
||||
def test_short_values_are_not_pretty_printed(self):
|
||||
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',)
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("FRONTEND: (u'mopidy.frontends.mpd.MpdFrontend',)" in result,
|
||||
result)
|
||||
|
||||
def test_long_values_are_pretty_printed(self):
|
||||
self.settings.FRONTEND = (u'mopidy.frontends.mpd.MpdFrontend',
|
||||
u'mopidy.frontends.lastfm.LastfmFrontend')
|
||||
result = format_settings_list(self.settings)
|
||||
self.assert_("""FRONTEND:
|
||||
(u'mopidy.frontends.mpd.MpdFrontend',
|
||||
u'mopidy.frontends.lastfm.LastfmFrontend')""" in result, result)
|
||||
|
||||
@ -18,8 +18,9 @@ class VersionTest(unittest.TestCase):
|
||||
self.assert_(SV('0.2.0') < SV('0.3.0'))
|
||||
self.assert_(SV('0.3.0') < SV('0.3.1'))
|
||||
self.assert_(SV('0.3.1') < SV('0.4.0'))
|
||||
self.assert_(SV('0.4.0') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.4.2'))
|
||||
self.assert_(SV('0.4.0') < SV('0.4.1'))
|
||||
self.assert_(SV('0.4.1') < SV(get_plain_version()))
|
||||
self.assert_(SV(get_plain_version()) < SV('0.5.1'))
|
||||
|
||||
def test_get_platform_contains_platform(self):
|
||||
self.assert_(platform.platform() in get_platform())
|
||||
|
||||
Loading…
Reference in New Issue
Block a user