comments, update client, load html from disk, blanking

This commit is contained in:
Jonathan Corwin 2010-05-01 13:10:48 +01:00
parent 6c52ad359f
commit 9ef2b13ad6
7 changed files with 295 additions and 140 deletions

View File

@ -83,6 +83,12 @@ class EventReceiver(QtCore.QObject):
Returns a slidecontroller_{live|preview}_text_response with an Returns a slidecontroller_{live|preview}_text_response with an
array of dictionaries with the tag and verse text array of dictionaries with the tag and verse text
``slidecontroller_{live|preview}_blank``
Request that the output screen is blanked
``slidecontroller_{live|preview}_unblank``
Request that the output screen is unblanked
``slidecontroller_live_spin_delay`` ``slidecontroller_live_spin_delay``
Pushes out the loop delay Pushes out the loop delay
@ -132,6 +138,9 @@ class EventReceiver(QtCore.QObject):
``videodisplay_stop`` ``videodisplay_stop``
Stop playing a media item Stop playing a media item
``videodisplay_background``
Replace the background video
``theme_update_list`` ``theme_update_list``
send out message with new themes send out message with new themes
@ -196,6 +205,10 @@ class EventReceiver(QtCore.QObject):
``bibles_stop_import`` ``bibles_stop_import``
Stops the Bible Import Stops the Bible Import
``remotes_poll_request``
Waits for openlp to do something "interesting" and sends a
remotes_poll_response signal when it does
""" """
def __init__(self): def __init__(self):
""" """

View File

@ -341,6 +341,12 @@ class SlideController(QtGui.QWidget):
QtCore.QObject.connect(Receiver.get_receiver(), QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix), QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix),
self.onSlideSelectedIndex) self.onSlideSelectedIndex)
QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(u'slidecontroller_%s_blank' % self.type_prefix),
self.onSlideBlank)
QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(u'slidecontroller_%s_unblank' % self.type_prefix),
self.onSlideUnblank)
QtCore.QObject.connect(Receiver.get_receiver(), QtCore.QObject.connect(Receiver.get_receiver(),
QtCore.SIGNAL(u'slidecontroller_%s_text_request' % self.type_prefix), QtCore.SIGNAL(u'slidecontroller_%s_text_request' % self.type_prefix),
self.onTextRequest) self.onTextRequest)
@ -616,6 +622,18 @@ class SlideController(QtGui.QWidget):
self.PreviewListWidget.selectRow(index) self.PreviewListWidget.selectRow(index)
self.onSlideSelected() self.onSlideSelected()
def onSlideBlank(self):
"""
Handle the slidecontroller blank event
"""
self.onBlankDisplay(True)
def onSlideUnblank(self):
"""
Handle the slidecontroller unblank event
"""
self.onBlankDisplay(False)
def onBlankDisplay(self, checked): def onBlankDisplay(self, checked):
""" """
Handle the blank screen button Handle the blank screen button

View File

@ -75,7 +75,10 @@ class AlertsManager(QtCore.QObject):
Called via a alerts_text event. Message is single element array Called via a alerts_text event. Message is single element array
containing text containing text
""" """
if message:
self.displayAlert(message[0]) self.displayAlert(message[0])
else:
self.displayAlert(u'')
def displayAlert(self, text=u''): def displayAlert(self, text=u''):
""" """

View File

