Merge pull request #235 from jodal/feature/http-frontend
HTTP/WebSocket frontend
This commit is contained in:
commit
1ee2935867
@ -5,6 +5,7 @@ install:
|
||||
- "sudo wget -q -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list"
|
||||
- "sudo apt-get update || true"
|
||||
- "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy/ {print $2}')"
|
||||
- "pip install -r requirements/http.txt" # Until ws4py is packaged as a .deb
|
||||
|
||||
before_script:
|
||||
- "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt"
|
||||
|
||||
@ -7,6 +7,7 @@ include mopidy/backends/spotify/spotify_appkey.key
|
||||
include pylintrc
|
||||
recursive-include docs *
|
||||
prune docs/_build
|
||||
recursive-include mopidy/frontends/http/data/
|
||||
recursive-include requirements *
|
||||
recursive-include tests *.py
|
||||
recursive-include tests/data *
|
||||
|
||||
@ -39,6 +39,7 @@ class Mock(object):
|
||||
|
||||
|
||||
MOCK_MODULES = [
|
||||
'cherrypy',
|
||||
'dbus',
|
||||
'dbus.mainloop',
|
||||
'dbus.mainloop.glib',
|
||||
@ -53,6 +54,11 @@ MOCK_MODULES = [
|
||||
'pykka.registry',
|
||||
'pylast',
|
||||
'serial',
|
||||
'ws4py',
|
||||
'ws4py.messaging',
|
||||
'ws4py.server',
|
||||
'ws4py.server.cherrypyserver',
|
||||
'ws4py.websocket',
|
||||
]
|
||||
for mod_name in MOCK_MODULES:
|
||||
sys.modules[mod_name] = Mock()
|
||||
|
||||
8
docs/modules/frontends/http.rst
Normal file
8
docs/modules/frontends/http.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _http-frontend:
|
||||
|
||||
*********************************************
|
||||
:mod:`mopidy.frontends.http` -- HTTP frontend
|
||||
*********************************************
|
||||
|
||||
.. automodule:: mopidy.frontends.http
|
||||
:synopsis: HTTP and WebSockets frontend
|
||||
@ -19,7 +19,20 @@ class CoreListener(object):
|
||||
"""Helper to allow calling of core listener events"""
|
||||
listeners = pykka.ActorRegistry.get_by_class(CoreListener)
|
||||
for listener in listeners:
|
||||
getattr(listener.proxy(), event)(**kwargs)
|
||||
listener.proxy().on_event(event, **kwargs)
|
||||
|
||||
def on_event(self, event, **kwargs):
|
||||
"""
|
||||
Called on all events.
|
||||
|
||||
*MAY* be implemented by actor. By default, this method forwards the
|
||||
event to the specific event methods.
|
||||
|
||||
:param event: the event name
|
||||
:type event: string
|
||||
:param kwargs: any other arguments to the specific event handlers
|
||||
"""
|
||||
getattr(self, event)(**kwargs)
|
||||
|
||||
def track_playback_paused(self, track, time_position):
|
||||
"""
|
||||
|
||||
132
mopidy/frontends/http/__init__.py
Normal file
132
mopidy/frontends/http/__init__.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""
|
||||
The HTTP frontends lets you control Mopidy through HTTP and WebSockets, e.g.
|
||||
from a web based client.
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- ``cherrypy``
|
||||
|
||||
- ``ws4py``
|
||||
|
||||
**Settings**
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_PORT`
|
||||
|
||||
- :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR`
|
||||
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
When this frontend is included in :attr:`mopidy.settings.FRONTENDS`, it starts
|
||||
a web server at the port specified by :attr:`mopidy.settings.HTTP_SERVER_PORT`.
|
||||
|
||||
.. warning:: Security
|
||||
|
||||
As a simple security measure, the web server is by default only available
|
||||
from localhost. To make it available from other computers, change
|
||||
:attr:`mopidy.settings.HTTP_SERVER_HOSTNAME`. Before you do so, note that
|
||||
the HTTP frontend does not feature any form of user authentication or
|
||||
authorization. Anyone able to access the web server can use the full core
|
||||
API of Mopidy. Thus, you probably only want to make the web server
|
||||
available from your local network or place it behind a web proxy which
|
||||
takes care or user authentication. You have been warned.
|
||||
|
||||
|
||||
Using a web based Mopidy client
|
||||
===============================
|
||||
|
||||
The web server can also host any static files, for example the HTML, CSS,
|
||||
JavaScript, and images needed for a web based Mopidy client. To host static
|
||||
files, change :attr:`mopidy.settings.HTTP_SERVER_STATIC_DIR` to point to the
|
||||
root directory of your web client, e.g.::
|
||||
|
||||
HTTP_SERVER_STATIC_DIR = u'/home/alice/dev/the-client'
|
||||
|
||||
If the directory includes a file named ``index.html``, it will be served on the
|
||||
root of Mopidy's web server.
|
||||
|
||||
If you're making a web based client and wants to do server side development as
|
||||
well, you are of course free to run your own web server and just use Mopidy's
|
||||
web server for the APIs. But, for clients implemented purely in JavaScript,
|
||||
letting Mopidy host the files is a simpler solution.
|
||||
|
||||
|
||||
WebSocket API
|
||||
=============
|
||||
|
||||
.. warning:: API stability
|
||||
|
||||
Since this frontend exposes our internal core API directly it is to be
|
||||
regarded as **experimental**. We cannot promise to keep any form of
|
||||
backwards compatibility between releases as we will need to change the core
|
||||
API while working out how to support new use cases. Thus, if you use this
|
||||
API, you must expect to do small adjustments to your client for every
|
||||
release of Mopidy.
|
||||
|
||||
From Mopidy 1.0 and onwards, we intend to keep the core API far more
|
||||
stable.
|
||||
|
||||
The web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you
|
||||
access to Mopidy's full API and enables Mopidy to instantly push events to the
|
||||
client, as they happen.
|
||||
|
||||
On the WebSocket we send two different kind of messages: The client can send
|
||||
JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses.
|
||||
In addition, the server will send event messages when something happens on the
|
||||
server. Both message types are encoded as JSON objects.
|
||||
|
||||
|
||||
Event messages
|
||||
--------------
|
||||
|
||||
Event objects will always have a key named ``event`` whose value is the event
|
||||
type. Depending on the event type, the event may include additional fields for
|
||||
related data. The events maps directly to the :class:`mopidy.core.CoreListener`
|
||||
API. Refer to the ``CoreListener`` method names is the available event types.
|
||||
The ``CoreListener`` method's keyword arguments are all included as extra
|
||||
fields on the event objects. Example event message::
|
||||
|
||||
{"event": "track_playback_started", "track": {...}}
|
||||
|
||||
|
||||
JSON-RPC 2.0 messaging
|
||||
----------------------
|
||||
|
||||
JSON-RPC 2.0 messages can be recognized by checking for the key named
|
||||
``jsonrpc`` with the string value ``2.0``. For details on the messaging format,
|
||||
please refer to the `JSON-RPC 2.0 spec
|
||||
<http://www.jsonrpc.org/specification>`_.
|
||||
|
||||
All methods (not attributes) in the :ref:`core-api` is made available through
|
||||
JSON-RPC calls over the WebSocket. For example,
|
||||
:meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method
|
||||
``core.playback.play``.
|
||||
|
||||
The core API's attributes is made available through setters and getters. For
|
||||
example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is
|
||||
available as the JSON-RPC method ``core.playback.get_current_track``.
|
||||
|
||||
Example JSON-RPC request::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_current_track"}
|
||||
|
||||
Example JSON-RPC response::
|
||||
|
||||
{"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", ...}}
|
||||
|
||||
The JSON-RPC method ``core.describe`` returns a data structure describing all
|
||||
available methods. If you're unsure how the core API maps to JSON-RPC, having a
|
||||
look at the ``core.describe`` response can be helpful.
|
||||
|
||||
JavaScript wrapper
|
||||
==================
|
||||
|
||||
A JavaScript library wrapping the JSON-RPC over WebSocket API is under
|
||||
development. Details on it will appear here when it's released.
|
||||
"""
|
||||
|
||||
# flake8: noqa
|
||||
from .actor import HttpFrontend
|
||||
113
mopidy/frontends/http/actor.py
Normal file
113
mopidy/frontends/http/actor.py
Normal file
@ -0,0 +1,113 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
import pykka
|
||||
|
||||
from mopidy import exceptions, models, settings
|
||||
from mopidy.core import CoreListener
|
||||
|
||||
try:
|
||||
import cherrypy
|
||||
from ws4py.messaging import TextMessage
|
||||
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
|
||||
except ImportError as import_error:
|
||||
raise exceptions.OptionalDependencyError(import_error)
|
||||
|
||||
from . import ws
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.http')
|
||||
|
||||
|
||||
class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
def __init__(self, core):
|
||||
super(HttpFrontend, self).__init__()
|
||||
self.core = core
|
||||
self._setup_server()
|
||||
self._setup_websocket_plugin()
|
||||
app = self._create_app()
|
||||
self._setup_logging(app)
|
||||
|
||||
def _setup_server(self):
|
||||
cherrypy.config.update({
|
||||
'engine.autoreload_on': False,
|
||||
'server.socket_host': (
|
||||
settings.HTTP_SERVER_HOSTNAME.encode('utf-8')),
|
||||
'server.socket_port': settings.HTTP_SERVER_PORT,
|
||||
})
|
||||
|
||||
def _setup_websocket_plugin(self):
|
||||
WebSocketPlugin(cherrypy.engine).subscribe()
|
||||
cherrypy.tools.websocket = WebSocketTool()
|
||||
|
||||
def _create_app(self):
|
||||
root = RootResource()
|
||||
root.mopidy = MopidyResource()
|
||||
root.mopidy.ws = ws.WebSocketResource(self.core)
|
||||
|
||||
if settings.HTTP_SERVER_STATIC_DIR:
|
||||
static_dir = settings.HTTP_SERVER_STATIC_DIR
|
||||
else:
|
||||
static_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
logger.debug('HTTP server will serve "%s" at /', static_dir)
|
||||
|
||||
mopidy_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
favicon = os.path.join(mopidy_dir, 'favicon.png')
|
||||
|
||||
config = {
|
||||
b'/': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.index': 'index.html',
|
||||
'tools.staticdir.dir': static_dir,
|
||||
},
|
||||
b'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': favicon,
|
||||
},
|
||||
b'/mopidy': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.index': 'mopidy.html',
|
||||
'tools.staticdir.dir': mopidy_dir,
|
||||
},
|
||||
b'/mopidy/ws': {
|
||||
'tools.websocket.on': True,
|
||||
'tools.websocket.handler_cls': ws.WebSocketHandler,
|
||||
},
|
||||
}
|
||||
|
||||
return cherrypy.tree.mount(root, '/', config)
|
||||
|
||||
def _setup_logging(self, app):
|
||||
cherrypy.log.access_log.setLevel(logging.NOTSET)
|
||||
cherrypy.log.error_log.setLevel(logging.NOTSET)
|
||||
cherrypy.log.screen = False
|
||||
|
||||
app.log.access_log.setLevel(logging.NOTSET)
|
||||
app.log.error_log.setLevel(logging.NOTSET)
|
||||
|
||||
def on_start(self):
|
||||
logger.debug('Starting HTTP server')
|
||||
cherrypy.engine.start()
|
||||
logger.info('HTTP server running at %s', cherrypy.server.base())
|
||||
|
||||
def on_stop(self):
|
||||
logger.debug('Stopping HTTP server')
|
||||
cherrypy.engine.exit()
|
||||
logger.info('Stopped HTTP server')
|
||||
|
||||
def on_event(self, name, **data):
|
||||
event = data
|
||||
event['event'] = name
|
||||
message = json.dumps(event, cls=models.ModelJSONEncoder)
|
||||
cherrypy.engine.publish('websocket-broadcast', TextMessage(message))
|
||||
|
||||
|
||||
class RootResource(object):
|
||||
pass
|
||||
|
||||
|
||||
class MopidyResource(object):
|
||||
pass
|
||||
BIN
mopidy/frontends/http/data/favicon.png
Normal file
BIN
mopidy/frontends/http/data/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
29
mopidy/frontends/http/data/index.html
Normal file
29
mopidy/frontends/http/data/index.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mopidy HTTP frontend</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy HTTP frontend</h1>
|
||||
|
||||
<p>This web server is a part of the music server Mopidy. To learn more
|
||||
about Mopidy, please visit
|
||||
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Static content serving</h2>
|
||||
|
||||
<p>To see your own content instead of this placeholder page, change the
|
||||
setting <tt>HTTP_SERVER_STATIC_DIR</tt> to point to the directory
|
||||
containing your static files. This can be used to host e.g. a pure
|
||||
HTML/CSS/JavaScript Mopidy client.</p>
|
||||
|
||||
<p>If you replace this page with your own content, the Mopidy resources
|
||||
at <a href="/mopidy/">/mopidy/</a> will still be available.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
75
mopidy/frontends/http/data/mopidy.css
Normal file
75
mopidy/frontends/http/data/mopidy.css
Normal file
@ -0,0 +1,75 @@
|
||||
html {
|
||||
background: #e8ecef;
|
||||
color: #555;
|
||||
font-family: "Droid Serif", "Georgia", "Times New Roman", "Palatino",
|
||||
"Hoefler Text", "Baskerville", serif;
|
||||
font-size: 150%;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
body {
|
||||
max-width: 20em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
div.box {
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 5px 5px 5px #d8dcdf;
|
||||
margin: 2em 0;
|
||||
padding: 1em;
|
||||
}
|
||||
div.box.focus {
|
||||
background: #465158;
|
||||
color: #e8ecef;
|
||||
}
|
||||
div.icon {
|
||||
float: right;
|
||||
}
|
||||
h1, h2 {
|
||||
font-family: "Ubuntu", "Arial", "Helvetica", "Lucida Grande",
|
||||
"Verdana", "Gill Sans", sans-serif;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
h2 {
|
||||
margin: 0.2em 0 0;
|
||||
}
|
||||
p.next {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
code, pre {
|
||||
font-family: "Droid Sans Mono", Menlo, Courier New, Courier, Mono, monospace;
|
||||
font-size: 9pt;
|
||||
line-height: 1.2em;
|
||||
padding: 0.5em 1em;
|
||||
margin: 1em 0;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
}
|
||||
.box code,
|
||||
.box pre {
|
||||
background: #e8ecef;
|
||||
color: #555;
|
||||
}
|
||||
.box a {
|
||||
color: #465158;
|
||||
}
|
||||
.box a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.box.focus a {
|
||||
color: #e8ecef;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
#ws-console {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
51
mopidy/frontends/http/data/mopidy.html
Normal file
51
mopidy/frontends/http/data/mopidy.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Mopidy HTTP frontend</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy HTTP frontend</h1>
|
||||
|
||||
<p>This web server is a part of the music server Mopidy. To learn more
|
||||
about Mopidy, please visit <a
|
||||
href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>WebSocket endpoint</h2>
|
||||
|
||||
<p>Mopidy has a WebSocket endpoint at <tt>/mopidy/ws/</tt>. You can use
|
||||
this end point to access Mopidy's full API, and to get notified about
|
||||
events happening in Mopidy.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Example</h2>
|
||||
|
||||
<p>Here you can see events arriving from Mopidy in real time:</p>
|
||||
|
||||
<pre id="ws-console"></pre>
|
||||
|
||||
<p>Nothing to see? Try playing a track using your MPD client.</p>
|
||||
</div>
|
||||
|
||||
<div class="box focus">
|
||||
<h2>Documentation</h2>
|
||||
|
||||
<p>For more information, please refer to the Mopidy documentation at
|
||||
<a href="http://docs.mopidy.com/">docs.mopidy.com</a>.</p>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var ws = new WebSocket("ws://" + document.location.host + "/mopidy/ws/");
|
||||
ws.onmessage = function (message) {
|
||||
var console = document.getElementById('ws-console');
|
||||
var newLine = (new Date()).toLocaleTimeString() + ": " +
|
||||
message.data + "\n";
|
||||
console.innerHTML = newLine + console.innerHTML;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
mopidy/frontends/http/ws.py
Normal file
72
mopidy/frontends/http/ws.py
Normal file
@ -0,0 +1,72 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from mopidy import core, exceptions, models
|
||||
from mopidy.utils import jsonrpc
|
||||
|
||||
try:
|
||||
import cherrypy
|
||||
from ws4py.websocket import WebSocket
|
||||
except ImportError as import_error:
|
||||
raise exceptions.OptionalDependencyError(import_error)
|
||||
|
||||
|
||||
logger = logging.getLogger('mopidy.frontends.http')
|
||||
|
||||
|
||||
class WebSocketResource(object):
|
||||
def __init__(self, core_proxy):
|
||||
self._core = core_proxy
|
||||
inspector = jsonrpc.JsonRpcInspector(
|
||||
objects={
|
||||
'core.library': core.LibraryController,
|
||||
'core.playback': core.PlaybackController,
|
||||
'core.playlists': core.PlaylistsController,
|
||||
'core.tracklist': core.TracklistController,
|
||||
})
|
||||
self.jsonrpc = jsonrpc.JsonRpcWrapper(
|
||||
objects={
|
||||
'core.describe': inspector.describe,
|
||||
'core.library': self._core.library,
|
||||
'core.playback': self._core.playback,
|
||||
'core.playlists': self._core.playlists,
|
||||
'core.tracklist': self._core.tracklist,
|
||||
},
|
||||
decoders=[models.model_json_decoder],
|
||||
encoders=[models.ModelJSONEncoder])
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
logger.debug('WebSocket handler created')
|
||||
cherrypy.request.ws_handler.jsonrpc = self.jsonrpc
|
||||
|
||||
|
||||
class WebSocketHandler(WebSocket):
|
||||
def opened(self):
|
||||
remote = cherrypy.request.remote
|
||||
logger.debug(
|
||||
'New WebSocket connection from %s:%d',
|
||||
remote.ip, remote.port)
|
||||
|
||||
def closed(self, code, reason=None):
|
||||
remote = cherrypy.request.remote
|
||||
logger.debug(
|
||||
'Closed WebSocket connection from %s:%d '
|
||||
'with code %s and reason %r',
|
||||
remote.ip, remote.port, code, reason)
|
||||
|
||||
def received_message(self, request):
|
||||
remote = cherrypy.request.remote
|
||||
request = str(request)
|
||||
|
||||
logger.debug(
|
||||
'Received WebSocket message from %s:%d: %r',
|
||||
remote.ip, remote.port, request)
|
||||
|
||||
response = self.jsonrpc.handle_json(request)
|
||||
if response:
|
||||
self.send(response)
|
||||
logger.debug(
|
||||
'Sent WebSocket message to %s:%d: %r',
|
||||
remote.ip, remote.port, response)
|
||||
@ -80,6 +80,39 @@ FRONTENDS = (
|
||||
'mopidy.frontends.mpris.MprisFrontend',
|
||||
)
|
||||
|
||||
#: Which address Mopidy's HTTP server should bind to.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.http`.
|
||||
#:
|
||||
#: Examples:
|
||||
#:
|
||||
#: ``127.0.0.1``
|
||||
#: Listens only on the IPv4 loopback interface. Default.
|
||||
#: ``::1``
|
||||
#: Listens only on the IPv6 loopback interface.
|
||||
#: ``0.0.0.0``
|
||||
#: Listens on all IPv4 interfaces.
|
||||
#: ``::``
|
||||
#: Listens on all interfaces, both IPv4 and IPv6.
|
||||
HTTP_SERVER_HOSTNAME = u'127.0.0.1'
|
||||
|
||||
#: Which TCP port Mopidy's HTTP server should listen to.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.http`.
|
||||
#:
|
||||
#: Default: 6680
|
||||
HTTP_SERVER_PORT = 6680
|
||||
|
||||
#: Which directory Mopidy's HTTP server should serve at /.
|
||||
#:
|
||||
#: Change this to have Mopidy serve e.g. files for your JavaScript client.
|
||||
#: /api and /ws will continue to work as usual even if you change this setting.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.http`.
|
||||
#:
|
||||
#: Default: None
|
||||
HTTP_SERVER_STATIC_DIR = None
|
||||
|
||||
#: Your `Last.fm <http://www.last.fm/>`_ username.
|
||||
#:
|
||||
#: Used by :mod:`mopidy.frontends.lastfm`.
|
||||
|
||||
@ -46,6 +46,9 @@ def setup_console_logging(verbosity_level):
|
||||
if verbosity_level < 3:
|
||||
logging.getLogger('pykka').setLevel(logging.INFO)
|
||||
|
||||
if verbosity_level < 2:
|
||||
logging.getLogger('cherrypy').setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def setup_debug_logging_to_file():
|
||||
formatter = logging.Formatter(settings.DEBUG_LOG_FORMAT)
|
||||
|
||||
2
requirements/http.txt
Normal file
2
requirements/http.txt
Normal file
@ -0,0 +1,2 @@
|
||||
cherrypy >= 3.2.2
|
||||
ws4py >= 0.2.3
|
||||
@ -1,5 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import mock
|
||||
|
||||
from mopidy.core import CoreListener, PlaybackState
|
||||
from mopidy.models import Playlist, Track
|
||||
|
||||
@ -10,6 +12,15 @@ class CoreListenerTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.listener = CoreListener()
|
||||
|
||||
def test_on_event_forwards_to_specific_handler(self):
|
||||
self.listener.track_playback_paused = mock.Mock()
|
||||
|
||||
self.listener.on_event(
|
||||
'track_playback_paused', track=Track(), position=0)
|
||||
|
||||
self.listener.track_playback_paused.assert_called_with(
|
||||
track=Track(), position=0)
|
||||
|
||||
def test_listener_has_default_impl_for_track_playback_paused(self):
|
||||
self.listener.track_playback_paused(Track(), 0)
|
||||
|
||||
|
||||
0
tests/frontends/http/__init__.py
Normal file
0
tests/frontends/http/__init__.py
Normal file
34
tests/frontends/http/events_test.py
Normal file
34
tests/frontends/http/events_test.py
Normal file
@ -0,0 +1,34 @@
|
||||
import json
|
||||
|
||||
import cherrypy
|
||||
import mock
|
||||
|
||||
from mopidy.frontends.http import HttpFrontend
|
||||
|
||||
from tests import unittest
|
||||
|
||||
|
||||
@mock.patch.object(cherrypy.engine, 'publish')
|
||||
class HttpEventsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.http = HttpFrontend(core=mock.Mock())
|
||||
|
||||
def test_track_playback_paused_is_broadcasted(self, publish):
|
||||
publish.reset_mock()
|
||||
self.http.on_event('track_playback_paused', foo='bar')
|
||||
self.assertEqual(publish.call_args[0][0], 'websocket-broadcast')
|
||||
self.assertDictEqual(
|
||||
json.loads(str(publish.call_args[0][1])), {
|
||||
'event': 'track_playback_paused',
|
||||
'foo': 'bar',
|
||||
})
|
||||
|
||||
def test_track_playback_resumed_is_broadcasted(self, publish):
|
||||
publish.reset_mock()
|
||||
self.http.on_event('track_playback_resumed', foo='bar')
|
||||
self.assertEqual(publish.call_args[0][0], 'websocket-broadcast')
|
||||
self.assertDictEqual(
|
||||
json.loads(str(publish.call_args[0][1])), {
|
||||
'event': 'track_playback_resumed',
|
||||
'foo': 'bar',
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user