forked from openlp/openlp
Fixed up the remotes documentation.
Set up routing and view methods in HttpConnection.
This commit is contained in:
parent
40a1d59a07
commit
fde2547dee
@ -17,3 +17,9 @@ Helper Classes & Functions
|
|||||||
|
|
||||||
.. automodule:: openlp.plugins.remotes.lib
|
.. automodule:: openlp.plugins.remotes.lib
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpConnection
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpResponse
|
||||||
|
:members:
|
||||||
|
@ -28,6 +28,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import urlparse
|
import urlparse
|
||||||
import re
|
import re
|
||||||
|
from pprint import pformat
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
@ -45,12 +46,18 @@ class HttpResponse(object):
|
|||||||
"""
|
"""
|
||||||
A simple object to encapsulate a pseudo-http response.
|
A simple object to encapsulate a pseudo-http response.
|
||||||
"""
|
"""
|
||||||
content = u''
|
code = '200 OK'
|
||||||
mimetype = None
|
content = ''
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'text/html; charset="utf-8"\r\n'
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, content=u'', mimetype=None):
|
def __init__(self, content='', headers={}, code=None):
|
||||||
self.content = content
|
self.content = content
|
||||||
self.mimetype = mimetype
|
for key, value in headers.iteritems():
|
||||||
|
self.headers[key] = value
|
||||||
|
if code:
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
class HttpServer(object):
|
class HttpServer(object):
|
||||||
@ -110,6 +117,7 @@ class HttpServer(object):
|
|||||||
Item (song) change listener. Store the slide and tell the clients
|
Item (song) change listener. Store the slide and tell the clients
|
||||||
"""
|
"""
|
||||||
self.current_item = items[0].title
|
self.current_item = items[0].title
|
||||||
|
log.debug(pformat(items[0].__dict__, 2))
|
||||||
self.send_poll()
|
self.send_poll()
|
||||||
|
|
||||||
def send_poll(self):
|
def send_poll(self):
|
||||||
@ -148,20 +156,86 @@ class HttpConnection(object):
|
|||||||
"""
|
"""
|
||||||
A single connection, this handles communication between the server
|
A single connection, this handles communication between the server
|
||||||
and the client
|
and the client
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
|
||||||
|
``/``
|
||||||
|
Go to the web interface.
|
||||||
|
|
||||||
|
``/files/{filename}``
|
||||||
|
Serve a static file.
|
||||||
|
|
||||||
|
``/api/poll``
|
||||||
|
Poll to see if there are any changes. Returns a JSON-encoded dict of
|
||||||
|
any changes that occurred::
|
||||||
|
|
||||||
|
{"results": {"type": "controller"}}
|
||||||
|
|
||||||
|
Or, if there were no results, False::
|
||||||
|
|
||||||
|
{"results": False}
|
||||||
|
|
||||||
|
``/api/controller/{live|preview}/{action}``
|
||||||
|
Perform ``{action}`` on the live or preview controller. Valid actions
|
||||||
|
are:
|
||||||
|
|
||||||
|
``next``
|
||||||
|
Load the next slide.
|
||||||
|
|
||||||
|
``previous``
|
||||||
|
Load the previous slide.
|
||||||
|
|
||||||
|
``jump``
|
||||||
|
Jump to a specific slide. Requires an id return in a JSON-encoded
|
||||||
|
dict like so::
|
||||||
|
|
||||||
|
{"request": {"id": 1}}
|
||||||
|
|
||||||
|
``first``
|
||||||
|
Load the first slide.
|
||||||
|
|
||||||
|
``last``
|
||||||
|
Load the last slide.
|
||||||
|
|
||||||
|
``text``
|
||||||
|
Request the text of the current slide.
|
||||||
|
|
||||||
|
``/api/service/{action}``
|
||||||
|
Perform ``{action}`` on the service manager (e.g. go live). Data is
|
||||||
|
passed as a json-encoded ``data`` parameter. Valid actions are:
|
||||||
|
|
||||||
|
``next``
|
||||||
|
Load the next item in the service.
|
||||||
|
|
||||||
|
``previous``
|
||||||
|
Load the previews item in the service.
|
||||||
|
|
||||||
|
``jump``
|
||||||
|
Jump to a specific item in the service. Requires an id returned in
|
||||||
|
a JSON-encoded dict like so::
|
||||||
|
|
||||||
|
{"request": {"id": 1}}
|
||||||
|
|
||||||
|
``list``
|
||||||
|
Request a list of items in the service.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent, socket):
|
def __init__(self, parent, socket):
|
||||||
"""
|
"""
|
||||||
Initialise the http connection. Listen out for socket signals
|
Initialise the http connection. Listen out for socket signals.
|
||||||
"""
|
"""
|
||||||
log.debug(u'Initialise HttpConnection: %s' %
|
log.debug(u'Initialise HttpConnection: %s' %
|
||||||
socket.peerAddress().toString())
|
socket.peerAddress().toString())
|
||||||
self.socket = socket
|
self.socket = socket
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.routes = [
|
self.routes = [
|
||||||
(u'^/$', self.serve_file, [u'filename'], {u'filename': u''}),
|
(u'^/$', self.serve_file),
|
||||||
(r'^/files/(?P<filename>.*)$', self.serve_file, [u'filename'], None),
|
(r'^/files/(.*)$', self.serve_file),
|
||||||
(r'^/send/(?P<name>.*)$', self.process_event, [u'name', u'parameters'], None),
|
#(r'^/send/(.*)$', self.process_event),
|
||||||
(r'^/request/(?P<name>.*)$', self.process_request, [u'name', u'parameters'], None)
|
#(r'^/request/(.*)$', self.process_request),
|
||||||
|
(r'^/api/poll$', self.poll),
|
||||||
|
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
|
||||||
|
(r'^/api/service/(.*)$', self.service)
|
||||||
]
|
]
|
||||||
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'),
|
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'),
|
||||||
self.ready_read)
|
self.ready_read)
|
||||||
@ -180,57 +254,34 @@ class HttpConnection(object):
|
|||||||
response = None
|
response = None
|
||||||
if words[0] == u'GET':
|
if words[0] == u'GET':
|
||||||
url = urlparse.urlparse(words[1])
|
url = urlparse.urlparse(words[1])
|
||||||
params = self.load_params(url.query)
|
self.url_params = urlparse.parse_qs(url.query)
|
||||||
# Loop through the routes we set up earlier and execute them
|
# Loop through the routes we set up earlier and execute them
|
||||||
for route, func, kws, defaults in self.routes:
|
for route, func in self.routes:
|
||||||
match = re.match(route, url.path)
|
match = re.match(route, url.path)
|
||||||
if match:
|
if match:
|
||||||
log.debug(u'Matched on "%s" from "%s"', route, url.path)
|
log.debug('Route "%s" matched "%s"', route, url.path)
|
||||||
log.debug(u'Groups: %s', match.groups())
|
args = []
|
||||||
kwargs = {}
|
for param in match.groups():
|
||||||
# Loop through all the keywords supplied
|
args.append(param)
|
||||||
for keyword in kws:
|
response = func(*args)
|
||||||
groups = match.groupdict()
|
|
||||||
if keyword in groups:
|
|
||||||
# If we find a valid keyword in our URL, use it
|
|
||||||
kwargs[keyword] = groups[keyword]
|
|
||||||
elif defaults and keyword in defaults:
|
|
||||||
# Otherwise, if we have defaults, use them
|
|
||||||
kwargs[keyword] = defaults[keyword]
|
|
||||||
else:
|
|
||||||
# Lastly, set our parameter to None.
|
|
||||||
kwargs[keyword] = None
|
|
||||||
if u'parameters' in kwargs:
|
|
||||||
kwargs[u'parameters'] = params
|
|
||||||
log.debug(u'Keyword arguments: %s', kwargs)
|
|
||||||
response = func(**kwargs)
|
|
||||||
break
|
break
|
||||||
"""
|
|
||||||
folders = url.path.split(u'/')
|
|
||||||
if folders[1] == u'':
|
|
||||||
mimetype, html = self.serve_file(u'')
|
|
||||||
elif folders[1] == u'files':
|
|
||||||
mimetype, html = self.serve_file(os.sep.join(folders[2:]))
|
|
||||||
elif folders[1] == u'send':
|
|
||||||
html = self.process_event(folders[2], params)
|
|
||||||
elif folders[1] == u'request':
|
|
||||||
if self.process_request(folders[2], params):
|
|
||||||
return
|
|
||||||
"""
|
|
||||||
if response:
|
if response:
|
||||||
|
self.send_response(response)
|
||||||
|
"""
|
||||||
if hasattr(response, u'mimetype'):
|
if hasattr(response, u'mimetype'):
|
||||||
self.send_200_ok(response.mimetype)
|
self.send_200_ok(response.mimetype)
|
||||||
else:
|
else:
|
||||||
self.send_200_ok()
|
self.send_200_ok()
|
||||||
if hasattr(response, u'content'):
|
if hasattr(response, u'content'):
|
||||||
self.socket.write(response.content)
|
self.socket.write(response.content)
|
||||||
else:
|
elif isinstance(response, basestring):
|
||||||
self.socket.write(response)
|
self.socket.write(response)
|
||||||
|
"""
|
||||||
else:
|
else:
|
||||||
self.send_404_not_found()
|
self.send_response(HttpResponse(code='404 Not Found'))
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def serve_file(self, **kwargs):
|
def serve_file(self, filename=None):
|
||||||
"""
|
"""
|
||||||
Send a file to the socket. For now, just a subset of file types
|
Send a file to the socket. For now, just a subset of file types
|
||||||
and must be top level inside the html folder.
|
and must be top level inside the html folder.
|
||||||
@ -239,13 +290,12 @@ class HttpConnection(object):
|
|||||||
Ultimately for i18n, this could first look for xx/file.html before
|
Ultimately for i18n, this could first look for xx/file.html before
|
||||||
falling back to file.html... where xx is the language, e.g. 'en'
|
falling back to file.html... where xx is the language, e.g. 'en'
|
||||||
"""
|
"""
|
||||||
filename = kwargs.get(u'filename', u'')
|
|
||||||
log.debug(u'serve file request %s' % filename)
|
log.debug(u'serve file request %s' % filename)
|
||||||
if not filename:
|
if not filename:
|
||||||
filename = u'index.html'
|
filename = u'index.html'
|
||||||
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
|
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
|
||||||
if not path.startswith(self.parent.html_dir):
|
if not path.startswith(self.parent.html_dir):
|
||||||
return None
|
return HttpResponse(code=u'404 Not Found')
|
||||||
ext = os.path.splitext(filename)[1]
|
ext = os.path.splitext(filename)[1]
|
||||||
if ext == u'.html':
|
if ext == u'.html':
|
||||||
mimetype = u'text/html'
|
mimetype = u'text/html'
|
||||||
@ -260,30 +310,62 @@ class HttpConnection(object):
|
|||||||
elif ext == u'.png':
|
elif ext == u'.png':
|
||||||
mimetype = u'image/png'
|
mimetype = u'image/png'
|
||||||
else:
|
else:
|
||||||
return (None, None)
|
mimetype = u'text/plain'
|
||||||
file_handle = None
|
file_handle = None
|
||||||
try:
|
try:
|
||||||
file_handle = open(path, u'rb')
|
file_handle = open(path, u'rb')
|
||||||
log.debug(u'Opened %s' % path)
|
log.debug(u'Opened %s' % path)
|
||||||
html = file_handle.read()
|
content = file_handle.read()
|
||||||
except IOError:
|
except IOError:
|
||||||
log.exception(u'Failed to open %s' % path)
|
log.exception(u'Failed to open %s' % path)
|
||||||
return None
|
return HttpResponse(code=u'404 Not Found')
|
||||||
finally:
|
finally:
|
||||||
if file_handle:
|
if file_handle:
|
||||||
file_handle.close()
|
file_handle.close()
|
||||||
return HttpResponse(content=html, mimetype=mimetype)
|
return HttpResponse(content, {u'Content-Type': mimetype})
|
||||||
|
|
||||||
def load_params(self, query):
|
def poll(self):
|
||||||
"""
|
"""
|
||||||
Decode the query string parameters sent from the browser
|
Poll OpenLP to determine the current slide number and item name.
|
||||||
"""
|
"""
|
||||||
log.debug(u'loading params %s' % query)
|
return HttpResponse(json.dumps({'slide': self.parent.current_slide,
|
||||||
params = urlparse.parse_qs(query)
|
'item': self.parent.current_item}),
|
||||||
if not params:
|
{'Content-Type': 'application/json'})
|
||||||
return None
|
|
||||||
|
def controller(self, type, action):
|
||||||
|
"""
|
||||||
|
Perform an action on the slide controller.
|
||||||
|
|
||||||
|
``type``
|
||||||
|
This is the type of slide controller, either ``preview`` or
|
||||||
|
``live``.
|
||||||
|
|
||||||
|
``action``
|
||||||
|
The action to perform.
|
||||||
|
"""
|
||||||
|
event = u'slidecontroller_%s_%s' % (type, action)
|
||||||
|
if action == u'text':
|
||||||
|
event += u'_request'
|
||||||
|
Receiver.send_message(event)
|
||||||
|
json_data = {u'results': {u'success': True}}
|
||||||
|
#if action == u'text':
|
||||||
|
# json_data = {u'results': }
|
||||||
|
return HttpResponse(json.dumps(json_data),
|
||||||
|
{'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
def service(self, action):
|
||||||
|
event = u'servicemanager_%s' % action
|
||||||
|
if action == u'list':
|
||||||
|
event += u'_request'
|
||||||
else:
|
else:
|
||||||
return params['q']
|
event += u'_item'
|
||||||
|
if self.url_params and self.url_params.get(u'data'):
|
||||||
|
data = json.loads(self.url_params[u'data'][0])
|
||||||
|
Receiver.send_message(event, data[u'request'][u'id'])
|
||||||
|
else:
|
||||||
|
Receiver.send_message(event)
|
||||||
|
return HttpResponse(json.dumps({'results': {u'success': True}}),
|
||||||
|
{'Content-Type': 'application/json'})
|
||||||
|
|
||||||
def process_event(self, **kwargs):
|
def process_event(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -291,11 +373,10 @@ class HttpConnection(object):
|
|||||||
Currently lets anything through. Later we should restrict and perform
|
Currently lets anything through. Later we should restrict and perform
|
||||||
basic parameter checking, otherwise rogue clients could crash openlp
|
basic parameter checking, otherwise rogue clients could crash openlp
|
||||||
"""
|
"""
|
||||||
event = kwargs.get(u'name')
|
event = kwargs.get(u'event')
|
||||||
params = kwargs.get(u'parameters')
|
|
||||||
log.debug(u'Processing event %s' % event)
|
log.debug(u'Processing event %s' % event)
|
||||||
if params:
|
if self.url_params:
|
||||||
Receiver.send_message(event, params)
|
Receiver.send_message(event, self.url_params)
|
||||||
else:
|
else:
|
||||||
Receiver.send_message(event)
|
Receiver.send_message(event)
|
||||||
return json.dumps([u'OK'])
|
return json.dumps([u'OK'])
|
||||||
@ -317,11 +398,10 @@ class HttpConnection(object):
|
|||||||
``params``
|
``params``
|
||||||
Parameters sent with the event.
|
Parameters sent with the event.
|
||||||
"""
|
"""
|
||||||
event = kwargs.get(u'name')
|
event = kwargs.get(u'event')
|
||||||
params = kwargs.get(u'parameters')
|
|
||||||
log.debug(u'Processing request %s' % event)
|
log.debug(u'Processing request %s' % event)
|
||||||
if not event.endswith(u'_request'):
|
if not event.endswith(u'_request'):
|
||||||
return False
|
return None
|
||||||
self.event = event
|
self.event = event
|
||||||
response = event.replace(u'_request', u'_response')
|
response = event.replace(u'_request', u'_response')
|
||||||
QtCore.QObject.connect(Receiver.get_receiver(),
|
QtCore.QObject.connect(Receiver.get_receiver(),
|
||||||
@ -334,11 +414,11 @@ class HttpConnection(object):
|
|||||||
self.timer.start(60000)
|
self.timer.start(60000)
|
||||||
else:
|
else:
|
||||||
self.timer.start(10000)
|
self.timer.start(10000)
|
||||||
if params:
|
if self.url_params:
|
||||||
Receiver.send_message(event, params)
|
Receiver.send_message(event, self.url_params)
|
||||||
else:
|
else:
|
||||||
Receiver.send_message(event)
|
Receiver.send_message(event)
|
||||||
return True
|
return None
|
||||||
|
|
||||||
def process_response(self, data):
|
def process_response(self, data):
|
||||||
"""
|
"""
|
||||||
@ -349,11 +429,18 @@ class HttpConnection(object):
|
|||||||
if not self.socket:
|
if not self.socket:
|
||||||
return
|
return
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
html = json.dumps(data)
|
json_data = json.dumps(data)
|
||||||
self.send_200_ok()
|
self.send_200_ok()
|
||||||
self.socket.write(html)
|
self.socket.write(json_data)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
def send_response(self, response):
|
||||||
|
http = u'HTTP/1.1 %s\r\n' % response.code
|
||||||
|
for header in response.headers.iteritems():
|
||||||
|
http += '%s: %s\r\n' % header
|
||||||
|
http += '\r\n'
|
||||||
|
self.socket.write(response.content)
|
||||||
|
|
||||||
def send_200_ok(self, mimetype='text/html; charset="utf-8"'):
|
def send_200_ok(self, mimetype='text/html; charset="utf-8"'):
|
||||||
"""
|
"""
|
||||||
Successful request. Send OK headers. Assume html for now.
|
Successful request. Send OK headers. Assume html for now.
|
||||||
|
Loading…
Reference in New Issue
Block a user