Head
@ -33,6 +33,7 @@ import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from traceback import format_exception
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@ -153,10 +154,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 +336,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()
|
||||
@ -346,15 +347,18 @@ def set_up_logging(log_path):
|
||||
"""
|
||||
Setup our logging using log_path
|
||||
|
||||
:param log_path: the path
|
||||
:param pathlib.Path log_path: The file to save the log to
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
check_directory_exists(log_path, True)
|
||||
filename = os.path.join(log_path, 'openlp.log')
|
||||
logfile = logging.FileHandler(filename, 'w', encoding="UTF-8")
|
||||
file_path = log_path / 'openlp.log'
|
||||
# TODO: FileHandler accepts a Path object in Py3.6
|
||||
logfile = logging.FileHandler(str(file_path), 'w', encoding='UTF-8')
|
||||
logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
|
||||
log.addHandler(logfile)
|
||||
if log.isEnabledFor(logging.DEBUG):
|
||||
print('Logging to: {name}'.format(name=filename))
|
||||
print('Logging to: {name}'.format(name=file_path))
|
||||
|
||||
|
||||
def main(args=None):
|
||||
@ -390,26 +394,27 @@ def main(args=None):
|
||||
application.setApplicationName('OpenLPPortable')
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
# Get location OpenLPPortable.ini
|
||||
application_path = str(AppLocation.get_directory(AppLocation.AppDir))
|
||||
set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other')))
|
||||
portable_path = (AppLocation.get_directory(AppLocation.AppDir) / '..' / '..').resolve()
|
||||
data_path = portable_path / 'Data'
|
||||
set_up_logging(portable_path / 'Other')
|
||||
log.info('Running portable')
|
||||
portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini'))
|
||||
portable_settings_path = data_path / 'OpenLP.ini'
|
||||
# Make this our settings file
|
||||
log.info('INI file: {name}'.format(name=portable_settings_file))
|
||||
Settings.set_filename(portable_settings_file)
|
||||
log.info('INI file: {name}'.format(name=portable_settings_path))
|
||||
Settings.set_filename(str(portable_settings_path))
|
||||
portable_settings = Settings()
|
||||
# Set our data path
|
||||
data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
|
||||
log.info('Data path: {name}'.format(name=data_path))
|
||||
# Point to our data path
|
||||
portable_settings.setValue('advanced/data path', data_path)
|
||||
portable_settings.setValue('advanced/data path', str(data_path))
|
||||
portable_settings.setValue('advanced/is portable', True)
|
||||
portable_settings.sync()
|
||||
else:
|
||||
application.setApplicationName('OpenLP')
|
||||
set_up_logging(str(AppLocation.get_directory(AppLocation.CacheDir)))
|
||||
set_up_logging(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 = 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)
|
||||
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 = str(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
|
135
openlp/core/api/endpoint/pluginhelpers.py
Normal file
@ -0,0 +1,135 @@
|
||||
# -*- 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 = str(AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name / slide)
|
||||
else:
|
||||
full_path = str(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
|
79
openlp/core/api/http/endpoint.py
Normal file
@ -0,0 +1,79 @@
|
||||
# -*- 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 = 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)
|
||||
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)
|
185
openlp/core/api/http/wsgiapp.py
Normal file
@ -0,0 +1,185 @@
|
||||
# -*- 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 = str(AppLocation.get_section_data_path('remotes'))
|
||||
static_path = os.path.abspath(os.path.join(root, static_dir))
|
||||
if not os.path.exists(static_path):
|
||||
log.error('Static path "%s" does not exist. Skipping creating static route/', static_path)
|
||||
return
|
||||
self.static_routes[route] = DirectoryApp(static_path)
|
||||
|
||||
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)
|
@ -32,7 +32,6 @@ import sys
|
||||
import traceback
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
from ipaddress import IPv4Address, IPv6Address, AddressValueError
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
|
||||
@ -65,17 +64,19 @@ def trace_error_handler(logger):
|
||||
|
||||
def check_directory_exists(directory, do_not_log=False):
|
||||
"""
|
||||
Check a theme directory exists and if not create it
|
||||
Check a directory exists and if not create it
|
||||
|
||||
:param directory: The directory to make sure exists
|
||||
:param do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
|
||||
:param pathlib.Path directory: The directory to make sure exists
|
||||
:param bool do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
if not do_not_log:
|
||||
log.debug('check_directory_exists {text}'.format(text=directory))
|
||||
try:
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
except IOError as e:
|
||||
if not directory.exists():
|
||||
directory.mkdir(parents=True)
|
||||
except IOError:
|
||||
if not do_not_log:
|
||||
log.exception('failed to check if directory exists or create directory')
|
||||
|
||||
@ -85,19 +86,15 @@ def extension_loader(glob_pattern, excluded_files=[]):
|
||||
A utility function to find and load OpenLP extensions, such as plugins, presentation and media controllers and
|
||||
importers.
|
||||
|
||||
:param glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
|
||||
application directory. i.e. openlp/plugins/*/*plugin.py
|
||||
:type glob_pattern: str
|
||||
|
||||
:param excluded_files: A list of file names to exclude that the glob pattern may find.
|
||||
:type excluded_files: list of strings
|
||||
|
||||
:param str glob_pattern: A glob pattern used to find the extension(s) to be imported. Should be relative to the
|
||||
application directory. i.e. plugins/*/*plugin.py
|
||||
:param list[str] excluded_files: A list of file names to exclude that the glob pattern may find.
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
base_dir_path = AppLocation.get_directory(AppLocation.AppDir).parent
|
||||
for extension_path in base_dir_path.glob(glob_pattern):
|
||||
extension_path = extension_path.relative_to(base_dir_path)
|
||||
app_dir = AppLocation.get_directory(AppLocation.AppDir)
|
||||
for extension_path in app_dir.glob(glob_pattern):
|
||||
extension_path = extension_path.relative_to(app_dir)
|
||||
if extension_path.name in excluded_files:
|
||||
continue
|
||||
module_name = path_to_module(extension_path)
|
||||
@ -106,21 +103,19 @@ def extension_loader(glob_pattern, excluded_files=[]):
|
||||
except (ImportError, OSError):
|
||||
# On some platforms importing vlc.py might cause OSError exceptions. (e.g. Mac OS X)
|
||||
log.warning('Failed to import {module_name} on path {extension_path}'
|
||||
.format(module_name=module_name, extension_path=str(extension_path)))
|
||||
.format(module_name=module_name, extension_path=extension_path))
|
||||
|
||||
|
||||
def path_to_module(path):
|
||||
"""
|
||||
Convert a path to a module name (i.e openlp.core.common)
|
||||
|
||||
:param path: The path to convert to a module name.
|
||||
:type path: Path
|
||||
|
||||
:param pathlib.Path path: The path to convert to a module name.
|
||||
:return: The module name.
|
||||
:rtype: str
|
||||
"""
|
||||
module_path = path.with_suffix('')
|
||||
return '.'.join(module_path.parts)
|
||||
return 'openlp.' + '.'.join(module_path.parts)
|
||||
|
||||
|
||||
def get_frozen_path(frozen_option, non_frozen_option):
|
||||
@ -378,20 +373,22 @@ def split_filename(path):
|
||||
return os.path.split(path)
|
||||
|
||||
|
||||
def delete_file(file_path_name):
|
||||
def delete_file(file_path):
|
||||
"""
|
||||
Deletes a file from the system.
|
||||
|
||||
:param file_path_name: The file, including path, to delete.
|
||||
:param pathlib.Path file_path: The file, including path, to delete.
|
||||
:return: True if the deletion was successful, or the file never existed. False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if not file_path_name:
|
||||
if not file_path:
|
||||
return False
|
||||
try:
|
||||
if os.path.exists(file_path_name):
|
||||
os.remove(file_path_name)
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
return True
|
||||
except (IOError, OSError):
|
||||
log.exception("Unable to delete file {text}".format(text=file_path_name))
|
||||
log.exception('Unable to delete file {file_path}'.format(file_path=file_path))
|
||||
return False
|
||||
|
||||
|
||||
@ -411,18 +408,19 @@ def get_images_filter():
|
||||
return IMAGES_FILTER
|
||||
|
||||
|
||||
def is_not_image_file(file_name):
|
||||
def is_not_image_file(file_path):
|
||||
"""
|
||||
Validate that the file is not an image file.
|
||||
|
||||
:param file_name: File name to be checked.
|
||||
:param pathlib.Path file_path: The file to be checked.
|
||||
:return: If the file is not an image
|
||||
:rtype: bool
|
||||
"""
|
||||
if not file_name:
|
||||
if not (file_path and file_path.exists()):
|
||||
return True
|
||||
else:
|
||||
formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
|
||||
file_part, file_extension = os.path.splitext(str(file_name))
|
||||
if file_extension[1:].lower() in formats and os.path.exists(file_name):
|
||||
if file_path.suffix[1:].lower() in formats:
|
||||
return False
|
||||
return True
|
||||
|
||||
@ -431,10 +429,10 @@ def clean_filename(filename):
|
||||
"""
|
||||
Removes invalid characters from the given ``filename``.
|
||||
|
||||
:param filename: The "dirty" file name to clean.
|
||||
:param str filename: The "dirty" file name to clean.
|
||||
:return: The cleaned string
|
||||
:rtype: str
|
||||
"""
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, 'utf-8')
|
||||
return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
|
||||
|
||||
|
||||
@ -442,8 +440,9 @@ def check_binary_exists(program_path):
|
||||
"""
|
||||
Function that checks whether a binary exists.
|
||||
|
||||
:param program_path: The full path to the binary to check.
|
||||
:param pathlib.Path program_path: The full path to the binary to check.
|
||||
:return: program output to be parsed
|
||||
:rtype: bytes
|
||||
"""
|
||||
log.debug('testing program_path: {text}'.format(text=program_path))
|
||||
try:
|
||||
@ -453,26 +452,27 @@ def check_binary_exists(program_path):
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
else:
|
||||
startupinfo = None
|
||||
runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo)
|
||||
run_log = check_output([str(program_path), '--help'], stderr=STDOUT, startupinfo=startupinfo)
|
||||
except CalledProcessError as e:
|
||||
runlog = e.output
|
||||
run_log = e.output
|
||||
except Exception:
|
||||
trace_error_handler(log)
|
||||
runlog = ''
|
||||
log.debug('check_output returned: {text}'.format(text=runlog))
|
||||
return runlog
|
||||
run_log = ''
|
||||
log.debug('check_output returned: {text}'.format(text=run_log))
|
||||
return run_log
|
||||
|
||||
|
||||
def get_file_encoding(filename):
|
||||
def get_file_encoding(file_path):
|
||||
"""
|
||||
Utility function to incrementally detect the file encoding.
|
||||
|
||||
:param filename: Filename for the file to determine the encoding for. Str
|
||||
:param pathlib.Path file_path: Filename for the file to determine the encoding for.
|
||||
:return: A dict with the keys 'encoding' and 'confidence'
|
||||
:rtype: dict[str, float]
|
||||
"""
|
||||
detector = UniversalDetector()
|
||||
try:
|
||||
with open(filename, 'rb') as detect_file:
|
||||
with file_path.open('rb') as detect_file:
|
||||
while not detector.done:
|
||||
chunk = detect_file.read(1024)
|
||||
if not chunk:
|
||||
|
@ -58,9 +58,6 @@ class AppLocation(object):
|
||||
CacheDir = 5
|
||||
LanguageDir = 6
|
||||
|
||||
# Base path where data/config/cache dir is located
|
||||
BaseDir = None
|
||||
|
||||
@staticmethod
|
||||
def get_directory(dir_type=AppDir):
|
||||
"""
|
||||
@ -78,8 +75,6 @@ class AppLocation(object):
|
||||
return get_frozen_path(FROZEN_APP_PATH, APP_PATH) / 'plugins'
|
||||
elif dir_type == AppLocation.LanguageDir:
|
||||
return get_frozen_path(FROZEN_APP_PATH, _get_os_dir_path(dir_type)) / 'i18n'
|
||||
elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
|
||||
return Path(AppLocation.BaseDir, 'data')
|
||||
else:
|
||||
return _get_os_dir_path(dir_type)
|
||||
|
||||
@ -96,7 +91,7 @@ class AppLocation(object):
|
||||
path = Path(Settings().value('advanced/data path'))
|
||||
else:
|
||||
path = AppLocation.get_directory(AppLocation.DataDir)
|
||||
check_directory_exists(str(path))
|
||||
check_directory_exists(path)
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
@ -104,14 +99,10 @@ class AppLocation(object):
|
||||
"""
|
||||
Get a list of files from the data files path.
|
||||
|
||||
:param section: Defaults to *None*. The section of code getting the files - used to load from a section's data
|
||||
subdirectory.
|
||||
:type section: None | str
|
||||
|
||||
:param extension: Defaults to ''. The extension to search for. For example::
|
||||
:param None | str section: Defaults to *None*. The section of code getting the files - used to load from a
|
||||
section's data subdirectory.
|
||||
:param str extension: Defaults to ''. The extension to search for. For example::
|
||||
'.png'
|
||||
:type extension: str
|
||||
|
||||
:return: List of files found.
|
||||
:rtype: list[pathlib.Path]
|
||||
"""
|
||||
@ -134,7 +125,7 @@ class AppLocation(object):
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
path = AppLocation.get_data_path() / section
|
||||
check_directory_exists(str(path))
|
||||
check_directory_exists(path)
|
||||
return path
|
||||
|
||||
|
||||
@ -143,14 +134,12 @@ def _get_os_dir_path(dir_type):
|
||||
Return a path based on which OS and environment we are running in.
|
||||
|
||||
:param dir_type: AppLocation Enum of the requested path type
|
||||
:type dir_type: AppLocation Enum
|
||||
|
||||
:return: The requested path
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
# If running from source, return the language directory from the source directory
|
||||
if dir_type == AppLocation.LanguageDir:
|
||||
directory = Path(os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')))
|
||||
directory = Path(openlp.__file__, '..', '..').resolve() / 'resources'
|
||||
if directory.exists():
|
||||
return directory
|
||||
if is_win():
|
||||
@ -158,14 +147,14 @@ def _get_os_dir_path(dir_type):
|
||||
if dir_type == AppLocation.DataDir:
|
||||
return openlp_folder_path / 'data'
|
||||
elif dir_type == AppLocation.LanguageDir:
|
||||
return os.path.dirname(openlp.__file__)
|
||||
return Path(openlp.__file__).parent
|
||||
return openlp_folder_path
|
||||
elif is_macosx():
|
||||
openlp_folder_path = Path(os.getenv('HOME'), 'Library', 'Application Support', 'openlp')
|
||||
if dir_type == AppLocation.DataDir:
|
||||
return openlp_folder_path / 'Data'
|
||||
elif dir_type == AppLocation.LanguageDir:
|
||||
return os.path.dirname(openlp.__file__)
|
||||
return Path(openlp.__file__).parent
|
||||
return openlp_folder_path
|
||||
else:
|
||||
if dir_type == AppLocation.LanguageDir:
|
||||
|
@ -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']
|
||||
|
61
openlp/core/common/path.py
Normal file
@ -0,0 +1,61 @@
|
||||
# -*- 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 #
|
||||
###############################################################################
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def path_to_str(path):
|
||||
"""
|
||||
A utility function to convert a Path object or NoneType to a string equivalent.
|
||||
|
||||
:param path: The value to convert to a string
|
||||
:type: pathlib.Path or None
|
||||
|
||||
:return: An empty string if :param:`path` is None, else a string representation of the :param:`path`
|
||||
:rtype: str
|
||||
"""
|
||||
if not isinstance(path, Path) and path is not None:
|
||||
raise TypeError('parameter \'path\' must be of type Path or NoneType')
|
||||
if path is None:
|
||||
return ''
|
||||
else:
|
||||
return str(path)
|
||||
|
||||
|
||||
def str_to_path(string):
|
||||
"""
|
||||
A utility function to convert a str object to a Path or NoneType.
|
||||
|
||||
This function is of particular use because initating a Path object with an empty string causes the Path object to
|
||||
point to the current working directory.
|
||||
|
||||
:param string: The string to convert
|
||||
:type string: str
|
||||
|
||||
:return: None if :param:`string` is empty, or a Path object representation of :param:`string`
|
||||
:rtype: pathlib.Path or None
|
||||
"""
|
||||
if not isinstance(string, str):
|
||||
raise TypeError('parameter \'string\' must be of type str')
|
||||
if string == '':
|
||||
return None
|
||||
return Path(string)
|
@ -121,6 +121,7 @@ class Settings(QtCore.QSettings):
|
||||
'advanced/enable exit confirmation': True,
|
||||
'advanced/expand service item': False,
|
||||
'advanced/hide mouse': True,
|
||||
'advanced/ignore aspect ratio': False,
|
||||
'advanced/is portable': False,
|
||||
'advanced/max recent files': 20,
|
||||
'advanced/print file meta data': False,
|
||||
@ -134,6 +135,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 +223,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.
|
||||
@ -482,31 +502,3 @@ class Settings(QtCore.QSettings):
|
||||
if isinstance(default_value, int):
|
||||
return int(setting)
|
||||
return setting
|
||||
|
||||
def get_files_from_config(self, plugin):
|
||||
"""
|
||||
This removes the settings needed for old way we saved files (e. g. the image paths for the image plugin). A list
|
||||
of file paths are returned.
|
||||
|
||||
**Note**: Only a list of paths is returned; this does not convert anything!
|
||||
|
||||
:param plugin: The Plugin object.The caller has to convert/save the list himself; o
|
||||
"""
|
||||
files_list = []
|
||||
# We need QSettings instead of Settings here to bypass our central settings dict.
|
||||
# Do NOT do this anywhere else!
|
||||
settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
|
||||
settings.beginGroup(plugin.settings_section)
|
||||
if settings.contains('{name} count'.format(name=plugin.name)):
|
||||
# Get the count.
|
||||
list_count = int(settings.value('{name} count'.format(name=plugin.name), 0))
|
||||
if list_count:
|
||||
for counter in range(list_count):
|
||||
# The keys were named e. g.: "image 0"
|
||||
item = settings.value('{name} {counter:d}'.format(name=plugin.name, counter=counter), '')
|
||||
if item:
|
||||
files_list.append(item)
|
||||
settings.remove('{name} {counter:d}'.format(name=plugin.name, counter=counter))
|
||||
settings.remove('{name} count'.format(name=plugin.name))
|
||||
settings.endGroup()
|
||||
return files_list
|
||||
|
@ -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)),
|
||||
|
@ -83,30 +83,28 @@ class ServiceItemAction(object):
|
||||
Next = 3
|
||||
|
||||
|
||||
def get_text_file_string(text_file):
|
||||
def get_text_file_string(text_file_path):
|
||||
"""
|
||||
Open a file and return its content as unicode string. If the supplied file name is not a file then the function
|
||||
Open a file and return its content as a string. If the supplied file path is not a file then the function
|
||||
returns False. If there is an error loading the file or the content can't be decoded then the function will return
|
||||
None.
|
||||
|
||||
:param text_file: The name of the file.
|
||||
:return: The file as a single string
|
||||
:param pathlib.Path text_file_path: The path to the file.
|
||||
:return: The contents of the file, False if the file does not exist, or None if there is an Error reading or
|
||||
decoding the file.
|
||||
:rtype: str | False | None
|
||||
"""
|
||||
if not os.path.isfile(text_file):
|
||||
if not text_file_path.is_file():
|
||||
return False
|
||||
file_handle = None
|
||||
content = None
|
||||
try:
|
||||
file_handle = open(text_file, 'r', encoding='utf-8')
|
||||
with text_file_path.open('r', encoding='utf-8') as file_handle:
|
||||
if file_handle.read(3) != '\xEF\xBB\xBF':
|
||||
# no BOM was found
|
||||
file_handle.seek(0)
|
||||
content = file_handle.read()
|
||||
except (IOError, UnicodeError):
|
||||
log.exception('Failed to open text file {text}'.format(text=text_file))
|
||||
finally:
|
||||
if file_handle:
|
||||
file_handle.close()
|
||||
log.exception('Failed to open text file {text}'.format(text=text_file_path))
|
||||
return content
|
||||
|
||||
|
||||
@ -230,7 +228,7 @@ def validate_thumb(file_path, thumb_path):
|
||||
return image_date <= thumb_date
|
||||
|
||||
|
||||
def resize_image(image_path, width, height, background='#000000'):
|
||||
def resize_image(image_path, width, height, background='#000000', ignore_aspect_ratio=False):
|
||||
"""
|
||||
Resize an image to fit on the current screen.
|
||||
|
||||
@ -247,7 +245,7 @@ def resize_image(image_path, width, height, background='#000000'):
|
||||
image_ratio = reader.size().width() / reader.size().height()
|
||||
resize_ratio = width / height
|
||||
# Figure out the size we want to resize the image to (keep aspect ratio).
|
||||
if image_ratio == resize_ratio:
|
||||
if image_ratio == resize_ratio or ignore_aspect_ratio:
|
||||
size = QtCore.QSize(width, height)
|
||||
elif image_ratio < resize_ratio:
|
||||
# Use the image's height as reference for the new size.
|
||||
@ -608,8 +606,42 @@ def create_separated_list(string_list):
|
||||
return list_to_string
|
||||
|
||||
|
||||
def replace_params(args, kwargs, params):
|
||||
"""
|
||||
Apply a transformation function to the specified args or kwargs
|
||||
|
||||
:param args: Positional arguments
|
||||
:type args: (,)
|
||||
|
||||
:param kwargs: Key Word arguments
|
||||
:type kwargs: dict
|
||||
|
||||
:param params: A tuple of tuples with the position and the key word to replace.
|
||||
:type params: ((int, str, path_to_str),)
|
||||
|
||||
:return: The modified positional and keyword arguments
|
||||
:rtype: (tuple, dict)
|
||||
|
||||
|
||||
Usage:
|
||||
Take a method with the following signature, and assume we which to apply the str function to arg2:
|
||||
def method(arg1=None, arg2=None, arg3=None)
|
||||
|
||||
As arg2 can be specified postitionally as the second argument (1 with a zero index) or as a keyword, the we
|
||||
would call this function as follows:
|
||||
|
||||
replace_params(args, kwargs, ((1, 'arg2', str),))
|
||||
"""
|
||||
args = list(args)
|
||||
for position, key_word, transform in params:
|
||||
if len(args) > position:
|
||||
args[position] = transform(args[position])
|
||||
elif key_word in kwargs:
|
||||
kwargs[key_word] = transform(kwargs[key_word])
|
||||
return tuple(args), kwargs
|
||||
|
||||
|
||||
from .exceptions import ValidationError
|
||||
from .filedialog import FileDialog
|
||||
from .screen import ScreenList
|
||||
from .formattingtags import FormattingTags
|
||||
from .plugin import PluginStatus, StringContent, Plugin
|
||||
|
@ -274,9 +274,9 @@ def delete_database(plugin_name, db_file_name=None):
|
||||
:param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
|
||||
"""
|
||||
if db_file_name:
|
||||
db_file_path = os.path.join(str(AppLocation.get_section_data_path(plugin_name)), db_file_name)
|
||||
db_file_path = AppLocation.get_section_data_path(plugin_name) / db_file_name
|
||||
else:
|
||||
db_file_path = os.path.join(str(AppLocation.get_section_data_path(plugin_name)), plugin_name)
|
||||
db_file_path = AppLocation.get_section_data_path(plugin_name) / plugin_name
|
||||
return delete_file(db_file_path)
|
||||
|
||||
|
||||
|
@ -31,7 +31,7 @@ import queue
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.lib import ScreenList, resize_image, image_to_byte
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -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')
|
||||
@ -306,7 +327,8 @@ class ImageManager(QtCore.QObject):
|
||||
# Let's see if the image was requested with specific dimensions
|
||||
width = self.width if image.width == -1 else image.width
|
||||
height = self.height if image.height == -1 else image.height
|
||||
image.image = resize_image(image.path, width, height, image.background)
|
||||
image.image = resize_image(image.path, width, height, image.background,
|
||||
Settings().value('advanced/ignore aspect ratio'))
|
||||
# Set the priority to Lowest and stop here as we need to process more important images first.
|
||||
if image.priority == Priority.Normal:
|
||||
self._conversion_queue.modify_priority(image, Priority.Lowest)
|
||||
|
@ -26,12 +26,14 @@ import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
|
||||
from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import ServiceItem, StringContent, ServiceItemContext
|
||||
from openlp.core.lib.searchedit import SearchEdit
|
||||
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
|
||||
from openlp.core.ui.lib.filedialog import FileDialog
|
||||
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
|
||||
from openlp.core.ui.lib.toolbar import OpenLPToolbar
|
||||
|
||||
@ -309,13 +311,14 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
|
||||
"""
|
||||
Add a file to the list widget to make it available for showing
|
||||
"""
|
||||
files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
|
||||
Settings().value(self.settings_section + '/last directory'),
|
||||
file_paths, selected_filter = FileDialog.getOpenFileNames(
|
||||
self, self.on_new_prompt,
|
||||
str_to_path(Settings().value(self.settings_section + '/last directory')),
|
||||
self.on_new_file_masks)
|
||||
log.info('New files(s) {files}'.format(files=files))
|
||||
if files:
|
||||
log.info('New files(s) {file_paths}'.format(file_paths=file_paths))
|
||||
if file_paths:
|
||||
self.application.set_busy_cursor()
|
||||
self.validate_and_load(files)
|
||||
self.validate_and_load([path_to_str(path) for path in file_paths])
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def load_file(self, data):
|
||||
|
@ -69,7 +69,7 @@ class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
Scan a directory for objects inheriting from the ``Plugin`` class.
|
||||
"""
|
||||
glob_pattern = os.path.join('openlp', 'plugins', '*', '*plugin.py')
|
||||
glob_pattern = os.path.join('plugins', '*', '*plugin.py')
|
||||
extension_loader(glob_pattern)
|
||||
plugin_classes = Plugin.__subclasses__()
|
||||
plugin_objects = []
|
||||
|
@ -46,7 +46,7 @@ __all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
|
||||
'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED',
|
||||
'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN',
|
||||
'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
|
||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||
'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_DATA', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
|
||||
'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
|
||||
'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS',
|
||||
'PJLINK_DEFAULT_SOURCES', 'PJLINK_DEFAULT_CODES', 'PJLINK_DEFAULT_ITEMS']
|
||||
@ -393,11 +393,32 @@ ERROR_MSG = {
|
||||
S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')
|
||||
}
|
||||
|
||||
# Map ERST return code positions to equipment
|
||||
PJLINK_ERST_DATA = {
|
||||
'DATA_LENGTH': 6,
|
||||
0: 'FAN',
|
||||
1: 'LAMP',
|
||||
2: 'TEMP',
|
||||
3: 'COVER',
|
||||
4: 'FILTER',
|
||||
5: 'OTHER',
|
||||
'FAN': 0,
|
||||
'LAMP': 1,
|
||||
'TEMP': 2,
|
||||
'COVER': 3,
|
||||
'FILTER': 4,
|
||||
'OTHER': 5
|
||||
}
|
||||
|
||||
# Map for ERST return codes to string
|
||||
PJLINK_ERST_STATUS = {
|
||||
'0': ERROR_STRING[E_OK],
|
||||
'0': 'OK',
|
||||
'1': ERROR_STRING[E_WARN],
|
||||
'2': ERROR_STRING[E_ERROR]
|
||||
'2': ERROR_STRING[E_ERROR],
|
||||
'OK': '0',
|
||||
E_OK: '0',
|
||||
E_WARN: '1',
|
||||
E_ERROR: '2'
|
||||
}
|
||||
|
||||
# Map for POWR return codes to status code
|
||||
|
@ -54,10 +54,10 @@ from PyQt5 import QtCore, QtNetwork
|
||||
|
||||
from openlp.core.common import translate, qmd5_hash
|
||||
from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \
|
||||
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
|
||||
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
|
||||
E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, E_OK, \
|
||||
E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, PJLINK_ERST_DATA, \
|
||||
PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
|
||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||
STATUS_STRING, S_CONNECTED, S_CONNECTING, S_INFO, S_NETWORK_RECEIVED, S_NETWORK_SENDING, \
|
||||
S_NOT_CONNECTED, S_OFF, S_OK, S_ON, S_STATUS
|
||||
|
||||
# Shortcuts
|
||||
@ -154,39 +154,37 @@ class PJLinkCommands(object):
|
||||
if _cmd not in PJLINK_VALID_CMD:
|
||||
log.error("({ip}) Ignoring command='{cmd}' (Invalid/Unknown)".format(ip=self.ip, cmd=cmd))
|
||||
return
|
||||
elif _data == 'OK':
|
||||
log.debug('({ip}) Command "{cmd}" returned OK'.format(ip=self.ip, cmd=cmd))
|
||||
# A command returned successfully, no further processing needed
|
||||
return
|
||||
elif _cmd not in self.pjlink_functions:
|
||||
log.warn("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd))
|
||||
log.warning("({ip}) Unable to process command='{cmd}' (Future option)".format(ip=self.ip, cmd=cmd))
|
||||
return
|
||||
elif _data in PJLINK_ERRORS:
|
||||
# Oops - projector error
|
||||
log.error('({ip}) Projector returned error "{data}"'.format(ip=self.ip, data=data))
|
||||
if _data == 'ERRA':
|
||||
if _data == PJLINK_ERRORS[E_AUTHENTICATION]:
|
||||
# Authentication error
|
||||
self.disconnect_from_host()
|
||||
self.change_status(E_AUTHENTICATION)
|
||||
log.debug('({ip}) emitting projectorAuthentication() signal'.format(ip=self.ip))
|
||||
self.projectorAuthentication.emit(self.name)
|
||||
elif _data == 'ERR1':
|
||||
# Undefined command
|
||||
elif _data == PJLINK_ERRORS[E_UNDEFINED]:
|
||||
# Projector does not recognize command
|
||||
self.change_status(E_UNDEFINED, '{error}: "{data}"'.format(error=ERROR_MSG[E_UNDEFINED],
|
||||
data=cmd))
|
||||
elif _data == 'ERR2':
|
||||
elif _data == PJLINK_ERRORS[E_PARAMETER]:
|
||||
# Invalid parameter
|
||||
self.change_status(E_PARAMETER)
|
||||
elif _data == 'ERR3':
|
||||
elif _data == PJLINK_ERRORS[E_UNAVAILABLE]:
|
||||
# Projector busy
|
||||
self.change_status(E_UNAVAILABLE)
|
||||
elif _data == 'ERR4':
|
||||
elif _data == PJLINK_ERRORS[E_PROJECTOR]:
|
||||
# Projector/display error
|
||||
self.change_status(E_PROJECTOR)
|
||||
self.receive_data_signal()
|
||||
return
|
||||
# Command succeeded - no extra information
|
||||
elif _data == 'OK':
|
||||
log.debug('({ip}) Command returned OK'.format(ip=self.ip))
|
||||
# A command returned successfully
|
||||
self.receive_data_signal()
|
||||
return
|
||||
# Command checks already passed
|
||||
log.debug('({ip}) Calling function for {cmd}'.format(ip=self.ip, cmd=cmd))
|
||||
self.receive_data_signal()
|
||||
@ -196,6 +194,10 @@ class PJLinkCommands(object):
|
||||
"""
|
||||
Process shutter and speaker status. See PJLink specification for format.
|
||||
Update self.mute (audio) and self.shutter (video shutter).
|
||||
11 = Shutter closed, audio unchanged
|
||||
21 = Shutter unchanged, Audio muted
|
||||
30 = Shutter closed, audio muted
|
||||
31 = Shutter open, audio normal
|
||||
|
||||
:param data: Shutter and audio status
|
||||
"""
|
||||
@ -204,15 +206,15 @@ class PJLinkCommands(object):
|
||||
'30': {'shutter': False, 'mute': False},
|
||||
'31': {'shutter': True, 'mute': True}
|
||||
}
|
||||
if data in settings:
|
||||
if data not in settings:
|
||||
log.warning('({ip}) Invalid shutter response: {data}'.format(ip=self.ip, data=data))
|
||||
return
|
||||
shutter = settings[data]['shutter']
|
||||
mute = settings[data]['mute']
|
||||
# Check if we need to update the icons
|
||||
update_icons = (shutter != self.shutter) or (mute != self.mute)
|
||||
self.shutter = shutter
|
||||
self.mute = mute
|
||||
else:
|
||||
log.warning('({ip}) Unknown shutter response: {data}'.format(ip=self.ip, data=data))
|
||||
if update_icons:
|
||||
self.projectorUpdateIcons.emit()
|
||||
return
|
||||
@ -229,17 +231,19 @@ class PJLinkCommands(object):
|
||||
# : Received: '%1CLSS=Class 1' (Optoma)
|
||||
# : Received: '%1CLSS=Version1' (BenQ)
|
||||
if len(data) > 1:
|
||||
log.warn("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data))
|
||||
log.warning("({ip}) Non-standard CLSS reply: '{data}'".format(ip=self.ip, data=data))
|
||||
# Due to stupid projectors not following standards (Optoma, BenQ comes to mind),
|
||||
# AND the different responses that can be received, the semi-permanent way to
|
||||
# fix the class reply is to just remove all non-digit characters.
|
||||
try:
|
||||
clss = re.findall('\d', data)[0] # Should only be the first match
|
||||
except IndexError:
|
||||
log.error("({ip}) No numbers found in class version reply - defaulting to class '1'".format(ip=self.ip))
|
||||
log.error("({ip}) No numbers found in class version reply '{data}' - "
|
||||
"defaulting to class '1'".format(ip=self.ip, data=data))
|
||||
clss = '1'
|
||||
elif not data.isdigit():
|
||||
log.error("({ip}) NAN class version reply - defaulting to class '1'".format(ip=self.ip))
|
||||
log.error("({ip}) NAN clss version reply '{data}' - "
|
||||
"defaulting to class '1'".format(ip=self.ip, data=data))
|
||||
clss = '1'
|
||||
else:
|
||||
clss = data
|
||||
@ -253,41 +257,50 @@ class PJLinkCommands(object):
|
||||
Error status. See PJLink Specifications for format.
|
||||
Updates self.projector_errors
|
||||
|
||||
\ :param data: Error status
|
||||
:param data: Error status
|
||||
"""
|
||||
if len(data) != PJLINK_ERST_DATA['DATA_LENGTH']:
|
||||
count = PJLINK_ERST_DATA['DATA_LENGTH']
|
||||
log.warning("{ip}) Invalid error status response '{data}': length != {count}".format(ip=self.ip,
|
||||
data=data,
|
||||
count=count))
|
||||
return
|
||||
try:
|
||||
datacheck = int(data)
|
||||
except ValueError:
|
||||
# Bad data - ignore
|
||||
log.warning("({ip}) Invalid error status response '{data}'".format(ip=self.ip, data=data))
|
||||
return
|
||||
if datacheck == 0:
|
||||
self.projector_errors = None
|
||||
else:
|
||||
# No errors
|
||||
return
|
||||
# We have some sort of status error, so check out what it/they are
|
||||
self.projector_errors = {}
|
||||
# Fan
|
||||
if data[0] != '0':
|
||||
fan, lamp, temp, cover, filt, other = (data[PJLINK_ERST_DATA['FAN']],
|
||||
data[PJLINK_ERST_DATA['LAMP']],
|
||||
data[PJLINK_ERST_DATA['TEMP']],
|
||||
data[PJLINK_ERST_DATA['COVER']],
|
||||
data[PJLINK_ERST_DATA['FILTER']],
|
||||
data[PJLINK_ERST_DATA['OTHER']])
|
||||
if fan != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
|
||||
PJLINK_ERST_STATUS[data[0]]
|
||||
# Lamp
|
||||
if data[1] != '0':
|
||||
PJLINK_ERST_STATUS[fan]
|
||||
if lamp != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
|
||||
PJLINK_ERST_STATUS[data[1]]
|
||||
# Temp
|
||||
if data[2] != '0':
|
||||
PJLINK_ERST_STATUS[lamp]
|
||||
if temp != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
|
||||
PJLINK_ERST_STATUS[data[2]]
|
||||
# Cover
|
||||
if data[3] != '0':
|
||||
PJLINK_ERST_STATUS[temp]
|
||||
if cover != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
|
||||
PJLINK_ERST_STATUS[data[3]]
|
||||
# Filter
|
||||
if data[4] != '0':
|
||||
PJLINK_ERST_STATUS[cover]
|
||||
if filt != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
|
||||
PJLINK_ERST_STATUS[data[4]]
|
||||
# Other
|
||||
if data[5] != '0':
|
||||
PJLINK_ERST_STATUS[filt]
|
||||
if other != PJLINK_ERST_STATUS[E_OK]:
|
||||
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
|
||||
PJLINK_ERST_STATUS[data[5]]
|
||||
PJLINK_ERST_STATUS[other]
|
||||
return
|
||||
|
||||
def process_inf1(self, data):
|
||||
@ -416,9 +429,9 @@ class PJLinkCommands(object):
|
||||
if self.model_filter is None:
|
||||
self.model_filter = data
|
||||
else:
|
||||
log.warn("({ip}) Filter model already set".format(ip=self.ip))
|
||||
log.warn("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter))
|
||||
log.warn("({ip}) New model: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warning("({ip}) Filter model already set".format(ip=self.ip))
|
||||
log.warning("({ip}) Saved model: '{old}'".format(ip=self.ip, old=self.model_filter))
|
||||
log.warning("({ip}) New model: '{new}'".format(ip=self.ip, new=data))
|
||||
|
||||
def process_rlmp(self, data):
|
||||
"""
|
||||
@ -427,9 +440,9 @@ class PJLinkCommands(object):
|
||||
if self.model_lamp is None:
|
||||
self.model_lamp = data
|
||||
else:
|
||||
log.warn("({ip}) Lamp model already set".format(ip=self.ip))
|
||||
log.warn("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp))
|
||||
log.warn("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warning("({ip}) Lamp model already set".format(ip=self.ip))
|
||||
log.warning("({ip}) Saved lamp: '{old}'".format(ip=self.ip, old=self.model_lamp))
|
||||
log.warning("({ip}) New lamp: '{new}'".format(ip=self.ip, new=data))
|
||||
|
||||
def process_snum(self, data):
|
||||
"""
|
||||
@ -444,27 +457,32 @@ class PJLinkCommands(object):
|
||||
else:
|
||||
# Compare serial numbers and see if we got the same projector
|
||||
if self.serial_no != data:
|
||||
log.warn("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip))
|
||||
log.warn("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.serial_no))
|
||||
log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
|
||||
log.warning("({ip}) Projector serial number does not match saved serial number".format(ip=self.ip))
|
||||
log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.serial_no))
|
||||
log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warning("({ip}) NOT saving serial number".format(ip=self.ip))
|
||||
self.serial_no_received = data
|
||||
|
||||
def process_sver(self, data):
|
||||
"""
|
||||
Software version of projector
|
||||
"""
|
||||
if self.sw_version is None:
|
||||
if len(data) > 32:
|
||||
# Defined in specs max version is 32 characters
|
||||
log.warning("Invalid software version - too long")
|
||||
return
|
||||
elif self.sw_version is None:
|
||||
log.debug("({ip}) Setting projector software version to '{data}'".format(ip=self.ip, data=data))
|
||||
self.sw_version = data
|
||||
self.db_update = True
|
||||
else:
|
||||
# Compare software version and see if we got the same projector
|
||||
if self.serial_no != data:
|
||||
log.warn("({ip}) Projector software version does not match saved software version".format(ip=self.ip))
|
||||
log.warn("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.sw_version))
|
||||
log.warn("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warn("({ip}) NOT saving serial number".format(ip=self.ip))
|
||||
log.warning("({ip}) Projector software version does not match saved "
|
||||
"software version".format(ip=self.ip))
|
||||
log.warning("({ip}) Saved: '{old}'".format(ip=self.ip, old=self.sw_version))
|
||||
log.warning("({ip}) Received: '{new}'".format(ip=self.ip, new=data))
|
||||
log.warning("({ip}) Saving new serial number as sw_version_received".format(ip=self.ip))
|
||||
self.sw_version_received = data
|
||||
|
||||
|
||||
@ -592,7 +610,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
Normally called by timer().
|
||||
"""
|
||||
if self.state() != self.ConnectedState:
|
||||
log.warn("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip))
|
||||
log.warning("({ip}) poll_loop(): Not connected - returning".format(ip=self.ip))
|
||||
return
|
||||
log.debug('({ip}) Updating projector status'.format(ip=self.ip))
|
||||
# Reset timer in case we were called from a set command
|
||||
@ -636,7 +654,9 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
:param status: Status/Error code
|
||||
:returns: (Status/Error code, String)
|
||||
"""
|
||||
if status in ERROR_STRING:
|
||||
if not isinstance(status, int):
|
||||
return -1, 'Invalid status code'
|
||||
elif status in ERROR_STRING:
|
||||
return ERROR_STRING[status], ERROR_MSG[status]
|
||||
elif status in STATUS_STRING:
|
||||
return STATUS_STRING[status], ERROR_MSG[status]
|
||||
@ -661,7 +681,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
elif status >= S_NOT_CONNECTED and status < S_STATUS:
|
||||
self.status_connect = status
|
||||
self.projector_status = S_NOT_CONNECTED
|
||||
elif status < S_NETWORK_SENDING:
|
||||
elif status <= S_INFO:
|
||||
self.status_connect = S_CONNECTED
|
||||
self.projector_status = status
|
||||
(status_code, status_message) = self._get_status(self.status_connect)
|
||||
@ -790,7 +810,8 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
log.debug('({ip}) get_data(): Not connected - returning'.format(ip=self.ip))
|
||||
self.send_busy = False
|
||||
return
|
||||
read = self.readLine(self.max_size)
|
||||
# Although we have a packet length limit, go ahead and use a larger buffer
|
||||
read = self.readLine(1024)
|
||||
log.debug("({ip}) get_data(): '{buff}'".format(ip=self.ip, buff=read))
|
||||
if read == -1:
|
||||
# No data available
|
||||
@ -803,6 +824,8 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
data = data_in.strip()
|
||||
if (len(data) < 7) or (not data.startswith(PJLINK_PREFIX)):
|
||||
return self._trash_buffer(msg='get_data(): Invalid packet - length or prefix')
|
||||
elif len(data) > self.max_size:
|
||||
return self._trash_buffer(msg='get_data(): Invalid packet - too long')
|
||||
elif '=' not in data:
|
||||
return self._trash_buffer(msg='get_data(): Invalid packet does not have equal')
|
||||
log.debug('({ip}) get_data(): Checking new data "{data}"'.format(ip=self.ip, data=data))
|
||||
@ -817,7 +840,7 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
log.warning('({ip}) get_data(): Invalid packet - unknown command "{data}"'.format(ip=self.ip, data=cmd))
|
||||
return self._trash_buffer(msg='get_data(): Unknown command "{data}"'.format(data=cmd))
|
||||
if int(self.pjlink_class) < int(version):
|
||||
log.warn('({ip}) get_data(): Projector returned class reply higher '
|
||||
log.warning('({ip}) get_data(): Projector returned class reply higher '
|
||||
'than projector stated class'.format(ip=self.ip))
|
||||
return self.process_command(cmd, data)
|
||||
|
||||
@ -980,6 +1003,13 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
self.reset_information()
|
||||
self.projectorUpdateIcons.emit()
|
||||
|
||||
def get_av_mute_status(self):
|
||||
"""
|
||||
Send command to retrieve shutter status.
|
||||
"""
|
||||
log.debug('({ip}) Sending AVMT command'.format(ip=self.ip))
|
||||
return self.send_command(cmd='AVMT')
|
||||
|
||||
def get_available_inputs(self):
|
||||
"""
|
||||
Send command to retrieve available source inputs.
|
||||
@ -1043,13 +1073,6 @@ class PJLink(PJLinkCommands, QtNetwork.QTcpSocket):
|
||||
log.debug('({ip}) Sending POWR command'.format(ip=self.ip))
|
||||
return self.send_command(cmd='POWR')
|
||||
|
||||
def get_shutter_status(self):
|
||||
"""
|
||||
Send command to retrieve shutter status.
|
||||
"""
|
||||
log.debug('({ip}) Sending AVMT command'.format(ip=self.ip))
|
||||
return self.send_command(cmd='AVMT')
|
||||
|
||||
def set_input_source(self, src=None):
|
||||
"""
|
||||
Verify input source available as listed in 'INST' command,
|
||||
|
@ -158,9 +158,8 @@ class Theme(object):
|
||||
Initialise the theme object.
|
||||
"""
|
||||
# basic theme object with defaults
|
||||
json_dir = os.path.join(str(AppLocation.get_directory(AppLocation.AppDir)), 'core', 'lib', 'json')
|
||||
json_file = os.path.join(json_dir, 'theme.json')
|
||||
jsn = get_text_file_string(json_file)
|
||||
json_path = AppLocation.get_directory(AppLocation.AppDir) / 'core' / 'lib' / 'json' / 'theme.json'
|
||||
jsn = get_text_file_string(json_path)
|
||||
jsn = json.loads(jsn)
|
||||
self.expand_json(jsn)
|
||||
self.background_filename = ''
|
||||
|
@ -30,6 +30,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import AppLocation, Settings, SlideLimits, UiStrings, translate
|
||||
from openlp.core.common.languagemanager import format_time
|
||||
from openlp.core.common.path import path_to_str
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.ui.lib import PathEdit, PathType
|
||||
|
||||
@ -156,7 +157,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.data_directory_new_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.data_directory_new_label.setObjectName('data_directory_current_label')
|
||||
self.data_directory_path_edit = PathEdit(self.data_directory_group_box, path_type=PathType.Directories,
|
||||
default_path=str(AppLocation.get_directory(AppLocation.DataDir)))
|
||||
default_path=AppLocation.get_directory(AppLocation.DataDir))
|
||||
self.data_directory_layout.addRow(self.data_directory_new_label, self.data_directory_path_edit)
|
||||
self.new_data_directory_has_files_label = QtWidgets.QLabel(self.data_directory_group_box)
|
||||
self.new_data_directory_has_files_label.setObjectName('new_data_directory_has_files_label')
|
||||
@ -207,6 +208,9 @@ class AdvancedTab(SettingsTab):
|
||||
self.display_workaround_group_box.setObjectName('display_workaround_group_box')
|
||||
self.display_workaround_layout = QtWidgets.QVBoxLayout(self.display_workaround_group_box)
|
||||
self.display_workaround_layout.setObjectName('display_workaround_layout')
|
||||
self.ignore_aspect_ratio_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.ignore_aspect_ratio_check_box.setObjectName('ignore_aspect_ratio_check_box')
|
||||
self.display_workaround_layout.addWidget(self.ignore_aspect_ratio_check_box)
|
||||
self.x11_bypass_check_box = QtWidgets.QCheckBox(self.display_workaround_group_box)
|
||||
self.x11_bypass_check_box.setObjectName('x11_bypass_check_box')
|
||||
self.display_workaround_layout.addWidget(self.x11_bypass_check_box)
|
||||
@ -310,6 +314,7 @@ class AdvancedTab(SettingsTab):
|
||||
translate('OpenLP.AdvancedTab', '<strong>WARNING:</strong> New data directory location contains '
|
||||
'OpenLP data files. These files WILL be replaced during a copy.'))
|
||||
self.display_workaround_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Display Workarounds'))
|
||||
self.ignore_aspect_ratio_check_box.setText(translate('OpenLP.AdvancedTab', 'Ignore Aspect Ratio'))
|
||||
self.x11_bypass_check_box.setText(translate('OpenLP.AdvancedTab', 'Bypass X11 Window Manager'))
|
||||
self.alternate_rows_check_box.setText(translate('OpenLP.AdvancedTab', 'Use alternating row colours in lists'))
|
||||
# Slide Limits
|
||||
@ -354,6 +359,7 @@ class AdvancedTab(SettingsTab):
|
||||
default_service_enabled = settings.value('default service enabled')
|
||||
self.service_name_check_box.setChecked(default_service_enabled)
|
||||
self.service_name_check_box_toggled(default_service_enabled)
|
||||
self.ignore_aspect_ratio_check_box.setChecked(settings.value('ignore aspect ratio'))
|
||||
self.x11_bypass_check_box.setChecked(settings.value('x11 bypass wm'))
|
||||
self.slide_limits = settings.value('slide limits')
|
||||
self.is_search_as_you_type_enabled = settings.value('search as type')
|
||||
@ -373,7 +379,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.new_data_directory_has_files_label.hide()
|
||||
self.data_directory_cancel_button.hide()
|
||||
# Since data location can be changed, make sure the path is present.
|
||||
self.data_directory_path_edit.path = str(AppLocation.get_data_path())
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
# Don't allow data directory move if running portable.
|
||||
if settings.value('advanced/is portable'):
|
||||
self.data_directory_group_box.hide()
|
||||
@ -409,6 +415,7 @@ class AdvancedTab(SettingsTab):
|
||||
settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked())
|
||||
settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked())
|
||||
settings.setValue('slide limits', self.slide_limits)
|
||||
settings.setValue('ignore aspect ratio', self.ignore_aspect_ratio_check_box.isChecked())
|
||||
if self.x11_bypass_check_box.isChecked() != settings.value('x11 bypass wm'):
|
||||
settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked())
|
||||
self.settings_form.register_post_process('config_screen_changed')
|
||||
@ -497,12 +504,12 @@ class AdvancedTab(SettingsTab):
|
||||
'closed.').format(path=new_data_path),
|
||||
defaultButton=QtWidgets.QMessageBox.No)
|
||||
if answer != QtWidgets.QMessageBox.Yes:
|
||||
self.data_directory_path_edit.path = str(AppLocation.get_data_path())
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
return
|
||||
# Check if data already exists here.
|
||||
self.check_data_overwrite(new_data_path)
|
||||
self.check_data_overwrite(path_to_str(new_data_path))
|
||||
# Save the new location.
|
||||
self.main_window.set_new_data_path(new_data_path)
|
||||
self.main_window.set_new_data_path(path_to_str(new_data_path))
|
||||
self.data_directory_cancel_button.show()
|
||||
|
||||
def on_data_directory_copy_check_box_toggled(self):
|
||||
@ -550,7 +557,7 @@ class AdvancedTab(SettingsTab):
|
||||
"""
|
||||
Cancel the data directory location change
|
||||
"""
|
||||
self.data_directory_path_edit.path = str(AppLocation.get_data_path())
|
||||
self.data_directory_path_edit.path = AppLocation.get_data_path()
|
||||
self.data_directory_copy_check_box.setChecked(False)
|
||||
self.main_window.set_new_data_path(None)
|
||||
self.main_window.set_copy_data(False)
|
||||
|
@ -29,8 +29,9 @@ import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
from configparser import ConfigParser, MissingSectionHeaderError, NoSectionError, NoOptionError
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
@ -202,7 +203,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 +214,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())
|
||||
@ -283,7 +283,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
|
||||
self.no_internet_cancel_button.setVisible(False)
|
||||
# Check if this is a re-run of the wizard.
|
||||
self.has_run_wizard = Settings().value('core/has run wizard')
|
||||
check_directory_exists(os.path.join(gettempdir(), 'openlp'))
|
||||
check_directory_exists(Path(gettempdir(), 'openlp'))
|
||||
|
||||
def update_screen_list_combo(self):
|
||||
"""
|
||||
@ -530,7 +530,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'))
|
||||
|
@ -23,10 +23,12 @@
|
||||
The general tab of the configuration dialog.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, Settings, UiStrings, translate, get_images_filter
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import SettingsTab, ScreenList
|
||||
from openlp.core.ui.lib import ColorButton, PathEdit
|
||||
|
||||
@ -172,7 +174,8 @@ class GeneralTab(SettingsTab):
|
||||
self.logo_layout.setObjectName('logo_layout')
|
||||
self.logo_file_label = QtWidgets.QLabel(self.logo_group_box)
|
||||
self.logo_file_label.setObjectName('logo_file_label')
|
||||
self.logo_file_path_edit = PathEdit(self.logo_group_box, default_path=':/graphics/openlp-splash-screen.png')
|
||||
self.logo_file_path_edit = PathEdit(self.logo_group_box,
|
||||
default_path=Path(':/graphics/openlp-splash-screen.png'))
|
||||
self.logo_layout.addRow(self.logo_file_label, self.logo_file_path_edit)
|
||||
self.logo_color_label = QtWidgets.QLabel(self.logo_group_box)
|
||||
self.logo_color_label.setObjectName('logo_color_label')
|
||||
@ -266,7 +269,7 @@ class GeneralTab(SettingsTab):
|
||||
self.audio_group_box.setTitle(translate('OpenLP.GeneralTab', 'Background Audio'))
|
||||
self.start_paused_check_box.setText(translate('OpenLP.GeneralTab', 'Start background audio paused'))
|
||||
self.repeat_list_check_box.setText(translate('OpenLP.GeneralTab', 'Repeat track list'))
|
||||
self.logo_file_path_edit.dialog_caption = dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
|
||||
self.logo_file_path_edit.dialog_caption = translate('OpenLP.AdvancedTab', 'Select Logo File')
|
||||
self.logo_file_path_edit.filters = '{text};;{names} (*)'.format(
|
||||
text=get_images_filter(), names=UiStrings().AllFiles)
|
||||
|
||||
@ -291,7 +294,7 @@ class GeneralTab(SettingsTab):
|
||||
self.auto_open_check_box.setChecked(settings.value('auto open'))
|
||||
self.show_splash_check_box.setChecked(settings.value('show splash'))
|
||||
self.logo_background_color = settings.value('logo background color')
|
||||
self.logo_file_path_edit.path = settings.value('logo file')
|
||||
self.logo_file_path_edit.path = str_to_path(settings.value('logo file'))
|
||||
self.logo_hide_on_startup_check_box.setChecked(settings.value('logo hide on startup'))
|
||||
self.logo_color_button.color = self.logo_background_color
|
||||
self.check_for_updates_check_box.setChecked(settings.value('update check'))
|
||||
@ -325,7 +328,7 @@ class GeneralTab(SettingsTab):
|
||||
settings.setValue('auto open', self.auto_open_check_box.isChecked())
|
||||
settings.setValue('show splash', self.show_splash_check_box.isChecked())
|
||||
settings.setValue('logo background color', self.logo_background_color)
|
||||
settings.setValue('logo file', self.logo_file_path_edit.path)
|
||||
settings.setValue('logo file', path_to_str(self.logo_file_path_edit.path))
|
||||
settings.setValue('logo hide on startup', self.logo_hide_on_startup_check_box.isChecked())
|
||||
settings.setValue('update check', self.check_for_updates_check_box.isChecked())
|
||||
settings.setValue('save prompt', self.save_check_service_check_box.isChecked())
|
||||
|
113
openlp/core/ui/lib/filedialog.py
Executable 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 #
|
||||
###############################################################################
|
||||
""" Patch the QFileDialog so it accepts and returns Path objects"""
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import replace_params
|
||||
|
||||
|
||||
class FileDialog(QtWidgets.QFileDialog):
|
||||
@classmethod
|
||||
def getExistingDirectory(cls, *args, **kwargs):
|
||||
"""
|
||||
Wraps `getExistingDirectory` so that it can be called with, and return Path objects
|
||||
|
||||
:type parent: QtWidgets.QWidget or None
|
||||
:type caption: str
|
||||
:type directory: pathlib.Path
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[Path, str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
return_value = super().getExistingDirectory(*args, **kwargs)
|
||||
|
||||
# getExistingDirectory returns a str that represents the path. The string is empty if the user cancels the
|
||||
# dialog.
|
||||
return str_to_path(return_value)
|
||||
|
||||
@classmethod
|
||||
def getOpenFileName(cls, *args, **kwargs):
|
||||
"""
|
||||
Wraps `getOpenFileName` so that it can be called with, and return Path objects
|
||||
|
||||
:type parent: QtWidgets.QWidget or None
|
||||
:type caption: str
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[Path, str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
file_name, selected_filter = super().getOpenFileName(*args, **kwargs)
|
||||
|
||||
# getOpenFileName returns a tuple. The first item is a str that represents the path. The string is empty if
|
||||
# the user cancels the dialog.
|
||||
return str_to_path(file_name), selected_filter
|
||||
|
||||
@classmethod
|
||||
def getOpenFileNames(cls, *args, **kwargs):
|
||||
"""
|
||||
Wraps `getOpenFileNames` so that it can be called with, and return Path objects
|
||||
|
||||
:type parent: QtWidgets.QWidget or None
|
||||
:type caption: str
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[list[Path], str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
file_names, selected_filter = super().getOpenFileNames(*args, **kwargs)
|
||||
|
||||
# getSaveFileName returns a tuple. The first item is a list of str's that represents the path. The list is
|
||||
# empty if the user cancels the dialog.
|
||||
paths = [str_to_path(path) for path in file_names]
|
||||
return paths, selected_filter
|
||||
|
||||
@classmethod
|
||||
def getSaveFileName(cls, *args, **kwargs):
|
||||
"""
|
||||
Wraps `getSaveFileName` so that it can be called with, and return Path objects
|
||||
|
||||
:type parent: QtWidgets.QWidget or None
|
||||
:type caption: str
|
||||
:type directory: pathlib.Path
|
||||
:type filter: str
|
||||
:type initialFilter: str
|
||||
:type options: QtWidgets.QFileDialog.Options
|
||||
:rtype: tuple[Path or None, str]
|
||||
"""
|
||||
args, kwargs = replace_params(args, kwargs, ((2, 'directory', path_to_str),))
|
||||
|
||||
file_name, selected_filter = super().getSaveFileName(*args, **kwargs)
|
||||
|
||||
# getSaveFileName returns a tuple. The first item represents the path as a str. The string is empty if the user
|
||||
# cancels the dialog.
|
||||
return str_to_path(file_name), selected_filter
|
56
openlp/core/ui/lib/pathedit.py
Executable file → Normal file
@ -20,12 +20,14 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
from enum import Enum
|
||||
import os.path
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
from openlp.core.common import UiStrings, translate
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import build_icon
|
||||
from openlp.core.ui.lib.filedialog import FileDialog
|
||||
|
||||
|
||||
class PathType(Enum):
|
||||
@ -38,11 +40,11 @@ class PathEdit(QtWidgets.QWidget):
|
||||
The :class:`~openlp.core.ui.lib.pathedit.PathEdit` class subclasses QWidget to create a custom widget for use when
|
||||
a file or directory needs to be selected.
|
||||
"""
|
||||
pathChanged = QtCore.pyqtSignal(str)
|
||||
pathChanged = QtCore.pyqtSignal(Path)
|
||||
|
||||
def __init__(self, parent=None, path_type=PathType.Files, default_path=None, dialog_caption=None, show_revert=True):
|
||||
"""
|
||||
Initalise the PathEdit widget
|
||||
Initialise the PathEdit widget
|
||||
|
||||
:param parent: The parent of the widget. This is just passed to the super method.
|
||||
:type parent: QWidget or None
|
||||
@ -51,9 +53,9 @@ class PathEdit(QtWidgets.QWidget):
|
||||
:type dialog_caption: str
|
||||
|
||||
:param default_path: The default path. This is set as the path when the revert button is clicked
|
||||
:type default_path: str
|
||||
:type default_path: pathlib.Path
|
||||
|
||||
:param show_revert: Used to determin if the 'revert button' should be visible.
|
||||
:param show_revert: Used to determine if the 'revert button' should be visible.
|
||||
:type show_revert: bool
|
||||
|
||||
:return: None
|
||||
@ -79,7 +81,6 @@ class PathEdit(QtWidgets.QWidget):
|
||||
widget_layout = QtWidgets.QHBoxLayout()
|
||||
widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.line_edit = QtWidgets.QLineEdit(self)
|
||||
self.line_edit.setText(self._path)
|
||||
widget_layout.addWidget(self.line_edit)
|
||||
self.browse_button = QtWidgets.QToolButton(self)
|
||||
self.browse_button.setIcon(build_icon(':/general/general_open.png'))
|
||||
@ -101,7 +102,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
A property getter method to return the selected path.
|
||||
|
||||
:return: The selected path
|
||||
:rtype: str
|
||||
:rtype: pathlib.Path
|
||||
"""
|
||||
return self._path
|
||||
|
||||
@ -111,11 +112,15 @@ class PathEdit(QtWidgets.QWidget):
|
||||
A Property setter method to set the selected path
|
||||
|
||||
:param path: The path to set the widget to
|
||||
:type path: str
|
||||
:type path: pathlib.Path
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
self._path = path
|
||||
self.line_edit.setText(path)
|
||||
self.line_edit.setToolTip(path)
|
||||
text = path_to_str(path)
|
||||
self.line_edit.setText(text)
|
||||
self.line_edit.setToolTip(text)
|
||||
|
||||
@property
|
||||
def path_type(self):
|
||||
@ -124,7 +129,7 @@ class PathEdit(QtWidgets.QWidget):
|
||||
selecting a file or directory.
|
||||
|
||||
:return: The type selected
|
||||
:rtype: Enum of PathEdit
|
||||
:rtype: PathType
|
||||
"""
|
||||
return self._path_type
|
||||
|
||||
@ -133,8 +138,11 @@ class PathEdit(QtWidgets.QWidget):
|
||||
"""
|
||||
A Property setter method to set the path type
|
||||
|
||||
:param path: The type of path to select
|
||||
:type path: Enum of PathEdit
|
||||
:param path_type: The type of path to select
|
||||
:type path_type: PathType
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
self._path_type = path_type
|
||||
self.update_button_tool_tips()
|
||||
@ -142,7 +150,9 @@ class PathEdit(QtWidgets.QWidget):
|
||||
def update_button_tool_tips(self):
|
||||
"""
|
||||
Called to update the tooltips on the buttons. This is changing path types, and when the widget is initalised
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
if self._path_type == PathType.Directories:
|
||||
self.browse_button.setToolTip(translate('OpenLP.PathEdit', 'Browse for directory.'))
|
||||
@ -156,21 +166,21 @@ class PathEdit(QtWidgets.QWidget):
|
||||
A handler to handle a click on the browse button.
|
||||
|
||||
Show the QFileDialog and process the input from the user
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
caption = self.dialog_caption
|
||||
path = ''
|
||||
path = None
|
||||
if self._path_type == PathType.Directories:
|
||||
if not caption:
|
||||
caption = translate('OpenLP.PathEdit', 'Select Directory')
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self, caption,
|
||||
self._path, QtWidgets.QFileDialog.ShowDirsOnly)
|
||||
path = FileDialog.getExistingDirectory(self, caption, self._path, FileDialog.ShowDirsOnly)
|
||||
elif self._path_type == PathType.Files:
|
||||
if not caption:
|
||||
caption = self.dialog_caption = translate('OpenLP.PathEdit', 'Select File')
|
||||
path, filter_used = QtWidgets.QFileDialog.getOpenFileName(self, caption, self._path, self.filters)
|
||||
path, filter_used = FileDialog.getOpenFileName(self, caption, self._path, self.filters)
|
||||
if path:
|
||||
path = os.path.normpath(path)
|
||||
self.on_new_path(path)
|
||||
|
||||
def on_revert_button_clicked(self):
|
||||
@ -178,16 +188,21 @@ class PathEdit(QtWidgets.QWidget):
|
||||
A handler to handle a click on the revert button.
|
||||
|
||||
Set the new path to the value of the default_path instance variable.
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
self.on_new_path(self.default_path)
|
||||
|
||||
def on_line_edit_editing_finished(self):
|
||||
"""
|
||||
A handler to handle when the line edit has finished being edited.
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
self.on_new_path(self.line_edit.text())
|
||||
path = str_to_path(self.line_edit.text())
|
||||
self.on_new_path(path)
|
||||
|
||||
def on_new_path(self, path):
|
||||
"""
|
||||
@ -196,9 +211,10 @@ class PathEdit(QtWidgets.QWidget):
|
||||
Emits the pathChanged Signal
|
||||
|
||||
:param path: The new path
|
||||
:type path: str
|
||||
:type path: pathlib.Path
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
if self._path != path:
|
||||
self.path = path
|
||||
|
@ -30,10 +30,13 @@ import time
|
||||
from datetime import datetime
|
||||
from distutils import dir_util
|
||||
from distutils.errors import DistutilsFileError
|
||||
from pathlib import Path
|
||||
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 +52,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 +517,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 +547,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 +810,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
|
||||
"""
|
||||
@ -864,7 +871,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
setting_sections.extend([plugin.name for plugin in self.plugin_manager.plugins])
|
||||
# Copy the settings file to the tmp dir, because we do not want to change the original one.
|
||||
temp_directory = os.path.join(str(gettempdir()), 'openlp')
|
||||
check_directory_exists(temp_directory)
|
||||
check_directory_exists(Path(temp_directory))
|
||||
temp_config = os.path.join(temp_directory, os.path.basename(import_file_name))
|
||||
shutil.copyfile(import_file_name, temp_config)
|
||||
settings = Settings()
|
||||
|
@ -146,5 +146,6 @@ def format_milliseconds(milliseconds):
|
||||
|
||||
from .mediacontroller import MediaController
|
||||
from .playertab import PlayerTab
|
||||
from .endpoint import media_endpoint
|
||||
|
||||
__all__ = ['MediaController', 'PlayerTab']
|
||||
|
@ -19,40 +19,54 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
"""
|
||||
Provide a work around for a bug in QFileDialog <https://bugs.launchpad.net/openlp/+bug/1209515>
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from urllib import parse
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
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.common import UiStrings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
media_endpoint = Endpoint('media')
|
||||
|
||||
class FileDialog(QtWidgets.QFileDialog):
|
||||
|
||||
@media_endpoint.route('play')
|
||||
@requires_auth
|
||||
def media_play(request):
|
||||
"""
|
||||
Subclass QFileDialog to work round a bug
|
||||
Handles requests for playing media
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
@staticmethod
|
||||
def getOpenFileNames(parent, *args, **kwargs):
|
||||
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):
|
||||
"""
|
||||
Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
|
||||
files
|
||||
Handles requests for pausing media
|
||||
|
||||
:param request: The http request object.
|
||||
"""
|
||||
files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs)
|
||||
file_list = []
|
||||
for file in files:
|
||||
if not os.path.exists(file):
|
||||
log.info('File not found. Attempting to unquote.')
|
||||
file = parse.unquote(file)
|
||||
if not os.path.exists(file):
|
||||
log.error('File {text} not found.'.format(text=file))
|
||||
QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
|
||||
UiStrings().FileNotFoundMessage.format(name=file))
|
||||
continue
|
||||
file_list.append(file)
|
||||
return file_list
|
||||
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):
|
||||
"""
|
||||
@ -174,7 +177,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
Check to see if we have any media Player's available.
|
||||
"""
|
||||
log.debug('_check_available_media_players')
|
||||
controller_dir = os.path.join('openlp', 'core', 'ui', 'media')
|
||||
controller_dir = os.path.join('core', 'ui', 'media')
|
||||
glob_pattern = os.path.join(controller_dir, '*player.py')
|
||||
extension_loader(glob_pattern, ['mediaplayer.py'])
|
||||
player_classes = MediaPlayer.__subclasses__()
|
||||
@ -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
|
||||
|
@ -176,7 +176,7 @@ class PrintServiceForm(QtWidgets.QDialog, Ui_PrintServiceDialog, RegistryPropert
|
||||
html_data = self._add_element('html')
|
||||
self._add_element('head', parent=html_data)
|
||||
self._add_element('title', self.title_line_edit.text(), html_data.head)
|
||||
css_path = os.path.join(str(AppLocation.get_data_path()), 'serviceprint', 'service_print.css')
|
||||
css_path = AppLocation.get_data_path() / 'serviceprint' / 'service_print.css'
|
||||
custom_css = get_text_file_string(css_path)
|
||||
if not custom_css:
|
||||
custom_css = DEFAULT_CSS
|
||||
|
@ -28,6 +28,7 @@ import os
|
||||
import shutil
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from tempfile import mkstemp
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@ -587,7 +588,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
audio_from = os.path.join(self.service_path, audio_from)
|
||||
save_file = os.path.join(self.service_path, audio_to)
|
||||
save_path = os.path.split(save_file)[0]
|
||||
check_directory_exists(save_path)
|
||||
check_directory_exists(Path(save_path))
|
||||
if not os.path.exists(save_file):
|
||||
shutil.copy(audio_from, save_file)
|
||||
zip_file.write(audio_from, audio_to)
|
||||
@ -614,7 +615,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
success = False
|
||||
self.main_window.add_recent_file(path_file_name)
|
||||
self.set_modified(False)
|
||||
delete_file(temp_file_name)
|
||||
delete_file(Path(temp_file_name))
|
||||
return success
|
||||
|
||||
def save_local_file(self):
|
||||
@ -669,7 +670,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
return self.save_file_as()
|
||||
self.main_window.add_recent_file(path_file_name)
|
||||
self.set_modified(False)
|
||||
delete_file(temp_file_name)
|
||||
delete_file(Path(temp_file_name))
|
||||
return success
|
||||
|
||||
def save_file_as(self, field=None):
|
||||
@ -774,7 +775,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
self.set_file_name(file_name)
|
||||
self.main_window.display_progress_bar(len(items))
|
||||
self.process_service_items(items)
|
||||
delete_file(p_file)
|
||||
delete_file(Path(p_file))
|
||||
self.main_window.add_recent_file(file_name)
|
||||
self.set_modified(False)
|
||||
Settings().setValue('servicemanager/last file', file_name)
|
||||
@ -1343,7 +1344,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
Empties the service_path of temporary files on system exit.
|
||||
"""
|
||||
for file_name in os.listdir(self.service_path):
|
||||
file_path = os.path.join(self.service_path, file_name)
|
||||
file_path = Path(self.service_path, file_name)
|
||||
delete_file(file_path)
|
||||
if os.path.exists(os.path.join(self.service_path, 'audio')):
|
||||
shutil.rmtree(os.path.join(self.service_path, 'audio'), True)
|
||||
|
@ -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,10 +24,12 @@ The Theme wizard
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, UiStrings, translate, get_images_filter, is_not_image_file
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib.theme import BackgroundType, BackgroundGradientType
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui import ThemeLayoutForm
|
||||
@ -187,7 +189,8 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
"""
|
||||
background_image = BackgroundType.to_string(BackgroundType.Image)
|
||||
if self.page(self.currentId()) == self.background_page and \
|
||||
self.theme.background_type == background_image and is_not_image_file(self.theme.background_filename):
|
||||
self.theme.background_type == background_image and \
|
||||
is_not_image_file(Path(self.theme.background_filename)):
|
||||
QtWidgets.QMessageBox.critical(self, translate('OpenLP.ThemeWizard', 'Background Image Empty'),
|
||||
translate('OpenLP.ThemeWizard', 'You have not selected a '
|
||||
'background image. Please select one before continuing.'))
|
||||
@ -316,11 +319,11 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
self.setField('background_type', 1)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Image):
|
||||
self.image_color_button.color = self.theme.background_border_color
|
||||
self.image_path_edit.path = self.theme.background_filename
|
||||
self.image_path_edit.path = str_to_path(self.theme.background_filename)
|
||||
self.setField('background_type', 2)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||
self.video_color_button.color = self.theme.background_border_color
|
||||
self.video_path_edit.path = self.theme.background_filename
|
||||
self.video_path_edit.path = str_to_path(self.theme.background_filename)
|
||||
self.setField('background_type', 4)
|
||||
elif self.theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
||||
self.setField('background_type', 3)
|
||||
@ -448,18 +451,18 @@ class ThemeForm(QtWidgets.QWizard, Ui_ThemeWizard, RegistryProperties):
|
||||
"""
|
||||
self.theme.background_end_color = color
|
||||
|
||||
def on_image_path_edit_path_changed(self, filename):
|
||||
def on_image_path_edit_path_changed(self, file_path):
|
||||
"""
|
||||
Background Image button pushed.
|
||||
"""
|
||||
self.theme.background_filename = filename
|
||||
self.theme.background_filename = path_to_str(file_path)
|
||||
self.set_background_page_values()
|
||||
|
||||
def on_video_path_edit_path_changed(self, filename):
|
||||
def on_video_path_edit_path_changed(self, file_path):
|
||||
"""
|
||||
Background video button pushed.
|
||||
"""
|
||||
self.theme.background_filename = filename
|
||||
self.theme.background_filename = path_to_str(file_path)
|
||||
self.set_background_page_values()
|
||||
|
||||
def on_main_color_changed(self, color):
|
||||
|
@ -22,22 +22,24 @@
|
||||
"""
|
||||
The Theme Manager manages adding, deleteing and modifying of themes.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from xml.etree.ElementTree import ElementTree, XML
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, OpenLPMixin, RegistryMixin, \
|
||||
UiStrings, check_directory_exists, translate, is_win, get_filesystem_encoding, delete_file
|
||||
from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import ImageSource, ValidationError, get_text_file_string, build_icon, \
|
||||
check_item_selected, create_thumb, validate_thumb
|
||||
from openlp.core.lib.theme import Theme, BackgroundType
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
|
||||
from openlp.core.ui import FileRenameForm, ThemeForm
|
||||
from openlp.core.ui.lib import OpenLPToolbar
|
||||
from openlp.core.ui.lib.filedialog import FileDialog
|
||||
from openlp.core.common.languagemanager import get_locale_key
|
||||
|
||||
|
||||
@ -160,9 +162,9 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
Set up the theme path variables
|
||||
"""
|
||||
self.path = str(AppLocation.get_section_data_path(self.settings_section))
|
||||
check_directory_exists(self.path)
|
||||
check_directory_exists(Path(self.path))
|
||||
self.thumb_path = os.path.join(self.path, 'thumbnails')
|
||||
check_directory_exists(self.thumb_path)
|
||||
check_directory_exists(Path(self.thumb_path))
|
||||
|
||||
def check_list_state(self, item, field=None):
|
||||
"""
|
||||
@ -354,8 +356,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
"""
|
||||
self.theme_list.remove(theme)
|
||||
thumb = '{name}.png'.format(name=theme)
|
||||
delete_file(os.path.join(self.path, thumb))
|
||||
delete_file(os.path.join(self.thumb_path, thumb))
|
||||
delete_file(Path(self.path, thumb))
|
||||
delete_file(Path(self.thumb_path, thumb))
|
||||
try:
|
||||
# Windows is always unicode, so no need to encode filenames
|
||||
if is_win():
|
||||
@ -424,15 +426,17 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
those files. This process will only load version 2 themes.
|
||||
:param field:
|
||||
"""
|
||||
files = FileDialog.getOpenFileNames(self,
|
||||
file_paths, selected_filter = FileDialog.getOpenFileNames(
|
||||
self,
|
||||
translate('OpenLP.ThemeManager', 'Select Theme Import File'),
|
||||
Settings().value(self.settings_section + '/last directory import'),
|
||||
str_to_path(Settings().value(self.settings_section + '/last directory import')),
|
||||
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
|
||||
self.log_info('New Themes {name}'.format(name=str(files)))
|
||||
if not files:
|
||||
self.log_info('New Themes {file_paths}'.format(file_paths=file_paths))
|
||||
if not file_paths:
|
||||
return
|
||||
self.application.set_busy_cursor()
|
||||
for file_name in files:
|
||||
for file_path in file_paths:
|
||||
file_name = path_to_str(file_path)
|
||||
Settings().setValue(self.settings_section + '/last directory import', str(file_name))
|
||||
self.unzip_theme(file_name, self.path)
|
||||
self.load_themes()
|
||||
@ -447,7 +451,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
for theme_file in files:
|
||||
theme_file = os.path.join(self.path, str(theme_file))
|
||||
self.unzip_theme(theme_file, self.path)
|
||||
delete_file(theme_file)
|
||||
delete_file(Path(theme_file))
|
||||
files = AppLocation.get_files(self.settings_section, '.png')
|
||||
# No themes have been found so create one
|
||||
if not files:
|
||||
@ -511,12 +515,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
:return: The theme object.
|
||||
"""
|
||||
self.log_debug('get theme data for theme {name}'.format(name=theme_name))
|
||||
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
|
||||
theme_data = get_text_file_string(theme_file)
|
||||
theme_file_path = Path(self.path, str(theme_name), '{file_name}.json'.format(file_name=theme_name))
|
||||
theme_data = get_text_file_string(theme_file_path)
|
||||
jsn = True
|
||||
if not theme_data:
|
||||
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
|
||||
theme_data = get_text_file_string(theme_file)
|
||||
theme_file_path = theme_file_path.with_suffix('.xml')
|
||||
theme_data = get_text_file_string(theme_file_path)
|
||||
jsn = False
|
||||
if not theme_data:
|
||||
self.log_debug('No theme data - using default theme')
|
||||
@ -589,7 +593,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
# is directory or preview file
|
||||
continue
|
||||
full_name = os.path.join(directory, out_name)
|
||||
check_directory_exists(os.path.dirname(full_name))
|
||||
check_directory_exists(Path(os.path.dirname(full_name)))
|
||||
if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
|
||||
file_xml = str(theme_zip.read(name), 'utf-8')
|
||||
out_file = open(full_name, 'w', encoding='utf-8')
|
||||
@ -667,10 +671,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
name = theme.theme_name
|
||||
theme_pretty = theme.export_theme()
|
||||
theme_dir = os.path.join(self.path, name)
|
||||
check_directory_exists(theme_dir)
|
||||
check_directory_exists(Path(theme_dir))
|
||||
theme_file = os.path.join(theme_dir, name + '.json')
|
||||
if self.old_background_image and image_to != self.old_background_image:
|
||||
delete_file(self.old_background_image)
|
||||
delete_file(Path(self.old_background_image))
|
||||
out_file = None
|
||||
try:
|
||||
out_file = open(theme_file, 'w', encoding='utf-8')
|
||||
|
@ -22,6 +22,8 @@
|
||||
"""
|
||||
The Create/Edit theme wizard
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
from openlp.core.common import UiStrings, translate, is_macosx
|
||||
|
@ -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': []}}
|
@ -51,6 +51,7 @@ All CSV files are expected to use a comma (',') as the delimiter and double quot
|
||||
"""
|
||||
import csv
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common import get_file_encoding, translate
|
||||
from openlp.core.lib.exceptions import ValidationError
|
||||
@ -100,7 +101,7 @@ class CSVBible(BibleImport):
|
||||
:return: An iterable yielding namedtuples of type results_tuple
|
||||
"""
|
||||
try:
|
||||
encoding = get_file_encoding(filename)['encoding']
|
||||
encoding = get_file_encoding(Path(filename))['encoding']
|
||||
with open(filename, 'r', encoding=encoding, newline='') as csv_file:
|
||||
csv_reader = csv.reader(csv_file, delimiter=',', quotechar='"')
|
||||
return [results_tuple(*line) for line in csv_reader]
|
||||
|
@ -255,7 +255,7 @@ class BGExtract(RegistryProperties):
|
||||
chapter=chapter,
|
||||
version=version)
|
||||
soup = get_soup_for_bible_ref(
|
||||
'http://biblegateway.com/passage/?{url}'.format(url=url_params),
|
||||
'http://www.biblegateway.com/passage/?{url}'.format(url=url_params),
|
||||
pre_parse_regex=r'<meta name.*?/>', pre_parse_substitute='')
|
||||
if not soup:
|
||||
return None
|
||||
@ -284,7 +284,7 @@ class BGExtract(RegistryProperties):
|
||||
"""
|
||||
log.debug('BGExtract.get_books_from_http("{version}")'.format(version=version))
|
||||
url_params = urllib.parse.urlencode({'action': 'getVersionInfo', 'vid': '{version}'.format(version=version)})
|
||||
reference_url = 'http://biblegateway.com/versions/?{url}#books'.format(url=url_params)
|
||||
reference_url = 'http://www.biblegateway.com/versions/?{url}#books'.format(url=url_params)
|
||||
page = get_web_page(reference_url)
|
||||
if not page:
|
||||
send_error_message('download')
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common import AppLocation, OpenLPMixin, RegistryProperties, Settings, translate, delete_file, UiStrings
|
||||
from openlp.plugins.bibles.lib import LanguageSelection, parse_reference
|
||||
@ -137,7 +138,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
# Remove corrupted files.
|
||||
if name is None:
|
||||
bible.session.close_all()
|
||||
delete_file(os.path.join(self.path, filename))
|
||||
delete_file(Path(self.path, filename))
|
||||
continue
|
||||
log.debug('Bible Name: "{name}"'.format(name=name))
|
||||
self.db_cache[name] = bible
|
||||
@ -185,7 +186,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
bible = self.db_cache[name]
|
||||
bible.session.close_all()
|
||||
bible.session = None
|
||||
return delete_file(os.path.join(bible.path, bible.file))
|
||||
return delete_file(Path(bible.path, bible.file))
|
||||
|
||||
def get_bibles(self):
|
||||
"""
|
||||
|
@ -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():
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
@ -99,7 +100,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
self.list_view.setIndentation(self.list_view.default_indentation)
|
||||
self.list_view.allow_internal_dnd = True
|
||||
self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
|
||||
check_directory_exists(self.service_path)
|
||||
check_directory_exists(Path(self.service_path))
|
||||
# Load images from the database
|
||||
self.load_full_list(
|
||||
self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.filename), initial_load=True)
|
||||
@ -210,8 +211,8 @@ class ImageMediaItem(MediaManagerItem):
|
||||
"""
|
||||
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
|
||||
for image in images:
|
||||
delete_file(os.path.join(self.service_path, os.path.split(image.filename)[1]))
|
||||
delete_file(self.generate_thumbnail_path(image))
|
||||
delete_file(Path(self.service_path, os.path.split(image.filename)[1]))
|
||||
delete_file(Path(self.generate_thumbnail_path(image)))
|
||||
self.manager.delete_object(ImageFilenames, image.id)
|
||||
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
|
||||
for group in image_groups:
|
||||
@ -233,8 +234,8 @@ class ImageMediaItem(MediaManagerItem):
|
||||
if row_item:
|
||||
item_data = row_item.data(0, QtCore.Qt.UserRole)
|
||||
if isinstance(item_data, ImageFilenames):
|
||||
delete_file(os.path.join(self.service_path, row_item.text(0)))
|
||||
delete_file(self.generate_thumbnail_path(item_data))
|
||||
delete_file(Path(self.service_path, row_item.text(0)))
|
||||
delete_file(Path(self.generate_thumbnail_path(item_data)))
|
||||
if item_data.group_id == 0:
|
||||
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
|
||||
else:
|
||||
|
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': []}}
|
@ -22,6 +22,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
@ -301,7 +302,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
"""
|
||||
self.list_view.clear()
|
||||
self.service_path = os.path.join(str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
|
||||
check_directory_exists(self.service_path)
|
||||
check_directory_exists(Path(self.service_path))
|
||||
self.load_list(Settings().value(self.settings_section + '/media files'))
|
||||
self.rebuild_players()
|
||||
|
||||
|
@ -26,12 +26,14 @@ The Media plugin
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from shutil import which
|
||||
from pathlib import Path
|
||||
|
||||
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 +60,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):
|
||||
"""
|
||||
@ -162,8 +166,7 @@ def process_check_binary(program_path):
|
||||
:param program_path:The full path to the binary to check.
|
||||
:return: If exists or not
|
||||
"""
|
||||
program_type = None
|
||||
runlog = check_binary_exists(program_path)
|
||||
runlog = check_binary_exists(Path(program_path))
|
||||
# Analyse the output to see it the program is mediainfo
|
||||
for line in runlog.splitlines():
|
||||
decoded_line = line.decode()
|
||||
|
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': []}}
|
@ -34,6 +34,7 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openlp.core.common import is_win, Registry, get_uno_command, get_uno_instance, delete_file
|
||||
|
||||
@ -275,7 +276,7 @@ class ImpressDocument(PresentationDocument):
|
||||
try:
|
||||
doc.storeToURL(url_path, properties)
|
||||
self.convert_thumbnail(path, index + 1)
|
||||
delete_file(path)
|
||||
delete_file(Path(path))
|
||||
except ErrorCodeIOException as exception:
|
||||
log.exception('ERROR! ErrorCodeIOException {error:d}'.format(error=exception.ErrCode))
|
||||
except:
|
||||
|
@ -23,6 +23,7 @@
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from shutil import which
|
||||
from subprocess import check_output, CalledProcessError
|
||||
|
||||
@ -69,7 +70,7 @@ class PdfController(PresentationController):
|
||||
:return: Type of the binary, 'gs' if ghostscript, 'mudraw' if mudraw, None if invalid.
|
||||
"""
|
||||
program_type = None
|
||||
runlog = check_binary_exists(program_path)
|
||||
runlog = check_binary_exists(Path(program_path))
|
||||
# Analyse the output to see it the program is mudraw, ghostscript or neither
|
||||
for line in runlog.splitlines():
|
||||
decoded_line = line.decode()
|
||||
|
@ -23,6 +23,7 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
@ -98,7 +99,7 @@ class PresentationDocument(object):
|
||||
"""
|
||||
self.slide_number = 0
|
||||
self.file_path = name
|
||||
check_directory_exists(self.get_thumbnail_folder())
|
||||
check_directory_exists(Path(self.get_thumbnail_folder()))
|
||||
|
||||
def load_presentation(self):
|
||||
"""
|
||||
@ -419,8 +420,8 @@ class PresentationController(object):
|
||||
self.thumbnail_folder = os.path.join(
|
||||
str(AppLocation.get_section_data_path(self.settings_section)), 'thumbnails')
|
||||
self.thumbnail_prefix = 'slide'
|
||||
check_directory_exists(self.thumbnail_folder)
|
||||
check_directory_exists(self.temp_folder)
|
||||
check_directory_exists(Path(self.thumbnail_folder))
|
||||
check_directory_exists(Path(self.temp_folder))
|
||||
|
||||
def enabled(self):
|
||||
"""
|
||||
|
@ -20,10 +20,11 @@
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import Settings, UiStrings, translate
|
||||
from openlp.core.lib import SettingsTab, build_icon
|
||||
from openlp.core.common.path import path_to_str, str_to_path
|
||||
from openlp.core.lib import SettingsTab
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.ui.lib import PathEdit
|
||||
from openlp.plugins.presentations.lib.pdfcontroller import PdfController
|
||||
@ -156,7 +157,7 @@ class PresentationTab(SettingsTab):
|
||||
self.program_path_edit.setEnabled(enable_pdf_program)
|
||||
pdf_program = Settings().value(self.settings_section + '/pdf_program')
|
||||
if pdf_program:
|
||||
self.program_path_edit.path = pdf_program
|
||||
self.program_path_edit.path = str_to_path(pdf_program)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
@ -192,7 +193,7 @@ class PresentationTab(SettingsTab):
|
||||
Settings().setValue(setting_key, self.ppt_window_check_box.checkState())
|
||||
changed = True
|
||||
# Save pdf-settings
|
||||
pdf_program = self.program_path_edit.path
|
||||
pdf_program = path_to_str(self.program_path_edit.path)
|
||||
enable_pdf_program = self.pdf_program_check_box.checkState()
|
||||
# If the given program is blank disable using the program
|
||||
if pdf_program == '':
|
||||
@ -219,12 +220,13 @@ class PresentationTab(SettingsTab):
|
||||
checkbox.setEnabled(controller.is_available())
|
||||
self.set_controller_text(checkbox, controller)
|
||||
|
||||
def on_program_path_edit_path_changed(self, filename):
|
||||
def on_program_path_edit_path_changed(self, new_path):
|
||||
"""
|
||||
Select the mudraw or ghostscript binary that should be used.
|
||||
"""
|
||||
if filename:
|
||||
if not PdfController.process_check_binary(filename):
|
||||
new_path = path_to_str(new_path)
|
||||
if new_path:
|
||||
if not PdfController.process_check_binary(new_path):
|
||||
critical_error_message_box(UiStrings().Error,
|
||||
translate('PresentationPlugin.PresentationTab',
|
||||
'The program is not ghostscript or mudraw which is required.'))
|
||||
|
@ -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):
|
||||
"""
|
||||
@ -121,7 +125,7 @@ class PresentationPlugin(Plugin):
|
||||
Check to see if we have any presentation software available. If not do not install the plugin.
|
||||
"""
|
||||
log.debug('check_pre_conditions')
|
||||
controller_dir = os.path.join('openlp', 'plugins', 'presentations', 'lib')
|
||||
controller_dir = os.path.join('plugins', 'presentations', 'lib')
|
||||
glob_pattern = os.path.join(controller_dir, '*controller.py')
|
||||
extension_loader(glob_pattern, ['presentationcontroller.py'])
|
||||
controller_classes = PresentationController.__subclasses__()
|
||||
|
@ -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()
|