diff --git a/docs/changelog.rst b/docs/changelog.rst
index e0004ebb..532aee3a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -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.
diff --git a/mopidy/http/__init__.py b/mopidy/http/__init__.py
index 2e131817..95675386 100644
--- a/mopidy/http/__init__.py
+++ b/mopidy/http/__init__.py
@@ -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']),
})
diff --git a/mopidy/http/actor.py b/mopidy/http/actor.py
index 3ab0a480..5a11c3aa 100644
--- a/mopidy/http/actor.py
+++ b/mopidy/http/actor.py
@@ -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',
diff --git a/mopidy/http/data/clients.html b/mopidy/http/data/clients.html
new file mode 100644
index 00000000..feff4fee
--- /dev/null
+++ b/mopidy/http/data/clients.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Mopidy
+
+
+
+
+
Mopidy
+
+
This web server is a part of the Mopidy music server. To learn more
+ about Mopidy, please visit
+ www.mopidy.com .
+
+
+
+
Web clients
+
+
+
+
Web clients which are installed as Mopidy extensions will
+ automatically appear here.
+
+
+
diff --git a/mopidy/http/data/index.html b/mopidy/http/data/index.html
deleted file mode 100644
index d85251c8..00000000
--- a/mopidy/http/data/index.html
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
- Mopidy HTTP frontend
-
-
-
-
-
Mopidy HTTP frontend
-
-
This web server is a part of the music server Mopidy. To learn more
- about Mopidy, please visit
- www.mopidy.com .
-
-
-
-
Static content serving
-
-
To see your own content instead of this placeholder page, change the
- config value http/static_dir to point to the directory
- containing your static files. This can be used to host e.g. a pure
- HTML/CSS/JavaScript Mopidy client.
-
-
If you replace this page with your own content, the Mopidy resources
- at /mopidy/ will still be available.
-
-
-
diff --git a/mopidy/http/data/mopidy.css b/mopidy/http/data/mopidy.css
index c5042769..0bf4522b 100644
--- a/mopidy/http/data/mopidy.css
+++ b/mopidy/http/data/mopidy.css
@@ -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;
-}
diff --git a/mopidy/http/data/mopidy.html b/mopidy/http/data/mopidy.html
deleted file mode 100644
index 38ea2036..00000000
--- a/mopidy/http/data/mopidy.html
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
- Mopidy HTTP frontend
-
-
-
-
-
Mopidy HTTP frontend
-
-
This web server is a part of the music server Mopidy. To learn more
- about Mopidy, please visit www.mopidy.com .
-
-
-
-
WebSocket endpoint
-
-
Mopidy has a WebSocket endpoint at /mopidy/ws/ . You can use
- this end point to access Mopidy's full API, and to get notified about
- events happening in Mopidy.
-
-
-
-
Example
-
-
Here you can see events arriving from Mopidy in real time:
-
-
-
-
Nothing to see? Try playing a track using your MPD client.
-
-
-
-
Documentation
-
-
For more information, please refer to the Mopidy documentation at
- docs.mopidy.com .
-
-
-
-
-
diff --git a/mopidy/http/handlers.py b/mopidy/http/handlers.py
index 5911fe09..00fbf083 100644
--- a/mopidy/http/handlers.py
+++ b/mopidy/http/handlers.py
@@ -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):
diff --git a/tests/http/test_server.py b/tests/http/test_server.py
index 5f3495a5..cc20efe0 100644
--- a/tests/http/test_server.py
+++ b/tests/http/test_server.py
@@ -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')