@ -0,0 +1,117 @@
<html>
<head>
<title>OpenLP Controller</title>
<script type='text/javascript'>
function send_event(eventname, data){
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if(req.readyState==4)
response(eventname, req);
}
var url = '';
if(eventname.substr(-8) == '_request')
url = 'request';
else
url = 'send';
url += '/' + eventname;
if(data!=null)
url += '?q=' + escape(data);
req.open('GET', url, true);
req.send();
}
function failed_response(eventname, req){
switch(eventname){
case 'remotes_poll_request':
if(req.status==408)
send_event("remotes_poll_request");
break;
}
}
function response(eventname, req){
if(req.status!=200){
failed_response(eventname, req);
return;
}
text = req.responseText;
switch(eventname){
case 'servicemanager_list_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'servicemanager_set_item', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>'
html += '<td>' + (parseInt(row)+1) + '</td>'
html += '<td>' + data[row]['title'] + '</td>'
html += '<td>' + data[row]['plugin'] + '</td>'
html += '<td>' + data[row]['notes'] + '</td>'
html += '</tr>';
}
html += '</table>';
document.getElementById('service').innerHTML = html;
break;
case 'slidecontroller_live_text_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'slidecontroller_live_set', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>';
html += '<td>' + data[row]['tag'] + '</td>';
html += '<td>' + data[row]['text'].replace(/\\n/g, '<br>');
html += '</td></tr>';
}
html += '</table>';
document.getElementById('currentitem').innerHTML = html;
break;
case 'remotes_poll_request':
send_event("remotes_poll_request");
send_event("servicemanager_list_request");
send_event("slidecontroller_live_text_request");
break;
}
}
send_event("servicemanager_list_request");
send_event("slidecontroller_live_text_request");
send_event("remotes_poll_request");
</script>
</head>
<body>
<h1>OpenLP Controller</h1>
<input type='button' value='<- Previous Slide'
onclick='send_event("slidecontroller_live_previous");' />
<input type='button' value='Next Slide ->'
onclick='send_event("slidecontroller_live_next");' />
<br/>
<input type='button' value='<- Previous Item'
onclick='send_event("servicemanager_previous_item");' />
<input type='button' value='Next Item ->'
onclick='send_event("servicemanager_next_item");' />
<br/>
<input type='button' value='Blank'
onclick='send_event("slidecontroller_live_blank");' />
<input type='button' value='Unblank'
onclick='send_event("slidecontroller_live_unblank");' />
<br/>
<label>Alert text</label><input id='alert' type='text' />
<input type='button' value='Send'
onclick='send_event("alerts_text",
document.getElementById("alert").value);' />
<hr>
<input type='button' value='Order of service'
onclick='send_event("servicemanager_list_request");'>
<div id='service'></div>
<hr>
<input type='button' value='Current item'
onclick='send_event("slidecontroller_live_text_request");'>
<div id='currentitem'></div>
<hr>
<a href="http://www.openlp.org/">OpenLP website</a>
</body>
</html>

View File

