From ef2f798f6fce2da9a0f71c90dbaf098ed936e66a Mon Sep 17 00:00:00 2001 From: Simon Hanna Date: Mon, 27 Jan 2020 22:57:58 +0000 Subject: [PATCH] Update remote API to use Flask, and be more RESTful --- openlp/core/api/__init__.py | 18 ++ openlp/core/api/deploy.py | 3 +- openlp/core/api/endpoint/__init__.py | 23 --- openlp/core/api/endpoint/controller.py | 148 -------------- openlp/core/api/endpoint/core.py | 174 ---------------- openlp/core/api/endpoint/pluginhelpers.py | 132 ------------- openlp/core/api/endpoint/remote.py | 40 ---- openlp/core/api/endpoint/service.py | 99 ---------- openlp/core/api/http/__init__.py | 31 --- openlp/core/api/http/endpoint.py | 80 -------- openlp/core/api/http/errors.py | 64 ------ openlp/core/api/http/server.py | 33 +--- openlp/core/api/http/wsgiapp.py | 185 ------------------ openlp/core/api/lib.py | 50 +++++ openlp/core/api/main.py | 14 ++ openlp/core/api/versions/__init__.py | 0 openlp/core/api/versions/v1/__init__.py | 9 + openlp/core/api/versions/v1/controller.py | 83 ++++++++ openlp/core/api/versions/v1/core.py | 40 ++++ openlp/core/api/versions/v1/service.py | 48 +++++ openlp/core/api/versions/v2/__init__.py | 9 + openlp/core/api/versions/v2/controller.py | 84 ++++++++ openlp/core/api/versions/v2/core.py | 68 +++++++ openlp/core/api/versions/v2/service.py | 61 ++++++ openlp/core/ui/media/endpoint.py | 73 ------- openlp/core/ui/media/mediacontroller.py | 5 +- openlp/core/ui/media/remote.py | 98 ++++++++++ openlp/core/ui/slidecontroller.py | 3 +- openlp/plugins/alerts/alertsplugin.py | 6 +- openlp/plugins/alerts/endpoint.py | 58 ------ openlp/plugins/alerts/remote.py | 39 ++++ openlp/plugins/bibles/bibleplugin.py | 6 +- openlp/plugins/bibles/endpoint.py | 99 ---------- openlp/plugins/bibles/remote.py | 112 +++++++++++ openlp/plugins/custom/customplugin.py | 6 +- openlp/plugins/custom/endpoint.py | 99 ---------- openlp/plugins/custom/remote.py | 118 +++++++++++ openlp/plugins/images/endpoint.py | 112 ----------- openlp/plugins/images/imageplugin.py | 6 +- openlp/plugins/images/remote.py | 118 +++++++++++ openlp/plugins/media/endpoint.py | 99 ---------- openlp/plugins/media/mediaplugin.py | 6 +- openlp/plugins/media/remote.py | 118 +++++++++++ openlp/plugins/presentations/endpoint.py | 113 ----------- .../presentations/presentationplugin.py | 7 +- openlp/plugins/presentations/remote.py | 118 +++++++++++ openlp/plugins/songs/endpoint.py | 99 ---------- openlp/plugins/songs/lib/mediaitem.py | 6 +- openlp/plugins/songs/remote.py | 118 +++++++++++ openlp/plugins/songs/songsplugin.py | 6 +- scripts/check_dependencies.py | 2 + setup.py | 2 + .../api/endpoint/test_controller.py | 15 +- .../openlp_core/api/endpoint/test_remote.py | 43 ---- .../openlp_core/api/http/test_error.py | 60 ------ .../openlp_core/api/http/test_wsgiapp.py | 88 --------- .../functional/openlp_core/api/v2/__init__.py | 0 .../functional/openlp_core/api/v2/conftest.py | 9 + .../openlp_core/api/v2/test_controller.py | 57 ++++++ .../openlp_core/api/v2/test_core.py | 113 +++++++++++ .../openlp_core/api/v2/test_service.py | 56 ++++++ .../openlp_core/ui/test_mainwindow.py | 4 +- 62 files changed, 1602 insertions(+), 1991 deletions(-) delete mode 100644 openlp/core/api/endpoint/__init__.py delete mode 100644 openlp/core/api/endpoint/controller.py delete mode 100644 openlp/core/api/endpoint/core.py delete mode 100644 openlp/core/api/endpoint/pluginhelpers.py delete mode 100644 openlp/core/api/endpoint/remote.py delete mode 100644 openlp/core/api/endpoint/service.py delete mode 100644 openlp/core/api/http/endpoint.py delete mode 100644 openlp/core/api/http/errors.py delete mode 100644 openlp/core/api/http/wsgiapp.py create mode 100644 openlp/core/api/lib.py create mode 100644 openlp/core/api/main.py create mode 100644 openlp/core/api/versions/__init__.py create mode 100644 openlp/core/api/versions/v1/__init__.py create mode 100644 openlp/core/api/versions/v1/controller.py create mode 100644 openlp/core/api/versions/v1/core.py create mode 100644 openlp/core/api/versions/v1/service.py create mode 100644 openlp/core/api/versions/v2/__init__.py create mode 100644 openlp/core/api/versions/v2/controller.py create mode 100644 openlp/core/api/versions/v2/core.py create mode 100644 openlp/core/api/versions/v2/service.py delete mode 100644 openlp/core/ui/media/endpoint.py create mode 100644 openlp/core/ui/media/remote.py delete mode 100644 openlp/plugins/alerts/endpoint.py create mode 100644 openlp/plugins/alerts/remote.py delete mode 100644 openlp/plugins/bibles/endpoint.py create mode 100644 openlp/plugins/bibles/remote.py delete mode 100644 openlp/plugins/custom/endpoint.py create mode 100644 openlp/plugins/custom/remote.py delete mode 100644 openlp/plugins/images/endpoint.py create mode 100644 openlp/plugins/images/remote.py delete mode 100644 openlp/plugins/media/endpoint.py create mode 100644 openlp/plugins/media/remote.py delete mode 100644 openlp/plugins/presentations/endpoint.py create mode 100644 openlp/plugins/presentations/remote.py delete mode 100644 openlp/plugins/songs/endpoint.py create mode 100644 openlp/plugins/songs/remote.py delete mode 100644 tests/functional/openlp_core/api/endpoint/test_remote.py delete mode 100644 tests/functional/openlp_core/api/http/test_error.py delete mode 100644 tests/functional/openlp_core/api/http/test_wsgiapp.py create mode 100644 tests/functional/openlp_core/api/v2/__init__.py create mode 100644 tests/functional/openlp_core/api/v2/conftest.py create mode 100644 tests/functional/openlp_core/api/v2/test_controller.py create mode 100644 tests/functional/openlp_core/api/v2/test_core.py create mode 100644 tests/functional/openlp_core/api/v2/test_service.py diff --git a/openlp/core/api/__init__.py b/openlp/core/api/__init__.py index fa1ec5512..0b8b3c015 100644 --- a/openlp/core/api/__init__.py +++ b/openlp/core/api/__init__.py @@ -18,3 +18,21 @@ # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## + +from flask import Flask +from flask_cors import CORS + +from openlp.core.api.versions import v1 +from openlp.core.api.versions import v2 +from openlp.core.api.main import main_views + +app = Flask(__name__) +CORS(app) + +app.register_blueprint(main_views) +v1.register_blueprints(app) +v2.register_blueprints(app) + + +def register_blueprint(blueprint, url_prefix=None): + app.register_blueprint(blueprint, url_prefix) diff --git a/openlp/core/api/deploy.py b/openlp/core/api/deploy.py index 492b6af35..48343712c 100644 --- a/openlp/core/api/deploy.py +++ b/openlp/core/api/deploy.py @@ -65,6 +65,5 @@ def download_and_check(callback=None): file_size = get_url_file_size('https://get.openlp.org/webclient/site.zip') callback.setRange(0, file_size) if download_file(callback, 'https://get.openlp.org/webclient/site.zip', - AppLocation.get_section_data_path('remotes') / 'site.zip', - sha256=sha256): + AppLocation.get_section_data_path('remotes') / 'site.zip'): deploy_zipfile(AppLocation.get_section_data_path('remotes'), 'site.zip') diff --git a/openlp/core/api/endpoint/__init__.py b/openlp/core/api/endpoint/__init__.py deleted file mode 100644 index 6fb21355f..000000000 --- a/openlp/core/api/endpoint/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -The Endpoint class, which provides plugins with a way to serve their own portion of the API -""" diff --git a/openlp/core/api/endpoint/controller.py b/openlp/core/api/endpoint/controller.py deleted file mode 100644 index f9200c1d7..000000000 --- a/openlp/core/api/endpoint/controller.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import json -import logging -import os -import urllib.error -import urllib.request -from pathlib import Path - -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.common.applocation import AppLocation -from openlp.core.common.registry import Registry -from openlp.core.common.settings import Settings -from openlp.core.lib import create_thumb -from openlp.core.lib.serviceitem import ItemCapabilities - - -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['verse']: - item['tag'] = str(frame['verse']) - else: - item['tag'] = str(index + 1) - # TODO: Figure out rendering chords - item['chords_text'] = str(frame.get('chords_text', '')) - item['text'] = frame['text'] - item['html'] = current_item.get_rendered_frame(index) - # 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 = AppLocation.get_data_path() / thumbnail_path - # Create thumbnail if it doesn't exists - if not full_thumbnail_path.exists(): - create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False) - Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88) - item['img'] = urllib.request.pathname2url(os.path.sep + str(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) - item['title'] = current_item.title - 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('{controller}/{action:next|previous}') -@requires_auth -def controller_direction(request, controller, action): - """ - Handles requests for setting service items in the slide controller - :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 - - :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}} diff --git a/openlp/core/api/endpoint/core.py b/openlp/core/api/endpoint/core.py deleted file mode 100644 index 2177655e2..000000000 --- a/openlp/core/api/endpoint/core.py +++ /dev/null @@ -1,174 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -The :mod:`~openlp.core.api.endpoint.core` module contains the core API endpoints -""" -import logging -import os - -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.common.i18n import UiStrings, translate -from openlp.core.common.registry import Registry -from openlp.core.lib import image_to_byte -from openlp.core.lib.plugin import PluginStatus, StringContent - - -template_dir = 'templates' -static_dir = 'static' -blank_dir = os.path.join(static_dir, 'index') - - -log = logging.getLogger(__name__) - -chords_endpoint = Endpoint('chords', template_dir=template_dir, static_dir=static_dir) -stage_endpoint = Endpoint('stage', template_dir=template_dir, static_dir=static_dir) -main_endpoint = Endpoint('main', template_dir=template_dir, static_dir=static_dir) -blank_endpoint = Endpoint('', template_dir=template_dir, static_dir=blank_dir) - -FILE_TYPES = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.jpg': 'image/jpeg', - '.gif': 'image/gif', - '.ico': 'image/x-icon', - '.png': 'image/png' -} - -remote = translate('RemotePlugin.Mobile', 'Remote') -stage = translate('RemotePlugin.Mobile', 'Stage View') -live = translate('RemotePlugin.Mobile', 'Live View') -chords = translate('RemotePlugin.Mobile', 'Chords View') - -TRANSLATED_STRINGS = { - 'app_title': "{main} {remote}".format(main=UiStrings().OpenLP, remote=remote), - 'stage_title': "{main} {stage}".format(main=UiStrings().OpenLP, stage=stage), - 'live_title': "{main} {live}".format(main=UiStrings().OpenLP, live=live), - 'chords_title': "{main} {chords}".format(main=UiStrings().OpenLP, chords=chords), - 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'), - 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'), - 'alerts': translate('RemotePlugin.Mobile', 'Alerts'), - 'search': translate('RemotePlugin.Mobile', 'Search'), - 'home': translate('RemotePlugin.Mobile', 'Home'), - 'refresh': translate('RemotePlugin.Mobile', 'Refresh'), - 'blank': translate('RemotePlugin.Mobile', 'Blank'), - 'theme': translate('RemotePlugin.Mobile', 'Theme'), - 'desktop': translate('RemotePlugin.Mobile', 'Desktop'), - 'show': translate('RemotePlugin.Mobile', 'Show'), - 'prev': translate('RemotePlugin.Mobile', 'Prev'), - 'next': translate('RemotePlugin.Mobile', 'Next'), - 'text': translate('RemotePlugin.Mobile', 'Text'), - 'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'), - 'go_live': translate('RemotePlugin.Mobile', 'Go Live'), - 'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'), - 'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'), - 'no_results': translate('RemotePlugin.Mobile', 'No Results'), - 'options': translate('RemotePlugin.Mobile', 'Options'), - 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides'), - 'settings': translate('RemotePlugin.Mobile', 'Settings'), -} - - -@stage_endpoint.route('') -def stage_index(request): - """ - Deliver the page for the /stage url - """ - return stage_endpoint.render_template('stage.mako', **TRANSLATED_STRINGS) - - -@chords_endpoint.route('') -def chords_index(request): - """ - Deliver the page for the /chords url - """ - return chords_endpoint.render_template('chords.mako', **TRANSLATED_STRINGS) - - -@main_endpoint.route('') -def main_index(request): - """ - Deliver the page for the /main url - """ - return main_endpoint.render_template('main.mako', **TRANSLATED_STRINGS) - - -@blank_endpoint.route('') -def index(request): - """ - Deliver the page for the / url - :param request: - """ - return blank_endpoint.render_template('index.mako', **TRANSLATED_STRINGS) - - -@blank_endpoint.route('api/poll') -@blank_endpoint.route('poll') -def poll(request): - """ - Deliver the page for the /poll url - - :param request: - """ - return Registry().get('poller').poll() - - -@blank_endpoint.route('api/display/{display:hide|show|blank|theme|desktop}') -@blank_endpoint.route('display/{display:hide|show|blank|theme|desktop}') -@requires_auth -def toggle_display(request, display): - """ - Deliver the functions for the /display url - :param request: the http request - not used - :param display: the display function to be triggered - """ - Registry().get('live_controller').slidecontroller_toggle_display.emit(display) - return {'results': {'success': True}} - - -@blank_endpoint.route('api/plugin/search') -@blank_endpoint.route('plugin/search') -def plugin_search_list(request): - """ - Deliver a list of active plugins that support search - :param request: the http request - not used - """ - searches = [] - for plugin in Registry().get('plugin_manager').plugins: - if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: - searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])]) - return {'results': {'items': searches}} - - -@main_endpoint.route('image') -def main_image(request): - """ - Return the latest display image as a byte stream. - :param request: base path of the URL. Not used but passed by caller - :return: - """ - live_controller = Registry().get('live_controller') - result = { - 'slide_image': 'data:image/png;base64,' + str(image_to_byte(live_controller.slide_image)) - } - return {'results': result} diff --git a/openlp/core/api/endpoint/pluginhelpers.py b/openlp/core/api/endpoint/pluginhelpers.py deleted file mode 100644 index b02c6f20a..000000000 --- a/openlp/core/api/endpoint/pluginhelpers.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import json -import re -import urllib - -from webob import Response - -from openlp.core.api.http.errors import NotFound -from openlp.core.common.applocation import AppLocation -from openlp.core.common.registry import Registry -from openlp.core.lib import image_to_byte -from openlp.core.lib.plugin import PluginStatus - - -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 str 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(r'(\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 - full_path = AppLocation.get_section_data_path(controller_name) / 'thumbnails' / file_name - if slide: - full_path = full_path / slide - if full_path.exists(): - Registry().get('image_manager').add_image(full_path, full_path.name, None, width, height) - image = Registry().get('image_manager').get_image(full_path, full_path.name, width, height) - return Response(body=image_to_byte(image, False), status=200, content_type='image/png', charset='utf8') diff --git a/openlp/core/api/endpoint/remote.py b/openlp/core/api/endpoint/remote.py deleted file mode 100644 index dcfde5fd5..000000000 --- a/openlp/core/api/endpoint/remote.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.core import TRANSLATED_STRINGS -from openlp.core.api.http.endpoint import Endpoint - - -log = logging.getLogger(__name__) - -remote_endpoint = Endpoint('remote', template_dir='remotes') - - -@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) diff --git a/openlp/core/api/endpoint/service.py b/openlp/core/api/endpoint/service.py deleted file mode 100644 index 305563984..000000000 --- a/openlp/core/api/endpoint/service.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import json -import logging - -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.common.registry 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 diff --git a/openlp/core/api/http/__init__.py b/openlp/core/api/http/__init__.py index 16b4ede89..d06059d38 100644 --- a/openlp/core/api/http/__init__.py +++ b/openlp/core/api/http/__init__.py @@ -24,39 +24,8 @@ from functools import wraps from webob import Response -from openlp.core.api.http.wsgiapp import WSGIApplication from openlp.core.common.settings import Settings -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): """ diff --git a/openlp/core/api/http/endpoint.py b/openlp/core/api/http/endpoint.py deleted file mode 100644 index 6d69824d9..000000000 --- a/openlp/core/api/http/endpoint.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -The Endpoint class, which provides plugins with a way to serve their own portion of the API -""" -from mako.template import Template - -from openlp.core.common.applocation import AppLocation - - -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 - - :param str filename: The file name of the template to render. - :return: The rendered template object. - :rtype: mako.template.Template - """ - root_path = AppLocation.get_section_data_path('remotes') - if not self.template_dir: - raise Exception('No template directory specified') - path = root_path / 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=str(path), input_encoding='utf-8').render(**kwargs) diff --git a/openlp/core/api/http/errors.py b/openlp/core/api/http/errors.py deleted file mode 100644 index 7012689d2..000000000 --- a/openlp/core/api/http/errors.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -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') diff --git a/openlp/core/api/http/server.py b/openlp/core/api/http/server.py index eed1fc829..ac3c35f37 100644 --- a/openlp/core/api/http/server.py +++ b/openlp/core/api/http/server.py @@ -24,16 +24,12 @@ with OpenLP. It uses JSON to communicate with the remotes. """ import logging import time +from secrets import token_hex from PyQt5 import QtCore, QtWidgets from waitress.server import create_server from openlp.core.api.deploy import download_and_check, download_sha256 -from openlp.core.api.endpoint.controller import api_controller_endpoint, controller_endpoint -from openlp.core.api.endpoint.core import blank_endpoint, chords_endpoint, main_endpoint, stage_endpoint -from openlp.core.api.endpoint.remote import remote_endpoint -from openlp.core.api.endpoint.service import api_service_endpoint, service_endpoint -from openlp.core.api.http import application, register_endpoint from openlp.core.api.poll import Poller from openlp.core.common.applocation import AppLocation from openlp.core.common.i18n import UiStrings, translate @@ -43,6 +39,7 @@ from openlp.core.common.registry import Registry, RegistryBase from openlp.core.common.settings import Settings from openlp.core.threading import ThreadWorker, run_thread +from openlp.core.api import app as application log = logging.getLogger(__name__) @@ -59,6 +56,7 @@ class HttpWorker(ThreadWorker): port = Settings().value('api/port') Registry().execute('get_website_version') try: + application.static_folder = str(AppLocation.get_section_data_path('remotes') / 'static') self.server = create_server(application, host=address, port=port) self.server.run() except OSError: @@ -85,6 +83,7 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin): Initialise the http server, and start the http server """ super(HttpServer, self).__init__(parent) + Registry().register('authentication_token', token_hex()) if not Registry().get_flag('no_web_server'): worker = HttpWorker() run_thread(worker, 'http_server') @@ -96,31 +95,9 @@ class HttpServer(RegistryBase, RegistryProperties, LogMixin): """ Register the poll return service and start the servers. """ - self.initialise() + create_paths(AppLocation.get_section_data_path('remotes')) 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) - register_endpoint(remote_endpoint) - - @staticmethod - def initialise(): - """ - Create the internal file structure if it does not exist - :return: - """ - create_paths(AppLocation.get_section_data_path('remotes') / 'assets', - AppLocation.get_section_data_path('remotes') / 'images', - AppLocation.get_section_data_path('remotes') / 'static', - AppLocation.get_section_data_path('remotes') / 'static' / 'index', - AppLocation.get_section_data_path('remotes') / 'templates') def first_time(self): """ diff --git a/openlp/core/api/http/wsgiapp.py b/openlp/core/api/http/wsgiapp.py deleted file mode 100644 index 723c22c6a..000000000 --- a/openlp/core/api/http/wsgiapp.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -App stuff -""" -import json -import logging -import re - -from webob import Request, Response -from webob.static import DirectoryApp - -from openlp.core.api.http.errors import HttpError, NotFound, ServerError -from openlp.core.common.applocation import AppLocation - - -ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE) - -log = logging.getLogger(__name__) - - -def _route_to_regex(route): - r""" - Convert a route to a regular expression - - For example: - - 'songs/{song_id}' becomes 'songs/(?P[^/]+)' - - and - - 'songs/{song_id:\d+}' becomes 'songs/(?P\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 - """ - log.debug("in Make 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') - else: - return Response(body=view_result, status=200, - content_type='text/plain', 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: - static_path = AppLocation.get_section_data_path('remotes') / static_dir - if not static_path.exists(): - log.error('Static path "%s" does not exist. Skipping creating static route/', static_path) - return - self.static_routes[route] = DirectoryApp(str(static_path.resolve())) - - 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) diff --git a/openlp/core/api/lib.py b/openlp/core/api/lib.py new file mode 100644 index 000000000..41790b05b --- /dev/null +++ b/openlp/core/api/lib.py @@ -0,0 +1,50 @@ +import json +from flask import jsonify, Response, request +from functools import wraps +from openlp.core.common.settings import Settings +from openlp.core.common.registry import Registry + + +def login_required(f): + @wraps(f) + def decorated(*args, **kwargs): + if not Settings().value('api/authentication enabled'): + return f(*args, **kwargs) + token = request.headers.get('Authorization', '') + if token == Registry().get('authentication_token'): + return f(*args, **kwargs) + else: + return '', 401 + return decorated + + +def old_success_response(): + return jsonify({'results': {'sucess': True}}) + + +def extract_request(json_string, name): + try: + if json_string: + return json.loads(json_string)['request'][name] + except KeyError: + pass + return None + + +# ----------------------- OLD AUTH SECTION --------------------- +def check_auth(username, password): + return Settings().value('api/user id') == username and \ + Settings().value('api/password') == password + + +def old_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + if not Settings().value('api/authentication enabled'): + return f(*args, **kwargs) + auth = request.authorization + if not auth or not check_auth(auth.username, auth.password): + return Response('', 401, {'WWW-Authenticate': 'Basic realm="OpenLP Login Required"'}) + else: + return f(*args, **kwargs) + return decorated diff --git a/openlp/core/api/main.py b/openlp/core/api/main.py new file mode 100644 index 000000000..fafa51395 --- /dev/null +++ b/openlp/core/api/main.py @@ -0,0 +1,14 @@ +from flask import Blueprint, send_from_directory +from openlp.core.common.applocation import AppLocation + +main_views = Blueprint('main', __name__) + + +@main_views.route('/') +def index(): + return send_from_directory(str(AppLocation.get_section_data_path('remotes')), 'index.html') + + +@main_views.route('/') +def assets(path): + return send_from_directory(str(AppLocation.get_section_data_path('remotes')), path) diff --git a/openlp/core/api/versions/__init__.py b/openlp/core/api/versions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openlp/core/api/versions/v1/__init__.py b/openlp/core/api/versions/v1/__init__.py new file mode 100644 index 000000000..3d2e61c46 --- /dev/null +++ b/openlp/core/api/versions/v1/__init__.py @@ -0,0 +1,9 @@ +from openlp.core.api.versions.v1.controller import controller_views +from openlp.core.api.versions.v1.core import core_views +from openlp.core.api.versions.v1.service import service_views + + +def register_blueprints(app): + app.register_blueprint(controller_views) + app.register_blueprint(core_views) + app.register_blueprint(service_views) diff --git a/openlp/core/api/versions/v1/controller.py b/openlp/core/api/versions/v1/controller.py new file mode 100644 index 000000000..0d26fd978 --- /dev/null +++ b/openlp/core/api/versions/v1/controller.py @@ -0,0 +1,83 @@ +import os +import logging +import urllib.request +from pathlib import Path + +from openlp.core.api.lib import old_auth, old_success_response, extract_request +from openlp.core.common.registry import Registry +from openlp.core.common.applocation import AppLocation +from openlp.core.common.settings import Settings +from openlp.core.lib import create_thumb +from openlp.core.lib.serviceitem import ItemCapabilities + +from flask import jsonify, request, Blueprint + +controller_views = Blueprint('old_controller', __name__) +log = logging.getLogger(__name__) + + +@controller_views.route('/api/controller/live/text') +def controller_text_api(): + live_controller = Registry().get('live_controller') + current_item = live_controller.service_item + result = {'results': {}} + data = [] + if current_item: + result['results']['item'] = current_item.unique_identifier + for index, frame in enumerate(current_item.get_frames()): + item = {} + item['tag'] = index + 1 + item['selected'] = live_controller.selected_row == index + item['title'] = current_item.title + if current_item.is_text(): + if frame['verse']: + item['tag'] = str(frame['verse']) + item['chords_text'] = str(frame.get('chords_text', '')) + item['text'] = frame['text'] + item['html'] = current_item.get_rendered_frame(index) + elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'): + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path + if not full_thumbnail_path.exists(): + create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False) + Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88) + item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path)) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # presentations and other things + 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']) + data.append(item) + result['results']['slides'] = data + return jsonify(result) + + +@controller_views.route('/api/controller/live/set') +@old_auth +def controller_set(): + event = Registry().get('live_controller').slidecontroller_live_set + try: + id = int(extract_request(request.args.get('data', ''), 'id')) + event.emit([id]) + except (KeyError, ValueError): + log.error('Received malformed request to set live controller') + return old_success_response() + + +@controller_views.route('/api/controller//') +@old_auth +def controller_direction(controller, action): + getattr(Registry().get('live_controller'), 'slidecontroller_{controller}_{action}'. + format(controller=controller, action=action)).emit() + return old_success_response() diff --git a/openlp/core/api/versions/v1/core.py b/openlp/core/api/versions/v1/core.py new file mode 100644 index 000000000..4782574b5 --- /dev/null +++ b/openlp/core/api/versions/v1/core.py @@ -0,0 +1,40 @@ +from openlp.core.api.lib import old_auth, old_success_response +from openlp.core.common.registry import Registry +from openlp.core.lib import image_to_byte +from openlp.core.lib.plugin import PluginStatus, StringContent +from openlp.core.state import State + +from flask import jsonify, Blueprint + + +core_views = Blueprint('old_core', __name__) + + +@core_views.route('/api/poll') +def poll(): + return jsonify(Registry().get('poller').poll()) + + +@core_views.route('/api/display/') +@old_auth +def toggle_display(display): + ALLOWED_ACTIONS = ['hide', 'show', 'blank', 'theme', 'desktop'] + display = display.lower() + if display in ALLOWED_ACTIONS: + Registry().get('live_controller').slidecontroller_toggle_display.emit(display) + return old_success_response() + + +@core_views.route('/api/plugin/search') +def plugin_list(): + searches = [] + for plugin in State().list_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 jsonify({'results': {'items': searches}}) + + +@core_views.route('/main/image') +def main_image(): + img = 'data:image/png;base64,{}'.format(image_to_byte(Registry().get('live_controller').grab_maindisplay())) + return jsonify({'slide_image': img}) diff --git a/openlp/core/api/versions/v1/service.py b/openlp/core/api/versions/v1/service.py new file mode 100644 index 000000000..4e99321c0 --- /dev/null +++ b/openlp/core/api/versions/v1/service.py @@ -0,0 +1,48 @@ +from openlp.core.api.lib import old_auth, old_success_response, extract_request +from flask import jsonify, request, Blueprint + +from openlp.core.common.registry import Registry + +service_views = Blueprint('old_service', __name__) + + +@service_views.route('/api/service/list') +def service_items(): + 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 jsonify({'results': {'items': service_items}}) + + +@service_views.route('/api/service/set') +@old_auth +def service_set(): + event = Registry().get('service_manager').servicemanager_set_item + try: + data = int(extract_request(request.args.get('data', ''), 'id')) + event.emit(data) + except (KeyError, ValueError): + pass + return old_success_response() + + +@service_views.route('/api/service/direction/') +@old_auth +def service_direction(action): + ALLOWED_ACTIONS = ['next', 'previous'] + action = action.lower() + if action in ALLOWED_ACTIONS: + getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action)).emit() + return old_success_response() diff --git a/openlp/core/api/versions/v2/__init__.py b/openlp/core/api/versions/v2/__init__.py new file mode 100644 index 000000000..9e76f8c11 --- /dev/null +++ b/openlp/core/api/versions/v2/__init__.py @@ -0,0 +1,9 @@ +from openlp.core.api.versions.v2.controller import controller_views +from openlp.core.api.versions.v2.core import core +from openlp.core.api.versions.v2.service import service_views + + +def register_blueprints(app): + app.register_blueprint(controller_views, url_prefix='/api/v2/controller/') + app.register_blueprint(core, url_prefix='/api/v2/core/') + app.register_blueprint(service_views, url_prefix='/api/v2/service/') diff --git a/openlp/core/api/versions/v2/controller.py b/openlp/core/api/versions/v2/controller.py new file mode 100644 index 000000000..fdf5faf3e --- /dev/null +++ b/openlp/core/api/versions/v2/controller.py @@ -0,0 +1,84 @@ +import os +import urllib.request +from pathlib import Path + +from openlp.core.api.lib import login_required +from openlp.core.common.registry import Registry +from openlp.core.common.applocation import AppLocation +from openlp.core.common.settings import Settings +from openlp.core.lib import create_thumb +from openlp.core.lib.serviceitem import ItemCapabilities + +from flask import jsonify, request, abort, Blueprint + +controller_views = Blueprint('controller', __name__) + + +@controller_views.route('/live-item') +def controller_text_api(): + 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 = {} + item['tag'] = index + 1 + item['selected'] = live_controller.selected_row == index + item['title'] = current_item.title + if current_item.is_text(): + if frame['verse']: + item['tag'] = str(frame['verse']) + item['chords_text'] = str(frame.get('chords_text', '')) + item['text'] = frame['text'] + item['html'] = current_item.get_rendered_frame(index) + elif current_item.is_image() and not frame.get('image', '') and Settings().value('api/thumbnails'): + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = AppLocation.get_data_path() / thumbnail_path + if not full_thumbnail_path.exists(): + create_thumb(Path(current_item.get_frame_path(index)), full_thumbnail_path, False) + Registry().get('image_manager').add_image(str(full_thumbnail_path), frame['title'], None, 88, 88) + item['img'] = urllib.request.pathname2url(os.path.sep + str(thumbnail_path)) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # presentations and other things + 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']) + data.append(item) + return jsonify(data) + + +@controller_views.route('/show', methods=['POST']) +@login_required +def controller_set(): + data = request.json + if not data: + abort(400) + num = data.get('id', -1) + Registry().get('live_controller').slidecontroller_live_set.emit([num]) + return '', 204 + + +@controller_views.route('/progress', methods=['POST']) +@login_required +def controller_direction(): + ALLOWED_ACTIONS = ['next', 'previous'] + data = request.json + if not data: + abort(400) + action = data.get('action', '').lower() + if action not in ALLOWED_ACTIONS: + abort(400) + getattr(Registry().get('live_controller'), 'slidecontroller_live_{action}'. + format(action=action)).emit() + return '', 204 diff --git a/openlp/core/api/versions/v2/core.py b/openlp/core/api/versions/v2/core.py new file mode 100644 index 000000000..4a9db0eda --- /dev/null +++ b/openlp/core/api/versions/v2/core.py @@ -0,0 +1,68 @@ +from openlp.core.api.lib import login_required +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings +from openlp.core.lib import image_to_byte +from openlp.core.lib.plugin import PluginStatus, StringContent +from openlp.core.state import State + +from flask import jsonify, request, abort, Blueprint + + +core = Blueprint('core', __name__) + + +@core.route('/poll') +def poll(): + return jsonify(Registry().get('poller').poll()) + + +@core.route('/display', methods=['POST']) +@login_required +def toggle_display(): + ALLOWED_ACTIONS = ['hide', 'show', 'blank', 'theme', 'desktop'] + data = request.json + if not data: + abort(400) + display = data.get('display', '').lower() + if display not in ALLOWED_ACTIONS: + abort(400) + + Registry().get('live_controller').slidecontroller_toggle_display.emit(display) + return '', 204 + + +@core.route('/plugins') +def plugin_list(): + searches = [] + for plugin in State().list_plugins(): + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + searches.append({'key': plugin.name, 'name': str(plugin.text_strings[StringContent.Name]['plural'])}) + return jsonify(searches) + + +@core.route('/system') +def system_information(): + data = {} + data['websocket_port'] = Settings().value('api/websocket port') + data['login_required'] = Settings().value('api/authentication enabled') + return jsonify(data) + + +@core.route('/login', methods=['POST']) +def login(): + data = request.json + if not data: + abort(400) + username = data.get('username', '') + password = data.get('password', '') + if username == Settings().value('api/user id') and password == Settings().value('api/password'): + return jsonify({'token': Registry().get('authentication_token')}) + else: + return '', 401 + + +@core.route('/live-image') +def main_image(): + controller = Registry().get('live_controller') + img = 'data:image/png;base64,{}'.format(image_to_byte(controller.grab_maindisplay())) + return jsonify({'binary_image': img}) diff --git a/openlp/core/api/versions/v2/service.py b/openlp/core/api/versions/v2/service.py new file mode 100644 index 000000000..89ab62c6c --- /dev/null +++ b/openlp/core/api/versions/v2/service.py @@ -0,0 +1,61 @@ +from openlp.core.api.lib import login_required + +from flask import jsonify, request, abort, Blueprint + +from openlp.core.common.registry import Registry + + +service_views = Blueprint('service', __name__) + + +@service_views.route('/items') +def service_items(): + 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'] + if 'ccli_number' in service_item.data_string: + ccli_number = service_item.data_string['ccli_number'] + else: + ccli_number = '' + service_items.append({ + 'id': str(service_item.unique_identifier), + 'title': str(service_item.get_display_title()), + 'plugin': str(service_item.name), + 'ccli_number': str(ccli_number), + 'notes': str(service_item.notes), + 'selected': (service_item.unique_identifier == current_unique_identifier) + }) + return jsonify(service_items) + + +@service_views.route('/show', methods=['POST']) +@login_required +def service_set(): + data = request.json + if not data: + abort(400) + try: + id = int(data.get('id', -1)) + except ValueError: + abort(400) + Registry().get('service_manager').set_item(id) + return '', 204 + + +@service_views.route('/progress', methods=['POST']) +@login_required +def service_direction(): + ALLOWED_ACTIONS = ['next', 'previous'] + data = request.json + if not data: + abort(400) + action = data.get('action', '').lower() + if action not in ALLOWED_ACTIONS: + abort(400) + getattr(Registry().get('service_manager'), 'servicemanager_{action}_item'.format(action=action)).emit() + return '', 204 diff --git a/openlp/core/ui/media/endpoint.py b/openlp/core/ui/media/endpoint.py deleted file mode 100644 index 334f7e6bb..000000000 --- a/openlp/core/ui/media/endpoint.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -The :mod:`~openlp.core.api.endpoint` module contains various API endpoints -""" -import logging - -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.common.registry import Registry - - -log = logging.getLogger(__name__) -media_endpoint = Endpoint('media') - - -@media_endpoint.route('play') -@requires_auth -def media_play(request): - """ - Handles requests for playing media - - :param request: The http request object. - """ - media = Registry().get('media_controller') - live = Registry().get('live_controller') - status = media.media_play(live, False) - return {'results': {'success': status}} - - -@media_endpoint.route('pause') -@requires_auth -def media_pause(request): - """ - Handles requests for pausing media - - :param request: The http request object. - """ - media = Registry().get('media_controller') - live = Registry().get('live_controller') - status = media.media_pause(live) - return {'results': {'success': status}} - - -@media_endpoint.route('stop') -@requires_auth -def media_stop(request): - """ - Handles requests for stopping - - :param request: The http request object. - """ - event = getattr(Registry().get('live_controller'), 'mediacontroller_live_stop') - event.emit() - return {'results': {'success': True}} diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index fa3dbe03c..47d0ca11e 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -33,7 +33,6 @@ from subprocess import check_output from PyQt5 import QtCore from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common import is_linux, is_macosx from openlp.core.common.i18n import translate from openlp.core.common.mixins import LogMixin, RegistryProperties @@ -43,7 +42,7 @@ from openlp.core.lib.serviceitem import ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui import DisplayControllerType from openlp.core.ui.media import MediaState, ItemMediaInfo, MediaType, parse_optical_path, VIDEO_EXT, AUDIO_EXT -from openlp.core.ui.media.endpoint import media_endpoint +from openlp.core.ui.media.remote import register_views from openlp.core.ui.media.vlcplayer import VlcPlayer, get_vlc @@ -81,7 +80,7 @@ class MediaController(RegistryBase, LogMixin, RegistryProperties): Registry().register_function('songs_hide', self.media_hide) Registry().register_function('songs_blank', self.media_blank) Registry().register_function('songs_unblank', self.media_unblank) - register_endpoint(media_endpoint) + register_views() def bootstrap_initialise(self): """ diff --git a/openlp/core/ui/media/remote.py b/openlp/core/ui/media/remote.py new file mode 100644 index 000000000..8bad4796f --- /dev/null +++ b/openlp/core/ui/media/remote.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2018 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.api.endpoint` module contains various API endpoints +""" +import logging +from flask import abort, jsonify, Blueprint + +from openlp.core.api import app +from openlp.core.api.lib import login_required, old_auth +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + +v1_media = Blueprint('v1-media-controller', __name__) +v2_media = Blueprint('v2-media-controller', __name__) + + +@v2_media.route('/play', methods=['POST']) +@login_required +def media_play(): + media = Registry().get('media_controller') + live = Registry().get('live_controller') + try: + status = media.media_play(live, True) + except Exception: + # The current item probably isn't a media item + abort(400) + if status: + return '', 204 + abort(400) + + +@v2_media.route('/pause', methods=['POST']) +@login_required +def media_pause(): + media = Registry().get('media_controller') + live = Registry().get('live_controller') + media.media_pause(live) + return '', 204 + + +@v2_media.route('/stop', methods=['POST']) +@login_required +def media_stop(): + Registry().get('live_controller').mediacontroller_live_stop.emit() + return '', 204 + + +# -------------- DEPRECATED ------------------------ +@v1_media.route('/play') +@old_auth +def v1_media_play(): + media = Registry().get('media_controller') + live = Registry().get('live_controller') + status = media.media_play(live, False) + return jsonify({'success': status}) + + +@v1_media.route('/pause') +@old_auth +def v1_media_pause(): + media = Registry().get('media_controller') + live = Registry().get('live_controller') + status = media.media_pause(live) + return jsonify({'success': status}) + + +@v1_media.route('/stop') +@old_auth +def v1_media_stop(): + Registry().get('live_controller').mediacontroller_live_stop.emit() + return '' +# -------------- END OF DEPRECATED ------------------------ + + +def register_views(): + app.register_blueprint(v2_media, url_prefix='/api/v2/media/') + app.register_blueprint(v1_media, url_prefix='/api/media/') diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index d75730458..628ec62fe 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -1238,8 +1238,9 @@ class SlideController(QtWidgets.QWidget, LogMixin, RegistryProperties): win_image.setDevicePixelRatio(self.preview_display.devicePixelRatio()) # self.slide_preview.setPixmap(win_image) self.slide_image = win_image - base64_image = image_to_byte(win_image, True) + base64_image = image_to_byte(win_image) self.preview_display.set_single_image_data('#000', base64_image) + return self.slide_image def on_slide_selected_next_action(self, checked): """ diff --git a/openlp/plugins/alerts/alertsplugin.py b/openlp/plugins/alerts/alertsplugin.py index cf8f97124..5b4933791 100644 --- a/openlp/plugins/alerts/alertsplugin.py +++ b/openlp/plugins/alerts/alertsplugin.py @@ -22,7 +22,6 @@ import logging from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.actions import ActionList from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.settings import Settings @@ -31,7 +30,7 @@ from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.lib.theme import VerticalType from openlp.core.lib.ui import create_action from openlp.core.ui.icons import UiIcons -from openlp.plugins.alerts.endpoint import api_alerts_endpoint, alerts_endpoint +from openlp.plugins.alerts.remote import register_views from openlp.plugins.alerts.forms.alertform import AlertForm from openlp.plugins.alerts.lib.alertsmanager import AlertsManager from openlp.plugins.alerts.lib.alertstab import AlertsTab @@ -129,8 +128,7 @@ 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) + register_views() State().add_service(self.name, self.weight, is_plugin=True) State().update_pre_conditions(self.name, self.check_pre_conditions()) diff --git a/openlp/plugins/alerts/endpoint.py b/openlp/plugins/alerts/endpoint.py deleted file mode 100644 index c1b2a53fc..000000000 --- a/openlp/plugins/alerts/endpoint.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import json -import logging -import urllib - -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.common.registry import Registry -from openlp.core.lib.plugin 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}} diff --git a/openlp/plugins/alerts/remote.py b/openlp/plugins/alerts/remote.py new file mode 100644 index 000000000..3bbbcb133 --- /dev/null +++ b/openlp/plugins/alerts/remote.py @@ -0,0 +1,39 @@ +from flask import Blueprint, request, abort + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_success_response, old_auth +from openlp.core.common.registry import Registry +from openlp.core.lib.plugin import PluginStatus + + +v1_views = Blueprint('v1-alert-plugin', __name__) +v2_views = Blueprint('v2-alert-plugin', __name__) + + +@v2_views.route('', methods=['POST']) +@login_required +def alert(): + data = request.json + if not data: + abort(400) + alert = data.get('text', '') + if alert: + if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: + Registry().get('alerts_manager').alerts_text.emit([alert]) + return '', 204 + abort(400) + + +@v1_views.route('') +@old_auth +def old_alert(): + alert = extract_request(request.args.get('data', ''), 'text') + if alert: + if Registry().get('plugin_manager').get_plugin_by_name('alerts').status == PluginStatus.Active: + Registry().get('alerts_manager').alerts_text.emit([alert]) + return old_success_response() + + +def register_views(): + app.register_blueprint(v2_views, url_prefix='/api/v2/plugins/alerts') + app.register_blueprint(v1_views, url_prefix='/api/alert') diff --git a/openlp/plugins/bibles/bibleplugin.py b/openlp/plugins/bibles/bibleplugin.py index 3eb53e560..0c2126c95 100644 --- a/openlp/plugins/bibles/bibleplugin.py +++ b/openlp/plugins/bibles/bibleplugin.py @@ -22,13 +22,12 @@ import logging from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.actions import ActionList from openlp.core.common.i18n import UiStrings, translate from openlp.core.ui.icons import UiIcons from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.lib.ui import create_action -from openlp.plugins.bibles.endpoint import api_bibles_endpoint, bibles_endpoint +from openlp.plugins.bibles.remote import register_views from openlp.plugins.bibles.lib.biblestab import BiblesTab from openlp.plugins.bibles.lib.manager import BibleManager from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem @@ -49,8 +48,7 @@ class BiblePlugin(Plugin): self.icon_path = UiIcons().bible self.icon = UiIcons().bible self.manager = BibleManager(self) - register_endpoint(bibles_endpoint) - register_endpoint(api_bibles_endpoint) + register_views() State().add_service('bible', self.weight, is_plugin=True) State().update_pre_conditions('bible', self.check_pre_conditions()) diff --git a/openlp/plugins/bibles/endpoint.py b/openlp/plugins/bibles/endpoint.py deleted file mode 100644 index ed6af0d1d..000000000 --- a/openlp/plugins/bibles/endpoint.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -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. - """ - return 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: - return search(request, 'bibles', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/bibles/remote.py b/openlp/plugins/bibles/remote.py new file mode 100644 index 000000000..d5fc05d96 --- /dev/null +++ b/openlp/plugins/bibles/remote.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import request, jsonify, Blueprint + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + + +log = logging.getLogger(__name__) + + +v1_views = Blueprint('v1-bibles-plugin', __name__) +v2_views = Blueprint('v2-bibles-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('bibles') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('bibles') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.bibles_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('bibles') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.bibles_add_to_service.emit([item_id, True]) + + +@v2_views.route('/search') +@login_required +def search_bible(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_views.route('/live', methods=['POST']) +@login_required +def send_live(): + id = request.json.get('id', -1) + live(id) + return '', 204 + + +@v2_views.route('/add', methods=['POST']) +@login_required +def add_to_service(): + id = request.json.get('id', -1) + add(id) + return '', 204 + + +# ---------------- DEPRECATED REMOVE AFTER RELEASE -------------- +@v1_views.route('/search') +@old_auth +def old_search_bible(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_views.route('/live') +@old_auth +def old_send_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 + + +@v1_views.route('/add') +@old_auth +def old_add_to_service(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 +# ------------ END DEPRECATED ---------------------------------- + + +def register_views(): + app.register_blueprint(v2_views, url_prefix='/api/v2/plugins/bibles') + app.register_blueprint(v1_views, url_prefix='/api/bibles') diff --git a/openlp/plugins/custom/customplugin.py b/openlp/plugins/custom/customplugin.py index 40cb430e6..6fad6bd59 100644 --- a/openlp/plugins/custom/customplugin.py +++ b/openlp/plugins/custom/customplugin.py @@ -26,13 +26,12 @@ for the Custom Slides plugin. import logging from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.i18n import translate from openlp.core.lib import build_icon from openlp.core.lib.db import Manager from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.ui.icons import UiIcons -from openlp.plugins.custom.endpoint import api_custom_endpoint, custom_endpoint +from openlp.plugins.custom.remote import register_views from openlp.plugins.custom.lib.db import CustomSlide, init_schema from openlp.plugins.custom.lib.mediaitem import CustomMediaItem from openlp.plugins.custom.lib.customtab import CustomTab @@ -56,8 +55,7 @@ class CustomPlugin(Plugin): self.db_manager = Manager('custom', init_schema) self.icon_path = UiIcons().clone self.icon = build_icon(self.icon_path) - register_endpoint(custom_endpoint) - register_endpoint(api_custom_endpoint) + register_views() State().add_service(self.name, self.weight, is_plugin=True) State().update_pre_conditions(self.name, self.check_pre_conditions()) diff --git a/openlp/plugins/custom/endpoint.py b/openlp/plugins/custom/endpoint.py deleted file mode 100644 index f6e45ed53..000000000 --- a/openlp/plugins/custom/endpoint.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -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. - """ - return 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: - return search(request, 'custom', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/custom/remote.py b/openlp/plugins/custom/remote.py new file mode 100644 index 000000000..8fb1a34df --- /dev/null +++ b/openlp/plugins/custom/remote.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import abort, request, Blueprint, jsonify + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + + +v1_custom = Blueprint('v1-custom-plugin', __name__) +v2_custom = Blueprint('v2-custom-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('custom') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('custom') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.custom_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('custom') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.custom_add_to_service.emit([item_id, True]) + + +@v2_custom.route('/search') +@login_required +def search_view(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_custom.route('/add', methods=['POST']) +@login_required +def add_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + add(id) + return '', 204 + + +@v2_custom.route('/live', methods=['POST']) +@login_required +def live_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + live(id) + return '', 204 + + +# ----------------- DEPRECATED -------------- +@v1_custom.route('/search') +@old_auth +def old_search(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_custom.route('/add') +@old_auth +def old_add(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 + + +@v1_custom.route('/live') +@old_auth +def old_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 +# ---------------- END DEPRECATED ---------------- + + +def register_views(): + app.register_blueprint(v2_custom, url_prefix='/api/v2/plugins/custom/') + app.register_blueprint(v1_custom, url_prefix='/api/custom/') diff --git a/openlp/plugins/images/endpoint.py b/openlp/plugins/images/endpoint.py deleted file mode 100644 index bc3ab1d5d..000000000 --- a/openlp/plugins/images/endpoint.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import display_thumbnails, live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -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. - """ - return 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: - return search(request, 'images', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/images/imageplugin.py b/openlp/plugins/images/imageplugin.py index 8a21691eb..7b74d908f 100644 --- a/openlp/plugins/images/imageplugin.py +++ b/openlp/plugins/images/imageplugin.py @@ -24,14 +24,13 @@ import logging from PyQt5 import QtGui from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings from openlp.core.lib import ImageSource, build_icon from openlp.core.lib.db import Manager from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.ui.icons import UiIcons -from openlp.plugins.images.endpoint import api_images_endpoint, images_endpoint +from openlp.plugins.images.remote import register_views from openlp.plugins.images.lib import upgrade from openlp.plugins.images.lib.mediaitem import ImageMediaItem from openlp.plugins.images.lib.imagetab import ImageTab @@ -50,8 +49,7 @@ class ImagePlugin(Plugin): self.weight = -7 self.icon_path = UiIcons().picture self.icon = build_icon(self.icon_path) - register_endpoint(images_endpoint) - register_endpoint(api_images_endpoint) + register_views() State().add_service('image', self.weight, is_plugin=True) State().update_pre_conditions('image', self.check_pre_conditions()) diff --git a/openlp/plugins/images/remote.py b/openlp/plugins/images/remote.py new file mode 100644 index 000000000..29d4507bd --- /dev/null +++ b/openlp/plugins/images/remote.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import abort, request, Blueprint, jsonify + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + + +v1_images = Blueprint('v1-images-plugin', __name__) +v2_images = Blueprint('v2-images-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('images') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('images') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.images_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('images') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.images_add_to_service.emit([item_id, True]) + + +@v2_images.route('/search') +@login_required +def search_view(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_images.route('/add', methods=['POST']) +@login_required +def add_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + add(id) + return '', 204 + + +@v2_images.route('/live', methods=['POST']) +@login_required +def live_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + live(id) + return '', 204 + + +# ----------------- DEPRECATED -------------- +@v1_images.route('/search') +@old_auth +def old_search(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_images.route('/add') +@old_auth +def old_add(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 + + +@v1_images.route('/live') +@old_auth +def old_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 +# ---------------- END DEPRECATED ---------------- + + +def register_views(): + app.register_blueprint(v2_images, url_prefix='/api/v2/plugins/images/') + app.register_blueprint(v1_images, url_prefix='/api/images/') diff --git a/openlp/plugins/media/endpoint.py b/openlp/plugins/media/endpoint.py deleted file mode 100644 index cd3f96f65..000000000 --- a/openlp/plugins/media/endpoint.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -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. - """ - return 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: - return search(request, 'media', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 728bff9b6..db50db733 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -24,12 +24,11 @@ The Media plugin import logging from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.i18n import translate from openlp.core.ui.icons import UiIcons from openlp.core.lib import build_icon from openlp.core.lib.plugin import Plugin, StringContent -from openlp.plugins.media.endpoint import api_media_endpoint, media_endpoint +from openlp.plugins.media.remote import register_views from openlp.plugins.media.lib.mediaitem import MediaMediaItem @@ -51,8 +50,7 @@ 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) + register_views() State().add_service(self.name, self.weight, requires='mediacontroller', is_plugin=True) State().update_pre_conditions(self.name, self.check_pre_conditions()) diff --git a/openlp/plugins/media/remote.py b/openlp/plugins/media/remote.py new file mode 100644 index 000000000..4ebddef00 --- /dev/null +++ b/openlp/plugins/media/remote.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import abort, request, Blueprint, jsonify + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + + +v1_media = Blueprint('v1-media-plugin', __name__) +v2_media = Blueprint('v2-media-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('media') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('media') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.media_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('media') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.media_add_to_service.emit([item_id, True]) + + +@v2_media.route('/search') +@login_required +def search_view(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_media.route('/add', methods=['POST']) +@login_required +def add_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + add(id) + return '', 204 + + +@v2_media.route('/live', methods=['POST']) +@login_required +def live_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + live(id) + return '', 204 + + +# ----------------- DEPRECATED -------------- +@v1_media.route('/search') +@old_auth +def old_search(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_media.route('/add') +@old_auth +def old_add(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 + + +@v1_media.route('/live') +@old_auth +def old_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 +# ---------------- END DEPRECATED ---------------- + + +def register_views(): + app.register_blueprint(v2_media, url_prefix='/api/v2/plugins/media/') + app.register_blueprint(v1_media, url_prefix='/api/media/') diff --git a/openlp/plugins/presentations/endpoint.py b/openlp/plugins/presentations/endpoint.py deleted file mode 100644 index ae1ffe0a6..000000000 --- a/openlp/plugins/presentations/endpoint.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import display_thumbnails, live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -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. - """ - return 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: - return search(request, 'presentations', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 1653cc120..1e2cd2922 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -25,7 +25,7 @@ presentations from a variety of document formats. import logging import os -from openlp.core.api.http import register_endpoint + from openlp.core.common import extension_loader from openlp.core.common.i18n import translate from openlp.core.common.settings import Settings @@ -33,7 +33,7 @@ from openlp.core.lib import build_icon from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.state import State from openlp.core.ui.icons import UiIcons -from openlp.plugins.presentations.endpoint import api_presentations_endpoint, presentations_endpoint +from openlp.plugins.presentations.remote import register_views from openlp.plugins.presentations.lib.presentationcontroller import PresentationController from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem from openlp.plugins.presentations.lib.presentationtab import PresentationTab @@ -59,8 +59,7 @@ class PresentationPlugin(Plugin): self.weight = -8 self.icon_path = UiIcons().presentation self.icon = build_icon(self.icon_path) - register_endpoint(presentations_endpoint) - register_endpoint(api_presentations_endpoint) + register_views() State().add_service('presentation', self.weight, is_plugin=True) State().update_pre_conditions('presentation', self.check_pre_conditions()) diff --git a/openlp/plugins/presentations/remote.py b/openlp/plugins/presentations/remote.py new file mode 100644 index 000000000..6cb3fd2ba --- /dev/null +++ b/openlp/plugins/presentations/remote.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import abort, request, Blueprint, jsonify + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + + +v1_presentations = Blueprint('v1-presentations-plugin', __name__) +v2_presentations = Blueprint('v2-presentations-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('presentations') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('presentations') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.presentations_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('presentations') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.presentations_add_to_service.emit([item_id, True]) + + +@v2_presentations.route('/search') +@login_required +def search_view(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_presentations.route('/add', methods=['POST']) +@login_required +def add_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + add(id) + return '', 204 + + +@v2_presentations.route('/live', methods=['POST']) +@login_required +def live_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + live(id) + return '', 204 + + +# ----------------- DEPRECATED -------------- +@v1_presentations.route('/search') +@old_auth +def old_search(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_presentations.route('/add') +@old_auth +def old_add(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 + + +@v1_presentations.route('/live') +@old_auth +def old_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 +# ---------------- END DEPRECATED ---------------- + + +def register_views(): + app.register_blueprint(v2_presentations, url_prefix='/api/v2/plugins/presentations/') + app.register_blueprint(v1_presentations, url_prefix='/api/presentations/') diff --git a/openlp/plugins/songs/endpoint.py b/openlp/plugins/songs/endpoint.py deleted file mode 100644 index bdad94e38..000000000 --- a/openlp/plugins/songs/endpoint.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -import logging - -from openlp.core.api.endpoint.pluginhelpers import live, search, service -from openlp.core.api.http import requires_auth -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -log = logging.getLogger(__name__) - -songs_endpoint = Endpoint('songs') -api_songs_endpoint = Endpoint('api') - - -@songs_endpoint.route('search') -def songs_search(request): - """ - Handles requests for searching the songs plugin - - :param request: The http request object. - """ - return search(request, 'songs', log) - - -@songs_endpoint.route('live') -@requires_auth -def songs_live(request): - """ - Handles requests for making a song live - - :param request: The http request object. - """ - return live(request, 'songs', log) - - -@songs_endpoint.route('add') -@requires_auth -def songs_service(request): - """ - Handles requests for adding a song to the service - - :param request: The http request object. - """ - return service(request, 'songs', log) - - -@api_songs_endpoint.route('songs/search') -def songs_search_api(request): - """ - Handles requests for searching the songs plugin - - :param request: The http request object. - """ - return search(request, 'songs', log) - - -@api_songs_endpoint.route('songs/live') -@requires_auth -def songs_live_api(request): - """ - Handles requests for making a song live - - :param request: The http request object. - """ - return live(request, 'songs', log) - - -@api_songs_endpoint.route('songs/add') -@requires_auth -def songs_service_api(request): - """ - Handles requests for adding a song to the service - - :param request: The http request object. - """ - try: - return service(request, 'songs', log) - except NotFound: - return {'results': {'items': []}} diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 1e39a04e3..b894bae20 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -613,7 +613,11 @@ class SongMediaItem(MediaManagerItem): service_item.add_from_text(split_verse, verse_def) service_item.title = song.title author_list = self.generate_footer(service_item, song) - service_item.data_string = {'title': song.search_title, 'authors': ', '.join(author_list)} + service_item.data_string = { + 'title': song.search_title, + 'authors': ', '.join(author_list), + 'ccli_number': song.ccli_number + } service_item.xml_version = self.open_lyrics.song_to_xml(song) # Add the audio file to the service item. if song.media_files: diff --git a/openlp/plugins/songs/remote.py b/openlp/plugins/songs/remote.py new file mode 100644 index 000000000..207230248 --- /dev/null +++ b/openlp/plugins/songs/remote.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2019 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 flask import abort, request, Blueprint, jsonify + +from openlp.core.api import app +from openlp.core.api.lib import login_required, extract_request, old_auth +from openlp.core.lib.plugin import PluginStatus +from openlp.core.common.registry import Registry + +log = logging.getLogger(__name__) + + +v1_songs = Blueprint('v1-songs-plugin', __name__) +v2_songs = Blueprint('v2-songs-plugin', __name__) + + +def search(text): + plugin = Registry().get('plugin_manager').get_plugin_by_name('songs') + if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: + results = plugin.media_item.search(text, False) + return results + return None + + +def live(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('songs') + if plugin.status == PluginStatus.Active and plugin.media_item: + plugin.media_item.songs_go_live.emit([id, True]) + + +def add(id): + plugin = Registry().get('plugin_manager').get_plugin_by_name('songs') + if plugin.status == PluginStatus.Active and plugin.media_item: + item_id = plugin.media_item.create_item_from_id(id) + plugin.media_item.songs_add_to_service.emit([item_id, True]) + + +@v2_songs.route('/search') +@login_required +def search_view(): + text = request.args.get('text', '') + result = search(text) + if result: + return jsonify(result) + return '', 400 + + +@v2_songs.route('/add', methods=['POST']) +@login_required +def add_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + add(id) + return '', 204 + + +@v2_songs.route('/live', methods=['POST']) +@login_required +def live_view(): + data = request.json + if not data: + abort(400) + id = data.get('id', -1) + live(id) + return '', 204 + + +# ----------------- DEPRECATED -------------- +@v1_songs.route('/search') +@old_auth +def old_search(): + text = extract_request(request.args.get('data', ''), 'text') + return jsonify({'results': {'items': search(text)}}) + + +@v1_songs.route('/add') +@old_auth +def old_add(): + id = extract_request(request.args.get('data', ''), 'id') + add(id) + return '', 204 + + +@v1_songs.route('/live') +@old_auth +def old_live(): + id = extract_request(request.args.get('data', ''), 'id') + live(id) + return '', 204 +# ---------------- END DEPRECATED ---------------- + + +def register_views(): + app.register_blueprint(v2_songs, url_prefix='/api/v2/plugins/songs/') + app.register_blueprint(v1_songs, url_prefix='/api/songs/') diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index bace7c1b3..ba9cf0bc6 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -31,7 +31,6 @@ from tempfile import gettempdir from PyQt5 import QtCore, QtWidgets from openlp.core.state import State -from openlp.core.api.http import register_endpoint from openlp.core.common.actions import ActionList from openlp.core.common.i18n import UiStrings, translate from openlp.core.common.registry import Registry @@ -41,7 +40,7 @@ from openlp.core.lib.plugin import Plugin, StringContent from openlp.core.lib.ui import create_action from openlp.core.ui.icons import UiIcons from openlp.plugins.songs import reporting -from openlp.plugins.songs.endpoint import api_songs_endpoint, songs_endpoint +from openlp.plugins.songs.remote import register_views from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm from openlp.plugins.songs.forms.songselectform import SongSelectForm from openlp.plugins.songs.lib import clean_song, upgrade @@ -128,8 +127,7 @@ class SongsPlugin(Plugin): self.icon = build_icon(self.icon_path) self.songselect_form = None self.settings.extend_default_settings(song_footer) - register_endpoint(songs_endpoint) - register_endpoint(api_songs_endpoint) + register_views() State().add_service(self.name, self.weight, is_plugin=True) State().update_pre_conditions(self.name, self.check_pre_conditions()) if not self.settings.value('songs/last import type'): diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index b525114c5..65c711403 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -83,6 +83,8 @@ MODULES = [ 'alembic', 'lxml', 'chardet', + 'flask', + 'flask_cors', 'bs4', 'mako', 'websockets', diff --git a/setup.py b/setup.py index d373650ad..8990d5a92 100644 --- a/setup.py +++ b/setup.py @@ -102,6 +102,8 @@ using a computer and a data projector.""", 'chardet', 'dbus-python; platform_system=="Linux"', 'distro; platform_system=="Linux"', + 'flask', + 'flask-cors', 'lxml', 'Mako', 'pymediainfo >= 2.2', diff --git a/tests/functional/openlp_core/api/endpoint/test_controller.py b/tests/functional/openlp_core/api/endpoint/test_controller.py index b27fd9fec..3ced8dc86 100644 --- a/tests/functional/openlp_core/api/endpoint/test_controller.py +++ b/tests/functional/openlp_core/api/endpoint/test_controller.py @@ -24,11 +24,12 @@ from unittest.mock import MagicMock, patch from PyQt5 import QtCore +from openlp.core.api import app as flask_app from openlp.core.state import State + # Mock QtWebEngineWidgets # sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock() -from openlp.core.api.endpoint.controller import controller_direction, controller_text from openlp.core.common.registry import Registry from openlp.core.display.screens import ScreenList from openlp.core.lib.serviceitem import ServiceItem @@ -75,6 +76,8 @@ class TestController(TestCase): self.mocked_renderer.format_slide = self.mocked_slide_formater Registry().register('live_controller', self.mocked_live_controller) Registry().register('renderer', self.mocked_renderer) + flask_app.config['TESTING'] = True + self.client = flask_app.test_client() Registry().register('settings', MagicMock(**{'value.return_value': 'english'})) def test_controller_text_empty(self): @@ -88,7 +91,7 @@ class TestController(TestCase): self.mocked_live_controller.service_item = mocked_service_item # WHEN: I trigger the method - ret = controller_text(MagicMock()) + ret = self.client.get('/api/controller/live/text').get_json() # THEN: I get a basic set of results assert ret['results']['item'] == 'mock-service-item' @@ -108,7 +111,7 @@ class TestController(TestCase): self.mocked_live_controller.service_item.set_from_service(line) self.mocked_live_controller.service_item._create_slides() # WHEN: I trigger the method - ret = controller_text("SomeText") + ret = self.client.get('/api/controller/live/text').get_json() # THEN: I get a basic set of results results = ret['results'] @@ -125,8 +128,7 @@ class TestController(TestCase): self.mocked_live_controller.service_item = MagicMock() # WHEN: I trigger the method - controller_direction(None, 'live', 'next') - + self.client.get('/api/controller/live/next') # THEN: The correct method is called mocked_emit.assert_called_once_with() @@ -140,7 +142,6 @@ class TestController(TestCase): self.mocked_live_controller.service_item = MagicMock() # WHEN: I trigger the method - controller_direction(None, 'live', 'previous') - + self.client.get('/api/controller/live/previous') # THEN: The correct method is called mocked_emit.assert_called_once_with() diff --git a/tests/functional/openlp_core/api/endpoint/test_remote.py b/tests/functional/openlp_core/api/endpoint/test_remote.py deleted file mode 100644 index 9251b5a2f..000000000 --- a/tests/functional/openlp_core/api/endpoint/test_remote.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -Functional tests to test the remote index -""" -from unittest.mock import MagicMock, patch - -from openlp.core.api.endpoint.core import TRANSLATED_STRINGS -from openlp.core.api.endpoint.remote import index - - -@patch('openlp.core.api.endpoint.remote.remote_endpoint') -def test_index(mocked_endpoint): - """ - Test the index method of the remote - """ - # GIVEN: A mocked Endpoint - mocked_endpoint.render_template.return_value = 'test template' - - # WHEN: index is called - result = index(MagicMock(), 'index') - - # THEN: The result should be "test template" and the right methods should have been called - mocked_endpoint.render_template.assert_called_once_with('index.mako', **TRANSLATED_STRINGS) - assert result == 'test template' diff --git a/tests/functional/openlp_core/api/http/test_error.py b/tests/functional/openlp_core/api/http/test_error.py deleted file mode 100644 index 0ea361441..000000000 --- a/tests/functional/openlp_core/api/http/test_error.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -Functional tests to test the API Error Class. -""" -from openlp.core.api.http.errors import HttpError, NotFound, ServerError - - -def test_http_error(): - """ - Test the HTTPError class - """ - # GIVEN: An HTTPError class - # WHEN: An instance is created - error = HttpError(400, 'Access Denied') - - # THEN: The to_response() method should return the correct information - assert error.to_response() == ('Access Denied', 400), 'to_response() should have returned the correct info' - - -def test_not_found(): - """ - Test the Not Found error displays the correct information - """ - # GIVEN: A NotFound class - # WHEN: An instance is created - error = NotFound() - - # THEN: The to_response() method should return the correct information - assert error.to_response() == ('Not Found', 404), 'to_response() should have returned the correct info' - - -def test_server_error(): - """ - Test the server error displays the correct information - """ - # GIVEN: A ServerError class - # WHEN: An instance of the class is created - error = ServerError() - - # THEN: The to_response() method should return the correct information - assert error.to_response() == ('Server Error', 500), 'to_response() should have returned the correct info' diff --git a/tests/functional/openlp_core/api/http/test_wsgiapp.py b/tests/functional/openlp_core/api/http/test_wsgiapp.py deleted file mode 100644 index 82d54fa3c..000000000 --- a/tests/functional/openlp_core/api/http/test_wsgiapp.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# OpenLP - Open Source Lyrics Projection # -# ---------------------------------------------------------------------- # -# Copyright (c) 2008-2020 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, either version 3 of the License, or # -# (at your option) any later version. # -# # -# 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, see . # -########################################################################## -""" -Functional test the routing code. -""" -import os -from unittest import TestCase -from unittest.mock import MagicMock - -from openlp.core.api.http import register_endpoint, application -from openlp.core.api.http.endpoint import Endpoint -from openlp.core.api.http.errors import NotFound - - -ROOT_DIR = os.path.dirname(os.path.realpath(__file__)) - -test_endpoint = Endpoint('test', template_dir=ROOT_DIR, static_dir=ROOT_DIR) - - -class TestRouting(TestCase): - """ - Test the HTTP routing - """ - def setUp(self): - """ - Convert the application to a test application - :return: - """ - for route, views in application.route_map.items(): - application.route_map[route]['GET'] = MagicMock() - - def test_routing(self): - """ - Test the Routing in the new application via dispatch - :return: - """ - # GIVE: I try to request and - # WHEN: when the URL is not correct and dispatch called - rqst = MagicMock() - rqst.path = '/test/api' - rqst.method = 'GET' - with self.assertRaises(NotFound) as context: - application.dispatch(rqst) - - # THEN: the not found returned - assert context.exception.args[0] == 'Not Found', 'URL not found in dispatcher' - - # WHEN: when the URL is correct and dispatch called - rqst = MagicMock() - rqst.path = '/test/image' - rqst.method = 'GET' - application.dispatch(rqst) - # THEN: the not found id called - route_key = next(iter(application.route_map)) - assert '/image' in route_key - assert 1 == application.route_map[route_key]['GET'].call_count, \ - 'main_index function should have been called' - - -@test_endpoint.route('image') -def image(request): - pass - - -@test_endpoint.route('') -def index(request): - pass - - -register_endpoint(test_endpoint) diff --git a/tests/functional/openlp_core/api/v2/__init__.py b/tests/functional/openlp_core/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_core/api/v2/conftest.py b/tests/functional/openlp_core/api/v2/conftest.py new file mode 100644 index 000000000..976957011 --- /dev/null +++ b/tests/functional/openlp_core/api/v2/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from openlp.core.api import app as flask_app + + +@pytest.fixture(scope='module') +def flask_client(): + flask_app.config['TESTING'] = True + return flask_app.test_client() diff --git a/tests/functional/openlp_core/api/v2/test_controller.py b/tests/functional/openlp_core/api/v2/test_controller.py new file mode 100644 index 000000000..514e821d0 --- /dev/null +++ b/tests/functional/openlp_core/api/v2/test_controller.py @@ -0,0 +1,57 @@ +import pytest +from unittest.mock import MagicMock + +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings + + +def test_retrieve_live_item(flask_client): + pytest.skip() + res = flask_client.get('/api/v2/controller/live-item').get_json() + assert len(res) == 0 + + +def test_controller_set_requires_login(flask_client): + pytest.skip('Need to figure out how to patch settings for one test only') + # Settings().setValue('api/authentication enabled', True) + res = flask_client.post('/api/v2/controller/show', json=dict()) + Settings().setValue('api/authentication enabled', False) + assert res.status_code == 401 + + +def test_controller_set_does_not_accept_get(flask_client): + res = flask_client.get('/api/v2/controller/show') + assert res.status_code == 405 + + +def test_controller_set_calls_live_controller(flask_client): + fake_live_controller = MagicMock() + Registry.create().register('live_controller', fake_live_controller) + res = flask_client.post('/api/v2/controller/show', json=dict(id=400)) + assert res.status_code == 204 + fake_live_controller.slidecontroller_live_set.emit.assert_called_once_with([400]) + + +def test_controller_direction_requires_login(flask_client): + Settings().setValue('api/authentication enabled', True) + res = flask_client.post('/api/v2/controller/progress', json=dict()) + Settings().setValue('api/authentication enabled', False) + assert res.status_code == 401 + + +def test_controller_direction_does_not_accept_get(flask_client): + res = flask_client.get('/api/v2/controller/progress') + assert res.status_code == 405 + + +def test_controller_direction_does_fails_on_wrong_data(flask_client): + res = flask_client.post('/api/v2/controller/progress', json=dict(action='foo')) + assert res.status_code == 400 + + +def test_controller_direction_calls_service_manager(flask_client): + fake_live_controller = MagicMock() + Registry.create().register('live_controller', fake_live_controller) + res = flask_client.post('/api/v2/controller/progress', json=dict(action='next')) + assert res.status_code == 204 + fake_live_controller.slidecontroller_live_next.emit.assert_called_once() diff --git a/tests/functional/openlp_core/api/v2/test_core.py b/tests/functional/openlp_core/api/v2/test_core.py new file mode 100644 index 000000000..1a67e63ac --- /dev/null +++ b/tests/functional/openlp_core/api/v2/test_core.py @@ -0,0 +1,113 @@ +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings +from openlp.core.state import State +from openlp.core.lib.plugin import PluginStatus, StringContent + + +def test_plugins_returns_list(flask_client): + State().load_settings() + res = flask_client.get('/api/v2/core/plugins').get_json() + assert len(res) == 0 + + class FakeMediaItem: + has_search = True + + class FakePlugin: + name = 'Faked' + is_plugin = True + status = PluginStatus.Active + media_item = FakeMediaItem() + text_strings = {StringContent.Name: {'plural': 'just a text'}} + plugin = FakePlugin() + State().modules['testplug'] = plugin + Registry.create().register('testplug_plugin', plugin) + res = flask_client.get('/api/v2/core/plugins').get_json() + assert len(res) == 1 + assert res[0]['key'] == plugin.name + assert res[0]['name'] == plugin.text_strings[StringContent.Name]['plural'] + + +def test_system_information(flask_client): + Settings().setValue('api/authentication enabled', False) + res = flask_client.get('/api/v2/core/system').get_json() + assert res['websocket_port'] > 0 + assert not res['login_required'] + + +def test_poll(flask_client): + class FakePoller: + def poll(self): + return {'foo': 'bar'} + Registry.create().register('poller', FakePoller()) + res = flask_client.get('/api/v2/core/poll').get_json() + assert res['foo'] == 'bar' + + +def test_login_get_is_refused(flask_client): + res = flask_client.get('/api/v2/core/login') + assert res.status_code == 405 + + +def test_login_without_data_returns_400(flask_client): + res = flask_client.post('/api/v2/core/login') + assert res.status_code == 400 + + +def test_login_with_invalid_credetials_returns_401(flask_client): + res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='invalid')) + assert res.status_code == 401 + + +def test_login_with_valid_credetials_returns_token(flask_client): + Registry().register('authentication_token', 'foobar') + res = flask_client.post('/api/v2/core/login', json=dict(username='openlp', password='password')) + assert res.status_code == 200 + assert res.get_json()['token'] == 'foobar' + + +def test_retrieving_image(flask_client): + class FakeController: + def grab_maindisplay(self): + class FakeImage: + def save(self, first, second): + pass + return FakeImage() + Registry.create().register('live_controller', FakeController()) + res = flask_client.get('/api/v2/core/live-image').get_json() + assert res['binary_image'] != '' + + +def test_toggle_display_requires_login(flask_client): + Settings().setValue('api/authentication enabled', True) + res = flask_client.post('/api/v2/core/display') + Settings().setValue('api/authentication enabled', False) + assert res.status_code == 401 + + +def test_toggle_display_does_not_allow_get(flask_client): + res = flask_client.get('/api/v2/core/display') + assert res.status_code == 405 + + +def test_toggle_display_invalid_action(flask_client): + res = flask_client.post('/api/v2/core/display', json={'display': 'foo'}) + assert res.status_code == 400 + + +def test_toggle_display_valid_action_updates_controller(flask_client): + class FakeController: + class Emitter: + def emit(self, value): + self.set = value + slidecontroller_toggle_display = Emitter() + controller = FakeController() + Registry.create().register('live_controller', controller) + res = flask_client.post('/api/v2/core/display', json={'display': 'show'}) + assert res.status_code == 204 + assert controller.slidecontroller_toggle_display.set == 'show' + + +def test_cors_headers_are_present(flask_client): + res = flask_client.get('/api/v2/core/system') + assert 'Access-Control-Allow-Origin' in res.headers + assert res.headers['Access-Control-Allow-Origin'] == '*' diff --git a/tests/functional/openlp_core/api/v2/test_service.py b/tests/functional/openlp_core/api/v2/test_service.py new file mode 100644 index 000000000..9341b3711 --- /dev/null +++ b/tests/functional/openlp_core/api/v2/test_service.py @@ -0,0 +1,56 @@ +import pytest +from unittest.mock import MagicMock + +from openlp.core.common.registry import Registry +from openlp.core.common.settings import Settings + + +def test_retrieve_service_items(flask_client): + pytest.skip() + res = flask_client.get('/api/v2/service/items').get_json() + assert len(res) == 0 + + +def test_service_set_requires_login(flask_client): + Settings().setValue('api/authentication enabled', True) + res = flask_client.post('/api/v2/service/show', json=dict()) + Settings().setValue('api/authentication enabled', False) + assert res.status_code == 401 + + +def test_service_set_does_not_accept_get(flask_client): + res = flask_client.get('/api/v2/service/show') + assert res.status_code == 405 + + +def test_service_set_calls_service_manager(flask_client): + fake_service_manager = MagicMock() + Registry.create().register('service_manager', fake_service_manager) + res = flask_client.post('/api/v2/service/show', json=dict(id=400)) + assert res.status_code == 204 + fake_service_manager.set_item.assert_called_once_with(400) + + +def test_service_direction_requires_login(flask_client): + Settings().setValue('api/authentication enabled', True) + res = flask_client.post('/api/v2/service/progress', json=dict()) + Settings().setValue('api/authentication enabled', False) + assert res.status_code == 401 + + +def test_service_direction_does_not_accept_get(flask_client): + res = flask_client.get('/api/v2/service/progress') + assert res.status_code == 405 + + +def test_service_direction_does_fails_on_wrong_data(flask_client): + res = flask_client.post('/api/v2/service/progress', json=dict(action='foo')) + assert res.status_code == 400 + + +def test_service_direction_calls_service_manager(flask_client): + fake_service_manager = MagicMock() + Registry.create().register('service_manager', fake_service_manager) + res = flask_client.post('/api/v2/service/progress', json=dict(action='next')) + assert res.status_code == 204 + fake_service_manager.servicemanager_next_item.emit.assert_called_once() diff --git a/tests/functional/openlp_core/ui/test_mainwindow.py b/tests/functional/openlp_core/ui/test_mainwindow.py index 71c456148..011ed3e50 100644 --- a/tests/functional/openlp_core/ui/test_mainwindow.py +++ b/tests/functional/openlp_core/ui/test_mainwindow.py @@ -161,8 +161,8 @@ class TestMainWindow(TestCase, TestMixin): # WHEN: you check the started functions # THEN: the following registry functions should have been registered - expected_service_list = ['application', 'main_window', 'http_server', 'settings_form', 'service_manager', - 'theme_manager', 'projector_manager'] + expected_service_list = ['application', 'main_window', 'http_server', 'authentication_token', 'settings_form', + 'service_manager', 'theme_manager', 'projector_manager'] expected_functions_list = ['bootstrap_initialise', 'bootstrap_post_set_up', 'bootstrap_completion', 'theme_update_global', 'config_screen_changed'] assert list(self.registry.service_list.keys()) == expected_service_list, \