This is getting to big to stay external any longer.
The web interface works with the existing HTML which has been externalised and can be pulled from OpenLP. - Add new web and socket servers to API and replace all existing API's - remove most of the Remote plugin but leave the base there to allow for the html and js code to land there. - amend the FTW to download a package of html, JS and CSS and install in the remote directory - add switch to turn off the servers to allow PyCharm to debug... bzr-revno: 2759
@ -153,10 +153,8 @@ class OpenLP(OpenLPMixin, QtWidgets.QApplication):
|
||||
self.processEvents()
|
||||
if not has_run_wizard:
|
||||
self.main_window.first_time()
|
||||
# update_check = Settings().value('core/update check')
|
||||
# if update_check:
|
||||
# version = VersionThread(self.main_window)
|
||||
# version.start()
|
||||
version = VersionThread(self.main_window)
|
||||
version.start()
|
||||
self.main_window.is_display_blank()
|
||||
self.main_window.app_startup()
|
||||
return self.exec()
|
||||
@ -337,6 +335,8 @@ def parse_options(args=None):
|
||||
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
|
||||
help='Ignore the version file and pull the version directly from Bazaar')
|
||||
parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
|
||||
parser.add_argument('-w', '--no-web-server', dest='no_web_server', action='store_false',
|
||||
help='Turn off the Web and Socket Server ')
|
||||
parser.add_argument('rargs', nargs='?', default=[])
|
||||
# Parse command line options and deal with them. Use args supplied pragmatically if possible.
|
||||
return parser.parse_args(args) if args else parser.parse_args()
|
||||
@ -410,6 +410,7 @@ def main(args=None):
|
||||
set_up_logging(str(AppLocation.get_directory(AppLocation.CacheDir)))
|
||||
Registry.create()
|
||||
Registry().register('application', application)
|
||||
Registry().set_flag('no_web_server', args.no_web_server)
|
||||
application.setApplicationVersion(get_application_version()['version'])
|
||||
# Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
|
||||
if application.is_already_running():
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@ -19,16 +19,10 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${live_title}</title>
|
||||
<link rel="stylesheet" href="/css/main.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<img id="image" class="size"/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import register_endpoint, requires_auth
|
||||
from openlp.core.api.tab import ApiTab
|
||||
from openlp.core.api.poll import Poller
|
||||
|
||||
__all__ = ['Endpoint', 'ApiTab', 'register_endpoint', 'requires_auth']
|
@ -19,9 +19,7 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
from .remotetab import RemoteTab
|
||||
from .httprouter import HttpRouter
|
||||
from .httpserver import OpenLPServer
|
||||
|
||||
__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']
|
||||
"""
|
||||
The Endpoint class, which provides plugins with a way to serve their own portion of the API
|
||||
"""
|
||||
from .pluginhelpers import search, live, service
|
144
openlp/core/api/endpoint/controller.py
Normal file
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.common import Registry, AppLocation, Settings
|
||||
from openlp.core.lib import ItemCapabilities, create_thumb
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
controller_endpoint = Endpoint('controller')
|
||||
api_controller_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@api_controller_endpoint.route('controller/live/text')
|
||||
@controller_endpoint.route('live/text')
|
||||
def controller_text(request):
|
||||
"""
|
||||
Perform an action on the slide controller.
|
||||
|
||||
:param request: the http request - not used
|
||||
"""
|
||||
log.debug("controller_text ")
|
||||
live_controller = Registry().get('live_controller')
|
||||
current_item = live_controller.service_item
|
||||
data = []
|
||||
if current_item:
|
||||
for index, frame in enumerate(current_item.get_frames()):
|
||||
item = {}
|
||||
# Handle text (songs, custom, bibles)
|
||||
if current_item.is_text():
|
||||
if frame['verseTag']:
|
||||
item['tag'] = str(frame['verseTag'])
|
||||
else:
|
||||
item['tag'] = str(index + 1)
|
||||
item['chords_text'] = str(frame['chords_text'])
|
||||
item['text'] = str(frame['text'])
|
||||
item['html'] = str(frame['html'])
|
||||
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
||||
elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'):
|
||||
item['tag'] = str(index + 1)
|
||||
thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
|
||||
full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path)
|
||||
# Create thumbnail if it doesn't exists
|
||||
if not os.path.exists(full_thumbnail_path):
|
||||
create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
|
||||
Registry().get('image_manager').add_image(full_thumbnail_path, frame['title'], None, 88, 88)
|
||||
item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
|
||||
item['text'] = str(frame['title'])
|
||||
item['html'] = str(frame['title'])
|
||||
else:
|
||||
# Handle presentation etc.
|
||||
item['tag'] = str(index + 1)
|
||||
if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
|
||||
item['title'] = str(frame['display_title'])
|
||||
if current_item.is_capable(ItemCapabilities.HasNotes):
|
||||
item['slide_notes'] = str(frame['notes'])
|
||||
if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
|
||||
Settings().value('api/thumbnails'):
|
||||
# If the file is under our app directory tree send the portion after the match
|
||||
data_path = AppLocation.get_data_path()
|
||||
if frame['image'][0:len(data_path)] == data_path:
|
||||
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
|
||||
Registry().get('image_manager').add_image(frame['image'], frame['title'], None, 88, 88)
|
||||
item['text'] = str(frame['title'])
|
||||
item['html'] = str(frame['title'])
|
||||
item['selected'] = (live_controller.selected_row == index)
|
||||
data.append(item)
|
||||
json_data = {'results': {'slides': data}}
|
||||
if current_item:
|
||||
json_data['results']['item'] = live_controller.service_item.unique_identifier
|
||||
return json_data
|
||||
|
||||
|
||||
@api_controller_endpoint.route('controller/live/set')
|
||||
@controller_endpoint.route('live/set')
|
||||
@requires_auth
|
||||
def controller_set(request):
|
||||
"""
|
||||
Perform an action on the slide controller.
|
||||
|
||||
:param request: The action to perform.
|
||||
"""
|
||||
event = getattr(Registry().get('live_controller'), 'slidecontroller_live_set')
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
data = int(json.loads(json_data)['request']['id'])
|
||||
event.emit([data])
|
||||
except KeyError:
|
||||
log.error("Endpoint controller/live/set request id not found")
|
||||
return {'results': {'success': True}}
|
||||
|
||||
|
||||
@controller_endpoint.route('{action:next|previous}')
|
||||
@requires_auth
|
||||
def controller_direction(request, controller, action):
|
||||
"""
|
||||
Handles requests for setting service items in the slide controller
|
||||
11
|
||||
:param request: The http request object.
|
||||
:param controller: the controller slides forward or backward.
|
||||
:param action: the controller slides forward or backward.
|
||||
"""
|
||||
event = getattr(Registry().get('live_controller'), 'slidecontroller_{controller}_{action}'.
|
||||
format(controller=controller, action=action))
|
||||
event.emit()
|
||||
|
||||
|
||||
@api_controller_endpoint.route('controller/{controller}/{action:next|previous}')
|
||||
@requires_auth
|
||||
def controller_direction_api(request, controller, action):
|
||||
"""
|
||||
Handles requests for setting service items in the slide controller
|
||||
11
|
||||
:param request: The http request object.
|
||||
:param controller: the controller slides forward or backward.
|
||||
:param action: the controller slides forward or backward.
|
||||
"""
|
||||
controller_direction(request, controller, action)
|
||||
return {'results': {'success': True}}
|
182
openlp/core/api/endpoint/core.py
Normal file
@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
import os
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.common import Registry, UiStrings, translate
|
||||
from openlp.core.lib import image_to_byte, PluginStatus, StringContent
|
||||
|
||||
|
||||
template_dir = 'templates'
|
||||
static_dir = 'static'
|
||||
blank_dir = os.path.join(static_dir, 'index')
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
chords_endpoint = Endpoint('chords', template_dir=template_dir, static_dir=static_dir)
|
||||
stage_endpoint = Endpoint('stage', template_dir=template_dir, static_dir=static_dir)
|
||||
main_endpoint = Endpoint('main', template_dir=template_dir, static_dir=static_dir)
|
||||
blank_endpoint = Endpoint('', template_dir=template_dir, static_dir=blank_dir)
|
||||
|
||||
FILE_TYPES = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.png': 'image/png'
|
||||
}
|
||||
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||
|
||||
TRANSLATED_STRINGS = {
|
||||
'app_title': "{main} {remote}".format(main=UiStrings().OpenLP, remote=remote),
|
||||
'stage_title': "{main} {stage}".format(main=UiStrings().OpenLP, stage=stage),
|
||||
'live_title': "{main} {live}".format(main=UiStrings().OpenLP, live=live),
|
||||
'chords_title': "{main} {chords}".format(main=UiStrings().OpenLP, chords=chords),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
||||
'search': translate('RemotePlugin.Mobile', 'Search'),
|
||||
'home': translate('RemotePlugin.Mobile', 'Home'),
|
||||
'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
|
||||
'blank': translate('RemotePlugin.Mobile', 'Blank'),
|
||||
'theme': translate('RemotePlugin.Mobile', 'Theme'),
|
||||
'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
|
||||
'show': translate('RemotePlugin.Mobile', 'Show'),
|
||||
'prev': translate('RemotePlugin.Mobile', 'Prev'),
|
||||
'next': translate('RemotePlugin.Mobile', 'Next'),
|
||||
'text': translate('RemotePlugin.Mobile', 'Text'),
|
||||
'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
|
||||
'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
|
||||
'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
|
||||
'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'),
|
||||
'no_results': translate('RemotePlugin.Mobile', 'No Results'),
|
||||
'options': translate('RemotePlugin.Mobile', 'Options'),
|
||||
'service': translate('RemotePlugin.Mobile', 'Service'),
|
||||
'slides': translate('RemotePlugin.Mobile', 'Slides'),
|
||||
'settings': translate('RemotePlugin.Mobile', 'Settings'),
|
||||
}
|
||||
|
||||
|
||||
@stage_endpoint.route('')
|
||||
def stage_index(request):
|
||||
"""
|
||||
Deliver the page for the /stage url
|
||||
"""
|
||||
return stage_endpoint.render_template('stage.mako', **TRANSLATED_STRINGS)
|
||||
|
||||
|
||||
@chords_endpoint.route('')
|
||||
def chords_index(request):
|
||||
"""
|
||||
Deliver the page for the /chords url
|
||||
"""
|
||||
return chords_endpoint.render_template('chords.mako', **TRANSLATED_STRINGS)
|
||||
|
||||
|
||||
@main_endpoint.route('')
|
||||
def main_index(request):
|
||||
"""
|
||||
Deliver the page for the /main url
|
||||
"""
|
||||
return main_endpoint.render_template('main.mako', **TRANSLATED_STRINGS)
|
||||
|
||||
|
||||
@blank_endpoint.route('')
|
||||
def index(request):
|
||||
"""
|
||||
Deliver the page for the / url
|
||||
:param request:
|
||||
"""
|
||||
return blank_endpoint.render_template('index.mako', **TRANSLATED_STRINGS)
|
||||
|
||||
|
||||
@blank_endpoint.route('api/poll')
|
||||
@blank_endpoint.route('poll')
|
||||
def poll(request):
|
||||
"""
|
||||
Deliver the page for the /poll url
|
||||
|
||||
:param request:
|
||||
"""
|
||||
return Registry().get('poller').poll()
|
||||
|
||||
|
||||
@blank_endpoint.route('api/display/{display:hide|show|blank|theme|desktop}')
|
||||
@blank_endpoint.route('display/{display:hide|show|blank|theme|desktop}')
|
||||
@requires_auth
|
||||
def toggle_display(request, display):
|
||||
"""
|
||||
Deliver the functions for the /display url
|
||||
:param request: the http request - not used
|
||||
:param display: the display function to be triggered
|
||||
"""
|
||||
Registry().get('live_controller').slidecontroller_toggle_display.emit(display)
|
||||
return {'results': {'success': True}}
|
||||
|
||||
|
||||
@blank_endpoint.route('api/plugin/search')
|
||||
@blank_endpoint.route('plugin/search')
|
||||
def plugin_search_list(request):
|
||||
"""
|
||||
Deliver a list of active plugins that support search
|
||||
:param request: the http request - not used
|
||||
"""
|
||||
searches = []
|
||||
for plugin in Registry().get('plugin_manager').plugins:
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
|
||||
return {'results': {'items': searches}}
|
||||
|
||||
|
||||
@main_endpoint.route('image')
|
||||
def main_image(request):
|
||||
"""
|
||||
Return the latest display image as a byte stream.
|
||||
:param request: base path of the URL. Not used but passed by caller
|
||||
:return:
|
||||
"""
|
||||
live_controller = Registry().get('live_controller')
|
||||
result = {
|
||||
'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image))
|
||||
}
|
||||
return {'results': result}
|
||||
|
||||
|
||||
def get_content_type(file_name):
|
||||
"""
|
||||
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
|
||||
Returns the extension and the content_type
|
||||
|
||||
:param file_name: name of file
|
||||
"""
|
||||
ext = os.path.splitext(file_name)[1]
|
||||
content_type = FILE_TYPES.get(ext, 'text/plain')
|
||||
return ext, content_type
|
138
openlp/core/api/endpoint/pluginhelpers.py
Normal file
@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import urllib
|
||||
|
||||
from urllib.parse import urlparse
|
||||
from webob import Response
|
||||
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.common import Registry, AppLocation
|
||||
from openlp.core.lib import PluginStatus, image_to_byte
|
||||
|
||||
|
||||
def search(request, plugin_name, log):
|
||||
"""
|
||||
Handles requests for searching the plugins
|
||||
|
||||
:param request: The http request object.
|
||||
:param plugin_name: The plugin name.
|
||||
:param log: The class log object.
|
||||
"""
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
text = json.loads(json_data)['request']['text']
|
||||
except KeyError:
|
||||
log.error("Endpoint {text} search request text not found".format(text=plugin_name))
|
||||
text = ""
|
||||
text = urllib.parse.unquote(text)
|
||||
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
results = plugin.media_item.search(text, False)
|
||||
return {'results': {'items': results}}
|
||||
else:
|
||||
raise NotFound()
|
||||
|
||||
|
||||
def live(request, plugin_name, log):
|
||||
"""
|
||||
Handles requests for making live of the plugins
|
||||
|
||||
:param request: The http request object.
|
||||
:param plugin_name: The plugin name.
|
||||
:param log: The class log object.
|
||||
"""
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
request_id = json.loads(json_data)['request']['id']
|
||||
except KeyError:
|
||||
log.error("Endpoint {text} search request text not found".format(text=plugin_name))
|
||||
return []
|
||||
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True])
|
||||
|
||||
|
||||
def service(request, plugin_name, log):
|
||||
"""
|
||||
Handles requests for adding to a service of the plugins
|
||||
|
||||
:param request: The http request object.
|
||||
:param plugin_name: The plugin name.
|
||||
:param log: The class log object.
|
||||
"""
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
request_id = json.loads(json_data)['request']['id']
|
||||
except KeyError:
|
||||
log.error("Endpoint {plugin} search request text not found".format(plugin=plugin_name))
|
||||
return []
|
||||
plugin = Registry().get('plugin_manager').get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
item_id = plugin.media_item.create_item_from_id(request_id)
|
||||
getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True])
|
||||
|
||||
|
||||
def display_thumbnails(request, controller_name, log, dimensions, file_name, slide=None):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
Return an image to a web page based on a URL
|
||||
:param request: Request object
|
||||
:param controller_name: which controller is requesting the image
|
||||
:param log: the logger object
|
||||
:param dimensions: the image size eg 88x88
|
||||
:param file_name: the file name of the image
|
||||
:param slide: the individual image name
|
||||
:return:
|
||||
"""
|
||||
log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}/{slide}'.format(cname=controller_name,
|
||||
dim=dimensions,
|
||||
fname=file_name,
|
||||
slide=slide))
|
||||
# -1 means use the default dimension in ImageManager
|
||||
width = -1
|
||||
height = -1
|
||||
image = None
|
||||
if dimensions:
|
||||
match = re.search('(\d+)x(\d+)', dimensions)
|
||||
if match:
|
||||
# let's make sure that the dimensions are within reason
|
||||
width = sorted([10, int(match.group(1)), 1000])[1]
|
||||
height = sorted([10, int(match.group(2)), 1000])[1]
|
||||
if controller_name and file_name:
|
||||
file_name = urllib.parse.unquote(file_name)
|
||||
if '..' not in file_name: # no hacking please
|
||||
if slide:
|
||||
full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
|
||||
'thumbnails', file_name, slide))
|
||||
else:
|
||||
full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name),
|
||||
|
||||
'thumbnails', file_name))
|
||||
if os.path.exists(full_path):
|
||||
path, just_file_name = os.path.split(full_path)
|
||||
Registry().get('image_manager').add_image(full_path, just_file_name, None, width, height)
|
||||
image = Registry().get('image_manager').get_image(full_path, just_file_name, width, height)
|
||||
return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8')
|
100
openlp/core/api/endpoint/service.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
import json
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import register_endpoint, requires_auth
|
||||
from openlp.core.common import Registry
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
service_endpoint = Endpoint('service')
|
||||
api_service_endpoint = Endpoint('api/service')
|
||||
|
||||
|
||||
@api_service_endpoint.route('list')
|
||||
@service_endpoint.route('list')
|
||||
def list_service(request):
|
||||
"""
|
||||
Handles requests for service items in the service manager
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return {'results': {'items': get_service_items()}}
|
||||
|
||||
|
||||
@api_service_endpoint.route('set')
|
||||
@service_endpoint.route('set')
|
||||
@requires_auth
|
||||
def service_set(request):
|
||||
"""
|
||||
Handles requests for setting service items in the service manager
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
event = getattr(Registry().get('service_manager'), 'servicemanager_set_item')
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
data = int(json.loads(json_data)['request']['id'])
|
||||
event.emit(data)
|
||||
except KeyError:
|
||||
log.error("Endpoint service/set request id not found")
|
||||
return {'results': {'success': True}}
|
||||
|
||||
|
||||
@api_service_endpoint.route('{action:next|previous}')
|
||||
@service_endpoint.route('{action:next|previous}')
|
||||
@requires_auth
|
||||
def service_direction(request, action):
|
||||
"""
|
||||
Handles requests for setting service items in the service manager
|
||||
|
||||
:param request: The http request object.
|
||||
:param action: the the service slides forward or backward.
|
||||
"""
|
||||
event = getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action))
|
||||
event.emit()
|
||||
return {'results': {'success': True}}
|
||||
|
||||
|
||||
def get_service_items():
|
||||
"""
|
||||
Read the service item in use and return the data as a json object
|
||||
"""
|
||||
live_controller = Registry().get('live_controller')
|
||||
service_items = []
|
||||
if live_controller.service_item:
|
||||
current_unique_identifier = live_controller.service_item.unique_identifier
|
||||
else:
|
||||
current_unique_identifier = None
|
||||
for item in Registry().get('service_manager').service_items:
|
||||
service_item = item['service_item']
|
||||
service_items.append({
|
||||
'id': str(service_item.unique_identifier),
|
||||
'title': str(service_item.get_display_title()),
|
||||
'plugin': str(service_item.name),
|
||||
'notes': str(service_item.notes),
|
||||
'selected': (service_item.unique_identifier == current_unique_identifier)
|
||||
})
|
||||
return service_items
|
110
openlp/core/api/http/__init__.py
Normal file
@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import base64
|
||||
from functools import wraps
|
||||
from webob import Response
|
||||
|
||||
from openlp.core.common.settings import Settings
|
||||
from openlp.core.api.http.wsgiapp import WSGIApplication
|
||||
from .errors import NotFound, ServerError, HttpError
|
||||
|
||||
application = WSGIApplication('api')
|
||||
|
||||
|
||||
def _route_from_url(url_prefix, url):
|
||||
"""
|
||||
Create a route from the URL
|
||||
"""
|
||||
url_prefix = '/{prefix}/'.format(prefix=url_prefix.strip('/'))
|
||||
if not url:
|
||||
url = url_prefix[:-1]
|
||||
else:
|
||||
url = url_prefix + url
|
||||
url = url.replace('//', '/')
|
||||
return url
|
||||
|
||||
|
||||
def register_endpoint(end_point):
|
||||
"""
|
||||
Register an endpoint with the app
|
||||
"""
|
||||
for url, view_func, method in end_point.routes:
|
||||
# Set the view functions
|
||||
route = _route_from_url(end_point.url_prefix, url)
|
||||
application.add_route(route, view_func, method)
|
||||
# Add a static route if necessary
|
||||
if end_point.static_dir:
|
||||
static_route = _route_from_url(end_point.url_prefix, 'static')
|
||||
static_route += '(.*)'
|
||||
application.add_static_route(static_route, end_point.static_dir)
|
||||
|
||||
|
||||
def check_auth(auth):
|
||||
"""
|
||||
This function is called to check if a username password combination is valid.
|
||||
|
||||
:param auth: the authorisation object which needs to be tested
|
||||
:return Whether authentication have been successful
|
||||
"""
|
||||
auth_code = "{user}:{password}".format(user=Settings().value('api/user id'),
|
||||
password=Settings().value('api/password'))
|
||||
try:
|
||||
auth_base = base64.b64encode(auth_code)
|
||||
except TypeError:
|
||||
auth_base = base64.b64encode(auth_code.encode()).decode()
|
||||
if auth[1] == auth_base:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def authenticate():
|
||||
"""
|
||||
Sends a 401 response that enables basic auth to be triggered
|
||||
"""
|
||||
resp = Response(status=401)
|
||||
resp.www_authenticate = 'Basic realm="OpenLP Login Required"'
|
||||
return resp
|
||||
|
||||
|
||||
def requires_auth(f):
|
||||
"""
|
||||
Decorates a function which needs to be authenticated before it can be used from the remote.
|
||||
|
||||
:param f: The function which has been wrapped
|
||||
:return: the called function or a request to authenticate
|
||||
"""
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not Settings().value('api/authentication enabled'):
|
||||
return f(*args, **kwargs)
|
||||
req = args[0]
|
||||
if not hasattr(req, 'authorization'):
|
||||
return authenticate()
|
||||
else:
|
||||
auth = req.authorization
|
||||
if auth and check_auth(auth):
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
return authenticate()
|
||||
return decorated
|
80
openlp/core/api/http/endpoint.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The Endpoint class, which provides plugins with a way to serve their own portion of the API
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from openlp.core.common import AppLocation
|
||||
from mako.template import Template
|
||||
|
||||
|
||||
class Endpoint(object):
|
||||
"""
|
||||
This is an endpoint for the HTTP API
|
||||
"""
|
||||
def __init__(self, url_prefix, template_dir=None, static_dir=None, assets_dir=None):
|
||||
"""
|
||||
Create an endpoint with a URL prefix
|
||||
"""
|
||||
self.url_prefix = url_prefix
|
||||
self.static_dir = static_dir
|
||||
self.template_dir = template_dir
|
||||
if assets_dir:
|
||||
self.assets_dir = assets_dir
|
||||
else:
|
||||
self.assets_dir = None
|
||||
self.routes = []
|
||||
|
||||
def add_url_route(self, url, view_func, method):
|
||||
"""
|
||||
Add a url route to the list of routes
|
||||
"""
|
||||
self.routes.append((url, view_func, method))
|
||||
|
||||
def route(self, rule, method='GET'):
|
||||
"""
|
||||
Set up a URL route
|
||||
"""
|
||||
def decorator(func):
|
||||
"""
|
||||
Make this a decorator
|
||||
"""
|
||||
self.add_url_route(rule, func, method)
|
||||
return func
|
||||
return decorator
|
||||
|
||||
def render_template(self, filename, **kwargs):
|
||||
"""
|
||||
Render a mako template
|
||||
"""
|
||||
root = os.path.join(str(AppLocation.get_section_data_path('remotes')))
|
||||
if not self.template_dir:
|
||||
raise Exception('No template directory specified')
|
||||
path = os.path.join(root, self.template_dir, filename)
|
||||
# path = os.path.abspath(os.path.join(self.template_dir, filename))
|
||||
if self.static_dir:
|
||||
kwargs['static_url'] = '/{prefix}/static'.format(prefix=self.url_prefix)
|
||||
kwargs['static_url'] = kwargs['static_url'].replace('//', '/')
|
||||
kwargs['assets_url'] = '/assets'
|
||||
return Template(filename=path, input_encoding='utf-8').render(**kwargs)
|
65
openlp/core/api/http/errors.py
Normal file
@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
HTTP Error classes
|
||||
"""
|
||||
|
||||
|
||||
class HttpError(Exception):
|
||||
"""
|
||||
A base HTTP error (aka status code)
|
||||
"""
|
||||
def __init__(self, status, message):
|
||||
"""
|
||||
Initialise the exception
|
||||
"""
|
||||
super(HttpError, self).__init__(message)
|
||||
self.status = status
|
||||
self.message = message
|
||||
|
||||
def to_response(self):
|
||||
"""
|
||||
Convert this exception to a Response object
|
||||
"""
|
||||
return self.message, self.status
|
||||
|
||||
|
||||
class NotFound(HttpError):
|
||||
"""
|
||||
A 404
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Make this a 404
|
||||
"""
|
||||
super(NotFound, self).__init__(404, 'Not Found')
|
||||
|
||||
|
||||
class ServerError(HttpError):
|
||||
"""
|
||||
A 500
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Make this a 500
|
||||
"""
|
||||
super(ServerError, self).__init__(500, 'Server Error')
|
97
openlp/core/api/http/server.py
Normal file
@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
|
||||
with OpenLP. It uses JSON to communicate with the remotes.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from waitress import serve
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.api.http import application
|
||||
from openlp.core.common import RegistryMixin, RegistryProperties, OpenLPMixin, Settings, Registry
|
||||
from openlp.core.api.poll import Poller
|
||||
from openlp.core.api.endpoint.controller import controller_endpoint, api_controller_endpoint
|
||||
from openlp.core.api.endpoint.core import chords_endpoint, stage_endpoint, blank_endpoint, main_endpoint
|
||||
from openlp.core.api.endpoint.service import service_endpoint, api_service_endpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpWorker(QtCore.QObject):
|
||||
"""
|
||||
A special Qt thread class to allow the HTTP server to run at the same time as the UI.
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
|
||||
:param server: The http server class.
|
||||
"""
|
||||
super(HttpWorker, self).__init__()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
address = Settings().value('api/ip address')
|
||||
port = Settings().value('api/port')
|
||||
serve(application, host=address, port=port)
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
class HttpServer(RegistryMixin, RegistryProperties, OpenLPMixin):
|
||||
"""
|
||||
Wrapper round a server instance
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Initialise the http server, and start the http server
|
||||
"""
|
||||
super(HttpServer, self).__init__(parent)
|
||||
self.worker = HttpWorker()
|
||||
self.thread = QtCore.QThread()
|
||||
self.worker.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.thread.start()
|
||||
|
||||
def bootstrap_post_set_up(self):
|
||||
"""
|
||||
Register the poll return service and start the servers.
|
||||
"""
|
||||
self.poller = Poller()
|
||||
Registry().register('poller', self.poller)
|
||||
application.initialise()
|
||||
register_endpoint(controller_endpoint)
|
||||
register_endpoint(api_controller_endpoint)
|
||||
register_endpoint(chords_endpoint)
|
||||
register_endpoint(stage_endpoint)
|
||||
register_endpoint(blank_endpoint)
|
||||
register_endpoint(main_endpoint)
|
||||
register_endpoint(service_endpoint)
|
||||
register_endpoint(api_service_endpoint)
|
181
openlp/core/api/http/wsgiapp.py
Normal file
@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
# pylint: disable=logging-format-interpolation
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
App stuff
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from webob import Request, Response
|
||||
from webob.static import DirectoryApp
|
||||
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.api.http.errors import HttpError, NotFound, ServerError
|
||||
|
||||
|
||||
ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _route_to_regex(route):
|
||||
"""
|
||||
Convert a route to a regular expression
|
||||
|
||||
For example:
|
||||
|
||||
'songs/{song_id}' becomes 'songs/(?P<song_id>[^/]+)'
|
||||
|
||||
and
|
||||
|
||||
'songs/{song_id:\d+}' becomes 'songs/(?P<song_id>\d+)'
|
||||
|
||||
"""
|
||||
route_regex = ''
|
||||
last_pos = 0
|
||||
for match in ARGS_REGEX.finditer(route):
|
||||
route_regex += re.escape(route[last_pos:match.start()])
|
||||
arg_name = match.group(1)
|
||||
expr = match.group(2) or '[^/]+'
|
||||
expr = '(?P<%s>%s)' % (arg_name, expr)
|
||||
route_regex += expr
|
||||
last_pos = match.end()
|
||||
route_regex += re.escape(route[last_pos:])
|
||||
route_regex = '^%s$' % route_regex
|
||||
return route_regex
|
||||
|
||||
|
||||
def _make_response(view_result):
|
||||
"""
|
||||
Create a Response object from response
|
||||
"""
|
||||
if isinstance(view_result, Response):
|
||||
return view_result
|
||||
elif isinstance(view_result, tuple):
|
||||
content_type = 'text/html'
|
||||
body = view_result[0]
|
||||
if isinstance(body, dict):
|
||||
content_type = 'application/json'
|
||||
body = json.dumps(body)
|
||||
response = Response(body=body, status=view_result[1],
|
||||
content_type=content_type, charset='utf8')
|
||||
if len(view_result) >= 3:
|
||||
response.headers.update(view_result[2])
|
||||
return response
|
||||
elif isinstance(view_result, dict):
|
||||
return Response(body=json.dumps(view_result), status=200,
|
||||
content_type='application/json', charset='utf8')
|
||||
elif isinstance(view_result, str):
|
||||
return Response(body=view_result, status=200,
|
||||
content_type='text/html', charset='utf8')
|
||||
|
||||
|
||||
def _handle_exception(error):
|
||||
"""
|
||||
Handle exceptions
|
||||
"""
|
||||
log.exception(error)
|
||||
if isinstance(error, HttpError):
|
||||
return error.to_response()
|
||||
else:
|
||||
return ServerError().to_response()
|
||||
|
||||
|
||||
class WSGIApplication(object):
|
||||
"""
|
||||
This is the core of the API, the WSGI app
|
||||
"""
|
||||
def __init__(self, name):
|
||||
"""
|
||||
Create the app object
|
||||
"""
|
||||
self.name = name
|
||||
self.static_routes = {}
|
||||
self.route_map = {}
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Set up generic roots for the whole application
|
||||
:return: None
|
||||
"""
|
||||
self.add_static_route('/assets(.*)', '')
|
||||
self.add_static_route('/images(.*)', '')
|
||||
pass
|
||||
|
||||
def add_route(self, route, view_func, method):
|
||||
"""
|
||||
Add a route
|
||||
"""
|
||||
route_regex = _route_to_regex(route)
|
||||
if route_regex not in self.route_map:
|
||||
self.route_map[route_regex] = {}
|
||||
self.route_map[route_regex][method.upper()] = view_func
|
||||
|
||||
def add_static_route(self, route, static_dir):
|
||||
"""
|
||||
Add a static directory as a route
|
||||
"""
|
||||
if route not in self.static_routes:
|
||||
root = os.path.join(str(AppLocation.get_section_data_path('remotes')))
|
||||
self.static_routes[route] = DirectoryApp(os.path.abspath(os.path.join(root, static_dir)))
|
||||
|
||||
def dispatch(self, request):
|
||||
"""
|
||||
Find the appropriate URL and run the view function
|
||||
"""
|
||||
# If not a static route, try the views
|
||||
for route, views in self.route_map.items():
|
||||
match = re.match(route, request.path)
|
||||
if match and request.method.upper() in views:
|
||||
kwargs = match.groupdict()
|
||||
log.debug('Found {method} {url}'.format(method=request.method, url=request.path))
|
||||
view_func = views[request.method.upper()]
|
||||
return _make_response(view_func(request, **kwargs))
|
||||
# Look to see if this is a static file request
|
||||
for route, static_app in self.static_routes.items():
|
||||
if re.match(route, request.path):
|
||||
return request.get_response(static_app)
|
||||
log.error('URL {url} - Not found'.format(url=request.path))
|
||||
raise NotFound()
|
||||
|
||||
def wsgi_app(self, environ, start_response):
|
||||
"""
|
||||
The actual WSGI application.
|
||||
"""
|
||||
request = Request(environ)
|
||||
try:
|
||||
response = self.dispatch(request)
|
||||
except Exception as e:
|
||||
response = _make_response(_handle_exception(e))
|
||||
response.headers.add("cache-control", "no-cache, no-store, must-revalidate")
|
||||
response.headers.add("pragma", "no-cache")
|
||||
response.headers.add("expires", "0")
|
||||
return response(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""
|
||||
Shortcut for wsgi_app.
|
||||
"""
|
||||
return self.wsgi_app(environ, start_response)
|
130
openlp/core/api/poll.py
Normal file
@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import json
|
||||
|
||||
from openlp.core.common import RegistryProperties, Settings
|
||||
from openlp.core.common.httputils import get_web_page
|
||||
|
||||
|
||||
class Poller(RegistryProperties):
|
||||
"""
|
||||
Accessed by the web layer to get status type information from the application
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Constructor for the poll builder class.
|
||||
"""
|
||||
super(Poller, self).__init__()
|
||||
self.live_cache = None
|
||||
self.stage_cache = None
|
||||
self.chords_cache = None
|
||||
|
||||
def raw_poll(self):
|
||||
return {
|
||||
'service': self.service_manager.service_id,
|
||||
'slide': self.live_controller.selected_row or 0,
|
||||
'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
|
||||
'twelve': Settings().value('api/twelve hour'),
|
||||
'blank': self.live_controller.blank_screen.isChecked(),
|
||||
'theme': self.live_controller.theme_screen.isChecked(),
|
||||
'display': self.live_controller.desktop_screen.isChecked(),
|
||||
'version': 3,
|
||||
'isSecure': Settings().value('api/authentication enabled'),
|
||||
'isAuthorised': False,
|
||||
'chordNotation': Settings().value('songs/chord notation'),
|
||||
'isStagedActive': self.is_stage_active(),
|
||||
'isLiveActive': self.is_live_active(),
|
||||
'isChordsActive': self.is_chords_active()
|
||||
}
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Poll OpenLP to determine the current slide number and item name.
|
||||
"""
|
||||
return {'results': self.raw_poll()}
|
||||
|
||||
def main_poll(self):
|
||||
"""
|
||||
Poll OpenLP to determine the current slide count.
|
||||
"""
|
||||
result = {
|
||||
'slide_count': self.live_controller.slide_count
|
||||
}
|
||||
return json.dumps({'results': result}).encode()
|
||||
|
||||
def reset_cache(self):
|
||||
"""
|
||||
Reset the caches as the web has changed
|
||||
:return:
|
||||
"""
|
||||
self.stage_cache = None
|
||||
self.live_cache = None
|
||||
self.chords.cache = None
|
||||
|
||||
def is_stage_active(self):
|
||||
"""
|
||||
Is stage active - call it and see but only once
|
||||
:return: if stage is active or not
|
||||
"""
|
||||
if self.stage_cache is None:
|
||||
try:
|
||||
page = get_web_page("http://localhost:4316/stage")
|
||||
except:
|
||||
page = None
|
||||
if page:
|
||||
self.stage_cache = True
|
||||
else:
|
||||
self.stage_cache = False
|
||||
return self.stage_cache
|
||||
|
||||
def is_live_active(self):
|
||||
"""
|
||||
Is main active - call it and see but only once
|
||||
:return: if live is active or not
|
||||
"""
|
||||
if self.live_cache is None:
|
||||
try:
|
||||
page = get_web_page("http://localhost:4316/main")
|
||||
except:
|
||||
page = None
|
||||
if page:
|
||||
self.live_cache = True
|
||||
else:
|
||||
self.live_cache = False
|
||||
return self.live_cache
|
||||
|
||||
def is_chords_active(self):
|
||||
"""
|
||||
Is chords active - call it and see but only once
|
||||
:return: if live is active or not
|
||||
"""
|
||||
if self.chords_cache is None:
|
||||
try:
|
||||
page = get_web_page("http://localhost:4316/chords")
|
||||
except:
|
||||
page = None
|
||||
if page:
|
||||
self.chords_cache = True
|
||||
else:
|
||||
self.chords_cache = False
|
||||
return self.chords_cache
|
@ -20,26 +20,28 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import os.path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, translate
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.common import UiStrings, Registry, Settings, translate
|
||||
from openlp.core.lib import SettingsTab
|
||||
|
||||
ZERO_URL = '0.0.0.0'
|
||||
|
||||
|
||||
class RemoteTab(SettingsTab):
|
||||
class ApiTab(SettingsTab):
|
||||
"""
|
||||
RemoteTab is the Remotes settings tab in the settings dialog.
|
||||
"""
|
||||
def __init__(self, parent, title, visible_title, icon_path):
|
||||
super(RemoteTab, self).__init__(parent, title, visible_title, icon_path)
|
||||
def __init__(self, parent):
|
||||
self.icon_path = ':/plugins/plugin_remote.png'
|
||||
advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced')
|
||||
super(ApiTab, self).__init__(parent, 'api', advanced_translated)
|
||||
self.define_main_window_icon()
|
||||
self.generate_icon()
|
||||
|
||||
def setupUi(self):
|
||||
self.setObjectName('RemoteTab')
|
||||
super(RemoteTab, self).setupUi()
|
||||
self.setObjectName('ApiTab')
|
||||
super(ApiTab, self).setupUi()
|
||||
self.server_settings_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.server_settings_group_box.setObjectName('server_settings_group_box')
|
||||
self.server_settings_layout = QtWidgets.QFormLayout(self.server_settings_group_box)
|
||||
@ -65,8 +67,7 @@ class RemoteTab(SettingsTab):
|
||||
self.http_setting_layout.setObjectName('http_setting_layout')
|
||||
self.port_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||
self.port_label.setObjectName('port_label')
|
||||
self.port_spin_box = QtWidgets.QSpinBox(self.http_settings_group_box)
|
||||
self.port_spin_box.setMaximum(32767)
|
||||
self.port_spin_box = QtWidgets.QLabel(self.http_settings_group_box)
|
||||
self.port_spin_box.setObjectName('port_spin_box')
|
||||
self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
|
||||
self.remote_url_label = QtWidgets.QLabel(self.http_settings_group_box)
|
||||
@ -111,6 +112,23 @@ class RemoteTab(SettingsTab):
|
||||
self.password.setObjectName('password')
|
||||
self.user_login_layout.addRow(self.password_label, self.password)
|
||||
self.left_layout.addWidget(self.user_login_group_box)
|
||||
self.update_site_group_box = QtWidgets.QGroupBox(self.left_column)
|
||||
self.update_site_group_box.setCheckable(True)
|
||||
self.update_site_group_box.setChecked(False)
|
||||
self.update_site_group_box.setObjectName('update_site_group_box')
|
||||
self.update_site_layout = QtWidgets.QFormLayout(self.update_site_group_box)
|
||||
self.update_site_layout.setObjectName('update_site_layout')
|
||||
self.current_version_label = QtWidgets.QLabel(self.update_site_group_box)
|
||||
self.current_version_label.setObjectName('current_version_label')
|
||||
self.current_version_value = QtWidgets.QLabel(self.update_site_group_box)
|
||||
self.current_version_value.setObjectName('current_version_value')
|
||||
self.update_site_layout.addRow(self.current_version_label, self.current_version_value)
|
||||
self.master_version_label = QtWidgets.QLabel(self.update_site_group_box)
|
||||
self.master_version_label.setObjectName('master_version_label')
|
||||
self.master_version_value = QtWidgets.QLabel(self.update_site_group_box)
|
||||
self.master_version_value.setObjectName('master_version_value')
|
||||
self.update_site_layout.addRow(self.master_version_label, self.master_version_value)
|
||||
self.left_layout.addWidget(self.update_site_group_box)
|
||||
self.android_app_group_box = QtWidgets.QGroupBox(self.right_column)
|
||||
self.android_app_group_box.setObjectName('android_app_group_box')
|
||||
self.right_layout.addWidget(self.android_app_group_box)
|
||||
@ -146,16 +164,34 @@ class RemoteTab(SettingsTab):
|
||||
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
|
||||
self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed)
|
||||
self.address_edit.textChanged.connect(self.set_urls)
|
||||
self.port_spin_box.valueChanged.connect(self.set_urls)
|
||||
|
||||
def define_main_window_icon(self):
|
||||
"""
|
||||
Define an icon on the main window to show the state of the server
|
||||
:return:
|
||||
"""
|
||||
self.remote_server_icon = QtWidgets.QLabel(self.main_window.status_bar)
|
||||
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
||||
size_policy.setHorizontalStretch(0)
|
||||
size_policy.setVerticalStretch(0)
|
||||
size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
|
||||
self.remote_server_icon.setSizePolicy(size_policy)
|
||||
self.remote_server_icon.setFrameShadow(QtWidgets.QFrame.Plain)
|
||||
self.remote_server_icon.setLineWidth(1)
|
||||
self.remote_server_icon.setScaledContents(True)
|
||||
self.remote_server_icon.setFixedSize(20, 20)
|
||||
self.remote_server_icon.setObjectName('remote_server_icon')
|
||||
self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
|
||||
|
||||
def retranslateUi(self):
|
||||
self.tab_title_visible = translate('RemotePlugin.RemoteTab', 'Remote Interface')
|
||||
self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
|
||||
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
|
||||
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
|
||||
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
|
||||
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
|
||||
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
|
||||
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
|
||||
self.chords_url_label.setText(translate('RemotePlugin.RemoteTab', 'Chords view URL:'))
|
||||
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
|
||||
self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab',
|
||||
'Show thumbnails of non-text slides in remote and stage view.'))
|
||||
@ -170,22 +206,27 @@ class RemoteTab(SettingsTab):
|
||||
'Scan the QR code or click <a href="{qr}">download</a> to install the iOS app from the App '
|
||||
'Store.').format(qr='https://itunes.apple.com/app/id1096218725'))
|
||||
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
|
||||
self.aa = UiStrings()
|
||||
self.update_site_group_box.setTitle(UiStrings().WebDownloadText)
|
||||
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
|
||||
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
|
||||
self.current_version_label.setText(translate('RemotePlugin.RemoteTab', 'Current Version number:'))
|
||||
self.master_version_label.setText(translate('RemotePlugin.RemoteTab', 'Latest Version number:'))
|
||||
|
||||
def set_urls(self):
|
||||
"""
|
||||
Update the display based on the data input on the screen
|
||||
"""
|
||||
ip_address = self.get_ip_address(self.address_edit.text())
|
||||
http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.value())
|
||||
http_url = 'http://{url}:{text}/'.format(url=ip_address, text=self.port_spin_box.text())
|
||||
self.remote_url.setText('<a href="{url}">{url}</a>'.format(url=http_url))
|
||||
http_url_temp = http_url + 'stage'
|
||||
self.stage_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
|
||||
http_url_temp = http_url + 'main'
|
||||
self.live_url.setText('<a href="{url}">{url}</a>'.format(url=http_url_temp))
|
||||
|
||||
def get_ip_address(self, ip_address):
|
||||
@staticmethod
|
||||
def get_ip_address(ip_address):
|
||||
"""
|
||||
returns the IP address in dependency of the passed address
|
||||
ip_address == 0.0.0.0: return the IP address of the first valid interface
|
||||
@ -209,7 +250,7 @@ class RemoteTab(SettingsTab):
|
||||
"""
|
||||
Load the configuration and update the server configuration if necessary
|
||||
"""
|
||||
self.port_spin_box.setValue(Settings().value(self.settings_section + '/port'))
|
||||
self.port_spin_box.setText(str(Settings().value(self.settings_section + '/port')))
|
||||
self.address_edit.setText(Settings().value(self.settings_section + '/ip address'))
|
||||
self.twelve_hour = Settings().value(self.settings_section + '/twelve hour')
|
||||
self.twelve_hour_check_box.setChecked(self.twelve_hour)
|
||||
@ -218,16 +259,18 @@ class RemoteTab(SettingsTab):
|
||||
self.user_login_group_box.setChecked(Settings().value(self.settings_section + '/authentication enabled'))
|
||||
self.user_id.setText(Settings().value(self.settings_section + '/user id'))
|
||||
self.password.setText(Settings().value(self.settings_section + '/password'))
|
||||
self.current_version_value.setText(Settings().value('remotes/download version'))
|
||||
self.master_version_value.setText(Registry().get_flag('website_version'))
|
||||
if self.master_version_value.text() == self.current_version_value.text():
|
||||
self.update_site_group_box.setEnabled(False)
|
||||
self.set_urls()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the configuration and update the server configuration if necessary
|
||||
"""
|
||||
if Settings().value(self.settings_section + '/ip address') != self.address_edit.text() or \
|
||||
Settings().value(self.settings_section + '/port') != self.port_spin_box.value():
|
||||
if Settings().value(self.settings_section + '/ip address') != self.address_edit.text():
|
||||
self.settings_form.register_post_process('remotes_config_updated')
|
||||
Settings().setValue(self.settings_section + '/port', self.port_spin_box.value())
|
||||
Settings().setValue(self.settings_section + '/ip address', self.address_edit.text())
|
||||
Settings().setValue(self.settings_section + '/twelve hour', self.twelve_hour)
|
||||
Settings().setValue(self.settings_section + '/thumbnails', self.thumbnails)
|
||||
@ -235,6 +278,8 @@ class RemoteTab(SettingsTab):
|
||||
Settings().setValue(self.settings_section + '/user id', self.user_id.text())
|
||||
Settings().setValue(self.settings_section + '/password', self.password.text())
|
||||
self.generate_icon()
|
||||
if self.update_site_group_box.isChecked():
|
||||
self.settings_form.register_post_process('download_website')
|
||||
|
||||
def on_twelve_hour_check_box_changed(self, check_state):
|
||||
"""
|
142
openlp/core/api/websockets.py
Normal file
@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
|
||||
with OpenLP. It uses JSON to communicate with the remotes.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import Settings, RegistryProperties, OpenLPMixin, Registry
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketWorker(QtCore.QObject):
|
||||
"""
|
||||
A special Qt thread class to allow the WebSockets server to run at the same time as the UI.
|
||||
"""
|
||||
def __init__(self, server):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
|
||||
:param server: The http server class.
|
||||
"""
|
||||
self.ws_server = server
|
||||
super(WebSocketWorker, self).__init__()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
self.ws_server.start_server()
|
||||
|
||||
def stop(self):
|
||||
self.ws_server.stop = True
|
||||
|
||||
|
||||
class WebSocketServer(RegistryProperties, OpenLPMixin):
|
||||
"""
|
||||
Wrapper round a server instance
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialise and start the WebSockets server
|
||||
"""
|
||||
super(WebSocketServer, self).__init__()
|
||||
self.settings_section = 'api'
|
||||
self.worker = WebSocketWorker(self)
|
||||
self.thread = QtCore.QThread()
|
||||
self.worker.moveToThread(self.thread)
|
||||
self.thread.started.connect(self.worker.run)
|
||||
self.thread.start()
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the correct server and save the handler
|
||||
"""
|
||||
address = Settings().value(self.settings_section + '/ip address')
|
||||
port = Settings().value(self.settings_section + '/websocket port')
|
||||
self.start_websocket_instance(address, port)
|
||||
# If web socket server start listening
|
||||
if hasattr(self, 'ws_server') and self.ws_server:
|
||||
event_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(event_loop)
|
||||
event_loop.run_until_complete(self.ws_server)
|
||||
event_loop.run_forever()
|
||||
else:
|
||||
log.debug('Failed to start ws server on port {port}'.format(port=port))
|
||||
|
||||
def start_websocket_instance(self, address, port):
|
||||
"""
|
||||
Start the server
|
||||
|
||||
:param address: The server address
|
||||
:param port: The run port
|
||||
"""
|
||||
loop = 1
|
||||
while loop < 4:
|
||||
try:
|
||||
self.ws_server = websockets.serve(self.handle_websocket, address, port)
|
||||
log.debug("Web Socket Server started for class {address} {port}".format(address=address, port=port))
|
||||
break
|
||||
except Exception as e:
|
||||
log.error('Failed to start ws server {why}'.format(why=e))
|
||||
loop += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
@staticmethod
|
||||
async def handle_websocket(request, path):
|
||||
"""
|
||||
Handle web socket requests and return the poll information.
|
||||
Check ever 0.2 seconds to get the latest position and send if changed.
|
||||
Only gets triggered when 1st client attaches
|
||||
|
||||
:param request: request from client
|
||||
:param path: determines the endpoints supported
|
||||
:return:
|
||||
"""
|
||||
log.debug("web socket handler registered with client")
|
||||
previous_poll = None
|
||||
previous_main_poll = None
|
||||
poller = Registry().get('poller')
|
||||
if path == '/state':
|
||||
while True:
|
||||
current_poll = poller.poll()
|
||||
if current_poll != previous_poll:
|
||||
await request.send(json.dumps(current_poll).encode())
|
||||
previous_poll = current_poll
|
||||
await asyncio.sleep(0.2)
|
||||
elif path == '/live_changed':
|
||||
while True:
|
||||
main_poll = poller.main_poll()
|
||||
if main_poll != previous_main_poll:
|
||||
await request.send(main_poll)
|
||||
previous_main_poll = main_poll
|
||||
await asyncio.sleep(0.2)
|
@ -25,8 +25,10 @@ The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
@ -215,6 +217,7 @@ def url_get_file(callback, url, f_path, sha256=None):
|
||||
block_count = 0
|
||||
block_size = 4096
|
||||
retries = 0
|
||||
log.debug("url_get_file: " + url)
|
||||
while True:
|
||||
try:
|
||||
filename = open(f_path, "wb")
|
||||
@ -253,4 +256,17 @@ def url_get_file(callback, url, f_path, sha256=None):
|
||||
return True
|
||||
|
||||
|
||||
def ping(host):
|
||||
"""
|
||||
Returns True if host responds to a ping request
|
||||
"""
|
||||
# Ping parameters as function of OS
|
||||
ping_str = "-n 1" if platform.system().lower() == "windows" else "-c 1"
|
||||
args = "ping " + " " + ping_str + " " + host
|
||||
need_sh = False if platform.system().lower() == "windows" else True
|
||||
|
||||
# Ping
|
||||
return subprocess.call(args, shell=need_sh) == 0
|
||||
|
||||
|
||||
__all__ = ['get_web_page']
|
||||
|
@ -134,6 +134,14 @@ class Settings(QtCore.QSettings):
|
||||
'advanced/single click service preview': False,
|
||||
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
'advanced/search as type': True,
|
||||
'api/twelve hour': True,
|
||||
'api/port': 4316,
|
||||
'api/websocket port': 4317,
|
||||
'api/user id': 'openlp',
|
||||
'api/password': 'password',
|
||||
'api/authentication enabled': False,
|
||||
'api/ip address': '0.0.0.0',
|
||||
'api/thumbnails': True,
|
||||
'crashreport/last directory': '',
|
||||
'formattingTags/html_tags': '',
|
||||
'core/audio repeat list': False,
|
||||
@ -214,6 +222,17 @@ class Settings(QtCore.QSettings):
|
||||
('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
|
||||
('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
|
||||
('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
|
||||
('advanced/default image', '/core/logo file', []), # Default image renamed + moved to general after 2.4.
|
||||
('remotes/https enabled', '', []),
|
||||
('remotes/https port', '', []),
|
||||
('remotes/twelve hour', 'api/twelve hour', []),
|
||||
('remotes/port', 'api/port', []),
|
||||
('remotes/websocket port', 'api/websocket port', []),
|
||||
('remotes/user id', 'api/user id', []),
|
||||
('remotes/password', 'api/password', []),
|
||||
('remotes/authentication enabled', 'api/authentication enabled', []),
|
||||
('remotes/ip address', 'api/ip address', []),
|
||||
('remotes/thumbnails', 'api/thumbnails', []),
|
||||
('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
|
||||
('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6.
|
||||
('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6.
|
||||
|
@ -153,6 +153,7 @@ class UiStrings(object):
|
||||
self.Split = translate('OpenLP.Ui', 'Optional &Split')
|
||||
self.SplitToolTip = translate('OpenLP.Ui',
|
||||
'Split a slide into two only if it does not fit on the screen as one slide.')
|
||||
self.StartingImport = translate('OpenLP.Ui', 'Starting import...')
|
||||
self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
|
||||
self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
|
||||
self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
|
||||
@ -166,6 +167,7 @@ class UiStrings(object):
|
||||
self.View = translate('OpenLP.Ui', 'View')
|
||||
self.ViewMode = translate('OpenLP.Ui', 'View Mode')
|
||||
self.Video = translate('OpenLP.Ui', 'Video')
|
||||
self.WebDownloadText = translate('OpenLP.Ui', 'Web Interface, Download and Install latest Version')
|
||||
book_chapter = translate('OpenLP.Ui', 'Book Chapter')
|
||||
chapter = translate('OpenLP.Ui', 'Chapter')
|
||||
verse = translate('OpenLP.Ui', 'Verse')
|
||||
|
@ -1,3 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`openlp.core.common` module downloads the version details for OpenLP.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
@ -12,7 +36,8 @@ from subprocess import Popen, PIPE
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, Settings
|
||||
from openlp.core.common import AppLocation, Registry, Settings
|
||||
from openlp.core.common.httputils import ping
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -42,6 +67,12 @@ class VersionThread(QtCore.QThread):
|
||||
"""
|
||||
self.sleep(1)
|
||||
log.debug('Version thread - run')
|
||||
found = ping("openlp.io")
|
||||
Registry().set_flag('internet_present', found)
|
||||
update_check = Settings().value('core/update check')
|
||||
if found:
|
||||
Registry().execute('get_website_version')
|
||||
if update_check:
|
||||
app_version = get_application_version()
|
||||
version = check_latest_version(app_version)
|
||||
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
|
||||
|
@ -56,7 +56,7 @@ class ImageThread(QtCore.QThread):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
self.image_manager._process()
|
||||
self.image_manager.process()
|
||||
|
||||
|
||||
class Priority(object):
|
||||
@ -235,8 +235,15 @@ class ImageManager(QtCore.QObject):
|
||||
def get_image(self, path, source, width=-1, height=-1):
|
||||
"""
|
||||
Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
|
||||
|
||||
:param: path: The image path
|
||||
:param: source: The source of the image
|
||||
:param: background: The image background colour
|
||||
:param: width: The processed image width
|
||||
:param: height: The processed image height
|
||||
"""
|
||||
log.debug('getImage {path}'.format(path=path))
|
||||
log.debug('get_image {path} {source} {width} {height}'.format(path=path, source=source,
|
||||
width=width, height=height))
|
||||
image = self._cache[(path, source, width, height)]
|
||||
if image.image is None:
|
||||
self._conversion_queue.modify_priority(image, Priority.High)
|
||||
@ -255,8 +262,15 @@ class ImageManager(QtCore.QObject):
|
||||
def get_image_bytes(self, path, source, width=-1, height=-1):
|
||||
"""
|
||||
Returns the byte string for an image. If not present wait for the background thread to process it.
|
||||
|
||||
:param: path: The image path
|
||||
:param: source: The source of the image
|
||||
:param: background: The image background colour
|
||||
:param: width: The processed image width
|
||||
:param: height: The processed image height
|
||||
"""
|
||||
log.debug('get_image_bytes {path}'.format(path=path))
|
||||
log.debug('get_image_bytes {path} {source} {width} {height}'.format(path=path, source=source,
|
||||
width=width, height=height))
|
||||
image = self._cache[(path, source, width, height)]
|
||||
if image.image_bytes is None:
|
||||
self._conversion_queue.modify_priority(image, Priority.Urgent)
|
||||
@ -270,9 +284,16 @@ class ImageManager(QtCore.QObject):
|
||||
def add_image(self, path, source, background, width=-1, height=-1):
|
||||
"""
|
||||
Add image to cache if it is not already there.
|
||||
|
||||
:param: path: The image path
|
||||
:param: source: The source of the image
|
||||
:param: background: The image background colour
|
||||
:param: width: The processed image width
|
||||
:param: height: The processed image height
|
||||
"""
|
||||
log.debug('add_image {path}'.format(path=path))
|
||||
if (path, source, width, height) not in self._cache:
|
||||
log.debug('add_image {path} {source} {width} {height}'.format(path=path, source=source,
|
||||
width=width, height=height))
|
||||
if not (path, source, width, height) in self._cache:
|
||||
image = Image(path, source, background, width, height)
|
||||
self._cache[(path, source, width, height)] = image
|
||||
self._conversion_queue.put((image.priority, image.secondary_priority, image))
|
||||
@ -286,11 +307,11 @@ class ImageManager(QtCore.QObject):
|
||||
if not self.image_thread.isRunning():
|
||||
self.image_thread.start()
|
||||
|
||||
def _process(self):
|
||||
def process(self):
|
||||
"""
|
||||
Controls the processing called from a ``QtCore.QThread``.
|
||||
"""
|
||||
log.debug('_process - started')
|
||||
log.debug('process - started')
|
||||
while not self._conversion_queue.empty() and not self.stop_manager:
|
||||
self._process_cache()
|
||||
log.debug('_process - ended')
|
||||
|
@ -202,7 +202,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
||||
self.themes_url = self.web + self.config.get('themes', 'directory') + '/'
|
||||
self.web_access = True
|
||||
except (NoSectionError, NoOptionError, MissingSectionHeaderError):
|
||||
log.debug('A problem occured while parsing the downloaded config file')
|
||||
log.debug('A problem occurred while parsing the downloaded config file')
|
||||
trace_error_handler(log)
|
||||
self.update_screen_list_combo()
|
||||
self.application.process_events()
|
||||
@ -213,7 +213,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
||||
self.presentation_check_box.setChecked(self.plugin_manager.get_plugin_by_name('presentations').is_active())
|
||||
self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
|
||||
self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
|
||||
self.remote_check_box.setChecked(self.plugin_manager.get_plugin_by_name('remotes').is_active())
|
||||
self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
|
||||
self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
|
||||
self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
|
||||
@ -530,7 +529,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
||||
self._set_plugin_status(self.presentation_check_box, 'presentations/status')
|
||||
self._set_plugin_status(self.image_check_box, 'images/status')
|
||||
self._set_plugin_status(self.media_check_box, 'media/status')
|
||||
self._set_plugin_status(self.remote_check_box, 'remotes/status')
|
||||
self._set_plugin_status(self.custom_check_box, 'custom/status')
|
||||
self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
|
||||
self._set_plugin_status(self.alert_check_box, 'alerts/status')
|
||||
|
@ -24,6 +24,7 @@ The UI widgets for the first time wizard.
|
||||
"""
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common.uistrings import UiStrings
|
||||
from openlp.core.common import translate, is_macosx, clean_button_text, Settings
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.lib.ui import add_welcome_page
|
||||
@ -254,8 +255,7 @@ class UiFirstTimeWizard(object):
|
||||
self.presentation_check_box.setText(translate('OpenLP.FirstTimeWizard',
|
||||
'Presentations – Show .ppt, .odp and .pdf files'))
|
||||
self.media_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Media – Playback of Audio and Video files'))
|
||||
self.remote_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Remote – Control OpenLP via browser or smart'
|
||||
'phone app'))
|
||||
self.remote_check_box.setText(str(UiStrings().WebDownloadText))
|
||||
self.song_usage_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Song Usage Monitor'))
|
||||
self.alert_check_box.setText(translate('OpenLP.FirstTimeWizard',
|
||||
'Alerts – Display informative messages while showing other slides'))
|
||||
|
@ -34,6 +34,8 @@ from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.api import websockets
|
||||
from openlp.core.api.http import server
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, LanguageManager, Settings, UiStrings, \
|
||||
check_directory_exists, translate, is_win, is_macosx, add_actions
|
||||
from openlp.core.common.actions import ActionList, CategoryOrder
|
||||
@ -49,6 +51,7 @@ from openlp.core.ui.projector.manager import ProjectorManager
|
||||
from openlp.core.ui.lib.dockwidget import OpenLPDockWidget
|
||||
from openlp.core.ui.lib.mediadockmanager import MediaDockManager
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MEDIA_MANAGER_STYLE = """
|
||||
@ -513,6 +516,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
Settings().set_up_default_values()
|
||||
self.about_form = AboutForm(self)
|
||||
MediaController()
|
||||
if Registry().get_flag('no_web_server'):
|
||||
websockets.WebSocketServer()
|
||||
server.HttpServer()
|
||||
SettingsForm(self)
|
||||
self.formatting_tag_form = FormattingTagForm(self)
|
||||
self.shortcut_form = ShortcutListForm(self)
|
||||
@ -540,7 +546,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
self.tools_first_time_wizard.triggered.connect(self.on_first_time_wizard_clicked)
|
||||
self.update_theme_images.triggered.connect(self.on_update_theme_images)
|
||||
self.formatting_tag_item.triggered.connect(self.on_formatting_tag_item_clicked)
|
||||
self.settings_configure_item.triggered.connect(self.on_settings_configure_iem_clicked)
|
||||
self.settings_configure_item.triggered.connect(self.on_settings_configure_item_clicked)
|
||||
self.settings_shortcuts_item.triggered.connect(self.on_settings_shortcuts_item_clicked)
|
||||
self.settings_import_item.triggered.connect(self.on_settings_import_item_clicked)
|
||||
self.settings_export_item.triggered.connect(self.on_settings_export_item_clicked)
|
||||
@ -803,7 +809,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
"""
|
||||
self.formatting_tag_form.exec()
|
||||
|
||||
def on_settings_configure_iem_clicked(self):
|
||||
def on_settings_configure_item_clicked(self):
|
||||
"""
|
||||
Show the Settings dialog
|
||||
"""
|
||||
|
@ -146,5 +146,6 @@ def format_milliseconds(milliseconds):
|
||||
|
||||
from .mediacontroller import MediaController
|
||||
from .playertab import PlayerTab
|
||||
from .endpoint import media_endpoint
|
||||
|
||||
__all__ = ['MediaController', 'PlayerTab']
|
||||
|
72
openlp/core/ui/media/endpoint.py
Normal file
@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.common import Registry
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
media_endpoint = Endpoint('media')
|
||||
|
||||
|
||||
@media_endpoint.route('play')
|
||||
@requires_auth
|
||||
def media_play(request):
|
||||
"""
|
||||
Handles requests for playing media
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
media = Registry().get('media_controller')
|
||||
live = Registry().get('live_controller')
|
||||
status = media.media_play(live, False)
|
||||
return {'results': {'success': status}}
|
||||
|
||||
|
||||
@media_endpoint.route('pause')
|
||||
@requires_auth
|
||||
def media_pause(request):
|
||||
"""
|
||||
Handles requests for pausing media
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
media = Registry().get('media_controller')
|
||||
live = Registry().get('live_controller')
|
||||
status = media.media_pause(live)
|
||||
return {'results': {'success': status}}
|
||||
|
||||
|
||||
@media_endpoint.route('stop')
|
||||
@requires_auth
|
||||
def media_stop(request):
|
||||
"""
|
||||
Handles requests for stopping
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
event = getattr(Registry().get('live_controller'), 'mediacontroller_live_stop')
|
||||
event.emit()
|
||||
return {'results': {'success': True}}
|
@ -28,12 +28,13 @@ import os
|
||||
import datetime
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, \
|
||||
extension_loader, translate
|
||||
from openlp.core.lib import ItemCapabilities
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.common import AppLocation
|
||||
from openlp.core.ui import DisplayControllerType
|
||||
from openlp.core.ui.media.endpoint import media_endpoint
|
||||
from openlp.core.ui.media.vendor.mediainfoWrapper import MediaInfoWrapper
|
||||
from openlp.core.ui.media.mediaplayer import MediaPlayer
|
||||
from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\
|
||||
@ -127,9 +128,11 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
Registry().register_function('media_unblank', self.media_unblank)
|
||||
# Signals for background video
|
||||
Registry().register_function('songs_hide', self.media_hide)
|
||||
Registry().register_function('songs_blank', self.media_blank)
|
||||
Registry().register_function('songs_unblank', self.media_unblank)
|
||||
Registry().register_function('mediaitem_media_rebuild', self._set_active_players)
|
||||
Registry().register_function('mediaitem_suffixes', self._generate_extensions_lists)
|
||||
register_endpoint(media_endpoint)
|
||||
|
||||
def _set_active_players(self):
|
||||
"""
|
||||
@ -611,6 +614,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
self.media_play(msg[0], status)
|
||||
|
||||
def on_media_play(self):
|
||||
"""
|
||||
Responds to the request to play a loaded video from the web.
|
||||
|
||||
:param msg: First element is the controller which should be used
|
||||
"""
|
||||
self.media_play(Registry().get('live_controller'), False)
|
||||
|
||||
def media_play(self, controller, first_time=True):
|
||||
"""
|
||||
Responds to the request to play a loaded video
|
||||
@ -685,6 +696,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
self.media_pause(msg[0])
|
||||
|
||||
def on_media_pause(self):
|
||||
"""
|
||||
Responds to the request to pause a loaded video from the web.
|
||||
|
||||
:param msg: First element is the controller which should be used
|
||||
"""
|
||||
self.media_pause(Registry().get('live_controller'))
|
||||
|
||||
def media_pause(self, controller):
|
||||
"""
|
||||
Responds to the request to pause a loaded video
|
||||
@ -725,6 +744,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
self.media_stop(msg[0])
|
||||
|
||||
def on_media_stop(self):
|
||||
"""
|
||||
Responds to the request to stop a loaded video from the web.
|
||||
|
||||
:param msg: First element is the controller which should be used
|
||||
"""
|
||||
self.media_stop(Registry().get('live_controller'))
|
||||
|
||||
def media_stop(self, controller, looping_background=False):
|
||||
"""
|
||||
Responds to the request to stop a loaded video
|
||||
|
@ -26,6 +26,7 @@ import logging
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.api import ApiTab
|
||||
from openlp.core.common import Registry, RegistryProperties
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.ui import AdvancedTab, GeneralTab, ThemesTab
|
||||
@ -56,12 +57,13 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
|
||||
self.projector_tab = None
|
||||
self.advanced_tab = None
|
||||
self.player_tab = None
|
||||
self.api_tab = None
|
||||
|
||||
def exec(self):
|
||||
"""
|
||||
Execute the form
|
||||
"""
|
||||
# load all the
|
||||
# load all the widgets
|
||||
self.setting_list_widget.blockSignals(True)
|
||||
self.setting_list_widget.clear()
|
||||
while self.stacked_layout.count():
|
||||
@ -72,6 +74,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
|
||||
self.insert_tab(self.advanced_tab)
|
||||
self.insert_tab(self.player_tab)
|
||||
self.insert_tab(self.projector_tab)
|
||||
self.insert_tab(self.api_tab)
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.settings_tab:
|
||||
self.insert_tab(plugin.settings_tab, plugin.is_active())
|
||||
@ -93,6 +96,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
|
||||
list_item = QtWidgets.QListWidgetItem(build_icon(tab_widget.icon_path), tab_widget.tab_title_visible)
|
||||
list_item.setData(QtCore.Qt.UserRole, tab_widget.tab_title)
|
||||
self.setting_list_widget.addItem(list_item)
|
||||
tab_widget.load()
|
||||
|
||||
def accept(self):
|
||||
"""
|
||||
@ -154,10 +158,13 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
|
||||
self.advanced_tab = AdvancedTab(self)
|
||||
# Advanced tab
|
||||
self.player_tab = PlayerTab(self)
|
||||
# Api tab
|
||||
self.api_tab = ApiTab(self)
|
||||
self.general_tab.post_set_up()
|
||||
self.themes_tab.post_set_up()
|
||||
self.advanced_tab.post_set_up()
|
||||
self.player_tab.post_set_up()
|
||||
self.api_tab.post_set_up()
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.settings_tab:
|
||||
plugin.settings_tab.post_set_up()
|
||||
|
@ -439,6 +439,10 @@ class SlideController(DisplayController, RegistryProperties):
|
||||
# NOTE: {t} used to keep line length < maxline
|
||||
getattr(self,
|
||||
'slidecontroller_{t}_previous'.format(t=self.type_prefix)).connect(self.on_slide_selected_previous)
|
||||
if self.is_live:
|
||||
getattr(self, 'mediacontroller_live_play').connect(self.media_controller.on_media_play)
|
||||
getattr(self, 'mediacontroller_live_pause').connect(self.media_controller.on_media_pause)
|
||||
getattr(self, 'mediacontroller_live_stop').connect(self.media_controller.on_media_stop)
|
||||
|
||||
def _slide_shortcut_activated(self):
|
||||
"""
|
||||
@ -1530,6 +1534,9 @@ class LiveController(RegistryMixin, OpenLPMixin, SlideController):
|
||||
slidecontroller_live_next = QtCore.pyqtSignal()
|
||||
slidecontroller_live_previous = QtCore.pyqtSignal()
|
||||
slidecontroller_toggle_display = QtCore.pyqtSignal(str)
|
||||
mediacontroller_live_play = QtCore.pyqtSignal()
|
||||
mediacontroller_live_pause = QtCore.pyqtSignal()
|
||||
mediacontroller_live_stop = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
|
@ -24,6 +24,7 @@ import logging
|
||||
|
||||
from PyQt5 import QtGui
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import Settings, UiStrings, translate
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
@ -31,6 +32,7 @@ from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.theme import VerticalType
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.core.ui import AlertLocation
|
||||
from openlp.plugins.alerts.endpoint import api_alerts_endpoint, alerts_endpoint
|
||||
from openlp.plugins.alerts.forms import AlertForm
|
||||
from openlp.plugins.alerts.lib import AlertsManager, AlertsTab
|
||||
from openlp.plugins.alerts.lib.db import init_schema
|
||||
@ -140,6 +142,8 @@ class AlertsPlugin(Plugin):
|
||||
AlertsManager(self)
|
||||
self.manager = Manager('alerts', init_schema)
|
||||
self.alert_form = AlertForm(self)
|
||||
register_endpoint(alerts_endpoint)
|
||||
register_endpoint(api_alerts_endpoint)
|
||||
|
||||
def add_tools_menu_item(self, tools_menu):
|
||||
"""
|
||||
|
60
openlp/plugins/alerts/endpoint.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
import json
|
||||
import urllib
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import requires_auth
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import PluginStatus
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
alerts_endpoint = Endpoint('alert')
|
||||
api_alerts_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@alerts_endpoint.route('')
|
||||
@api_alerts_endpoint.route('alert')
|
||||
@requires_auth
|
||||
def alert(request):
|
||||
"""
|
||||
Handles requests for setting service items in the service manager
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
plugin = Registry().get('plugin_manager').get_plugin_by_name("alerts")
|
||||
if plugin.status == PluginStatus.Active:
|
||||
try:
|
||||
json_data = request.GET.get('data')
|
||||
text = json.loads(json_data)['request']['text']
|
||||
except KeyError:
|
||||
log.error("Endpoint alerts request text not found")
|
||||
text = urllib.parse.unquote(text)
|
||||
Registry().get('alerts_manager').alerts_text.emit([text])
|
||||
success = True
|
||||
else:
|
||||
success = False
|
||||
return {'results': {'success': success}}
|
@ -22,9 +22,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import UiStrings
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon, translate
|
||||
from openlp.plugins.bibles.endpoint import api_bibles_endpoint, bibles_endpoint
|
||||
from openlp.core.lib.ui import create_action
|
||||
from openlp.plugins.bibles.lib import BibleManager, BiblesTab, BibleMediaItem, LayoutStyle, DisplayStyle, \
|
||||
LanguageSelection
|
||||
@ -75,6 +77,8 @@ class BiblePlugin(Plugin):
|
||||
self.icon_path = ':/plugins/plugin_bibles.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.manager = BibleManager(self)
|
||||
register_endpoint(bibles_endpoint)
|
||||
register_endpoint(api_bibles_endpoint)
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
|
100
openlp/plugins/bibles/endpoint.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
bibles_endpoint = Endpoint('bibles')
|
||||
api_bibles_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@bibles_endpoint.route('search')
|
||||
def bibles_search(request):
|
||||
"""
|
||||
Handles requests for searching the bibles plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'bibles', log)
|
||||
|
||||
|
||||
@bibles_endpoint.route('live')
|
||||
@requires_auth
|
||||
def bibles_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'bibles', log)
|
||||
|
||||
|
||||
@bibles_endpoint.route('add')
|
||||
@requires_auth
|
||||
def bibles_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'bibles', log)
|
||||
|
||||
|
||||
@api_bibles_endpoint.route('bibles/search')
|
||||
def bibles_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the bibles plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'bibles', log)
|
||||
|
||||
|
||||
@api_bibles_endpoint.route('bibles/live')
|
||||
@requires_auth
|
||||
def bibles_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'bibles', log)
|
||||
|
||||
|
||||
@api_bibles_endpoint.route('bibles/add')
|
||||
@requires_auth
|
||||
def bibles_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'bibles', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
@ -26,8 +26,10 @@ for the Custom Slides plugin.
|
||||
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon, translate
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.plugins.custom.endpoint import api_custom_endpoint, custom_endpoint
|
||||
from openlp.plugins.custom.lib import CustomMediaItem, CustomTab
|
||||
from openlp.plugins.custom.lib.db import CustomSlide, init_schema
|
||||
from openlp.plugins.custom.lib.mediaitem import CustomSearch
|
||||
@ -61,6 +63,8 @@ class CustomPlugin(Plugin):
|
||||
self.db_manager = Manager('custom', init_schema)
|
||||
self.icon_path = ':/plugins/plugin_custom.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
register_endpoint(custom_endpoint)
|
||||
register_endpoint(api_custom_endpoint)
|
||||
|
||||
@staticmethod
|
||||
def about():
|
||||
|
100
openlp/plugins/custom/endpoint.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
custom_endpoint = Endpoint('custom')
|
||||
api_custom_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@custom_endpoint.route('search')
|
||||
def custom_search(request):
|
||||
"""
|
||||
Handles requests for searching the custom plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'custom', log)
|
||||
|
||||
|
||||
@custom_endpoint.route('live')
|
||||
@requires_auth
|
||||
def custom_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'custom', log)
|
||||
|
||||
|
||||
@custom_endpoint.route('add')
|
||||
@requires_auth
|
||||
def custom_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'custom', log)
|
||||
|
||||
|
||||
@api_custom_endpoint.route('custom/search')
|
||||
def custom_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the custom plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'custom', log)
|
||||
|
||||
|
||||
@api_custom_endpoint.route('custom/live')
|
||||
@requires_auth
|
||||
def custom_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'custom', log)
|
||||
|
||||
|
||||
@api_custom_endpoint.route('custom/add')
|
||||
@requires_auth
|
||||
def custom_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'custom', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
113
openlp/plugins/images/endpoint.py
Normal file
@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service, display_thumbnails
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
images_endpoint = Endpoint('images')
|
||||
api_images_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
# images/thumbnails/320x240/1.jpg
|
||||
@images_endpoint.route('thumbnails/{dimensions}/{file_name}')
|
||||
def images_thumbnails(request, dimensions, file_name):
|
||||
"""
|
||||
Return an image to a web page based on a URL
|
||||
:param request: Request object
|
||||
:param dimensions: the image size eg 88x88
|
||||
:param file_name: the individual image name
|
||||
:return:
|
||||
"""
|
||||
return display_thumbnails(request, 'images', log, dimensions, file_name)
|
||||
|
||||
|
||||
@images_endpoint.route('search')
|
||||
def images_search(request):
|
||||
"""
|
||||
Handles requests for searching the images plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'images', log)
|
||||
|
||||
|
||||
@images_endpoint.route('live')
|
||||
@requires_auth
|
||||
def images_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'images', log)
|
||||
|
||||
|
||||
@images_endpoint.route('add')
|
||||
@requires_auth
|
||||
def images_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'images', log)
|
||||
|
||||
|
||||
@api_images_endpoint.route('images/search')
|
||||
def images_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the images plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'images', log)
|
||||
|
||||
|
||||
@api_images_endpoint.route('images/live')
|
||||
@requires_auth
|
||||
def images_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'images', log)
|
||||
|
||||
|
||||
@api_images_endpoint.route('images/add')
|
||||
@requires_auth
|
||||
def images_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'images', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
@ -24,9 +24,11 @@ from PyQt5 import QtGui
|
||||
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import Settings, translate
|
||||
from openlp.core.lib import Plugin, StringContent, ImageSource, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint
|
||||
from openlp.plugins.images.lib import ImageMediaItem, ImageTab
|
||||
from openlp.plugins.images.lib.db import init_schema
|
||||
|
||||
@ -51,6 +53,8 @@ class ImagePlugin(Plugin):
|
||||
self.weight = -7
|
||||
self.icon_path = ':/plugins/plugin_images.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
register_endpoint(images_endpoint)
|
||||
register_endpoint(api_images_endpoint)
|
||||
|
||||
@staticmethod
|
||||
def about():
|
||||
|
100
openlp/plugins/media/endpoint.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
media_endpoint = Endpoint('media')
|
||||
api_media_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@media_endpoint.route('search')
|
||||
def media_search(request):
|
||||
"""
|
||||
Handles requests for searching the media plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'media', log)
|
||||
|
||||
|
||||
@media_endpoint.route('live')
|
||||
@requires_auth
|
||||
def media_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'media', log)
|
||||
|
||||
|
||||
@media_endpoint.route('add')
|
||||
@requires_auth
|
||||
def media_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'media', log)
|
||||
|
||||
|
||||
@api_media_endpoint.route('media/search')
|
||||
def media_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the media plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'media', log)
|
||||
|
||||
|
||||
@api_media_endpoint.route('media/live')
|
||||
@requires_auth
|
||||
def media_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'media', log)
|
||||
|
||||
|
||||
@api_media_endpoint.route('media/add')
|
||||
@requires_auth
|
||||
def media_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'media', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
@ -26,12 +26,13 @@ The Media plugin
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from shutil import which
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, translate, check_binary_exists, is_win
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import AppLocation, translate, check_binary_exists
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.plugins.media.endpoint import api_media_endpoint, media_endpoint
|
||||
from openlp.plugins.media.lib import MediaMediaItem, MediaTab
|
||||
|
||||
|
||||
@ -58,6 +59,8 @@ class MediaPlugin(Plugin):
|
||||
self.icon = build_icon(self.icon_path)
|
||||
# passed with drag and drop messages
|
||||
self.dnd_id = 'Media'
|
||||
register_endpoint(media_endpoint)
|
||||
register_endpoint(api_media_endpoint)
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
|
114
openlp/plugins/presentations/endpoint.py
Normal file
@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service, display_thumbnails
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
presentations_endpoint = Endpoint('presentations')
|
||||
api_presentations_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
# /presentations/thumbnails88x88/PA%20Rota.pdf/slide5.png
|
||||
@presentations_endpoint.route('thumbnails/{dimensions}/{file_name}/{slide}')
|
||||
def presentations_thumbnails(request, dimensions, file_name, slide):
|
||||
"""
|
||||
Return a presentation to a web page based on a URL
|
||||
:param request: Request object
|
||||
:param dimensions: the image size eg 88x88
|
||||
:param file_name: the file name of the image
|
||||
:param slide: the individual image name
|
||||
:return:
|
||||
"""
|
||||
return display_thumbnails(request, 'presentations', log, dimensions, file_name, slide)
|
||||
|
||||
|
||||
@presentations_endpoint.route('search')
|
||||
def presentations_search(request):
|
||||
"""
|
||||
Handles requests for searching the presentations plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'presentations', log)
|
||||
|
||||
|
||||
@presentations_endpoint.route('live')
|
||||
@requires_auth
|
||||
def presentations_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'presentations', log)
|
||||
|
||||
|
||||
@presentations_endpoint.route('add')
|
||||
@requires_auth
|
||||
def presentations_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'presentations', log)
|
||||
|
||||
|
||||
@api_presentations_endpoint.route('presentations/search')
|
||||
def presentations_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the presentations plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'presentations', log)
|
||||
|
||||
|
||||
@api_presentations_endpoint.route('presentations/live')
|
||||
@requires_auth
|
||||
def presentations_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'presentations', log)
|
||||
|
||||
|
||||
@api_presentations_endpoint.route('presentations/add')
|
||||
@requires_auth
|
||||
def presentations_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'presentations', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
@ -28,8 +28,10 @@ import logging
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, extension_loader, translate
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import extension_loader, translate
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint
|
||||
from openlp.plugins.presentations.lib import PresentationController, PresentationMediaItem, PresentationTab
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -66,6 +68,8 @@ class PresentationPlugin(Plugin):
|
||||
self.weight = -8
|
||||
self.icon_path = ':/plugins/plugin_presentations.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
register_endpoint(presentations_endpoint)
|
||||
register_endpoint(api_presentations_endpoint)
|
||||
|
||||
def create_settings_tab(self, parent):
|
||||
"""
|
||||
|
@ -19,71 +19,3 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`remotes` plugin allows OpenLP to be controlled from another machine
|
||||
over a network connection.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
"""
|
||||
|
69
openlp/plugins/remotes/deploy.py
Normal file
@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
import urllib.error
|
||||
|
||||
from openlp.core.common import AppLocation, Registry
|
||||
from openlp.core.common.httputils import url_get_file, get_web_page, get_url_file_size
|
||||
|
||||
|
||||
def deploy_zipfile(app_root, zip_name):
|
||||
"""
|
||||
Process the downloaded zip file and add to the correct directory
|
||||
|
||||
:param zip_name: the zip file to be processed
|
||||
:param app_root: the directory where the zip get expanded to
|
||||
|
||||
:return: None
|
||||
"""
|
||||
zip_file = os.path.join(app_root, zip_name)
|
||||
web_zip = zipfile.ZipFile(zip_file)
|
||||
web_zip.extractall(app_root)
|
||||
|
||||
|
||||
def download_sha256():
|
||||
"""
|
||||
Download the config file to extract the sha256 and version number
|
||||
"""
|
||||
user_agent = 'OpenLP/' + Registry().get('application').applicationVersion()
|
||||
try:
|
||||
web_config = get_web_page('{host}{name}'.format(host='https://get.openlp.org/webclient/', name='download.cfg'),
|
||||
header=('User-Agent', user_agent))
|
||||
except (urllib.error.URLError, ConnectionError) as err:
|
||||
return False
|
||||
file_bits = web_config.read().decode('utf-8').split()
|
||||
return file_bits[0], file_bits[2]
|
||||
|
||||
|
||||
def download_and_check(callback=None):
|
||||
"""
|
||||
Download the web site and deploy it.
|
||||
"""
|
||||
sha256, version = download_sha256()
|
||||
file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip')
|
||||
callback.setRange(0, file_size)
|
||||
if url_get_file(callback, '{host}{name}'.format(host='https://get.openlp.org/webclient/', name='site.zip'),
|
||||
os.path.join(str(AppLocation.get_section_data_path('remotes')), 'site.zip'),
|
||||
sha256=sha256):
|
||||
deploy_zipfile(str(AppLocation.get_section_data_path('remotes')), 'site.zip')
|
@ -1,6 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@ -19,28 +19,28 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${chords_title}</title>
|
||||
<link rel="stylesheet" href="/css/stage.css" />
|
||||
<link rel="stylesheet" href="/css/chords.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/chords.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="hidden" id="next-text" value="${next}" />
|
||||
<div id="right">
|
||||
<div id="clock"></div>
|
||||
<div id="chords" class="button">Toggle Chords</div>
|
||||
<div id="notes"></div>
|
||||
</div>
|
||||
<div id="header">
|
||||
<div id="verseorder"></div>
|
||||
<div id="transpose">Transpose:</div> <div class="button" id="transposedown">-</div> <div id="transposevalue">0</div> <div class="button" id="transposeup">+</div> <div id="capodisplay">(Capo)</div>
|
||||
</div>
|
||||
<div id="currentslide"></div>
|
||||
<div id="nextslide"></div>
|
||||
</body>
|
||||
</html>
|
||||
import logging
|
||||
|
||||
import os
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.endpoint.core import TRANSLATED_STRINGS
|
||||
from openlp.core.common import AppLocation
|
||||
|
||||
|
||||
static_dir = os.path.join(str(AppLocation.get_section_data_path('remotes')))
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
remote_endpoint = Endpoint('remote', template_dir=static_dir, static_dir=static_dir)
|
||||
|
||||
|
||||
@remote_endpoint.route('{view}')
|
||||
def index(request, view):
|
||||
"""
|
||||
Handles requests for /remotes url
|
||||
|
||||
:param request: The http request object.
|
||||
:param view: The view name to be servered.
|
||||
"""
|
||||
return remote_endpoint.render_template('{view}.mako'.format(view=view), **TRANSLATED_STRINGS)
|
9404
openlp/plugins/remotes/html/assets/jquery.js
vendored
@ -1,96 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
|
||||
#header {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#transpose,
|
||||
#transposevalue,
|
||||
#capodisplay {
|
||||
display: inline-block;
|
||||
font-size: 30pt;
|
||||
color: gray;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid gray;
|
||||
border-radius: .3em;
|
||||
padding: 0 .2em;
|
||||
min-width: 1.2em;
|
||||
line-height: 1.2em;
|
||||
font-size: 25pt;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: 0px 1px 0px white;
|
||||
color: black;
|
||||
background: linear-gradient(to bottom, white 5%, gray 100%);
|
||||
background-color: gray;
|
||||
cursor: pointer;
|
||||
}
|
||||
.button:hover {
|
||||
background: linear-gradient(to bottom, white 10%, gray 150%);
|
||||
color: darkslategray ;
|
||||
background-color: gray;
|
||||
}
|
||||
.button:active {
|
||||
position:relative;
|
||||
top:1px;
|
||||
}
|
||||
|
||||
/* Extending existing definition in stage.css */
|
||||
#verseorder {
|
||||
line-height: 1.5;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chordline {
|
||||
line-height: 2.0;
|
||||
}
|
||||
|
||||
.chordline1 {
|
||||
line-height: 1.0
|
||||
}
|
||||
|
||||
.chordline span.chord span {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chordline span.chord span strong {
|
||||
position: absolute;
|
||||
top: -0.8em;
|
||||
left: 0;
|
||||
font-size: 30pt;
|
||||
font-weight: normal;
|
||||
line-height: normal;
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.ws {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
#nextslide .chordline span.chord span strong {
|
||||
color: gray;
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
body {
|
||||
background-color: black;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.size {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
|
||||
.ui-icon-blank {
|
||||
background-image: url(../images/ui-icon-blank.png);
|
||||
}
|
||||
|
||||
.ui-icon-unblank {
|
||||
background-image: url(../images/ui-icon-unblank.png);
|
||||
}
|
||||
|
||||
/* Overwrite style from jquery-mobile.min.css */
|
||||
.ui-li .ui-btn-text a.ui-link-inherit{
|
||||
white-space: normal;
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
font-family: sans-serif;
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none; /* Chrome/Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE 10+ */
|
||||
user-select: none; /* Future */
|
||||
}
|
||||
|
||||
#currentslide {
|
||||
font-size: 40pt;
|
||||
color: white;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
#nextslide {
|
||||
font-size: 40pt;
|
||||
color: grey;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
#right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-size: 30pt;
|
||||
color: yellow;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#notes {
|
||||
font-size: 36pt;
|
||||
color: salmon;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#verseorder {
|
||||
font-size: 30pt;
|
||||
color: green;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.currenttag {
|
||||
color: lightgreen;
|
||||
font-weight: bold;
|
||||
}
|
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 340 B |
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 364 B |
Before Width: | Height: | Size: 460 B |
Before Width: | Height: | Size: 453 B |
Before Width: | Height: | Size: 519 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 231 B |
@ -1,177 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1" />
|
||||
<title>${app_title}</title>
|
||||
<link rel="stylesheet" href="/assets/jquery.mobile.min.css" />
|
||||
<link rel="stylesheet" href="/css/openlp.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/openlp.js"></script>
|
||||
<script type="text/javascript" src="/assets/jquery.mobile.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
translationStrings = {
|
||||
"go_live": "${go_live}",
|
||||
"add_to_service": "${add_to_service}",
|
||||
"no_results": "${no_results}",
|
||||
"home": "${home}"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div data-role="page" id="home">
|
||||
<div data-role="header">
|
||||
<h1>${app_title}</h1>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<div data-role="controlgroup">
|
||||
<a href="#service-manager" data-role="button" data-icon="arrow-r" data-iconpos="right">${service_manager}</a>
|
||||
<a href="#slide-controller" data-role="button" data-icon="arrow-r" data-iconpos="right">${slide_controller}</a>
|
||||
<a href="#alerts" data-role="button" data-icon="arrow-r" data-iconpos="right">${alerts}</a>
|
||||
<a href="#search" data-role="button" data-icon="arrow-r" data-iconpos="right">${search}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="page" id="service-manager">
|
||||
<div data-role="header" data-position="fixed">
|
||||
<a href="#home" data-role="button" data-icon="home" data-iconpos="left">${home}</a>
|
||||
<h1>${service_manager}</h1>
|
||||
<a href="#" id="service-refresh" data-role="button" data-icon="refresh">${refresh}</a>
|
||||
<div data-role="navbar">
|
||||
<ul>
|
||||
<li><a href="#service-manager" data-theme="e">${service}</a></li>
|
||||
<li><a href="#slide-controller">${slides}</a></li>
|
||||
<li><a href="#alerts">${alerts}</a></li>
|
||||
<li><a href="#search">${search}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<ul data-role="listview" data-inset="true">
|
||||
</ul>
|
||||
</div>
|
||||
<div data-role="footer" data-theme="b" class="ui-bar" data-position="fixed">
|
||||
<div data-role="controlgroup" data-type="horizontal" style="float: left;">
|
||||
<a href="#" id="service-blank" data-role="button" data-icon="blank">${blank}</a>
|
||||
<a href="#" id="service-theme" data-role="button">${theme}</a>
|
||||
<a href="#" id="service-desktop" data-role="button">${desktop}</a>
|
||||
<a href="#" id="service-show" data-role="button" data-icon="unblank" data-iconpos="right">${show}</a>
|
||||
</div>
|
||||
<div data-role="controlgroup" data-type="horizontal" style="float: left;">
|
||||
<a href="#" id="service-previous" data-role="button" data-icon="arrow-l">${prev}</a>
|
||||
<a href="#" id="service-next" data-role="button" data-icon="arrow-r" data-iconpos="right">${next}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="page" id="slide-controller">
|
||||
<div data-role="header" data-position="fixed">
|
||||
<a href="#home" data-role="button" data-icon="home" data-iconpos="left">${home}</a>
|
||||
<h1>${slide_controller}</h1>
|
||||
<a href="#" id="controller-refresh" data-role="button" data-icon="refresh">${refresh}</a>
|
||||
<div data-role="navbar">
|
||||
<ul>
|
||||
<li><a href="#service-manager">${service}</a></li>
|
||||
<li><a href="#slide-controller" data-theme="e">${slides}</a></li>
|
||||
<li><a href="#alerts">${alerts}</a></li>
|
||||
<li><a href="#search">${search}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<ul data-role="listview" data-inset="true">
|
||||
</ul>
|
||||
</div>
|
||||
<div data-role="footer" data-theme="b" class="ui-bar" data-position="fixed">
|
||||
<div data-role="controlgroup" data-type="horizontal" style="float: left;">
|
||||
<a href="#" id="controller-blank" data-role="button" data-icon="blank">${blank}</a>
|
||||
<a href="#" id="controller-theme" data-role="button">${theme}</a>
|
||||
<a href="#" id="controller-desktop" data-role="button">${desktop}</a>
|
||||
<a href="#" id="controller-show" data-role="button" data-icon="unblank" data-iconpos="right">${show}</a>
|
||||
</div>
|
||||
<div data-role="controlgroup" data-type="horizontal" style="float: left;">
|
||||
<a href="#" id="controller-previous" data-role="button" data-icon="arrow-l">${prev}</a>
|
||||
<a href="#" id="controller-next" data-role="button" data-icon="arrow-r" data-iconpos="right">${next}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="page" id="alerts">
|
||||
<div data-role="header">
|
||||
<a href="#home" data-role="button" data-icon="home" data-iconpos="left">${home}</a>
|
||||
<h1>${alerts}</h1>
|
||||
<div data-role="navbar">
|
||||
<ul>
|
||||
<li><a href="#service-manager">${service}</a></li>
|
||||
<li><a href="#slide-controller">${slides}</a></li>
|
||||
<li><a href="#alerts" data-theme="e">${alerts}</a></li>
|
||||
<li><a href="#search">${search}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<div data-role="fieldcontain">
|
||||
<label for="alert-text">${text}:</label>
|
||||
<input type="text" name="alert-text" id="alert-text" value="" />
|
||||
</div>
|
||||
<a href="#" id="alert-submit" data-role="button">${show_alert}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="page" id="search">
|
||||
<div data-role="header" data-position="fixed">
|
||||
<a href="#home" data-role="button" data-icon="home" data-iconpos="left">${home}</a>
|
||||
<h1>${search}</h1>
|
||||
<div data-role="navbar">
|
||||
<ul>
|
||||
<li><a href="#service-manager">${service}</a></li>
|
||||
<li><a href="#slide-controller">${slides}</a></li>
|
||||
<li><a href="#alerts">${alerts}</a></li>
|
||||
<li><a href="#search" data-theme="e">${search}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<div data-role="fieldcontain">
|
||||
<label for="search-plugin">${search}:</label>
|
||||
<select name="search-plugin" id="search-plugin" data-native-menu="false"></select>
|
||||
</div>
|
||||
<div data-role="fieldcontain">
|
||||
<label for="search-text">${text}:</label>
|
||||
<input type="search" name="search-text" id="search-text" value="" />
|
||||
</div>
|
||||
<a href="#" id="search-submit" data-role="button">${search}</a>
|
||||
<ul data-role="listview" data-inset="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="page" id="options">
|
||||
<div data-role="header" data-position="inline" data-theme="b">
|
||||
<h1>${options}</h1>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<input type="hidden" id="selected-item" value="" />
|
||||
<a href="#" id="go-live" data-role="button">${go_live}</a>
|
||||
<a href="#" id="add-to-service" data-role="button">${add_to_service}</a>
|
||||
<a href="#" id="add-and-go-to-service" data-role="button">${add_and_go_to_service}</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,331 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
var lastChord;
|
||||
|
||||
var notesSharpNotation = {}
|
||||
var notesFlatNotation = {}
|
||||
|
||||
// See https://en.wikipedia.org/wiki/Musical_note#12-tone_chromatic_scale
|
||||
notesSharpNotation['german'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','H'];
|
||||
notesFlatNotation['german'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','B','H'];
|
||||
notesSharpNotation['english'] = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];
|
||||
notesFlatNotation['english'] = ['C','Db','D','Eb','Fb','F','Gb','G','Ab','A','Bb','B'];
|
||||
notesSharpNotation['neo-latin'] = ['Do','Do#','Re','Re#','Mi','Fa','Fa#','Sol','Sol#','La','La#','Si'];
|
||||
notesFlatNotation['neo-latin'] = ['Do','Reb','Re','Mib','Fab','Fa','Solb','Sol','Lab','La','Sib','Si'];
|
||||
|
||||
function getTransposeValue(songId) {
|
||||
if (localStorage.getItem(songId + '_transposeValue')) {return localStorage.getItem(songId + '_transposeValue');}
|
||||
else {return 0;}
|
||||
}
|
||||
|
||||
function storeTransposeValue(songId,transposeValueToSet) {
|
||||
localStorage.setItem(songId + '_transposeValue', transposeValueToSet);
|
||||
}
|
||||
|
||||
// NOTE: This function has a python equivalent in openlp/plugins/songs/lib/__init__.py - make sure to update both!
|
||||
function transposeChord(chord, transposeValue, notation) {
|
||||
var chordSplit = chord.replace('♭', 'b').split(/[\/]/);
|
||||
var transposedChord = '', note, notenumber, rest, currentChord;
|
||||
var notesSharp = notesSharpNotation[notation];
|
||||
var notesFlat = notesFlatNotation[notation];
|
||||
var notesPreferred = ['b','#','#','#','#','#','#','#','#','#','#','#'];
|
||||
for (i = 0; i <= chordSplit.length - 1; i++) {
|
||||
if (i > 0) {
|
||||
transposedChord += '/';
|
||||
}
|
||||
currentchord = chordSplit[i];
|
||||
if (currentchord.length > 0 && currentchord.charAt(0) === '(') {
|
||||
transposedChord += '(';
|
||||
if (currentchord.length > 1) {
|
||||
currentchord = currentchord.substr(1);
|
||||
} else {
|
||||
currentchord = "";
|
||||
}
|
||||
}
|
||||
if (currentchord.length > 0) {
|
||||
if (currentchord.length > 1) {
|
||||
if ('#b'.indexOf(currentchord.charAt(1)) === -1) {
|
||||
note = currentchord.substr(0, 1);
|
||||
rest = currentchord.substr(1);
|
||||
} else {
|
||||
note = currentchord.substr(0, 2);
|
||||
rest = currentchord.substr(2);
|
||||
}
|
||||
} else {
|
||||
note = currentchord;
|
||||
rest = "";
|
||||
}
|
||||
notenumber = (notesSharp.indexOf(note) === -1 ? notesFlat.indexOf(note) : notesSharp.indexOf(note));
|
||||
notenumber += parseInt(transposeValue);
|
||||
while (notenumber > 11) {
|
||||
notenumber -= 12;
|
||||
}
|
||||
while (notenumber < 0) {
|
||||
notenumber += 12;
|
||||
}
|
||||
if (i === 0) {
|
||||
currentChord = notesPreferred[notenumber] === '#' ? notesSharp[notenumber] : notesFlat[notenumber];
|
||||
lastChord = currentChord;
|
||||
} else {
|
||||
currentChord = notesSharp.indexOf(lastChord) === -1 ? notesFlat[notenumber] : notesSharp[notenumber];
|
||||
}
|
||||
if (!(notesFlat.indexOf(note) === -1 && notesSharp.indexOf(note) === -1)) {
|
||||
transposedChord += currentChord + rest;
|
||||
} else {
|
||||
transposedChord += note + rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
return transposedChord;
|
||||
}
|
||||
|
||||
var OpenLPChordOverflowFillCount = 0;
|
||||
window.OpenLP = {
|
||||
showchords:true,
|
||||
loadService: function (event) {
|
||||
$.getJSON(
|
||||
"/api/service/list",
|
||||
function (data, status) {
|
||||
OpenLP.nextSong = "";
|
||||
$("#notes").html("");
|
||||
for (idx in data.results.items) {
|
||||
idx = parseInt(idx, 10);
|
||||
if (data.results.items[idx]["selected"]) {
|
||||
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
|
||||
if (data.results.items.length > idx + 1) {
|
||||
OpenLP.nextSong = data.results.items[idx + 1]["title"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
OpenLP.updateSlide();
|
||||
}
|
||||
);
|
||||
},
|
||||
loadSlides: function (event) {
|
||||
$.getJSON(
|
||||
"/api/controller/live/text",
|
||||
function (data, status) {
|
||||
OpenLP.currentSlides = data.results.slides;
|
||||
$('#transposevalue').text(getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]));
|
||||
OpenLP.currentSlide = 0;
|
||||
OpenLP.currentTags = Array();
|
||||
var div = $("#verseorder");
|
||||
div.html("");
|
||||
var tag = "";
|
||||
var tags = 0;
|
||||
var lastChange = 0;
|
||||
$.each(data.results.slides, function(idx, slide) {
|
||||
var prevtag = tag;
|
||||
tag = slide["tag"];
|
||||
if (tag != prevtag) {
|
||||
// If the tag has changed, add new one to the list
|
||||
lastChange = idx;
|
||||
tags = tags + 1;
|
||||
div.append(" <span>");
|
||||
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||
}
|
||||
else {
|
||||
if ((slide["chords_text"] == data.results.slides[lastChange]["chords_text"]) &&
|
||||
(data.results.slides.length > idx + (idx - lastChange))) {
|
||||
// If the tag hasn't changed, check to see if the same verse
|
||||
// has been repeated consecutively. Note the verse may have been
|
||||
// split over several slides, so search through. If so, repeat the tag.
|
||||
var match = true;
|
||||
for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
|
||||
if(data.results.slides[lastChange + idx2]["chords_text"] != data.results.slides[idx + idx2]["chords_text"]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
lastChange = idx;
|
||||
tags = tags + 1;
|
||||
div.append(" <span>");
|
||||
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
OpenLP.currentTags[idx] = tags;
|
||||
if (slide["selected"])
|
||||
OpenLP.currentSlide = idx;
|
||||
})
|
||||
OpenLP.loadService();
|
||||
}
|
||||
);
|
||||
},
|
||||
updateSlide: function() {
|
||||
// Show the current slide on top. Any trailing slides for the same verse
|
||||
// are shown too underneath in grey.
|
||||
// Then leave a blank line between following verses
|
||||
var transposeValue = getTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0]);
|
||||
var chordclass=/class="[a-z\s]*chord[a-z\s]*"\s*style="display:\s?none"/g;
|
||||
var chordclassshow='class="chord"';
|
||||
var regchord=/<span class="chord"><span><strong>([\(\w#b♭\+\*\d/\)-]+)<\/strong><\/span><\/span>([\u0080-\uFFFF,\w]*)(<span class="ws">.+?<\/span>)?([\u0080-\uFFFF,\w,\s,\.,\,,\!,\?,\;,\:,\|,\",\',\-,\_]*)(<br>)?/g;
|
||||
// NOTE: There is equivalent python code in openlp/core/lib/__init__.py, in the expand_and_align_chords_in_line function. Make sure to update both!
|
||||
var replaceChords=function(mstr,$chord,$tail,$skips,$remainder,$end) {
|
||||
var w='';
|
||||
var $chordlen = 0;
|
||||
var $taillen = 0;
|
||||
var slimchars='fiíIÍjlĺľrtť.,;/ ()|"\'!:\\';
|
||||
// Transpose chord as dictated by the transpose value in local storage
|
||||
if (transposeValue != 0) {
|
||||
$chord = transposeChord($chord, transposeValue, OpenLP.chordNotation);
|
||||
}
|
||||
for (var i = 0; i < $chord.length; i++) if (slimchars.indexOf($chord.charAt(i)) === -1) {$chordlen += 2;} else {$chordlen += 1;}
|
||||
for (var i = 0; i < $tail.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||
for (var i = 0; i < $remainder.length; i++) if (slimchars.indexOf($tail.charAt(i)) === -1) {$taillen += 2;} else {$taillen += 1;}
|
||||
if ($chordlen >= $taillen && !$end) {
|
||||
if ($tail.length){
|
||||
if (!$remainder.length) {
|
||||
for (c = 0; c < Math.ceil(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||
} else {
|
||||
for (c = 0; c < $chordlen - $taillen + 2; c++) {w += ' ';}
|
||||
}
|
||||
} else {
|
||||
if (!$remainder.length) {
|
||||
for (c = 0; c < Math.floor(($chordlen - $taillen) / 2) + 1; c++) {w += '_';}
|
||||
} else {
|
||||
for (c = 0; c < $chordlen - $taillen + 1; c++) {w += ' ';}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += ' ';}}
|
||||
}
|
||||
if (w!='') {
|
||||
if (w[0] == '_') {
|
||||
ws_length = w.length;
|
||||
if (ws_length==1) {
|
||||
w = '–';
|
||||
} else {
|
||||
wsl_mod = Math.floor(ws_length / 2);
|
||||
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
|
||||
w = ws_left + '–' + ws_right;
|
||||
}
|
||||
}
|
||||
w='<span class="ws">' + w + '</span>';
|
||||
}
|
||||
return $.grep(['<span class="chord"><span><strong>', $chord, '</strong></span></span>', $tail, w, $remainder, $end], Boolean).join('');
|
||||
};
|
||||
$("#verseorder span").removeClass("currenttag");
|
||||
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
|
||||
var text = "";
|
||||
// use title if available
|
||||
if (slide["title"]) {
|
||||
text = slide["title"];
|
||||
} else {
|
||||
text = slide["chords_text"];
|
||||
if(OpenLP.showchords) {
|
||||
text = text.replace(chordclass,chordclassshow);
|
||||
text = text.replace(regchord, replaceChords);
|
||||
}
|
||||
}
|
||||
// use thumbnail if available
|
||||
if (slide["img"]) {
|
||||
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
|
||||
}
|
||||
// use notes if available
|
||||
if (slide["slide_notes"]) {
|
||||
text += '<br />' + slide["slide_notes"];
|
||||
}
|
||||
text = text.replace(/\n/g, "<br />");
|
||||
$("#currentslide").html(text);
|
||||
text = "";
|
||||
if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
|
||||
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
|
||||
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||
text = text + "<p class=\"nextslide\">";
|
||||
if (OpenLP.currentSlides[idx]["title"]) {
|
||||
text = text + OpenLP.currentSlides[idx]["title"];
|
||||
} else {
|
||||
text = text + OpenLP.currentSlides[idx]["chords_text"];
|
||||
if(OpenLP.showchords) {
|
||||
text = text.replace(chordclass,chordclassshow);
|
||||
text = text.replace(regchord, replaceChords);
|
||||
}
|
||||
}
|
||||
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||
text = text + "</p>";
|
||||
else
|
||||
text = text + "<br />";
|
||||
}
|
||||
text = text.replace(/\n/g, "<br />");
|
||||
$("#nextslide").html(text);
|
||||
}
|
||||
else {
|
||||
text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
|
||||
$("#nextslide").html(text);
|
||||
}
|
||||
if(!OpenLP.showchords) {
|
||||
$(".chordline").toggleClass('chordline1');
|
||||
$(".chord").toggle();
|
||||
$(".ws").toggle();
|
||||
}
|
||||
},
|
||||
updateClock: function(data) {
|
||||
var div = $("#clock");
|
||||
var t = new Date();
|
||||
var h = t.getHours();
|
||||
if (data.results.twelve && h > 12)
|
||||
h = h - 12;
|
||||
if (h < 10) h = '0' + h + '';
|
||||
var m = t.getMinutes();
|
||||
if (m < 10)
|
||||
m = '0' + m + '';
|
||||
div.html(h + ":" + m);
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/api/poll",
|
||||
function (data, status) {
|
||||
OpenLP.updateClock(data);
|
||||
OpenLP.chordNotation = data.results.chordNotation;
|
||||
if (OpenLP.currentItem != data.results.item || OpenLP.currentService != data.results.service) {
|
||||
OpenLP.currentItem = data.results.item;
|
||||
OpenLP.currentService = data.results.service;
|
||||
OpenLP.loadSlides();
|
||||
}
|
||||
else if (OpenLP.currentSlide != data.results.slide) {
|
||||
OpenLP.currentSlide = parseInt(data.results.slide, 10);
|
||||
OpenLP.updateSlide();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
$.ajaxSetup({ cache: false });
|
||||
setInterval("OpenLP.pollServer();", 500);
|
||||
OpenLP.pollServer();
|
||||
$(document).ready(function() {
|
||||
$('#transposeup').click(function(e) {
|
||||
$('#transposevalue').text(parseInt($('#transposevalue').text()) + 1);
|
||||
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||
OpenLP.loadSlides();
|
||||
});
|
||||
$('#transposedown').click(function(e) {
|
||||
$('#transposevalue').text(parseInt($('#transposevalue').text()) - 1);
|
||||
storeTransposeValue(OpenLP.currentSlides[0].text.split("\n")[0], $('#transposevalue').text());
|
||||
OpenLP.loadSlides();
|
||||
});
|
||||
$('#chords').click(function () {
|
||||
OpenLP.showchords = OpenLP.showchords ? false : true;
|
||||
OpenLP.loadSlides();
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
window.OpenLP = {
|
||||
loadSlide: function (event) {
|
||||
$.getJSON(
|
||||
"/main/image",
|
||||
function (data, status) {
|
||||
var img = document.getElementById('image');
|
||||
img.src = data.results.slide_image;
|
||||
img.style.display = 'block';
|
||||
}
|
||||
);
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/main/poll",
|
||||
function (data, status) {
|
||||
if (OpenLP.slideCount != data.results.slide_count) {
|
||||
OpenLP.slideCount = data.results.slide_count;
|
||||
OpenLP.loadSlide();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
$.ajaxSetup({ cache: false });
|
||||
setInterval("OpenLP.pollServer();", 500);
|
||||
OpenLP.pollServer();
|
||||
|
@ -1,384 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
|
||||
window.OpenLP = {
|
||||
getElement: function(event) {
|
||||
var targ;
|
||||
if (!event) {
|
||||
var event = window.event;
|
||||
}
|
||||
if (event.target) {
|
||||
targ = event.target;
|
||||
}
|
||||
else if (event.srcElement) {
|
||||
targ = event.srcElement;
|
||||
}
|
||||
if (targ.nodeType == 3) {
|
||||
// defeat Safari bug
|
||||
targ = targ.parentNode;
|
||||
}
|
||||
var isSecure = false;
|
||||
var isAuthorised = false;
|
||||
return $(targ);
|
||||
},
|
||||
getSearchablePlugins: function () {
|
||||
$.getJSON(
|
||||
"/api/plugin/search",
|
||||
function (data, status) {
|
||||
var select = $("#search-plugin");
|
||||
select.html("");
|
||||
$.each(data.results.items, function (idx, value) {
|
||||
select.append("<option value='" + value[0] + "'>" + value[1] + "</option>");
|
||||
});
|
||||
select.selectmenu("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
loadService: function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
$.getJSON(
|
||||
"/api/service/list",
|
||||
function (data, status) {
|
||||
var ul = $("#service-manager > div[data-role=content] > ul[data-role=listview]");
|
||||
ul.html("");
|
||||
$.each(data.results.items, function (idx, value) {
|
||||
var text = value["title"];
|
||||
if (value["notes"]) {
|
||||
text += ' - ' + value["notes"];
|
||||
}
|
||||
var li = $("<li data-icon=\"false\">").append(
|
||||
$("<a href=\"#\">").attr("value", parseInt(idx, 10)).text(text));
|
||||
li.attr("uuid", value["id"])
|
||||
li.children("a").click(OpenLP.setItem);
|
||||
ul.append(li);
|
||||
});
|
||||
ul.listview("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
loadController: function (event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
$.getJSON(
|
||||
"/api/controller/live/text",
|
||||
function (data, status) {
|
||||
var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]");
|
||||
ul.html("");
|
||||
for (idx in data.results.slides) {
|
||||
var indexInt = parseInt(idx,10);
|
||||
var slide = data.results.slides[idx];
|
||||
var text = slide["tag"];
|
||||
if (text != "") {
|
||||
text = text + ": ";
|
||||
}
|
||||
if (slide["title"]) {
|
||||
text += slide["title"]
|
||||
} else {
|
||||
text += slide["text"];
|
||||
}
|
||||
if (slide["slide_notes"]) {
|
||||
text += ("<div style='font-size:smaller;font-weight:normal'>" + slide["slide_notes"] + "</div>");
|
||||
}
|
||||
text = text.replace(/\n/g, '<br />');
|
||||
if (slide["img"]) {
|
||||
text += "<img src='" + slide["img"].replace("/thumbnails/", "/thumbnails88x88/") + "'>";
|
||||
}
|
||||
var li = $("<li data-icon=\"false\">").append($("<a href=\"#\">").html(text));
|
||||
if (slide["selected"]) {
|
||||
li.attr("data-theme", "e");
|
||||
}
|
||||
li.children("a").click(OpenLP.setSlide);
|
||||
li.find("*").attr("value", indexInt );
|
||||
ul.append(li);
|
||||
}
|
||||
OpenLP.currentItem = data.results.item;
|
||||
ul.listview("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
setItem: function (event) {
|
||||
event.preventDefault();
|
||||
var item = OpenLP.getElement(event);
|
||||
var id = item.attr("value");
|
||||
if (typeof id !== "number") {
|
||||
id = "\"" + id + "\"";
|
||||
}
|
||||
var text = "{\"request\": {\"id\": " + id + "}}";
|
||||
$.getJSON(
|
||||
"/api/service/set",
|
||||
{"data": text},
|
||||
function (data, status) {
|
||||
$.mobile.changePage("#slide-controller");
|
||||
$("#service-manager > div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
|
||||
while (item[0].tagName != "LI") {
|
||||
item = item.parent();
|
||||
}
|
||||
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
|
||||
$("#service-manager > div[data-role=content] ul[data-role=listview]").listview("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
setSlide: function (event) {
|
||||
event.preventDefault();
|
||||
var slide = OpenLP.getElement(event);
|
||||
var id = slide.attr("value");
|
||||
if (typeof id !== "number") {
|
||||
id = "\"" + id + "\"";
|
||||
}
|
||||
var text = "{\"request\": {\"id\": " + id + "}}";
|
||||
$.getJSON(
|
||||
"/api/controller/live/set",
|
||||
{"data": text},
|
||||
function (data, status) {
|
||||
$("#slide-controller div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
|
||||
while (slide[0].tagName != "LI") {
|
||||
slide = slide.parent();
|
||||
}
|
||||
slide.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
|
||||
$("#slide-controller div[data-role=content] ul[data-role=listview]").listview("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/api/poll",
|
||||
function (data, status) {
|
||||
var prevItem = OpenLP.currentItem;
|
||||
OpenLP.currentSlide = data.results.slide;
|
||||
OpenLP.currentItem = data.results.item;
|
||||
OpenLP.isSecure = data.results.isSecure;
|
||||
OpenLP.isAuthorised = data.results.isAuthorised;
|
||||
if ($("#service-manager").is(":visible")) {
|
||||
if (OpenLP.currentService != data.results.service) {
|
||||
OpenLP.currentService = data.results.service;
|
||||
OpenLP.loadService();
|
||||
}
|
||||
$("#service-manager div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
|
||||
$("#service-manager div[data-role=content] ul[data-role=listview] li a").each(function () {
|
||||
var item = $(this);
|
||||
while (item[0].tagName != "LI") {
|
||||
item = item.parent();
|
||||
}
|
||||
if (item.attr("uuid") == OpenLP.currentItem) {
|
||||
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
$("#service-manager div[data-role=content] ul[data-role=listview]").listview("refresh");
|
||||
}
|
||||
if ($("#slide-controller").is(":visible")) {
|
||||
if (prevItem != OpenLP.currentItem) {
|
||||
OpenLP.loadController();
|
||||
return;
|
||||
}
|
||||
var idx = 0;
|
||||
$("#slide-controller div[data-role=content] ul[data-role=listview] li").attr("data-theme", "c").removeClass("ui-btn-up-e").addClass("ui-btn-up-c");
|
||||
$("#slide-controller div[data-role=content] ul[data-role=listview] li a").each(function () {
|
||||
var item = $(this);
|
||||
if (idx == OpenLP.currentSlide) {
|
||||
while (item[0].tagName != "LI") {
|
||||
item = item.parent();
|
||||
}
|
||||
item.attr("data-theme", "e").removeClass("ui-btn-up-c").addClass("ui-btn-up-e");
|
||||
return false;
|
||||
}
|
||||
idx++;
|
||||
});
|
||||
$("#slide-controller div[data-role=content] ul[data-role=listview]").listview("refresh");
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
nextItem: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/service/next");
|
||||
},
|
||||
previousItem: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/service/previous");
|
||||
},
|
||||
nextSlide: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/controller/live/next");
|
||||
},
|
||||
previousSlide: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/controller/live/previous");
|
||||
},
|
||||
blankDisplay: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/display/blank");
|
||||
},
|
||||
themeDisplay: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/display/theme");
|
||||
},
|
||||
desktopDisplay: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/display/desktop");
|
||||
},
|
||||
showDisplay: function (event) {
|
||||
event.preventDefault();
|
||||
$.getJSON("/api/display/show");
|
||||
},
|
||||
showAlert: function (event) {
|
||||
event.preventDefault();
|
||||
var alert = OpenLP.escapeString($("#alert-text").val())
|
||||
var text = "{\"request\": {\"text\": \"" + alert + "\"}}";
|
||||
$.getJSON(
|
||||
"/api/alert",
|
||||
{"data": text},
|
||||
function () {
|
||||
$("#alert-text").val("");
|
||||
}
|
||||
);
|
||||
},
|
||||
search: function (event) {
|
||||
event.preventDefault();
|
||||
var query = OpenLP.escapeString($("#search-text").val())
|
||||
var text = "{\"request\": {\"text\": \"" + query + "\"}}";
|
||||
$.getJSON(
|
||||
"/api/" + $("#search-plugin").val() + "/search",
|
||||
{"data": text},
|
||||
function (data, status) {
|
||||
var ul = $("#search > div[data-role=content] > ul[data-role=listview]");
|
||||
ul.html("");
|
||||
if (data.results.items.length == 0) {
|
||||
var li = $("<li data-icon=\"false\">").text(translationStrings["no_results"]);
|
||||
ul.append(li);
|
||||
}
|
||||
else {
|
||||
$.each(data.results.items, function (idx, value) {
|
||||
if (typeof value[0] !== "number"){
|
||||
value[0] = OpenLP.escapeString(value[0])
|
||||
}
|
||||
var txt = "";
|
||||
if (value.length > 2) {
|
||||
txt = value[1] + " ( " + value[2] + " )";
|
||||
} else {
|
||||
txt = value[1];
|
||||
}
|
||||
ul.append($("<li>").append($("<a>").attr("href", "#options")
|
||||
.attr("data-rel", "dialog").attr("value", value[0])
|
||||
.click(OpenLP.showOptions).text(txt)));
|
||||
});
|
||||
}
|
||||
ul.listview("refresh");
|
||||
}
|
||||
);
|
||||
},
|
||||
showOptions: function (event) {
|
||||
event.preventDefault();
|
||||
var element = OpenLP.getElement(event);
|
||||
$("#selected-item").val(element.attr("value"));
|
||||
},
|
||||
goLive: function (event) {
|
||||
event.preventDefault();
|
||||
var id = $("#selected-item").val();
|
||||
if (typeof id !== "number") {
|
||||
id = "\"" + id + "\"";
|
||||
}
|
||||
var text = "{\"request\": {\"id\": " + id + "}}";
|
||||
$.getJSON(
|
||||
"/api/" + $("#search-plugin").val() + "/live",
|
||||
{"data": text}
|
||||
);
|
||||
$.mobile.changePage("#slide-controller");
|
||||
},
|
||||
addToService: function (event) {
|
||||
event.preventDefault();
|
||||
var id = $("#selected-item").val();
|
||||
if (typeof id !== "number") {
|
||||
id = "\"" + id + "\"";
|
||||
}
|
||||
var text = "{\"request\": {\"id\": " + id + "}}";
|
||||
$.getJSON(
|
||||
"/api/" + $("#search-plugin").val() + "/add",
|
||||
{"data": text},
|
||||
function () {
|
||||
$("#options").dialog("close");
|
||||
}
|
||||
);
|
||||
},
|
||||
addAndGoToService: function (event) {
|
||||
event.preventDefault();
|
||||
var id = $("#selected-item").val();
|
||||
if (typeof id !== "number") {
|
||||
id = "\"" + id + "\"";
|
||||
}
|
||||
var text = "{\"request\": {\"id\": " + id + "}}";
|
||||
$.getJSON(
|
||||
"/api/" + $("#search-plugin").val() + "/add",
|
||||
{"data": text},
|
||||
function () {
|
||||
//$("#options").dialog("close");
|
||||
$.mobile.changePage("#service-manager");
|
||||
}
|
||||
);
|
||||
},
|
||||
escapeString: function (string) {
|
||||
return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")
|
||||
}
|
||||
}
|
||||
// Initial jQueryMobile options
|
||||
$(document).bind("mobileinit", function(){
|
||||
$.mobile.defaultDialogTransition = "none";
|
||||
$.mobile.defaultPageTransition = "none";
|
||||
});
|
||||
// Service Manager
|
||||
$("#service-manager").live("pagebeforeshow", OpenLP.loadService);
|
||||
$("#service-refresh").live("click", OpenLP.loadService);
|
||||
$("#service-next").live("click", OpenLP.nextItem);
|
||||
$("#service-previous").live("click", OpenLP.previousItem);
|
||||
$("#service-blank").live("click", OpenLP.blankDisplay);
|
||||
$("#service-theme").live("click", OpenLP.themeDisplay);
|
||||
$("#service-desktop").live("click", OpenLP.desktopDisplay);
|
||||
$("#service-show").live("click", OpenLP.showDisplay);
|
||||
// Slide Controller
|
||||
$("#slide-controller").live("pagebeforeshow", OpenLP.loadController);
|
||||
$("#controller-refresh").live("click", OpenLP.loadController);
|
||||
$("#controller-next").live("click", OpenLP.nextSlide);
|
||||
$("#controller-previous").live("click", OpenLP.previousSlide);
|
||||
$("#controller-blank").live("click", OpenLP.blankDisplay);
|
||||
$("#controller-theme").live("click", OpenLP.themeDisplay);
|
||||
$("#controller-desktop").live("click", OpenLP.desktopDisplay);
|
||||
$("#controller-show").live("click", OpenLP.showDisplay);
|
||||
// Alerts
|
||||
$("#alert-submit").live("click", OpenLP.showAlert);
|
||||
// Search
|
||||
$("#search-submit").live("click", OpenLP.search);
|
||||
$("#search-text").live("keypress", function(event) {
|
||||
if (event.which == 13)
|
||||
{
|
||||
OpenLP.search(event);
|
||||
}
|
||||
});
|
||||
$("#go-live").live("click", OpenLP.goLive);
|
||||
$("#add-to-service").live("click", OpenLP.addToService);
|
||||
$("#add-and-go-to-service").live("click", OpenLP.addAndGoToService);
|
||||
// Poll the server twice a second to get any updates.
|
||||
$.ajaxSetup({cache: false});
|
||||
$("#search").live("pageinit", function (event) {
|
||||
OpenLP.getSearchablePlugins();
|
||||
});
|
||||
setInterval("OpenLP.pollServer();", 500);
|
||||
OpenLP.pollServer();
|
@ -1,170 +0,0 @@
|
||||
/******************************************************************************
|
||||
* OpenLP - Open Source Lyrics Projection *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* Copyright (c) 2008-2017 OpenLP Developers *
|
||||
* --------------------------------------------------------------------------- *
|
||||
* This program is free software; you can redistribute it and/or modify it *
|
||||
* under the terms of the GNU General Public License as published by the Free *
|
||||
* Software Foundation; version 2 of the License. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT *
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
|
||||
* more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License along *
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
|
||||
******************************************************************************/
|
||||
window.OpenLP = {
|
||||
loadService: function (event) {
|
||||
$.getJSON(
|
||||
"/api/service/list",
|
||||
function (data, status) {
|
||||
OpenLP.nextSong = "";
|
||||
$("#notes").html("");
|
||||
for (idx in data.results.items) {
|
||||
idx = parseInt(idx, 10);
|
||||
if (data.results.items[idx]["selected"]) {
|
||||
$("#notes").html(data.results.items[idx]["notes"].replace(/\n/g, "<br />"));
|
||||
if (data.results.items.length > idx + 1) {
|
||||
OpenLP.nextSong = data.results.items[idx + 1]["title"];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
OpenLP.updateSlide();
|
||||
}
|
||||
);
|
||||
},
|
||||
loadSlides: function (event) {
|
||||
$.getJSON(
|
||||
"/api/controller/live/text",
|
||||
function (data, status) {
|
||||
OpenLP.currentSlides = data.results.slides;
|
||||
OpenLP.currentSlide = 0;
|
||||
OpenLP.currentTags = Array();
|
||||
var div = $("#verseorder");
|
||||
div.html("");
|
||||
var tag = "";
|
||||
var tags = 0;
|
||||
var lastChange = 0;
|
||||
$.each(data.results.slides, function(idx, slide) {
|
||||
var prevtag = tag;
|
||||
tag = slide["tag"];
|
||||
if (tag != prevtag) {
|
||||
// If the tag has changed, add new one to the list
|
||||
lastChange = idx;
|
||||
tags = tags + 1;
|
||||
div.append(" <span>");
|
||||
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||
}
|
||||
else {
|
||||
if ((slide["text"] == data.results.slides[lastChange]["text"]) &&
|
||||
(data.results.slides.length >= idx + (idx - lastChange))) {
|
||||
// If the tag hasn't changed, check to see if the same verse
|
||||
// has been repeated consecutively. Note the verse may have been
|
||||
// split over several slides, so search through. If so, repeat the tag.
|
||||
var match = true;
|
||||
for (var idx2 = 0; idx2 < idx - lastChange; idx2++) {
|
||||
if(data.results.slides[lastChange + idx2]["text"] != data.results.slides[idx + idx2]["text"]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
lastChange = idx;
|
||||
tags = tags + 1;
|
||||
div.append(" <span>");
|
||||
$("#verseorder span").last().attr("id", "tag" + tags).text(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
OpenLP.currentTags[idx] = tags;
|
||||
if (slide["selected"])
|
||||
OpenLP.currentSlide = idx;
|
||||
})
|
||||
OpenLP.loadService();
|
||||
}
|
||||
);
|
||||
},
|
||||
updateSlide: function() {
|
||||
// Show the current slide on top. Any trailing slides for the same verse
|
||||
// are shown too underneath in grey.
|
||||
// Then leave a blank line between following verses
|
||||
$("#verseorder span").removeClass("currenttag");
|
||||
$("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag");
|
||||
var slide = OpenLP.currentSlides[OpenLP.currentSlide];
|
||||
var text = "";
|
||||
// use title if available
|
||||
if (slide["title"]) {
|
||||
text = slide["title"];
|
||||
} else {
|
||||
text = slide["text"];
|
||||
}
|
||||
// use thumbnail if available
|
||||
if (slide["img"]) {
|
||||
text += "<br /><img src='" + slide["img"].replace("/thumbnails/", "/thumbnails320x240/") + "'><br />";
|
||||
}
|
||||
// use notes if available
|
||||
if (slide["slide_notes"]) {
|
||||
text += '<br />' + slide["slide_notes"];
|
||||
}
|
||||
text = text.replace(/\n/g, "<br />");
|
||||
$("#currentslide").html(text);
|
||||
text = "";
|
||||
if (OpenLP.currentSlide < OpenLP.currentSlides.length - 1) {
|
||||
for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) {
|
||||
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||
text = text + "<p class=\"nextslide\">";
|
||||
if (OpenLP.currentSlides[idx]["title"]) {
|
||||
text = text + OpenLP.currentSlides[idx]["title"];
|
||||
} else {
|
||||
text = text + OpenLP.currentSlides[idx]["text"];
|
||||
}
|
||||
if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1])
|
||||
text = text + "</p>";
|
||||
else
|
||||
text = text + "<br />";
|
||||
}
|
||||
text = text.replace(/\n/g, "<br />");
|
||||
$("#nextslide").html(text);
|
||||
}
|
||||
else {
|
||||
text = "<p class=\"nextslide\">" + $("#next-text").val() + ": " + OpenLP.nextSong + "</p>";
|
||||
$("#nextslide").html(text);
|
||||
}
|
||||
},
|
||||
updateClock: function(data) {
|
||||
var div = $("#clock");
|
||||
var t = new Date();
|
||||
var h = t.getHours();
|
||||
if (data.results.twelve && h > 12)
|
||||
h = h - 12;
|
||||
var m = t.getMinutes();
|
||||
if (m < 10)
|
||||
m = '0' + m + '';
|
||||
div.html(h + ":" + m);
|
||||
},
|
||||
pollServer: function () {
|
||||
$.getJSON(
|
||||
"/api/poll",
|
||||
function (data, status) {
|
||||
OpenLP.updateClock(data);
|
||||
if (OpenLP.currentItem != data.results.item ||
|
||||
OpenLP.currentService != data.results.service) {
|
||||
OpenLP.currentItem = data.results.item;
|
||||
OpenLP.currentService = data.results.service;
|
||||
OpenLP.loadSlides();
|
||||
}
|
||||
else if (OpenLP.currentSlide != data.results.slide) {
|
||||
OpenLP.currentSlide = parseInt(data.results.slide, 10);
|
||||
OpenLP.updateSlide();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
$.ajaxSetup({ cache: false });
|
||||
setInterval("OpenLP.pollServer();", 500);
|
||||
OpenLP.pollServer();
|
@ -1,709 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
The :mod:`http` module contains the API web server. This is a lightweight web
|
||||
server used by remotes to interact with OpenLP. It uses JSON to communicate with
|
||||
the remotes.
|
||||
|
||||
*Routes:*
|
||||
|
||||
``/``
|
||||
Go to the web interface.
|
||||
|
||||
``/stage``
|
||||
Show the stage view.
|
||||
|
||||
``/files/{filename}``
|
||||
Serve a static file.
|
||||
|
||||
``/stage/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/display/{hide|show}``
|
||||
Blank or unblank the screen.
|
||||
|
||||
``/api/alert``
|
||||
Sends an alert message to the alerts plugin. This method expects a
|
||||
JSON-encoded dict like this::
|
||||
|
||||
{"request": {"text": "<your alert text>"}}
|
||||
|
||||
``/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.
|
||||
|
||||
``set``
|
||||
Set a specific slide. Requires an id return in a JSON-encoded dict like
|
||||
this::
|
||||
|
||||
{"request": {"id": 1}}
|
||||
|
||||
``first``
|
||||
Load the first slide.
|
||||
|
||||
``last``
|
||||
Load the last slide.
|
||||
|
||||
``text``
|
||||
Fetches the text of the current song. The output is a JSON-encoded
|
||||
dict which looks like this::
|
||||
|
||||
{"result": {"slides": ["...", "..."]}}
|
||||
|
||||
``/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.
|
||||
|
||||
``set``
|
||||
Set a specific item in the service. Requires an id returned in a
|
||||
JSON-encoded dict like this::
|
||||
|
||||
{"request": {"id": 1}}
|
||||
|
||||
``list``
|
||||
Request a list of items in the service. Returns a list of items in the
|
||||
current service in a JSON-encoded dict like this::
|
||||
|
||||
{"results": {"items": [{...}, {...}]}}
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from mako.template import Template
|
||||
|
||||
from openlp.core.common import RegistryProperties, AppLocation, Settings, translate, UiStrings
|
||||
from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities, create_thumb
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
FILE_TYPES = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.png': 'image/png'
|
||||
}
|
||||
|
||||
|
||||
class HttpRouter(RegistryProperties):
|
||||
"""
|
||||
This code is called by the HttpServer upon a request and it processes it based on the routing table.
|
||||
This code is stateless and is created on each request.
|
||||
Some variables may look incorrect but this extends BaseHTTPRequestHandler.
|
||||
"""
|
||||
def initialise(self):
|
||||
"""
|
||||
Initialise the router stack and any other variables.
|
||||
"""
|
||||
auth_code = "{user}:{password}".format(user=Settings().value('remotes/user id'),
|
||||
password=Settings().value('remotes/password'))
|
||||
try:
|
||||
self.auth = base64.b64encode(auth_code)
|
||||
except TypeError:
|
||||
self.auth = base64.b64encode(auth_code.encode()).decode()
|
||||
self.default_route = {'function': self.serve_file, 'secure': False}
|
||||
self.routes = [
|
||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||
('^/(chords)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||
(r'^/api/poll$', {'function': self.poll, 'secure': False}),
|
||||
(r'^/main/poll$', {'function': self.main_poll, 'secure': False}),
|
||||
(r'^/main/image$', {'function': self.main_image, 'secure': False}),
|
||||
(r'^/api/controller/(live|preview)/text$', {'function': self.controller_text, 'secure': False}),
|
||||
(r'^/api/controller/(live|preview)/(.*)$', {'function': self.controller, 'secure': True}),
|
||||
(r'^/api/service/list$', {'function': self.service_list, 'secure': False}),
|
||||
(r'^/api/service/(.*)$', {'function': self.service, 'secure': True}),
|
||||
(r'^/api/display/(hide|show|blank|theme|desktop)$', {'function': self.display, 'secure': True}),
|
||||
(r'^/api/alert$', {'function': self.alert, 'secure': True}),
|
||||
(r'^/api/plugin/(search)$', {'function': self.plugin_info, 'secure': False}),
|
||||
(r'^/api/(.*)/search$', {'function': self.search, 'secure': False}),
|
||||
(r'^/api/(.*)/live$', {'function': self.go_live, 'secure': True}),
|
||||
(r'^/api/(.*)/add$', {'function': self.add_to_service, 'secure': True})
|
||||
]
|
||||
self.settings_section = 'remotes'
|
||||
self.translate()
|
||||
self.html_dir = os.path.join(str(AppLocation.get_directory(AppLocation.PluginsDir)), 'remotes', 'html')
|
||||
self.config_dir = os.path.join(str(AppLocation.get_data_path()), 'stages')
|
||||
|
||||
def do_post_processor(self):
|
||||
"""
|
||||
Handle the POST amd GET requests placed on the server.
|
||||
"""
|
||||
if self.path == '/favicon.ico':
|
||||
return
|
||||
if not hasattr(self, 'auth'):
|
||||
self.initialise()
|
||||
function, args = self.process_http_request(self.path)
|
||||
if not function:
|
||||
self.do_http_error()
|
||||
return
|
||||
self.authorised = self.headers['Authorization'] is None
|
||||
if function['secure'] and Settings().value(self.settings_section + '/authentication enabled'):
|
||||
if self.headers['Authorization'] is None:
|
||||
self.do_authorisation()
|
||||
self.wfile.write(bytes('no auth header received', 'UTF-8'))
|
||||
elif self.headers['Authorization'] == 'Basic {auth}'.format(auth=self.auth):
|
||||
self.do_http_success()
|
||||
self.call_function(function, *args)
|
||||
else:
|
||||
self.do_authorisation()
|
||||
self.wfile.write(bytes(self.headers['Authorization'], 'UTF-8'))
|
||||
self.wfile.write(bytes(' not authenticated', 'UTF-8'))
|
||||
else:
|
||||
self.call_function(function, *args)
|
||||
|
||||
def call_function(self, function, *args):
|
||||
"""
|
||||
Invoke the route function passing the relevant values
|
||||
|
||||
:param function: The function to be called.
|
||||
:param args: Any passed data.
|
||||
"""
|
||||
response = function['function'](*args)
|
||||
if response:
|
||||
self.wfile.write(response)
|
||||
return
|
||||
|
||||
def process_http_request(self, url_path, *args):
|
||||
"""
|
||||
Common function to process HTTP requests
|
||||
|
||||
:param url_path: The requested URL.
|
||||
:param args: Any passed data.
|
||||
"""
|
||||
self.request_data = None
|
||||
url_path_split = urlparse(url_path)
|
||||
url_query = parse_qs(url_path_split.query)
|
||||
# Get data from HTTP request
|
||||
if self.command == 'GET':
|
||||
if 'data' in url_query.keys():
|
||||
self.request_data = url_query['data'][0]
|
||||
elif self.command == 'POST':
|
||||
content_len = int(self.headers['content-length'])
|
||||
self.request_data = self.rfile.read(content_len).decode("utf-8")
|
||||
for route, func in self.routes:
|
||||
match = re.match(route, url_path_split.path)
|
||||
if match:
|
||||
log.debug('Route "{route}" matched "{path}"'.format(route=route, path=url_path))
|
||||
args = []
|
||||
for param in match.groups():
|
||||
args.append(param)
|
||||
return func, args
|
||||
return self.default_route, [url_path_split.path]
|
||||
|
||||
def set_cache_headers(self):
|
||||
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
self.send_header("Pragma", "no-cache")
|
||||
self.send_header("Expires", "0")
|
||||
|
||||
def do_http_success(self):
|
||||
"""
|
||||
Create a success http header.
|
||||
"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.set_cache_headers()
|
||||
self.end_headers()
|
||||
|
||||
def do_json_header(self):
|
||||
"""
|
||||
Create a header for JSON messages
|
||||
"""
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
self.set_cache_headers()
|
||||
self.end_headers()
|
||||
|
||||
def do_http_error(self):
|
||||
"""
|
||||
Create a error http header.
|
||||
"""
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.set_cache_headers()
|
||||
self.end_headers()
|
||||
|
||||
def do_authorisation(self):
|
||||
"""
|
||||
Create a needs authorisation http header.
|
||||
"""
|
||||
self.send_response(401)
|
||||
header = 'Basic realm=\"{}\"'.format(UiStrings().OpenLP)
|
||||
self.send_header('WWW-Authenticate', header)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.set_cache_headers()
|
||||
self.end_headers()
|
||||
|
||||
def do_not_found(self):
|
||||
"""
|
||||
Create a not found http header.
|
||||
"""
|
||||
self.send_response(404)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.set_cache_headers()
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes('<html><body>Sorry, an error occurred </body></html>', 'UTF-8'))
|
||||
|
||||
def _get_service_items(self):
|
||||
"""
|
||||
Read the service item in use and return the data as a json object
|
||||
"""
|
||||
service_items = []
|
||||
if self.live_controller.service_item:
|
||||
current_unique_identifier = self.live_controller.service_item.unique_identifier
|
||||
else:
|
||||
current_unique_identifier = None
|
||||
for item in self.service_manager.service_items:
|
||||
service_item = item['service_item']
|
||||
service_items.append({
|
||||
'id': str(service_item.unique_identifier),
|
||||
'title': str(service_item.get_display_title()),
|
||||
'plugin': str(service_item.name),
|
||||
'notes': str(service_item.notes),
|
||||
'selected': (service_item.unique_identifier == current_unique_identifier)
|
||||
})
|
||||
return service_items
|
||||
|
||||
def translate(self):
|
||||
"""
|
||||
Translate various strings in the mobile app.
|
||||
"""
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
chords = translate('RemotePlugin.Mobile', 'Chords View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
self.template_vars = {
|
||||
'app_title': "{main} {remote}".format(main=UiStrings().OpenLP, remote=remote),
|
||||
'stage_title': "{main} {stage}".format(main=UiStrings().OpenLP, stage=stage),
|
||||
'chords_title': "{main} {chords}".format(main=UiStrings().OpenLP, chords=chords),
|
||||
'live_title': "{main} {live}".format(main=UiStrings().OpenLP, live=live),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
||||
'search': translate('RemotePlugin.Mobile', 'Search'),
|
||||
'home': translate('RemotePlugin.Mobile', 'Home'),
|
||||
'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
|
||||
'blank': translate('RemotePlugin.Mobile', 'Blank'),
|
||||
'theme': translate('RemotePlugin.Mobile', 'Theme'),
|
||||
'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
|
||||
'show': translate('RemotePlugin.Mobile', 'Show'),
|
||||
'prev': translate('RemotePlugin.Mobile', 'Prev'),
|
||||
'next': translate('RemotePlugin.Mobile', 'Next'),
|
||||
'text': translate('RemotePlugin.Mobile', 'Text'),
|
||||
'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
|
||||
'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
|
||||
'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
|
||||
'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'),
|
||||
'no_results': translate('RemotePlugin.Mobile', 'No Results'),
|
||||
'options': translate('RemotePlugin.Mobile', 'Options'),
|
||||
'service': translate('RemotePlugin.Mobile', 'Service'),
|
||||
'slides': translate('RemotePlugin.Mobile', 'Slides'),
|
||||
'settings': translate('RemotePlugin.Mobile', 'Settings'),
|
||||
}
|
||||
|
||||
def stages(self, url_path, file_name):
|
||||
"""
|
||||
Allow Stage view to be delivered with custom views.
|
||||
|
||||
:param url_path: base path of the URL. Not used but passed by caller
|
||||
:param file_name: file name with path
|
||||
:return:
|
||||
"""
|
||||
log.debug('serve file request {name}'.format(name=file_name))
|
||||
parts = file_name.split('/')
|
||||
if len(parts) == 1:
|
||||
file_name = os.path.join(parts[0], 'stage.html')
|
||||
elif len(parts) == 3:
|
||||
file_name = os.path.join(parts[1], parts[2])
|
||||
path = os.path.normpath(os.path.join(self.config_dir, file_name))
|
||||
if not path.startswith(self.config_dir):
|
||||
return self.do_not_found()
|
||||
return self._process_file(path)
|
||||
|
||||
def _process_file(self, path):
|
||||
"""
|
||||
Common file processing code
|
||||
|
||||
:param path: path to file to be loaded
|
||||
:return: web resource to be loaded
|
||||
"""
|
||||
content = None
|
||||
ext, content_type = self.get_content_type(path)
|
||||
file_handle = None
|
||||
try:
|
||||
if ext == '.html':
|
||||
variables = self.template_vars
|
||||
content = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables)
|
||||
else:
|
||||
file_handle = open(path, 'rb')
|
||||
log.debug('Opened {path}'.format(path=path))
|
||||
content = file_handle.read()
|
||||
except IOError:
|
||||
log.exception('Failed to open {path}'.format(path=path))
|
||||
return self.do_not_found()
|
||||
finally:
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', content_type)
|
||||
self.end_headers()
|
||||
return content
|
||||
|
||||
def serve_file(self, file_name=None):
|
||||
"""
|
||||
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
|
||||
If subfolders requested return 404, easier for security for the present.
|
||||
|
||||
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'
|
||||
"""
|
||||
log.debug('serve file request {name}'.format(name=file_name))
|
||||
if not file_name:
|
||||
file_name = 'index.html'
|
||||
if '.' not in file_name:
|
||||
file_name += '.html'
|
||||
if file_name.startswith('/'):
|
||||
file_name = file_name[1:]
|
||||
path = os.path.normpath(os.path.join(self.html_dir, file_name))
|
||||
if not path.startswith(self.html_dir):
|
||||
return self.do_not_found()
|
||||
return self._process_file(path)
|
||||
|
||||
def get_content_type(self, file_name):
|
||||
"""
|
||||
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
|
||||
Returns the extension and the content_type
|
||||
|
||||
:param file_name: name of file
|
||||
"""
|
||||
ext = os.path.splitext(file_name)[1]
|
||||
content_type = FILE_TYPES.get(ext, 'text/plain')
|
||||
return ext, content_type
|
||||
|
||||
def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
|
||||
"""
|
||||
Serve an image file. If not found return 404.
|
||||
|
||||
:param file_name: file name to be served
|
||||
:param dimensions: image size
|
||||
:param controller_name: controller to be called
|
||||
"""
|
||||
log.debug('serve thumbnail {cname}/thumbnails{dim}/{fname}'.format(cname=controller_name,
|
||||
dim=dimensions,
|
||||
fname=file_name))
|
||||
supported_controllers = ['presentations', 'images']
|
||||
# -1 means use the default dimension in ImageManager
|
||||
width = -1
|
||||
height = -1
|
||||
if dimensions:
|
||||
match = re.search('(\d+)x(\d+)', dimensions)
|
||||
if match:
|
||||
# let's make sure that the dimensions are within reason
|
||||
width = sorted([10, int(match.group(1)), 1000])[1]
|
||||
height = sorted([10, int(match.group(2)), 1000])[1]
|
||||
content = ''
|
||||
content_type = None
|
||||
if controller_name and file_name:
|
||||
if controller_name in supported_controllers:
|
||||
full_path = urllib.parse.unquote(file_name)
|
||||
if '..' not in full_path: # no hacking please
|
||||
full_path = os.path.normpath(os.path.join(str(AppLocation.get_section_data_path(controller_name)),
|
||||
'thumbnails/' + full_path))
|
||||
if os.path.exists(full_path):
|
||||
path, just_file_name = os.path.split(full_path)
|
||||
self.image_manager.add_image(full_path, just_file_name, None, width, height)
|
||||
ext, content_type = self.get_content_type(full_path)
|
||||
image = self.image_manager.get_image(full_path, just_file_name, width, height)
|
||||
content = image_to_byte(image, False)
|
||||
if len(content) == 0:
|
||||
return self.do_not_found()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', content_type)
|
||||
self.end_headers()
|
||||
return content
|
||||
|
||||
def poll(self):
|
||||
"""
|
||||
Poll OpenLP to determine the current slide number and item name.
|
||||
"""
|
||||
result = {
|
||||
'service': self.service_manager.service_id,
|
||||
'slide': self.live_controller.selected_row or 0,
|
||||
'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
|
||||
'twelve': Settings().value('remotes/twelve hour'),
|
||||
'blank': self.live_controller.blank_screen.isChecked(),
|
||||
'theme': self.live_controller.theme_screen.isChecked(),
|
||||
'display': self.live_controller.desktop_screen.isChecked(),
|
||||
'version': 2,
|
||||
'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
|
||||
'isAuthorised': self.authorised,
|
||||
'chordNotation': Settings().value('songs/chord notation'),
|
||||
}
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': result}).encode()
|
||||
|
||||
def main_poll(self):
|
||||
"""
|
||||
Poll OpenLP to determine the current slide count.
|
||||
"""
|
||||
result = {
|
||||
'slide_count': self.live_controller.slide_count
|
||||
}
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': result}).encode()
|
||||
|
||||
def main_image(self):
|
||||
"""
|
||||
Return the latest display image as a byte stream.
|
||||
"""
|
||||
result = {
|
||||
'slide_image': 'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
|
||||
}
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': result}).encode()
|
||||
|
||||
def display(self, action):
|
||||
"""
|
||||
Hide or show the display screen.
|
||||
This is a cross Thread call and UI is updated so Events need to be used.
|
||||
|
||||
:param action: This is the action, either ``hide`` or ``show``.
|
||||
"""
|
||||
self.live_controller.slidecontroller_toggle_display.emit(action)
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'success': True}}).encode()
|
||||
|
||||
def alert(self):
|
||||
"""
|
||||
Send an alert.
|
||||
"""
|
||||
plugin = self.plugin_manager.get_plugin_by_name("alerts")
|
||||
if plugin.status == PluginStatus.Active:
|
||||
try:
|
||||
text = json.loads(self.request_data)['request']['text']
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
text = urllib.parse.unquote(text)
|
||||
self.alerts_manager.alerts_text.emit([text])
|
||||
success = True
|
||||
else:
|
||||
success = False
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'success': success}}).encode()
|
||||
|
||||
def controller_text(self, var):
|
||||
"""
|
||||
Perform an action on the slide controller.
|
||||
|
||||
:param var: variable - not used
|
||||
"""
|
||||
log.debug("controller_text var = {var}".format(var=var))
|
||||
current_item = self.live_controller.service_item
|
||||
data = []
|
||||
if current_item:
|
||||
for index, frame in enumerate(current_item.get_frames()):
|
||||
item = {}
|
||||
# Handle text (songs, custom, bibles)
|
||||
if current_item.is_text():
|
||||
if frame['verseTag']:
|
||||
item['tag'] = str(frame['verseTag'])
|
||||
else:
|
||||
item['tag'] = str(index + 1)
|
||||
item['chords_text'] = str(frame['chords_text'])
|
||||
item['text'] = str(frame['text'])
|
||||
item['html'] = str(frame['html'])
|
||||
# Handle images, unless a custom thumbnail is given or if thumbnails is disabled
|
||||
elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'):
|
||||
item['tag'] = str(index + 1)
|
||||
thumbnail_path = os.path.join('images', 'thumbnails', frame['title'])
|
||||
full_thumbnail_path = os.path.join(str(AppLocation.get_data_path()), thumbnail_path)
|
||||
# Create thumbnail if it doesn't exists
|
||||
if not os.path.exists(full_thumbnail_path):
|
||||
create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False)
|
||||
item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path)
|
||||
item['text'] = str(frame['title'])
|
||||
item['html'] = str(frame['title'])
|
||||
else:
|
||||
# Handle presentation etc.
|
||||
item['tag'] = str(index + 1)
|
||||
if current_item.is_capable(ItemCapabilities.HasDisplayTitle):
|
||||
item['title'] = str(frame['display_title'])
|
||||
if current_item.is_capable(ItemCapabilities.HasNotes):
|
||||
item['slide_notes'] = str(frame['notes'])
|
||||
if current_item.is_capable(ItemCapabilities.HasThumbnails) and \
|
||||
Settings().value('remotes/thumbnails'):
|
||||
# If the file is under our app directory tree send the portion after the match
|
||||
data_path = str(AppLocation.get_data_path())
|
||||
if frame['image'][0:len(data_path)] == data_path:
|
||||
item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):])
|
||||
item['text'] = str(frame['title'])
|
||||
item['html'] = str(frame['title'])
|
||||
item['selected'] = (self.live_controller.selected_row == index)
|
||||
data.append(item)
|
||||
json_data = {'results': {'slides': data}}
|
||||
if current_item:
|
||||
json_data['results']['item'] = self.live_controller.service_item.unique_identifier
|
||||
self.do_json_header()
|
||||
return json.dumps(json_data).encode()
|
||||
|
||||
def controller(self, display_type, action):
|
||||
"""
|
||||
Perform an action on the slide controller.
|
||||
|
||||
:param display_type: This is the type of slide controller, either ``preview`` or ``live``.
|
||||
:param action: The action to perform.
|
||||
"""
|
||||
event = getattr(self.live_controller, 'slidecontroller_{display}_{action}'.format(display=display_type,
|
||||
action=action))
|
||||
if self.request_data:
|
||||
try:
|
||||
data = json.loads(self.request_data)['request']['id']
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
log.info(data)
|
||||
# This slot expects an int within a list.
|
||||
event.emit([data])
|
||||
else:
|
||||
event.emit()
|
||||
json_data = {'results': {'success': True}}
|
||||
self.do_json_header()
|
||||
return json.dumps(json_data).encode()
|
||||
|
||||
def service_list(self):
|
||||
"""
|
||||
Handles requests for service items in the service manager
|
||||
|
||||
"""
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'items': self._get_service_items()}}).encode()
|
||||
|
||||
def service(self, action):
|
||||
"""
|
||||
Handles requests for service items in the service manager
|
||||
|
||||
:param action: The action to perform.
|
||||
"""
|
||||
event = getattr(self.service_manager, 'servicemanager_{action}_item'.format(action=action))
|
||||
if self.request_data:
|
||||
try:
|
||||
data = int(json.loads(self.request_data)['request']['id'])
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
event.emit(data)
|
||||
else:
|
||||
event.emit()
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'success': True}}).encode()
|
||||
|
||||
def plugin_info(self, action):
|
||||
"""
|
||||
Return plugin related information, based on the action.
|
||||
|
||||
:param action: The action to perform. If *search* return a list of plugin names which support search.
|
||||
"""
|
||||
if action == 'search':
|
||||
searches = []
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'items': searches}}).encode()
|
||||
|
||||
def search(self, plugin_name):
|
||||
"""
|
||||
Return a list of items that match the search text.
|
||||
|
||||
:param plugin_name: The plugin name to search in.
|
||||
"""
|
||||
try:
|
||||
text = json.loads(self.request_data)['request']['text']
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
text = urllib.parse.unquote(text)
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
|
||||
results = plugin.media_item.search(text, False)
|
||||
else:
|
||||
results = []
|
||||
self.do_json_header()
|
||||
return json.dumps({'results': {'items': results}}).encode()
|
||||
|
||||
def go_live(self, plugin_name):
|
||||
"""
|
||||
Go live on an item of type ``plugin``.
|
||||
|
||||
:param plugin_name: name of plugin
|
||||
"""
|
||||
try:
|
||||
request_id = json.loads(self.request_data)['request']['id']
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
getattr(plugin.media_item, '{name}_go_live'.format(name=plugin_name)).emit([request_id, True])
|
||||
return self.do_http_success()
|
||||
|
||||
def add_to_service(self, plugin_name):
|
||||
"""
|
||||
Add item of type ``plugin_name`` to the end of the service.
|
||||
|
||||
:param plugin_name: name of plugin to be called
|
||||
"""
|
||||
try:
|
||||
request_id = json.loads(self.request_data)['request']['id']
|
||||
except KeyError:
|
||||
return self.do_http_error()
|
||||
plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
|
||||
if plugin.status == PluginStatus.Active and plugin.media_item:
|
||||
item_id = plugin.media_item.create_item_from_id(request_id)
|
||||
getattr(plugin.media_item, '{name}_add_to_service'.format(name=plugin_name)).emit([item_id, True])
|
||||
self.do_http_success()
|
@ -1,155 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
The :mod:`http` module contains the API web server. This is a lightweight web server used by remotes to interact
|
||||
with OpenLP. It uses JSON to communicate with the remotes.
|
||||
"""
|
||||
|
||||
import ssl
|
||||
import socket
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, RegistryProperties
|
||||
|
||||
from openlp.plugins.remotes.lib import HttpRouter
|
||||
|
||||
from socketserver import BaseServer, ThreadingMixIn
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CustomHandler(BaseHTTPRequestHandler, HttpRouter):
|
||||
"""
|
||||
Stateless session handler to handle the HTTP request and process it.
|
||||
This class handles just the overrides to the base methods and the logic to invoke the methods within the HttpRouter
|
||||
class.
|
||||
DO not try change the structure as this is as per the documentation.
|
||||
"""
|
||||
|
||||
def do_POST(self):
|
||||
"""
|
||||
Present pages / data and invoke URL level user authentication.
|
||||
"""
|
||||
self.do_post_processor()
|
||||
|
||||
def do_GET(self):
|
||||
"""
|
||||
Present pages / data and invoke URL level user authentication.
|
||||
"""
|
||||
self.do_post_processor()
|
||||
|
||||
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
pass
|
||||
|
||||
|
||||
class HttpThread(QtCore.QThread):
|
||||
"""
|
||||
A special Qt thread class to allow the HTTP server to run at the same time as the UI.
|
||||
"""
|
||||
def __init__(self, server):
|
||||
"""
|
||||
Constructor for the thread class.
|
||||
|
||||
:param server: The http server class.
|
||||
"""
|
||||
super(HttpThread, self).__init__(None)
|
||||
self.http_server = server
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
"""
|
||||
self.http_server.start_server()
|
||||
|
||||
def stop(self):
|
||||
log.debug("stop called")
|
||||
self.http_server.stop = True
|
||||
|
||||
|
||||
class OpenLPServer(RegistryProperties):
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialise the http server, and start the server of the correct type http / https
|
||||
"""
|
||||
super(OpenLPServer, self).__init__()
|
||||
log.debug('Initialise OpenLP')
|
||||
self.settings_section = 'remotes'
|
||||
self.http_thread = HttpThread(self)
|
||||
self.http_thread.start()
|
||||
|
||||
def start_server(self):
|
||||
"""
|
||||
Start the correct server and save the handler
|
||||
"""
|
||||
address = Settings().value(self.settings_section + '/ip address')
|
||||
self.address = address
|
||||
self.is_secure = Settings().value(self.settings_section + '/https enabled')
|
||||
self.needs_authentication = Settings().value(self.settings_section + '/authentication enabled')
|
||||
port = Settings().value(self.settings_section + '/port')
|
||||
self.port = port
|
||||
self.start_server_instance(address, port, ThreadingHTTPServer)
|
||||
if hasattr(self, 'httpd') and self.httpd:
|
||||
self.httpd.serve_forever()
|
||||
else:
|
||||
log.debug('Failed to start server')
|
||||
|
||||
def start_server_instance(self, address, port, server_class):
|
||||
"""
|
||||
Start the server
|
||||
|
||||
:param address: The server address
|
||||
:param port: The run port
|
||||
:param server_class: the class to start
|
||||
"""
|
||||
loop = 1
|
||||
while loop < 4:
|
||||
try:
|
||||
self.httpd = server_class((address, port), CustomHandler)
|
||||
log.debug("Server started for class {name} {address} {port:d}".format(name=server_class,
|
||||
address=address,
|
||||
port=port))
|
||||
break
|
||||
except OSError:
|
||||
log.debug("failed to start http server thread state "
|
||||
"{loop:d} {running}".format(loop=loop, running=self.http_thread.isRunning()))
|
||||
loop += 1
|
||||
time.sleep(0.1)
|
||||
except:
|
||||
log.error('Failed to start server ')
|
||||
loop += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop_server(self):
|
||||
"""
|
||||
Stop the server
|
||||
"""
|
||||
if self.http_thread.isRunning():
|
||||
self.http_thread.stop()
|
||||
self.httpd = None
|
||||
log.debug('Stopped the server.')
|
@ -21,82 +21,59 @@
|
||||
###############################################################################
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import AppLocation, Registry, Settings, OpenLPMixin, UiStrings, check_directory_exists
|
||||
from openlp.core.lib import Plugin, StringContent, translate, build_icon
|
||||
from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
|
||||
from openlp.plugins.remotes.endpoint import remote_endpoint
|
||||
from openlp.plugins.remotes.deploy import download_and_check, download_sha256
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__default_settings__ = {
|
||||
'remotes/twelve hour': True,
|
||||
'remotes/port': 4316,
|
||||
'remotes/https port': 4317,
|
||||
'remotes/https enabled': False,
|
||||
'remotes/user id': 'openlp',
|
||||
'remotes/password': 'password',
|
||||
'remotes/authentication enabled': False,
|
||||
'remotes/ip address': '0.0.0.0',
|
||||
'remotes/thumbnails': True
|
||||
'remotes/download version': '0000_00_00'
|
||||
}
|
||||
|
||||
|
||||
class RemotesPlugin(Plugin):
|
||||
log.info('Remote Plugin loaded')
|
||||
class RemotesPlugin(Plugin, OpenLPMixin):
|
||||
log.info('Remotes Plugin loaded')
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
remotes constructor
|
||||
"""
|
||||
super(RemotesPlugin, self).__init__('remotes', __default_settings__, settings_tab_class=RemoteTab)
|
||||
super(RemotesPlugin, self).__init__('remotes', __default_settings__, {})
|
||||
self.icon_path = ':/plugins/plugin_remote.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.weight = -1
|
||||
self.server = None
|
||||
register_endpoint(remote_endpoint)
|
||||
Registry().register_function('download_website', self.first_time)
|
||||
Registry().register_function('get_website_version', self.website_version)
|
||||
Registry().set_flag('website_version', '0001_01_01')
|
||||
|
||||
def initialise(self):
|
||||
"""
|
||||
Initialise the remotes plugin, and start the http server
|
||||
Create the internal file structure if it does not exist
|
||||
:return:
|
||||
"""
|
||||
log.debug('initialise')
|
||||
super(RemotesPlugin, self).initialise()
|
||||
self.server = OpenLPServer()
|
||||
if not hasattr(self, 'remote_server_icon'):
|
||||
self.remote_server_icon = QtWidgets.QLabel(self.main_window.status_bar)
|
||||
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
|
||||
size_policy.setHorizontalStretch(0)
|
||||
size_policy.setVerticalStretch(0)
|
||||
size_policy.setHeightForWidth(self.remote_server_icon.sizePolicy().hasHeightForWidth())
|
||||
self.remote_server_icon.setSizePolicy(size_policy)
|
||||
self.remote_server_icon.setFrameShadow(QtWidgets.QFrame.Plain)
|
||||
self.remote_server_icon.setLineWidth(1)
|
||||
self.remote_server_icon.setScaledContents(True)
|
||||
self.remote_server_icon.setFixedSize(20, 20)
|
||||
self.remote_server_icon.setObjectName('remote_server_icon')
|
||||
self.main_window.status_bar.insertPermanentWidget(2, self.remote_server_icon)
|
||||
self.settings_tab.remote_server_icon = self.remote_server_icon
|
||||
self.settings_tab.generate_icon()
|
||||
|
||||
def finalise(self):
|
||||
"""
|
||||
Tidy up and close down the http server
|
||||
"""
|
||||
log.debug('finalise')
|
||||
super(RemotesPlugin, self).finalise()
|
||||
if self.server:
|
||||
self.server.stop_server()
|
||||
self.server = None
|
||||
check_directory_exists(os.path.join(AppLocation.get_section_data_path('remotes'), 'assets'))
|
||||
check_directory_exists(os.path.join(AppLocation.get_section_data_path('remotes'), 'images'))
|
||||
check_directory_exists(os.path.join(AppLocation.get_section_data_path('remotes'), 'static'))
|
||||
check_directory_exists(os.path.join(AppLocation.get_section_data_path('remotes'), 'static', 'index'))
|
||||
check_directory_exists(os.path.join(AppLocation.get_section_data_path('remotes'), 'templates'))
|
||||
|
||||
@staticmethod
|
||||
def about():
|
||||
"""
|
||||
Information about this plugin
|
||||
"""
|
||||
about_text = translate('RemotePlugin', '<strong>Remote Plugin</strong>'
|
||||
'<br />The remote plugin provides the ability to send messages to '
|
||||
'a running version of OpenLP on a different computer via a web '
|
||||
'browser or through the remote API.')
|
||||
about_text = translate('RemotePlugin', '<strong>Web Interface</strong>'
|
||||
'<br />The web interface plugin provides the ability develop web based '
|
||||
'interfaces using openlp web services. \nPredefined interfaces can be '
|
||||
'download as well as custom developed interfaces')
|
||||
return about_text
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
@ -105,21 +82,73 @@ class RemotesPlugin(Plugin):
|
||||
"""
|
||||
# Name PluginList
|
||||
self.text_strings[StringContent.Name] = {
|
||||
'singular': translate('RemotePlugin', 'Remote', 'name singular'),
|
||||
'plural': translate('RemotePlugin', 'Remotes', 'name plural')
|
||||
'singular': translate('RemotePlugin', 'Web Interface', 'name singular'),
|
||||
'plural': translate('RemotePlugin', 'Web Interface', 'name plural')
|
||||
}
|
||||
# Name for MediaDockManager, SettingsManager
|
||||
self.text_strings[StringContent.VisibleName] = {
|
||||
'title': translate('RemotePlugin', 'Remote', 'container title')
|
||||
'title': translate('RemotePlugin', 'Web Remote', 'container title')
|
||||
}
|
||||
|
||||
def config_update(self):
|
||||
def first_time(self):
|
||||
"""
|
||||
Called when Config is changed to requests a restart with the server on new address or port
|
||||
Import web site code if active
|
||||
"""
|
||||
log.debug('remote config changed')
|
||||
QtWidgets.QMessageBox.information(self.main_window,
|
||||
translate('RemotePlugin', 'Server Config Change'),
|
||||
translate('RemotePlugin',
|
||||
'Server configuration changes will require a restart '
|
||||
'to take effect.'))
|
||||
self.application.process_events()
|
||||
progress = Progress(self)
|
||||
progress.forceShow()
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
download_and_check(progress)
|
||||
self.application.process_events()
|
||||
time.sleep(1)
|
||||
progress.close()
|
||||
self.application.process_events()
|
||||
Settings().setValue('remotes/download version', self.version)
|
||||
|
||||
def website_version(self):
|
||||
"""
|
||||
Download and save the website version and sha256
|
||||
:return: None
|
||||
"""
|
||||
sha256, self.version = download_sha256()
|
||||
Registry().set_flag('website_sha256', sha256)
|
||||
Registry().set_flag('website_version', self.version)
|
||||
|
||||
|
||||
class Progress(QtWidgets.QProgressDialog):
|
||||
"""
|
||||
Local class to handle download display based and supporting httputils:get_web_page
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
super(Progress, self).__init__(parent.main_window)
|
||||
self.parent = parent
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.setWindowTitle(translate('RemotePlugin', 'Importing Website'))
|
||||
self.setLabelText(UiStrings().StartingImport)
|
||||
self.setCancelButton(None)
|
||||
self.setRange(0, 1)
|
||||
self.setMinimumDuration(0)
|
||||
self.was_cancelled = False
|
||||
self.previous_size = 0
|
||||
|
||||
def _download_progress(self, count, block_size):
|
||||
"""
|
||||
Calculate and display the download progress.
|
||||
"""
|
||||
increment = (count * block_size) - self.previous_size
|
||||
self._increment_progress_bar(None, increment)
|
||||
self.previous_size = count * block_size
|
||||
|
||||
def _increment_progress_bar(self, status_text, increment=1):
|
||||
"""
|
||||
Update the wizard progress page.
|
||||
|
||||
:param status_text: Current status information to display.
|
||||
:param increment: The value to increment the progress bar by.
|
||||
"""
|
||||
if status_text:
|
||||
self.setText(status_text)
|
||||
if increment > 0:
|
||||
self.setValue(self.value() + increment)
|
||||
self.parent.application.process_events()
|
||||
|
100
openlp/plugins/songs/endpoint.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
import logging
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http.errors import NotFound
|
||||
from openlp.core.api.endpoint.pluginhelpers import search, live, service
|
||||
from openlp.core.api.http import requires_auth
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
songs_endpoint = Endpoint('songs')
|
||||
api_songs_endpoint = Endpoint('api')
|
||||
|
||||
|
||||
@songs_endpoint.route('search')
|
||||
def songs_search(request):
|
||||
"""
|
||||
Handles requests for searching the songs plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'songs', log)
|
||||
|
||||
|
||||
@songs_endpoint.route('live')
|
||||
@requires_auth
|
||||
def songs_live(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'songs', log)
|
||||
|
||||
|
||||
@songs_endpoint.route('add')
|
||||
@requires_auth
|
||||
def songs_service(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
service(request, 'songs', log)
|
||||
|
||||
|
||||
@api_songs_endpoint.route('songs/search')
|
||||
def songs_search_api(request):
|
||||
"""
|
||||
Handles requests for searching the songs plugin
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return search(request, 'songs', log)
|
||||
|
||||
|
||||
@api_songs_endpoint.route('songs/live')
|
||||
@requires_auth
|
||||
def songs_live_api(request):
|
||||
"""
|
||||
Handles requests for making a song live
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
return live(request, 'songs', log)
|
||||
|
||||
|
||||
@api_songs_endpoint.route('songs/add')
|
||||
@requires_auth
|
||||
def songs_service_api(request):
|
||||
"""
|
||||
Handles requests for adding a song to the service
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
try:
|
||||
search(request, 'songs', log)
|
||||
except NotFound:
|
||||
return {'results': {'items': []}}
|
@ -603,7 +603,7 @@ class SongMediaItem(MediaManagerItem):
|
||||
else:
|
||||
verse_index = VerseType.from_tag(verse[0]['type'])
|
||||
verse_tag = VerseType.translated_tags[verse_index]
|
||||
verse_def = '{tag}{text}'.format(tag=verse_tag, text=verse[0]['label'])
|
||||
verse_def = '{tag}{label}'.format(tag=verse_tag, label=verse[0]['label'])
|
||||
service_item.add_from_text(verse[1], verse_def)
|
||||
service_item.title = song.title
|
||||
author_list = self.generate_footer(service_item, song)
|
||||
|
@ -31,12 +31,15 @@ from tempfile import gettempdir
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.api.http import register_endpoint
|
||||
from openlp.core.common import UiStrings, Registry, translate
|
||||
from openlp.core.common.actions import ActionList
|
||||
from openlp.core.lib import Plugin, StringContent, build_icon
|
||||
from openlp.core.lib.db import Manager
|
||||
from openlp.core.lib.ui import create_action
|
||||
|
||||
from openlp.plugins.songs import reporting
|
||||
from openlp.plugins.songs.endpoint import api_songs_endpoint, songs_endpoint
|
||||
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
|
||||
from openlp.plugins.songs.forms.songselectform import SongSelectForm
|
||||
from openlp.plugins.songs.lib import clean_song, upgrade
|
||||
@ -47,6 +50,7 @@ from openlp.plugins.songs.lib.mediaitem import SongMediaItem
|
||||
from openlp.plugins.songs.lib.mediaitem import SongSearch
|
||||
from openlp.plugins.songs.lib.songstab import SongsTab
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
__default_settings__ = {
|
||||
'songs/db type': 'sqlite',
|
||||
@ -91,6 +95,8 @@ class SongsPlugin(Plugin):
|
||||
self.icon_path = ':/plugins/plugin_songs.png'
|
||||
self.icon = build_icon(self.icon_path)
|
||||
self.songselect_form = None
|
||||
register_endpoint(songs_endpoint)
|
||||
register_endpoint(api_songs_endpoint)
|
||||
|
||||
def check_pre_conditions(self):
|
||||
"""
|
||||
@ -326,8 +332,8 @@ class SongsPlugin(Plugin):
|
||||
self.application.process_events()
|
||||
progress = QtWidgets.QProgressDialog(self.main_window)
|
||||
progress.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progress.setWindowTitle(translate('OpenLP.Ui', 'Importing Songs'))
|
||||
progress.setLabelText(translate('OpenLP.Ui', 'Starting import...'))
|
||||
progress.setWindowTitle(translate('SongsPlugin', 'Importing Songs'))
|
||||
progress.setLabelText(UiStrings().StartingImport)
|
||||
progress.setCancelButton(None)
|
||||
progress.setRange(0, song_count)
|
||||
progress.setMinimumDuration(0)
|
||||
|
@ -93,7 +93,11 @@ MODULES = [
|
||||
'bs4',
|
||||
'mako',
|
||||
'uno',
|
||||
'six'
|
||||
'websockets',
|
||||
'asyncio',
|
||||
'waitress',
|
||||
'six',
|
||||
'webob'
|
||||
]
|
||||
|
||||
|
||||
|
@ -171,7 +171,7 @@ def get_repo_name():
|
||||
# Determine the branch's name
|
||||
repo_name = ''
|
||||
for line in output_list:
|
||||
# Check if it is remote branch.
|
||||
# Check if it is api branch.
|
||||
if 'push branch' in line:
|
||||
match = re.match(REPO_REGEX, line)
|
||||
if match:
|
||||
|
42
openlp/plugins/remotes/html/stage.html → scripts/websocket_client.py
Normal file → Executable file
@ -1,6 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@ -19,23 +20,18 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${stage_title}</title>
|
||||
<link rel="stylesheet" href="/css/stage.css" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/images/favicon.ico">
|
||||
<script type="text/javascript" src="/assets/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="/js/stage.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="hidden" id="next-text" value="${next}" />
|
||||
<div id="right">
|
||||
<div id="clock"></div>
|
||||
<div id="notes"></div>
|
||||
</div>
|
||||
<div id="verseorder"></div>
|
||||
<div id="currentslide"></div>
|
||||
<div id="nextslide"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import random
|
||||
|
||||
async def tester():
|
||||
async with websockets.connect('ws://localhost:4317/poll') as websocket:
|
||||
|
||||
while True:
|
||||
greeting = await websocket.recv()
|
||||
print("< {}".format(greeting))
|
||||
import time
|
||||
time.sleep(random.random() * 3)
|
||||
|
||||
asyncio.get_event_loop().run_until_complete(tester())
|
@ -29,29 +29,32 @@ from unittest.mock import patch
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.plugins.remotes.lib.remotetab import RemoteTab
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.api.tab import ApiTab
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'remotes/twelve hour': True,
|
||||
'remotes/port': 4316,
|
||||
'remotes/user id': 'openlp',
|
||||
'remotes/password': 'password',
|
||||
'remotes/authentication enabled': False,
|
||||
'remotes/ip address': '0.0.0.0',
|
||||
'remotes/thumbnails': True
|
||||
'api/twelve hour': True,
|
||||
'api/port': 4316,
|
||||
'api/user id': 'openlp',
|
||||
'api/password': 'password',
|
||||
'api/authentication enabled': False,
|
||||
'api/ip address': '0.0.0.0',
|
||||
'api/thumbnails': True,
|
||||
'remotes/download version': '0000_00_00'
|
||||
}
|
||||
ZERO_URL = '0.0.0.0'
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
|
||||
|
||||
|
||||
class TestRemoteTab(TestCase, TestMixin):
|
||||
class TestApiTab(TestCase, TestMixin):
|
||||
"""
|
||||
Test the functions in the :mod:`lib` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
@patch('openlp.core.api.tab.ApiTab.define_main_window_icon')
|
||||
@patch('openlp.core.api.tab.ApiTab.generate_icon')
|
||||
def setUp(self, mocked_main_window, mocked_icon):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
@ -59,7 +62,9 @@ class TestRemoteTab(TestCase, TestMixin):
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.parent = QtWidgets.QMainWindow()
|
||||
self.form = RemoteTab(self.parent, 'Remotes', None, None)
|
||||
Registry().create()
|
||||
Registry().set_flag('website_version', '00-00-0000')
|
||||
self.form = ApiTab(self.parent)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
119
tests/functional/openlp_core_api/test_websockets.py
Normal file
@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the Http Server Class.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.api.websockets import WebSocketServer
|
||||
from openlp.core.api.poll import Poller
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'api/twelve hour': True,
|
||||
'api/port': 4316,
|
||||
'api/user id': 'openlp',
|
||||
'api/password': 'password',
|
||||
'api/authentication enabled': False,
|
||||
'api/ip address': '0.0.0.0',
|
||||
'api/thumbnails': True,
|
||||
'songs/chord notation': True
|
||||
}
|
||||
|
||||
|
||||
class TestWSServer(TestCase, TestMixin):
|
||||
"""
|
||||
A test suite to test starting the websocket server
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
Registry().create()
|
||||
self.poll = Poller()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
self.destroy_settings()
|
||||
|
||||
@patch('openlp.core.api.websockets.WebSocketWorker')
|
||||
@patch('openlp.core.api.websockets.QtCore.QThread')
|
||||
def test_serverstart(self, mock_qthread, mock_worker):
|
||||
"""
|
||||
Test the starting of the WebSockets Server
|
||||
"""
|
||||
# GIVEN: A new httpserver
|
||||
# WHEN: I start the server
|
||||
server = WebSocketServer()
|
||||
|
||||
# THEN: the api environment should have been created
|
||||
self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once')
|
||||
self.assertEquals(1, mock_worker.call_count, 'The http thread should have been called once')
|
||||
|
||||
def test_main_poll(self):
|
||||
"""
|
||||
Test the main_poll function returns the correct JSON
|
||||
"""
|
||||
# WHEN: the live controller has 5 slides
|
||||
mocked_live_controller = MagicMock()
|
||||
mocked_live_controller.slide_count = 5
|
||||
Registry().register('live_controller', mocked_live_controller)
|
||||
# THEN: the live json should be generated
|
||||
main_json = self.poll.main_poll()
|
||||
self.assertEquals(b'{"results": {"slide_count": 5}}', main_json,
|
||||
'The return value should match the defined json')
|
||||
|
||||
def test_poll(self):
|
||||
"""
|
||||
Test the poll function returns the correct JSON
|
||||
"""
|
||||
# WHEN: the system is configured with a set of data
|
||||
mocked_service_manager = MagicMock()
|
||||
mocked_service_manager.service_id = 21
|
||||
mocked_live_controller = MagicMock()
|
||||
mocked_live_controller.selected_row = 5
|
||||
mocked_live_controller.service_item = MagicMock()
|
||||
mocked_live_controller.service_item.unique_identifier = '23-34-45'
|
||||
mocked_live_controller.blank_screen.isChecked.return_value = True
|
||||
mocked_live_controller.theme_screen.isChecked.return_value = False
|
||||
mocked_live_controller.desktop_screen.isChecked.return_value = False
|
||||
Registry().register('live_controller', mocked_live_controller)
|
||||
Registry().register('service_manager', mocked_service_manager)
|
||||
# THEN: the live json should be generated and match expected results
|
||||
poll_json = self.poll.poll()
|
||||
self.assertTrue(poll_json['results']['blank'], 'The blank return value should be True')
|
||||
self.assertFalse(poll_json['results']['theme'], 'The theme return value should be False')
|
||||
self.assertFalse(poll_json['results']['display'], 'The display return value should be False')
|
||||
self.assertFalse(poll_json['results']['isSecure'], 'The isSecure return value should be False')
|
||||
self.assertFalse(poll_json['results']['isAuthorised'], 'The isAuthorised return value should be False')
|
||||
self.assertTrue(poll_json['results']['twelve'], 'The twelve return value should be False')
|
||||
self.assertEquals(poll_json['results']['version'], 3, 'The version return value should be 3')
|
||||
self.assertEquals(poll_json['results']['slide'], 5, 'The slide return value should be 5')
|
||||
self.assertEquals(poll_json['results']['service'], 21, 'The version return value should be 21')
|
||||
self.assertEquals(poll_json['results']['item'], '23-34-45', 'The item return value should match 23-34-45')
|
21
tests/functional/openlp_core_api_http/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
59
tests/functional/openlp_core_api_http/test_error.py
Normal file
@ -0,0 +1,59 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the API Error Class.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core.api.http.errors import NotFound, ServerError
|
||||
|
||||
|
||||
class TestApiError(TestCase):
|
||||
"""
|
||||
A test suite to test out the Error in the API code
|
||||
"""
|
||||
def test_not_found(self):
|
||||
"""
|
||||
Test the Not Found error displays the correct information
|
||||
"""
|
||||
# GIVEN:
|
||||
# WHEN: I raise an exception
|
||||
with self.assertRaises(Exception) as context:
|
||||
raise NotFound()
|
||||
|
||||
# THEN: we get an error and a status
|
||||
self.assertEquals('Not Found', context.exception.message, 'A Not Found exception should be thrown')
|
||||
self.assertEquals(404, context.exception.status, 'A 404 status should be thrown')
|
||||
|
||||
def test_server_error(self):
|
||||
"""
|
||||
Test the server error displays the correct information
|
||||
"""
|
||||
# GIVEN:
|
||||
# WHEN: I raise an exception
|
||||
with self.assertRaises(Exception) as context:
|
||||
raise ServerError()
|
||||
|
||||
# THEN: we get an error and a status
|
||||
self.assertEquals('Server Error', context.exception.message, 'A Not Found exception should be thrown')
|
||||
self.assertEquals(500, context.exception.status, 'A 500 status should be thrown')
|
57
tests/functional/openlp_core_api_http/test_http.py
Normal file
@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional tests to test the Http Server Class.
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.api.http.server import HttpServer
|
||||
|
||||
|
||||
class TestHttpServer(TestCase):
|
||||
"""
|
||||
A test suite to test starting the http server
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Registry().create()
|
||||
Registry().register('service_list', MagicMock())
|
||||
|
||||
@patch('openlp.core.api.http.server.HttpWorker')
|
||||
@patch('openlp.core.api.http.server.QtCore.QThread')
|
||||
def test_server_start(self, mock_qthread, mock_thread):
|
||||
"""
|
||||
Test the starting of the Waitress Server
|
||||
"""
|
||||
# GIVEN: A new httpserver
|
||||
# WHEN: I start the server
|
||||
server = HttpServer()
|
||||
|
||||
# THEN: the api environment should have been created
|
||||
self.assertEquals(1, mock_qthread.call_count, 'The qthread should have been called once')
|
||||
self.assertEquals(1, mock_thread.call_count, 'The http thread should have been called once')
|
86
tests/functional/openlp_core_api_http/test_wsgiapp.py
Normal file
@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Functional test the routing code.
|
||||
"""
|
||||
import os
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openlp.core.api.http.endpoint import Endpoint
|
||||
from openlp.core.api.http import register_endpoint, application, NotFound
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
test_endpoint = Endpoint('test', template_dir=ROOT_DIR, static_dir=ROOT_DIR)
|
||||
|
||||
|
||||
class TestRouting(TestCase):
|
||||
"""
|
||||
Test the HTTP routing
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Convert the application to a test application
|
||||
:return:
|
||||
"""
|
||||
for route, views in application.route_map.items():
|
||||
application.route_map[route]['GET'] = MagicMock()
|
||||
|
||||
def test_routing(self):
|
||||
"""
|
||||
Test the Routing in the new application via dispatch
|
||||
:return:
|
||||
"""
|
||||
# GIVE: I try to request and
|
||||
# WHEN: when the URL is not correct and dispatch called
|
||||
rqst = MagicMock()
|
||||
rqst.path = '/test/api'
|
||||
rqst.method = 'GET'
|
||||
with self.assertRaises(NotFound) as context:
|
||||
application.dispatch(rqst)
|
||||
|
||||
# THEN: the not found returned
|
||||
self.assertEqual(context.exception.args[0], 'Not Found', 'URL not found in dispatcher')
|
||||
|
||||
# WHEN: when the URL is correct and dispatch called
|
||||
rqst = MagicMock()
|
||||
rqst.path = '/test/image'
|
||||
rqst.method = 'GET'
|
||||
application.dispatch(rqst)
|
||||
# THEN: the not found id called
|
||||
self.assertEqual(1, application.route_map['^\\/test\\/image$']['GET'].call_count,
|
||||
'main_index function should have been called')
|
||||
|
||||
|
||||
@test_endpoint.route('image')
|
||||
def image(request):
|
||||
pass
|
||||
|
||||
|
||||
@test_endpoint.route('')
|
||||
def index(request):
|
||||
pass
|
||||
|
||||
|
||||
register_endpoint(test_endpoint)
|
@ -28,7 +28,7 @@ import socket
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file
|
||||
from openlp.core.common.httputils import get_user_agent, get_web_page, get_url_file_size, url_get_file, ping
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
@ -272,3 +272,29 @@ class TestHttpUtils(TestCase, TestMixin):
|
||||
# THEN: socket.timeout should have been caught
|
||||
# NOTE: Test is if $tmpdir/tempfile is still there, then test fails since ftw deletes bad downloaded files
|
||||
self.assertFalse(os.path.exists(self.tempfile), 'FTW url_get_file should have caught socket.timeout')
|
||||
|
||||
def test_ping_valid(self):
|
||||
"""
|
||||
Test ping for OpenLP
|
||||
"""
|
||||
# GIVEN: a valid url to test
|
||||
url = "openlp.io"
|
||||
|
||||
# WHEN: Attempt to check the url exists
|
||||
url_found = ping(url)
|
||||
|
||||
# THEN: It should be found
|
||||
self.assertTrue(url_found, 'OpenLP.io is not found')
|
||||
|
||||
def test_ping_invalid(self):
|
||||
"""
|
||||
Test ping for OpenLP
|
||||
"""
|
||||
# GIVEN: a valid url to test
|
||||
url = "trb143.io"
|
||||
|
||||
# WHEN: Attempt to check the url exists
|
||||
url_found = ping(url)
|
||||
|
||||
# THEN: It should be found
|
||||
self.assertFalse(url_found, 'TRB143.io is found')
|
||||
|
@ -56,6 +56,8 @@ class TestImageManager(TestCase, TestMixin):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
self.image_manager.stop_manager = True
|
||||
self.image_manager.image_thread.wait()
|
||||
del self.app
|
||||
|
||||
def test_basic_image_manager(self):
|
||||
|
@ -46,6 +46,7 @@ class TestMainWindow(TestCase, TestMixin):
|
||||
self.app.set_normal_cursor = MagicMock()
|
||||
self.app.args = []
|
||||
Registry().register('application', self.app)
|
||||
Registry().set_flag('no_web_server', False)
|
||||
# Mock classes and methods used by mainwindow.
|
||||
with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \
|
||||
patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \
|
||||
@ -55,7 +56,9 @@ class TestMainWindow(TestCase, TestMixin):
|
||||
patch('openlp.core.ui.mainwindow.QtWidgets.QToolBox') as mocked_q_tool_box_class, \
|
||||
patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \
|
||||
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer:
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
|
||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
|
||||
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
|
||||
self.mocked_settings_form = mocked_settings_form
|
||||
self.mocked_image_manager = mocked_image_manager
|
||||
self.mocked_live_controller = mocked_live_controller
|
||||
@ -148,7 +151,7 @@ class TestMainWindow(TestCase, TestMixin):
|
||||
|
||||
# THEN: the following registry functions should have been registered
|
||||
self.assertEqual(len(self.registry.service_list), 6, 'The registry should have 6 services.')
|
||||
self.assertEqual(len(self.registry.functions_list), 17, 'The registry should have 17 functions')
|
||||
self.assertEqual(len(self.registry.functions_list), 18, 'The registry should have 18 functions')
|
||||
self.assertTrue('application' in self.registry.service_list, 'The application should have been registered.')
|
||||
self.assertTrue('main_window' in self.registry.service_list, 'The main_window should have been registered.')
|
||||
self.assertTrue('media_controller' in self.registry.service_list, 'The media_controller should have been '
|
||||
|
@ -89,6 +89,8 @@ class TestSettingsForm(TestCase):
|
||||
general_tab.icon_path = ':/icon/openlp-logo.svg'
|
||||
mocked_general_save = MagicMock()
|
||||
general_tab.save = mocked_general_save
|
||||
mocked_general_load = MagicMock()
|
||||
general_tab.load = mocked_general_load
|
||||
settings_form.insert_tab(general_tab, is_visible=True)
|
||||
themes_tab = QtWidgets.QWidget(None)
|
||||
themes_tab.tab_title = 'mock-themes'
|
||||
@ -96,6 +98,8 @@ class TestSettingsForm(TestCase):
|
||||
themes_tab.icon_path = ':/icon/openlp-logo.svg'
|
||||
mocked_theme_save = MagicMock()
|
||||
themes_tab.save = mocked_theme_save
|
||||
mocked_theme_load = MagicMock()
|
||||
themes_tab.load = mocked_theme_load
|
||||
settings_form.insert_tab(themes_tab, is_visible=False)
|
||||
|
||||
# WHEN: The accept() method is called
|
||||
@ -115,6 +119,8 @@ class TestSettingsForm(TestCase):
|
||||
general_tab.tab_title = 'mock'
|
||||
general_tab.tab_title_visible = 'Mock'
|
||||
general_tab.icon_path = ':/icon/openlp-logo.svg'
|
||||
mocked_general_load = MagicMock()
|
||||
general_tab.load = mocked_general_load
|
||||
settings_form.insert_tab(general_tab, is_visible=True)
|
||||
|
||||
with patch.object(settings_form.stacked_layout, 'count') as mocked_count:
|
||||
@ -136,6 +142,8 @@ class TestSettingsForm(TestCase):
|
||||
general_tab.icon_path = ':/icon/openlp-logo.svg'
|
||||
mocked_general_cancel = MagicMock()
|
||||
general_tab.cancel = mocked_general_cancel
|
||||
mocked_general_load = MagicMock()
|
||||
general_tab.load = mocked_general_load
|
||||
settings_form.insert_tab(general_tab, is_visible=True)
|
||||
themes_tab = QtWidgets.QWidget(None)
|
||||
themes_tab.tab_title = 'mock-themes'
|
||||
|
@ -39,8 +39,7 @@ class MediaPluginTest(TestCase, TestMixin):
|
||||
Registry.create()
|
||||
|
||||
@patch(u'openlp.plugins.media.mediaplugin.Plugin.initialise')
|
||||
@patch(u'openlp.plugins.media.mediaplugin.Settings')
|
||||
def test_initialise(self, _mocked_settings, mocked_initialise):
|
||||
def test_initialise(self, mocked_initialise):
|
||||
"""
|
||||
Test that the initialise() method overwrites the built-in one, but still calls it
|
||||
"""
|
||||
@ -48,7 +47,6 @@ class MediaPluginTest(TestCase, TestMixin):
|
||||
media_plugin = MediaPlugin()
|
||||
mocked_settings = MagicMock()
|
||||
mocked_settings.get_files_from_config.return_value = True # Not the real value, just need something "true-ish"
|
||||
_mocked_settings.return_value = mocked_settings
|
||||
|
||||
# WHEN: initialise() is called
|
||||
media_plugin.initialise()
|
||||
|
@ -23,7 +23,7 @@
|
||||
Functional tests to test the Impress class and related methods.
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock
|
||||
import os
|
||||
import shutil
|
||||
from tempfile import mkdtemp
|
||||
|
64
tests/functional/openlp_plugins/remotes/test_deploy.py
Normal file
@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from tempfile import mkdtemp
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.plugins.remotes.deploy import deploy_zipfile
|
||||
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources'))
|
||||
|
||||
|
||||
class TestRemoteDeploy(TestCase):
|
||||
"""
|
||||
Test the Remote plugin deploy functions
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup for tests
|
||||
"""
|
||||
self.app_root = mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up after tests
|
||||
"""
|
||||
shutil.rmtree(self.app_root)
|
||||
|
||||
def test_deploy_zipfile(self):
|
||||
"""
|
||||
Remote Deploy tests - test the dummy zip file is processed correctly
|
||||
"""
|
||||
# GIVEN: A new downloaded zip file
|
||||
zip_file = os.path.join(TEST_PATH, 'remotes', 'site.zip')
|
||||
app_root = os.path.join(self.app_root, 'site.zip')
|
||||
shutil.copyfile(zip_file, app_root)
|
||||
# WHEN: I process the zipfile
|
||||
deploy_zipfile(self.app_root, 'site.zip')
|
||||
|
||||
# THEN test if www directory has been created
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.app_root, 'www')), 'We should have a www directory')
|
@ -1,399 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
This module contains tests for the lib submodule of the Remotes plugin.
|
||||
"""
|
||||
import os
|
||||
import urllib.request
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
|
||||
from openlp.core.common import Settings, Registry
|
||||
from openlp.core.ui import ServiceManager
|
||||
from openlp.plugins.remotes.lib.httpserver import HttpRouter
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'remotes/twelve hour': True,
|
||||
'remotes/port': 4316,
|
||||
'remotes/user id': 'openlp',
|
||||
'remotes/password': 'password',
|
||||
'remotes/authentication enabled': False,
|
||||
'remotes/ip address': '0.0.0.0'
|
||||
}
|
||||
|
||||
TEST_PATH = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class TestRouter(TestCase, TestMixin):
|
||||
"""
|
||||
Test the functions in the :mod:`lib` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
self.setup_application()
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.service_manager = ServiceManager()
|
||||
self.router = HttpRouter()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
self.destroy_settings()
|
||||
|
||||
def test_password_encrypter(self):
|
||||
"""
|
||||
Test hash userid and password function
|
||||
"""
|
||||
# GIVEN: A default configuration
|
||||
Settings().setValue('remotes/user id', 'openlp')
|
||||
Settings().setValue('remotes/password', 'password')
|
||||
|
||||
# WHEN: called with the defined userid
|
||||
router = HttpRouter()
|
||||
router.initialise()
|
||||
test_value = 'b3BlbmxwOnBhc3N3b3Jk'
|
||||
|
||||
# THEN: the function should return the correct password
|
||||
self.assertEqual(router.auth, test_value,
|
||||
'The result for make_sha_hash should return the correct encrypted password')
|
||||
|
||||
def test_process_http_request(self):
|
||||
"""
|
||||
Test the router control functionality
|
||||
"""
|
||||
# GIVEN: A testing set of Routes
|
||||
mocked_function = MagicMock()
|
||||
test_route = [
|
||||
(r'^/stage/api/poll$', {'function': mocked_function, 'secure': False}),
|
||||
]
|
||||
self.router.routes = test_route
|
||||
self.router.command = 'GET'
|
||||
|
||||
# WHEN: called with a poll route
|
||||
function, args = self.router.process_http_request('/stage/api/poll', None)
|
||||
|
||||
# THEN: the function should have been called only once
|
||||
self.assertEqual(mocked_function, function['function'], 'The mocked function should match defined value.')
|
||||
self.assertFalse(function['secure'], 'The mocked function should not require any security.')
|
||||
|
||||
def test_process_secure_http_request(self):
|
||||
"""
|
||||
Test the router control functionality
|
||||
"""
|
||||
# GIVEN: A testing set of Routes
|
||||
mocked_function = MagicMock()
|
||||
test_route = [
|
||||
(r'^/stage/api/poll$', {'function': mocked_function, 'secure': True}),
|
||||
]
|
||||
self.router.routes = test_route
|
||||
self.router.settings_section = 'remotes'
|
||||
Settings().setValue('remotes/authentication enabled', True)
|
||||
self.router.path = '/stage/api/poll'
|
||||
self.router.auth = ''
|
||||
self.router.headers = {'Authorization': None}
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router.command = 'GET'
|
||||
|
||||
# WHEN: called with a poll route
|
||||
self.router.do_post_processor()
|
||||
|
||||
# THEN: the function should have been called only once
|
||||
self.router.send_response.assert_called_once_with(401)
|
||||
self.assertEqual(self.router.send_header.call_count, 5, 'The header should have been called five times.')
|
||||
|
||||
def test_get_content_type(self):
|
||||
"""
|
||||
Test the get_content_type logic
|
||||
"""
|
||||
# GIVEN: a set of files and their corresponding types
|
||||
headers = [
|
||||
['test.html', 'text/html'], ['test.css', 'text/css'],
|
||||
['test.js', 'application/javascript'], ['test.jpg', 'image/jpeg'],
|
||||
['test.gif', 'image/gif'], ['test.ico', 'image/x-icon'],
|
||||
['test.png', 'image/png'], ['test.whatever', 'text/plain'],
|
||||
['test', 'text/plain'], ['', 'text/plain'],
|
||||
[os.path.join(TEST_PATH, 'test.html'), 'text/html']]
|
||||
|
||||
# WHEN: calling each file type
|
||||
for header in headers:
|
||||
ext, content_type = self.router.get_content_type(header[0])
|
||||
|
||||
# THEN: all types should match
|
||||
self.assertEqual(content_type, header[1], 'Mismatch of content type')
|
||||
|
||||
def test_main_poll(self):
|
||||
"""
|
||||
Test the main poll logic
|
||||
"""
|
||||
# GIVEN: a defined router with two slides
|
||||
Registry.create()
|
||||
Registry().register('live_controller', MagicMock)
|
||||
router = HttpRouter()
|
||||
router.send_response = MagicMock()
|
||||
router.send_header = MagicMock()
|
||||
router.end_headers = MagicMock()
|
||||
router.live_controller.slide_count = 2
|
||||
|
||||
# WHEN: main poll called
|
||||
results = router.main_poll()
|
||||
|
||||
# THEN: the correct response should be returned
|
||||
self.assertEqual(results.decode('utf-8'), '{"results": {"slide_count": 2}}',
|
||||
'The resulting json strings should match')
|
||||
|
||||
def test_serve_file_without_params(self):
|
||||
"""
|
||||
Test the serve_file method without params
|
||||
"""
|
||||
# GIVEN: mocked environment
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router.html_dir = os.path.normpath('test/dir')
|
||||
self.router.template_vars = MagicMock()
|
||||
|
||||
# WHEN: call serve_file with no file_name
|
||||
self.router.serve_file()
|
||||
|
||||
# THEN: it should return a 404
|
||||
self.router.send_response.assert_called_once_with(404)
|
||||
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
|
||||
|
||||
def test_serve_file_with_valid_params(self):
|
||||
"""
|
||||
Test the serve_file method with an existing file
|
||||
"""
|
||||
# GIVEN: mocked environment
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router.html_dir = os.path.normpath('test/dir')
|
||||
self.router.template_vars = MagicMock()
|
||||
with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
|
||||
patch('builtins.open', mock_open(read_data='123')):
|
||||
mocked_exists.return_value = True
|
||||
|
||||
# WHEN: call serve_file with an existing html file
|
||||
self.router.serve_file(os.path.normpath('test/dir/test.html'))
|
||||
|
||||
# THEN: it should return a 200 and the file
|
||||
self.router.send_response.assert_called_once_with(200)
|
||||
self.router.send_header.assert_called_once_with('Content-type', 'text/html')
|
||||
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
|
||||
|
||||
def test_serve_file_with_partial_params(self):
|
||||
"""
|
||||
Test the serve_file method with an existing file
|
||||
"""
|
||||
# GIVEN: mocked environment
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router.html_dir = os.path.normpath('test/dir')
|
||||
self.router.template_vars = MagicMock()
|
||||
with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
|
||||
patch('builtins.open', mock_open(read_data='123')):
|
||||
mocked_exists.return_value = True
|
||||
|
||||
# WHEN: call serve_file with an existing html file
|
||||
self.router.serve_file(os.path.normpath('test/dir/test'))
|
||||
|
||||
# THEN: it should return a 200 and the file
|
||||
self.router.send_response.assert_called_once_with(200)
|
||||
self.router.send_header.assert_called_once_with('Content-type', 'text/html')
|
||||
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
|
||||
|
||||
def test_serve_thumbnail_without_params(self):
|
||||
"""
|
||||
Test the serve_thumbnail routine without params
|
||||
"""
|
||||
# GIVEN: mocked environment
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
|
||||
# WHEN: I request a thumbnail
|
||||
self.router.serve_thumbnail()
|
||||
|
||||
# THEN: The headers should be set correctly
|
||||
self.router.send_response.assert_called_once_with(404)
|
||||
self.assertEqual(self.router.send_response.call_count, 1, 'Send response should be called once')
|
||||
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers should be called once')
|
||||
|
||||
def test_serve_thumbnail_with_invalid_params(self):
|
||||
"""
|
||||
Test the serve_thumbnail routine with invalid params
|
||||
"""
|
||||
# GIVEN: Mocked send_header, send_response, end_headers and wfile
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
|
||||
# WHEN: pass a bad controller
|
||||
self.router.serve_thumbnail('badcontroller', 'tecnologia 1.pptx/slide1.png')
|
||||
|
||||
# THEN: a 404 should be returned
|
||||
self.assertEqual(len(self.router.send_header.mock_calls), 4, 'header should be called 4 times')
|
||||
self.assertEqual(len(self.router.send_response.mock_calls), 1, 'One response')
|
||||
self.assertEqual(len(self.router.wfile.mock_calls), 1, 'Once call to write to the socket')
|
||||
self.router.send_response.assert_called_once_with(404)
|
||||
|
||||
# WHEN: pass a bad filename
|
||||
self.router.send_response.reset_mock()
|
||||
self.router.serve_thumbnail('presentations', 'tecnologia 1.pptx/badfilename.png')
|
||||
|
||||
# THEN: return a 404
|
||||
self.router.send_response.assert_called_once_with(404)
|
||||
|
||||
# WHEN: a dangerous URL is passed
|
||||
self.router.send_response.reset_mock()
|
||||
self.router.serve_thumbnail('presentations', '../tecnologia 1.pptx/slide1.png')
|
||||
|
||||
# THEN: return a 404
|
||||
self.router.send_response.assert_called_once_with(404)
|
||||
|
||||
def test_serve_thumbnail_with_valid_params(self):
|
||||
"""
|
||||
Test the serve_thumbnail routine with valid params
|
||||
"""
|
||||
# GIVEN: Mocked send_header, send_response, end_headers and wfile
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
mocked_image_manager = MagicMock()
|
||||
Registry().register('image_manager', mocked_image_manager)
|
||||
file_name = 'another%20test/slide1.png'
|
||||
full_path = os.path.normpath(os.path.join('thumbnails', file_name))
|
||||
width = 120
|
||||
height = 90
|
||||
with patch('openlp.core.lib.os.path.exists') as mocked_exists, \
|
||||
patch('builtins.open', mock_open(read_data='123')), \
|
||||
patch('openlp.plugins.remotes.lib.httprouter.AppLocation') as mocked_location, \
|
||||
patch('openlp.plugins.remotes.lib.httprouter.image_to_byte') as mocked_image_to_byte:
|
||||
mocked_exists.return_value = True
|
||||
mocked_image_to_byte.return_value = '123'
|
||||
mocked_location.get_section_data_path.return_value = ''
|
||||
|
||||
# WHEN: pass good controller and filename
|
||||
self.router.serve_thumbnail('presentations', '{0}x{1}'.format(width, height), file_name)
|
||||
|
||||
# THEN: a file should be returned
|
||||
self.assertEqual(self.router.send_header.call_count, 1, 'One header')
|
||||
self.assertEqual(self.router.send_response.call_count, 1, 'Send response called once')
|
||||
self.assertEqual(self.router.end_headers.call_count, 1, 'end_headers called once')
|
||||
mocked_exists.assert_called_with(urllib.parse.unquote(full_path))
|
||||
self.assertEqual(mocked_image_to_byte.call_count, 1, 'Called once')
|
||||
mocked_image_manager.add_image.assert_any_call(os.path.normpath(os.path.join('thumbnails', 'another test',
|
||||
'slide1.png')),
|
||||
'slide1.png', None, width, height)
|
||||
mocked_image_manager.get_image.assert_any_call(os.path.normpath(os.path.join('thumbnails', 'another test',
|
||||
'slide1.png')),
|
||||
'slide1.png', width, height)
|
||||
|
||||
def test_remote_next(self):
|
||||
"""
|
||||
Test service manager receives remote next click properly (bug 1407445)
|
||||
"""
|
||||
# GIVEN: initial setup and mocks
|
||||
self.router.routes = [(r'^/api/service/(.*)$', {'function': self.router.service, 'secure': False})]
|
||||
self.router.request_data = False
|
||||
mocked_next_item = MagicMock()
|
||||
self.service_manager.next_item = mocked_next_item
|
||||
with patch.object(self.service_manager, 'setup_ui'), \
|
||||
patch.object(self.router, 'do_json_header'):
|
||||
self.service_manager.bootstrap_initialise()
|
||||
|
||||
# WHEN: Remote next is received
|
||||
self.router.service(action='next')
|
||||
|
||||
# THEN: service_manager.next_item() should have been called
|
||||
self.assertTrue(mocked_next_item.called, 'next_item() should have been called in service_manager')
|
||||
|
||||
def test_remote_previous(self):
|
||||
"""
|
||||
Test service manager receives remote previous click properly (bug 1407445)
|
||||
"""
|
||||
# GIVEN: initial setup and mocks
|
||||
self.router.routes = [(r'^/api/service/(.*)$', {'function': self.router.service, 'secure': False})]
|
||||
self.router.request_data = False
|
||||
mocked_previous_item = MagicMock()
|
||||
self.service_manager.previous_item = mocked_previous_item
|
||||
with patch.object(self.service_manager, 'setup_ui'), \
|
||||
patch.object(self.router, 'do_json_header'):
|
||||
self.service_manager.bootstrap_initialise()
|
||||
|
||||
# WHEN: Remote next is received
|
||||
self.router.service(action='previous')
|
||||
|
||||
# THEN: service_manager.next_item() should have been called
|
||||
self.assertTrue(mocked_previous_item.called, 'previous_item() should have been called in service_manager')
|
||||
|
||||
def test_remote_stage_personal_html(self):
|
||||
"""
|
||||
Test the stage url with a parameter after loaded a url/stage.html file
|
||||
"""
|
||||
# GIVEN: initial route
|
||||
self.router.config_dir = ''
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router._process_file = MagicMock()
|
||||
|
||||
# WHEN: I call stage with a suffix
|
||||
self.router.stages('stages', 'trb')
|
||||
|
||||
# THEN: we should use the specific stage file instance
|
||||
self.router._process_file.assert_called_with(os.path.join('trb', 'stage.html'))
|
||||
|
||||
def test_remote_stage_personal_css(self):
|
||||
"""
|
||||
Test the html with reference stages/trb/trb.css then loaded a stages/trb/trb.css file
|
||||
"""
|
||||
# GIVEN: initial route
|
||||
self.router.config_dir = ''
|
||||
self.router.send_response = MagicMock()
|
||||
self.router.send_header = MagicMock()
|
||||
self.router.end_headers = MagicMock()
|
||||
self.router.wfile = MagicMock()
|
||||
self.router._process_file = MagicMock()
|
||||
|
||||
# WHEN: I call stage with a suffix
|
||||
self.router.stages('stages', 'stages/trb/trb.css')
|
||||
|
||||
# THEN: we should use the specific stage file instance
|
||||
self.router._process_file.assert_called_with(os.path.join('trb', 'trb.css'))
|
21
tests/interfaces/openlp_core_api/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2017 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
@ -45,6 +45,7 @@ class TestMainWindow(TestCase, TestMixin):
|
||||
self.app.set_normal_cursor = MagicMock()
|
||||
self.app.args = []
|
||||
Registry().register('application', self.app)
|
||||
Registry().set_flag('no_web_server', False)
|
||||
# Mock classes and methods used by mainwindow.
|
||||
with patch('openlp.core.ui.mainwindow.SettingsForm') as mocked_settings_form, \
|
||||
patch('openlp.core.ui.mainwindow.ImageManager') as mocked_image_manager, \
|
||||
@ -56,7 +57,9 @@ class TestMainWindow(TestCase, TestMixin):
|
||||
patch('openlp.core.ui.mainwindow.ServiceManager') as mocked_service_manager, \
|
||||
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
|
||||
patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer:
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
|
||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
|
||||
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
|
||||
self.main_window = MainWindow()
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -43,6 +43,7 @@ class TestServiceManager(TestCase, TestMixin):
|
||||
Create the UI
|
||||
"""
|
||||
Registry.create()
|
||||
Registry().set_flag('no_web_server', False)
|
||||
self.setup_application()
|
||||
ScreenList.create(self.app.desktop())
|
||||
Registry().register('application', MagicMock())
|
||||
@ -56,7 +57,9 @@ class TestServiceManager(TestCase, TestMixin):
|
||||
patch('openlp.core.ui.mainwindow.QtWidgets.QMainWindow.addDockWidget') as mocked_add_dock_method, \
|
||||
patch('openlp.core.ui.mainwindow.ThemeManager') as mocked_theme_manager, \
|
||||
patch('openlp.core.ui.mainwindow.ProjectorManager') as mocked_projector_manager, \
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer:
|
||||
patch('openlp.core.ui.mainwindow.Renderer') as mocked_renderer, \
|
||||
patch('openlp.core.ui.mainwindow.websockets.WebSocketServer') as mocked_websocketserver, \
|
||||
patch('openlp.core.ui.mainwindow.server.HttpServer') as mocked_httpserver:
|
||||
self.main_window = MainWindow()
|
||||
self.service_manager = Registry().get('service_manager')
|
||||
|
||||
|