This commit is contained in:
Tim Bentley 2017-08-27 17:26:06 +01:00
commit b546f3cecb
152 changed files with 5225 additions and 22383 deletions

View File

@ -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():

View File

@ -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']

View File

@ -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

View 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}}

View 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 &amp; 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

View 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')

View 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

View 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

View 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)

View 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')

View 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)

View 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
View 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

View File

@ -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):
"""

View 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)

View File

@ -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:

View File

@ -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:

View File

@ -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']

View 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)

View File

@ -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

View File

@ -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')

View File

@ -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,12 +67,18 @@ class VersionThread(QtCore.QThread):
"""
self.sleep(1)
log.debug('Version thread - run')
app_version = get_application_version()
version = check_latest_version(app_version)
log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
version2=LooseVersion(str(app_version['full']))))
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
self.main_window.openlp_version_check.emit('{version}'.format(version=version))
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)),
version2=LooseVersion(str(app_version['full']))))
if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
self.main_window.openlp_version_check.emit('{version}'.format(version=version))
def get_application_version():

View File

@ -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')
if file_handle.read(3) != '\xEF\xBB\xBF':
# no BOM was found
file_handle.seek(0)
content = file_handle.read()
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

View File

@ -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)

View File

@ -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)

View File

@ -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'),
self.on_new_file_masks)
log.info('New files(s) {files}'.format(files=files))
if files:
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) {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):

View File

@ -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 = []

View File

@ -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

View File

@ -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:
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 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
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:
self.projector_errors = {}
# Fan
if data[0] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
PJLINK_ERST_STATUS[data[0]]
# Lamp
if data[1] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
PJLINK_ERST_STATUS[data[1]]
# Temp
if data[2] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
PJLINK_ERST_STATUS[data[2]]
# Cover
if data[3] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
PJLINK_ERST_STATUS[data[3]]
# Filter
if data[4] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
PJLINK_ERST_STATUS[data[4]]
# Other
if data[5] != '0':
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
PJLINK_ERST_STATUS[data[5]]
# No errors
return
# We have some sort of status error, so check out what it/they are
self.projector_errors = {}
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[fan]
if lamp != PJLINK_ERST_STATUS[E_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
PJLINK_ERST_STATUS[lamp]
if temp != PJLINK_ERST_STATUS[E_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
PJLINK_ERST_STATUS[temp]
if cover != PJLINK_ERST_STATUS[E_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
PJLINK_ERST_STATUS[cover]
if filt != PJLINK_ERST_STATUS[E_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
PJLINK_ERST_STATUS[filt]
if other != PJLINK_ERST_STATUS[E_OK]:
self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
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,8 +840,8 @@ 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 '
'than projector stated class'.format(ip=self.ip))
log.warning('({ip}) get_data(): Projector returned class reply higher '
'than projector stated class'.format(ip=self.ip))
return self.process_command(cmd, data)
@QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
@ -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,

View File

@ -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 = ''

View File

@ -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)

View File

@ -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')

View File

@ -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'))

View File

@ -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
View 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
View 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

View File

@ -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()

View File

@ -146,5 +146,6 @@ def format_milliseconds(milliseconds):
from .mediacontroller import MediaController
from .playertab import PlayerTab
from .endpoint import media_endpoint
__all__ = ['MediaController', 'PlayerTab']

View File

@ -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):
"""
Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
files
"""
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_play(live, False)
return {'results': {'success': status}}
@media_endpoint.route('pause')
@requires_auth
def media_pause(request):
"""
Handles requests for pausing media
:param request: The http request object.
"""
media = Registry().get('media_controller')
live = Registry().get('live_controller')
status = media.media_pause(live)
return {'results': {'success': status}}
@media_endpoint.route('stop')
@requires_auth
def media_stop(request):
"""
Handles requests for stopping
:param request: The http request object.
"""
event = getattr(Registry().get('live_controller'), 'mediacontroller_live_stop')
event.emit()
return {'results': {'success': True}}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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):
"""

View File

@ -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):

View File

@ -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,
translate('OpenLP.ThemeManager', 'Select Theme Import File'),
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:
file_paths, selected_filter = FileDialog.getOpenFileNames(
self,
translate('OpenLP.ThemeManager', 'Select Theme Import File'),
str_to_path(Settings().value(self.settings_section + '/last directory import')),
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
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')

View File

@ -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

View File

@ -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):
"""

View 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}}

View File

@ -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):
"""

View 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': []}}

View File

@ -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]

View File

@ -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')

View File

@ -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):
"""

View File

@ -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():

View 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': []}}

View 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': []}}

View File

@ -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():

View File

@ -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:

View 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': []}}

View File

@ -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()

View File

@ -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()

View 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': []}}

View File

@ -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:

View File

@ -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()

View File

@ -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):
"""

View File

@ -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.'))

View File

@ -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__()

View File

@ -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.
"""

View 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')

View File

@ -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)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

View File

@ -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>

View File

@ -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("&nbsp;<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("&nbsp;<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 += '&nbsp;';}
}
} 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 += '&nbsp;';}
}
};
} else {
if (!$tail && $remainder.charAt(0) == ' ') {for (c = 0; c < $chordlen; c++) {w += '&nbsp;';}}
}
if (w!='') {
if (w[0] == '_') {
ws_length = w.length;
if (ws_length==1) {
w = '&ndash;';
} else {
wsl_mod = Math.floor(ws_length / 2);
ws_right = ws_left = new Array(wsl_mod + 1).join(' ');
w = ws_left + '&ndash;' + 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();
});
});

View File

@ -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();

View File

@ -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();

View File

@ -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("&nbsp;<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("&nbsp;<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();

View File

@ -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 &amp; 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()

Some files were not shown because too many files have changed in this diff Show More