@ -24,12 +24,14 @@
############################################################################### ###############################################################################
import logging import logging
import os
import json import json
import urlparse import urlparse
from PyQt4 import QtCore, QtNetwork from PyQt4 import QtCore, QtNetwork
from openlp.core.lib import Receiver from openlp.core.lib import Receiver
from openlp.core.utils import AppLocation
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -40,14 +42,25 @@ class HttpServer(object):
http://localhost:4316/send/alerts_text?q=your%20alert%20text http://localhost:4316/send/alerts_text?q=your%20alert%20text
""" """
def __init__(self, parent): def __init__(self, parent):
"""
Initialise the httpserver, and start the server
"""
log.debug(u'Initialise httpserver') log.debug(u'Initialise httpserver')
self.parent = parent self.parent = parent
self.html_dir = os.path.join(
AppLocation.get_directory(AppLocation.PluginsDir),
u'remotes', u'html')
self.connections = [] self.connections = []
self.current_item = None self.current_item = None
self.current_slide = None self.current_slide = None
self.start_tcp() self.start_tcp()
def start_tcp(self): def start_tcp(self):
"""
Start the http server, use the port in the settings default to 4316
Listen out for slide and song changes so they can be broadcast to
clients. Listen out for socket connections
"""
log.debug(u'Start TCP server') log.debug(u'Start TCP server')
port = QtCore.QSettings().value( port = QtCore.QSettings().value(
self.parent.settingsSection + u'/remote port', self.parent.settingsSection + u'/remote port',
@ -66,29 +79,48 @@ class HttpServer(object):
log.debug(u'TCP listening on port %d' % port) log.debug(u'TCP listening on port %d' % port)
def slide_change(self, row): def slide_change(self, row):
"""
Slide change listener. Store the item and tell the clients
"""
self.current_slide = row self.current_slide = row
self.send_poll() self.send_poll()
def item_change(self, items): def item_change(self, items):
"""
Item (song) change listener. Store the slide and tell the clients
"""
self.current_item = items[0].title self.current_item = items[0].title
self.send_poll() self.send_poll()
def send_poll(self): def send_poll(self):
Receiver.send_message(u'remote_poll_response', """
Tell the clients something has changed
"""
Receiver.send_message(u'remotes_poll_response',
{'slide': self.current_slide, {'slide': self.current_slide,
'item': self.current_item}) 'item': self.current_item})
def new_connection(self): def new_connection(self):
"""
A new http connection has been made. Create a client object to handle
communication
"""
log.debug(u'new http connection') log.debug(u'new http connection')
socket = self.server.nextPendingConnection() socket = self.server.nextPendingConnection()
if socket: if socket:
self.connections.append(HttpConnection(self, socket)) self.connections.append(HttpConnection(self, socket))
def close_connection(self, connection): def close_connection(self, connection):
"""
The connection has been closed. Clean up
"""
log.debug(u'close http connection') log.debug(u'close http connection')
self.connections.remove(connection) self.connections.remove(connection)
def close(self): def close(self):
"""
Close down the http server
"""
log.debug(u'close http server') log.debug(u'close http server')
self.server.close() self.server.close()
@ -98,6 +130,9 @@ class HttpConnection(object):
and the client and the client
""" """
def __init__(self, parent, socket): def __init__(self, parent, socket):
"""
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
@ -108,6 +143,9 @@ class HttpConnection(object):
self.disconnected) self.disconnected)
def ready_read(self): def ready_read(self):
"""
Data has been sent from the client. Respond to it
"""
log.debug(u'ready to read socket') log.debug(u'ready to read socket')
if self.socket.canReadLine(): if self.socket.canReadLine():
data = unicode(self.socket.readLine()) data = unicode(self.socket.readLine())
@ -119,7 +157,9 @@ class HttpConnection(object):
params = self.load_params(url.query) params = self.load_params(url.query)
folders = url.path.split(u'/') folders = url.path.split(u'/')
if folders[1] == u'': if folders[1] == u'':
html = self.process_index() html = self.serve_file(u'')
elif folders[1] == u'files':
html = self.serve_file(folders[2])
elif folders[1] == u'send': elif folders[1] == u'send':
html = self.process_event(folders[2], params) html = self.process_event(folders[2], params)
elif folders[1] == u'request': elif folders[1] == u'request':
@ -132,119 +172,38 @@ class HttpConnection(object):
self.socket.write(html) self.socket.write(html)
self.close() self.close()
def process_index(self): def serve_file(self, filename):
return u""" """
<html> Send a file to the socket. For now, just .html files
<head> and must be top level inside the html folder.
<title>OpenLP Controller</title> If subfolders requested return 404, easier for security for the present.
<script type='text/javascript'>
function send_event(eventname, data){ Ultimately for i18n, this could first look for xx/file.html before
var req = new XMLHttpRequest(); falling back to file.html... where xx is the language, e.g. 'en'
req.onreadystatechange = function() { """
if(req.readyState==4) log.debug(u'serve file request %s' % filename)
response(eventname, req); if not filename:
} filename = u'index.html'
var url = ''; if os.path.basename(filename) != filename:
if(eventname.substr(-8) == '_request') return None
url = 'request'; (fileroot, ext) = os.path.splitext(filename)
else if ext != u'.html':
url = 'send'; return None
url += '/' + eventname; path = os.path.join(self.parent.html_dir, filename)
if(data!=null) try:
url += '?q=' + escape(data); f = open(path, u'rb')
req.open('GET', url, true); except:
req.send(); log.exception(u'Failed to open %s' % path)
} return None
function failed_response(eventname, req){ log.debug(u'Opened %s' % path)
switch(eventname){ html = f.read()
case 'remote_poll_request': f.close()
if(req.status==408) return html
send_event("remote_poll_request");
break;
}
}
function response(eventname, req){
if(req.status!=200){
failed_response(eventname, req);
return;
}
text = req.responseText;
switch(eventname){
case 'servicemanager_list_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'servicemanager_set_item', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>'
html += '<td>' + (parseInt(row)+1) + '</td>'
html += '<td>' + data[row]['title'] + '</td>'
html += '<td>' + data[row]['plugin'] + '</td>'
html += '<td>' + data[row]['notes'] + '</td>'
html += '</tr>';
}
html += '</table>';
document.getElementById('service').innerHTML = html;
break;
case 'slidecontroller_live_text_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'slidecontroller_live_set', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>';
html += '<td>' + data[row]['tag'] + '</td>';
html += '<td>' + data[row]['text'].replace(/\\n/g, '<br>');
html += '</td></tr>';
}
html += '</table>';
document.getElementById('currentitem').innerHTML = html;
break;
case 'remote_poll_request':
send_event("remote_poll_request");
send_event("servicemanager_list_request");
send_event("slidecontroller_live_text_request");
break;
}
}
send_event("servicemanager_list_request");
send_event("slidecontroller_live_text_request");
send_event("remote_poll_request");
</script>
</head>
<body>
<h1>OpenLP Controller</h1>
<input type='button' value='<- Previous Slide'
onclick='send_event("slidecontroller_live_previous");' />
<input type='button' value='Next Slide ->'
onclick='send_event("slidecontroller_live_next");' />
<br>
<input type='button' value='<- Previous Item'
onclick='send_event("servicemanager_previous_item");' />
<input type='button' value='Next Item ->'
onclick='send_event("servicemanager_next_item");' />
<br/>
<label>Alert text</label><input id='alert' type='text' />
<input type='button' value='Send'
onclick='send_event("alerts_text",
document.getElementById("alert").value);' />
<br/>
<input type='button' value='Order of service'
onclick='send_event("servicemanager_list_request");'>
<div id='service'></div>
<input type='button' value='Current item'
onclick='send_event("slidecontroller_live_text_request");'>
<div id='currentitem'></div>
</body>
</html>
"""
def load_params(self, query): def load_params(self, query):
"""
Decode the query string parameters sent from the browser
"""
params = urlparse.parse_qs(query) params = urlparse.parse_qs(query)
if not params: if not params:
return None return None
@ -252,6 +211,11 @@ send_event("remote_poll_request");
return params['q'] return params['q']
def process_event(self, event, params): def process_event(self, event, params):
"""
Send a signal to openlp to perform an action.
Currently lets anything through. Later we should restrict and perform
basic parameter checking, otherwise rogue clients could crash openlp
"""
if params: if params:
Receiver.send_message(event, params) Receiver.send_message(event, params)
else: else:
@ -259,6 +223,16 @@ send_event("remote_poll_request");
return u'OK' return u'OK'
def process_request(self, event, params): def process_request(self, event, params):
"""
Client has requested data. Send the signal and parameters for openlp
to handle, then listen out for a corresponding _request signal
which will have the data to return.
For most event timeout after 10 seconds (i.e. incase the signal
recipient isn't listening)
remotes_poll_request is a special case, this is a ajax long poll which
is just waiting for slide change/song change activity. This can wait
longer (one minute)
"""
if not event.endswith(u'_request'): if not event.endswith(u'_request'):
return False return False
self.event = event self.event = event
@ -269,7 +243,7 @@ send_event("remote_poll_request");
self.timer.setSingleShot(True) self.timer.setSingleShot(True)
QtCore.QObject.connect(self.timer, QtCore.QObject.connect(self.timer,
QtCore.SIGNAL(u'timeout()'), self.timeout) QtCore.SIGNAL(u'timeout()'), self.timeout)
if event == 'remote_poll_request': if event == 'remotes_poll_request':
self.timer.start(60000) self.timer.start(60000)
else: else:
self.timer.start(10000) self.timer.start(10000)
@ -280,6 +254,10 @@ send_event("remote_poll_request");
return True return True
def process_response(self, data): def process_response(self, data):
"""
The recipient of a _request signal has sent data. Convert this to
json and return it to client
"""
if not self.socket: if not self.socket:
return return
self.timer.stop() self.timer.stop()
@ -289,19 +267,32 @@ send_event("remote_poll_request");
self.close() self.close()
def get_200_ok(self): def get_200_ok(self):
"""
Successful request. Send OK headers. Assume html for now.
"""
return u'HTTP/1.1 200 OK\r\n' + \ return u'HTTP/1.1 200 OK\r\n' + \
u'Content-Type: text/html; charset="utf-8"\r\n' + \ u'Content-Type: text/html; charset="utf-8"\r\n' + \
u'\r\n' u'\r\n'
def get_404_not_found(self): def get_404_not_found(self):
"""
Invalid url. Say so
"""
return u'HTTP/1.1 404 Not Found\r\n'+ \ return u'HTTP/1.1 404 Not Found\r\n'+ \
u'Content-Type: text/html; charset="utf-8"\r\n' + \ u'Content-Type: text/html; charset="utf-8"\r\n' + \
u'\r\n' u'\r\n'
def get_408_timeout(self): def get_408_timeout(self):
"""
A _request hasn't returned anything in the timeout period.
Return timeout
"""
return u'HTTP/1.1 408 Request Timeout\r\n' return u'HTTP/1.1 408 Request Timeout\r\n'
def timeout(self): def timeout(self):
"""
Listener for timeout signal
"""
if not self.socket: if not self.socket:
return return
html = self.get_408_timeout() html = self.get_408_timeout()
@ -309,10 +300,16 @@ send_event("remote_poll_request");
self.close() self.close()
def disconnected(self): def disconnected(self):
"""
The client has disconnected. Tidy up
"""
log.debug(u'socket disconnected') log.debug(u'socket disconnected')
self.close() self.close()
def close(self): def close(self):
"""
The server has closed the connection. Tidy up
"""
if not self.socket: if not self.socket:
return return
log.debug(u'close socket') log.debug(u'close socket')

View File

@ -36,17 +36,26 @@ class RemotesPlugin(Plugin):
log.info(u'Remote Plugin loaded') log.info(u'Remote Plugin loaded')
def __init__(self, plugin_helpers): def __init__(self, plugin_helpers):
"""
remotes constructor
"""
Plugin.__init__(self, u'Remotes', u'1.9.1', plugin_helpers) Plugin.__init__(self, u'Remotes', u'1.9.1', plugin_helpers)
self.weight = -1 self.weight = -1
self.server = None self.server = None
def initialise(self): def initialise(self):
"""
Initialise the remotes plugin, and start the http server
"""
log.debug(u'initialise') log.debug(u'initialise')
Plugin.initialise(self) Plugin.initialise(self)
self.insert_toolbox_item() self.insert_toolbox_item()
self.server = HttpServer(self) self.server = HttpServer(self)
def finalise(self): def finalise(self):
"""
Tidy up and close down the http server
"""
log.debug(u'finalise') log.debug(u'finalise')
self.remove_toolbox_item() self.remove_toolbox_item()
if self.server: if self.server:
@ -59,8 +68,11 @@ class RemotesPlugin(Plugin):
return RemoteTab(self.name) return RemoteTab(self.name)
def about(self): def about(self):
"""
Information about this plugin
"""
about_text = self.trUtf8('<b>Remote Plugin</b><br>This plugin ' about_text = self.trUtf8('<b>Remote Plugin</b><br>This plugin '
'provides the ability to send messages to a running version of ' 'provides the ability to send messages to a running version of '
'openlp on a different computer.<br>The Primary use for this ' 'openlp on a different computer via a web browser or other app<br>'
'would be to send alerts from a creche') 'The Primary use for this would be to send alerts from a creche')
return about_text return about_text

View File

@ -24,34 +24,33 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Temple Place, Suite 330, Boston, MA 02111-1307 USA #
############################################################################### ###############################################################################
import socket import urllib
import sys import sys
from optparse import OptionParser from optparse import OptionParser
def sendData(options, message): def sendData(options):
addr = (options.address, options.port) addr = 'http://%s:%s/send/%s?q=%s' % (options.address, options.port,
options.event, options.message)
try: try:
UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) urllib.urlopen(addr)
UDPSock.sendto(message, addr) print u'Message sent ', addr
print u'message sent ', message, addr
except: except:
print u'Errow thrown ', sys.exc_info()[1] print u'Error thrown ', sys.exc_info()[1]
def format_message(options):
return u'%s:%s' % (u'alert', options.message)
def main(): def main():
usage = "usage: %prog [options] arg1 arg2" usage = "usage: %prog [-a address] [-p port] [-e event] [-m message]"
parser = OptionParser(usage=usage) parser = OptionParser(usage=usage)
parser.add_option("-v", "--verbose",
action="store_true", dest="verbose", default=True,
help="make lots of noise [%default]")
parser.add_option("-p", "--port", default=4316, parser.add_option("-p", "--port", default=4316,
help="IP Port number %default ") help="IP Port number %default ")
parser.add_option("-a", "--address", parser.add_option("-a", "--address",
help="Recipient address ") help="Recipient address ",
default="localhost")
parser.add_option("-e", "--event",
help="Action to be performed",
default="alerts_text")
parser.add_option("-m", "--message", parser.add_option("-m", "--message",
help="Message to be passed for the action") help="Message to be passed for the action",
default="")
(options, args) = parser.parse_args() (options, args) = parser.parse_args()
if args: if args:
@ -60,12 +59,8 @@ def main():
elif options.address is None: elif options.address is None:
parser.print_help() parser.print_help()
parser.error("IP address missing") parser.error("IP address missing")
elif options.message is None:
parser.print_help()
parser.error("No message passed")
else: else:
text = format_message(options) sendData(options)
sendData(options, text)
if __name__ == u'__main__': if __name__ == u'__main__':
main() main()