Merge pull request #759 from jodal/feature/web-client-selector
Add web client selector
This commit is contained in:
commit
e4d4652c68
@ -67,6 +67,9 @@ Feature release.
|
||||
them using pip. See the :ref:`http-server-api` for details. (Fixes:
|
||||
:issue:`440`)
|
||||
|
||||
- Added web page at ``/mopidy/`` which lists all web clients installed as
|
||||
Mopidy extensions. (Fixes: :issue:`440`)
|
||||
|
||||
- Added support for extending the HTTP frontend with additional server side
|
||||
functionality. See :ref:`http-server-api` for details.
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ class Extension(ext.Extension):
|
||||
|
||||
def setup(self, registry):
|
||||
from .actor import HttpFrontend
|
||||
from .handlers import mopidy_app_factory
|
||||
from .handlers import make_mopidy_app_factory
|
||||
|
||||
HttpFrontend.apps = registry['http:app']
|
||||
HttpFrontend.statics = registry['http:static']
|
||||
@ -43,5 +43,6 @@ class Extension(ext.Extension):
|
||||
registry.add('frontend', HttpFrontend)
|
||||
registry.add('http:app', {
|
||||
'name': 'mopidy',
|
||||
'factory': mopidy_app_factory,
|
||||
'factory': make_mopidy_app_factory(
|
||||
registry['http:app'], registry['http:static']),
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@ from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
import pykka
|
||||
@ -68,13 +67,16 @@ class HttpFrontend(pykka.ThreadingActor, CoreListener):
|
||||
request_handlers.extend(self._get_static_request_handlers())
|
||||
|
||||
# Either default Mopidy or user defined path to files
|
||||
static_dir = self.config['http']['static_dir']
|
||||
data_dir = os.path.join(os.path.dirname(__file__), 'data')
|
||||
root_handler = (r'/(.*)', handlers.StaticFileHandler, {
|
||||
'path': static_dir if static_dir else data_dir,
|
||||
'default_filename': 'index.html'
|
||||
})
|
||||
request_handlers.append(root_handler)
|
||||
if self.config['http']['static_dir']:
|
||||
request_handlers.append((r'/(.*)', handlers.StaticFileHandler, {
|
||||
'path': self.config['http']['static_dir'],
|
||||
'default_filename': 'index.html',
|
||||
}))
|
||||
else:
|
||||
request_handlers.append((r'/', tornado.web.RedirectHandler, {
|
||||
'url': '/mopidy/',
|
||||
'permanent': False,
|
||||
}))
|
||||
|
||||
logger.debug(
|
||||
'HTTP routes from extensions: %s',
|
||||
|
||||
31
mopidy/http/data/clients.html
Normal file
31
mopidy/http/data/clients.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mopidy</title>
|
||||
<link rel="stylesheet" type="text/css" href="mopidy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="box focus">
|
||||
<h1>Mopidy</h1>
|
||||
|
||||
<p>This web server is a part of the Mopidy music server. To learn more
|
||||
about Mopidy, please visit
|
||||
<a href="http://www.mopidy.com/">www.mopidy.com</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2>Web clients</h2>
|
||||
|
||||
<ul>
|
||||
{% for app in apps %}
|
||||
<li><a href="/{{ url_escape(app) }}/">{{ escape(app) }}</a></li>
|
||||
{% end %}
|
||||
</ul>
|
||||
|
||||
<p>Web clients which are installed as Mopidy extensions will
|
||||
automatically appear here.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,29 +0,0 @@
|
||||
<!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
|
||||
config value <tt>http/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>
|
||||
@ -1,40 +1,17 @@
|
||||
html {
|
||||
background: #e8ecef;
|
||||
background: #f8f8f8;
|
||||
color: #555;
|
||||
font-family: "Droid Serif", "Georgia", "Times New Roman", "Palatino",
|
||||
"Hoefler Text", "Baskerville", serif;
|
||||
font-size: 150%;
|
||||
font-family: Geneva, Tahoma, Verdana, sans-serif;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
body {
|
||||
max-width: 20em;
|
||||
max-width: 600px;
|
||||
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;
|
||||
font-weight: 500;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
h2 {
|
||||
margin: 0.2em 0 0;
|
||||
}
|
||||
p.next {
|
||||
text-align: right;
|
||||
}
|
||||
a {
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
@ -43,20 +20,18 @@ a {
|
||||
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 {
|
||||
background: white;
|
||||
box-shadow: 0px 5px 5px #f0f0f0;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
}
|
||||
.box code,
|
||||
.box pre {
|
||||
background: #e8ecef;
|
||||
color: #555;
|
||||
.box.focus {
|
||||
background: #465158;
|
||||
color: #e8ecef;
|
||||
}
|
||||
|
||||
.box a {
|
||||
color: #465158;
|
||||
}
|
||||
@ -66,10 +41,3 @@ code, pre {
|
||||
.box.focus a {
|
||||
color: #e8ecef;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
#ws-console {
|
||||
height: 200px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
<!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 src="mopidy.js" type="text/javascript"></script>
|
||||
<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>
|
||||
@ -15,19 +15,24 @@ from mopidy.utils import jsonrpc
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mopidy_app_factory(config, core):
|
||||
return [
|
||||
(r'/ws/?', WebSocketHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/rpc', JsonRpcHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/(.*)', StaticFileHandler, {
|
||||
'path': os.path.join(os.path.dirname(__file__), 'data'),
|
||||
'default_filename': 'mopidy.html'
|
||||
}),
|
||||
]
|
||||
def make_mopidy_app_factory(apps, statics):
|
||||
def mopidy_app_factory(config, core):
|
||||
return [
|
||||
(r'/ws/?', WebSocketHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/rpc', JsonRpcHandler, {
|
||||
'core': core,
|
||||
}),
|
||||
(r'/(.+)', StaticFileHandler, {
|
||||
'path': os.path.join(os.path.dirname(__file__), 'data'),
|
||||
}),
|
||||
(r'/', ClientListHandler, {
|
||||
'apps': apps,
|
||||
'statics': statics,
|
||||
}),
|
||||
]
|
||||
return mopidy_app_factory
|
||||
|
||||
|
||||
def make_jsonrpc_wrapper(core_actor):
|
||||
@ -102,6 +107,12 @@ class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||
self.close()
|
||||
|
||||
|
||||
def set_mopidy_headers(request_handler):
|
||||
request_handler.set_header('Cache-Control', 'no-cache')
|
||||
request_handler.set_header(
|
||||
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
||||
|
||||
|
||||
class JsonRpcHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, core):
|
||||
self.jsonrpc = make_jsonrpc_wrapper(core)
|
||||
@ -131,18 +142,32 @@ class JsonRpcHandler(tornado.web.RequestHandler):
|
||||
self.write_error(500)
|
||||
|
||||
def set_extra_headers(self):
|
||||
set_mopidy_headers(self)
|
||||
self.set_header('Accept', 'application/json')
|
||||
self.set_header('Cache-Control', 'no-cache')
|
||||
self.set_header(
|
||||
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
||||
self.set_header('Content-Type', 'application/json; utf-8')
|
||||
|
||||
|
||||
class ClientListHandler(tornado.web.RequestHandler):
|
||||
def initialize(self, apps, statics):
|
||||
self.apps = apps
|
||||
self.statics = statics
|
||||
|
||||
def get(self):
|
||||
set_mopidy_headers(self)
|
||||
|
||||
names = set()
|
||||
for app in self.apps:
|
||||
names.add(app['name'])
|
||||
for static in self.statics:
|
||||
names.add(static['name'])
|
||||
names.discard('mopidy')
|
||||
|
||||
self.render('data/clients.html', apps=sorted(list(names)))
|
||||
|
||||
|
||||
class StaticFileHandler(tornado.web.StaticFileHandler):
|
||||
def set_extra_headers(self, path):
|
||||
self.set_header('Cache-Control', 'no-cache')
|
||||
self.set_header(
|
||||
'X-Mopidy-Version', mopidy.__version__.encode('utf-8'))
|
||||
set_mopidy_headers(self)
|
||||
|
||||
|
||||
class AddSlashHandler(tornado.web.RequestHandler):
|
||||
|
||||
@ -12,8 +12,8 @@ from mopidy.http import actor, handlers
|
||||
|
||||
|
||||
class HttpServerTest(tornado.testing.AsyncHTTPTestCase):
|
||||
def get_app(self):
|
||||
config = {
|
||||
def get_config(self):
|
||||
return {
|
||||
'http': {
|
||||
'hostname': '127.0.0.1',
|
||||
'port': 6680,
|
||||
@ -21,34 +21,49 @@ class HttpServerTest(tornado.testing.AsyncHTTPTestCase):
|
||||
'zeroconf': '',
|
||||
}
|
||||
}
|
||||
|
||||
def get_app(self):
|
||||
core = mock.Mock()
|
||||
core.get_version = mock.MagicMock(name='get_version')
|
||||
core.get_version.return_value = mopidy.__version__
|
||||
|
||||
http_frontend = actor.HttpFrontend(config=config, core=core)
|
||||
apps = [dict(name='testapp')]
|
||||
statics = [dict(name='teststatic')]
|
||||
|
||||
http_frontend = actor.HttpFrontend(config=self.get_config(), core=core)
|
||||
http_frontend.apps = [{
|
||||
'name': 'mopidy',
|
||||
'factory': handlers.mopidy_app_factory,
|
||||
'factory': handlers.make_mopidy_app_factory(apps, statics),
|
||||
}]
|
||||
|
||||
return tornado.web.Application(http_frontend._get_request_handlers())
|
||||
|
||||
|
||||
class RootAppTest(HttpServerTest):
|
||||
def test_should_return_index(self):
|
||||
response = self.fetch('/', method='GET')
|
||||
class RootRedirectTest(HttpServerTest):
|
||||
def test_should_redirect_to_mopidy_app(self):
|
||||
response = self.fetch('/', method='GET', follow_redirects=False)
|
||||
|
||||
self.assertIn(
|
||||
'Static content serving',
|
||||
tornado.escape.to_unicode(response.body))
|
||||
self.assertEqual(
|
||||
response.headers['X-Mopidy-Version'], mopidy.__version__)
|
||||
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
|
||||
self.assertEqual(response.code, 302)
|
||||
self.assertEqual(response.headers['Location'], '/mopidy/')
|
||||
|
||||
|
||||
class LegacyStaticDirAppTest(HttpServerTest):
|
||||
def get_config(self):
|
||||
config = super(LegacyStaticDirAppTest, self).get_config()
|
||||
config['http']['static_dir'] = os.path.dirname(__file__)
|
||||
return config
|
||||
|
||||
def test_should_return_index(self):
|
||||
response = self.fetch('/', method='GET', follow_redirects=False)
|
||||
|
||||
self.assertEqual(response.code, 404, 'No index.html in this dir')
|
||||
|
||||
def test_should_return_static_files(self):
|
||||
response = self.fetch('/mopidy.css', method='GET')
|
||||
response = self.fetch('/test_server.py', method='GET')
|
||||
|
||||
self.assertIn('html {', tornado.escape.to_unicode(response.body))
|
||||
self.assertIn(
|
||||
'test_should_return_static_files',
|
||||
tornado.escape.to_unicode(response.body))
|
||||
self.assertEqual(
|
||||
response.headers['X-Mopidy-Version'], mopidy.__version__)
|
||||
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
|
||||
@ -57,10 +72,12 @@ class RootAppTest(HttpServerTest):
|
||||
class MopidyAppTest(HttpServerTest):
|
||||
def test_should_return_index(self):
|
||||
response = self.fetch('/mopidy/', method='GET')
|
||||
body = tornado.escape.to_unicode(response.body)
|
||||
|
||||
self.assertIn(
|
||||
'Here you can see events arriving from Mopidy in real time:',
|
||||
tornado.escape.to_unicode(response.body))
|
||||
'This web server is a part of the Mopidy music server.', body)
|
||||
self.assertIn('testapp', body)
|
||||
self.assertIn('teststatic', body)
|
||||
self.assertEqual(
|
||||
response.headers['X-Mopidy-Version'], mopidy.__version__)
|
||||
self.assertEqual(response.headers['Cache-Control'], 'no-cache')
|
||||
|
||||
Loading…
Reference in New Issue
Block a user