From fcf9d8dceba9afb4edaf82f4f39919ec2f0120f7 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sun, 18 Apr 2021 02:03:44 +0100 Subject: [PATCH] Switch to sanic from flask. --- build/requirements.txt | 4 +- {templates => ui-templates}/404.html | 0 {templates => ui-templates}/base.html | 0 .../config_player.html | 0 .../config_server.html | 0 {templates => ui-templates}/index.html | 0 {templates => ui-templates}/log.html | 0 {templates => ui-templates}/loglist.html | 0 {templates => ui-templates}/status.html | 0 web_server.py | 212 +++++++++--------- 10 files changed, 114 insertions(+), 102 deletions(-) rename {templates => ui-templates}/404.html (100%) rename {templates => ui-templates}/base.html (100%) rename {templates => ui-templates}/config_player.html (100%) rename {templates => ui-templates}/config_server.html (100%) rename {templates => ui-templates}/index.html (100%) rename {templates => ui-templates}/log.html (100%) rename {templates => ui-templates}/loglist.html (100%) rename {templates => ui-templates}/status.html (100%) diff --git a/build/requirements.txt b/build/requirements.txt index 14f19fb..db85fbc 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,7 +1,7 @@ autopep8 pygame==2.0.1 -flask -flask-cors +sanic +sanic-cors mutagen sounddevice autopep8 diff --git a/templates/404.html b/ui-templates/404.html similarity index 100% rename from templates/404.html rename to ui-templates/404.html diff --git a/templates/base.html b/ui-templates/base.html similarity index 100% rename from templates/base.html rename to ui-templates/base.html diff --git a/templates/config_player.html b/ui-templates/config_player.html similarity index 100% rename from templates/config_player.html rename to ui-templates/config_player.html diff --git a/templates/config_server.html b/ui-templates/config_server.html similarity index 100% rename from templates/config_server.html rename to ui-templates/config_server.html diff --git a/templates/index.html b/ui-templates/index.html similarity index 100% rename from templates/index.html rename to ui-templates/index.html diff --git a/templates/log.html b/ui-templates/log.html similarity index 100% rename from templates/log.html rename to ui-templates/log.html diff --git a/templates/loglist.html b/ui-templates/loglist.html similarity index 100% rename from templates/loglist.html rename to ui-templates/loglist.html diff --git a/templates/status.html b/ui-templates/status.html similarity index 100% rename from templates/status.html rename to ui-templates/status.html diff --git a/web_server.py b/web_server.py index 3187254..4330825 100644 --- a/web_server.py +++ b/web_server.py @@ -1,5 +1,13 @@ -from flask import Flask, render_template, send_from_directory, request, jsonify, abort -from flask_cors import CORS +from sanic import Sanic +from sanic.exceptions import NotFound, abort +from sanic.response import html, text, file, redirect +from sanic.response import json as resp_json +from sanic_cors import CORS + +from jinja2 import Environment, FileSystemLoader +from urllib.parse import unquote +# , render_template, send_from_directory, request, jsonify, abort +#from flask_cors import CORS from setproctitle import setproctitle import logging from typing import Any, Optional, List @@ -7,13 +15,22 @@ from multiprocessing.queues import Queue from queue import Empty from time import sleep import json +import os from helpers.os_environment import isBundelled, isMacOS from helpers.logging_manager import LoggingManager from helpers.device_manager import DeviceManager from helpers.state_manager import StateManager +from helpers.the_terminator import Terminator -app = Flask(__name__, static_url_path="") +env = Environment(loader=FileSystemLoader('%s/ui-templates/' % os.path.dirname(__file__))) +app = Sanic("BAPSicle Web Server") + + +def render_template(file, data, status=200): + template = env.get_template(file) + html_content = template.render(data=data) + return html(html_content, status=status) logger: LoggingManager @@ -28,14 +45,14 @@ player_from_q: List[Queue] = [] # General UI Endpoints -@app.errorhandler(404) -def page_not_found(e: Any): +@app.exception(NotFound) +def page_not_found(request, e: Any): data = {"ui_page": "404", "ui_title": "404"} - return render_template("404.html", data=data), 404 + return render_template("404.html", data=data, status=404) @app.route("/") -def ui_index(): +def ui_index(request): data = { "ui_page": "index", "ui_title": "", @@ -47,7 +64,7 @@ def ui_index(): @app.route("/status") -def ui_status(): +def ui_status(request): channel_states = [] for i in range(server_state.state["num_channels"]): channel_states.append(status(i)) @@ -58,7 +75,7 @@ def ui_status(): @app.route("/config/player") -def ui_config_player(): +def ui_config_player(request): channel_states = [] for i in range(server_state.state["num_channels"]): channel_states.append(status(i)) @@ -75,7 +92,7 @@ def ui_config_player(): @app.route("/config/server") -def ui_config_server(): +def ui_config_server(request): data = { "ui_page": "server", "ui_title": "Server Config", @@ -86,7 +103,7 @@ def ui_config_server(): @app.route("/config/server/update", methods=["POST"]) -def ui_config_server_update(): +def ui_config_server_update(request): server_state.update("server_name", request.form["name"]) server_state.update("host", request.form["host"]) server_state.update("port", int(request.form["port"])) @@ -101,11 +118,11 @@ def ui_config_server_update(): server_state.update("myradio_base_url", request.form["myradio_base_url"]) server_state.update("myradio_api_url", request.form["myradio_api_url"]) # stopServer() - return ui_config_server() + return ui_config_server(request) @app.route("/logs") -def ui_logs_list(): +def ui_logs_list(request): data = { "ui_page": "logs", "ui_title": "Logs", @@ -116,7 +133,7 @@ def ui_logs_list(): @app.route("/logs/") -def ui_logs_render(path): +def ui_logs_render(request, path): log_file = open("logs/{}.log".format(path)) data = { "logs": log_file.read().splitlines(), @@ -131,99 +148,99 @@ def ui_logs_render(path): # Just useful for messing arround without presenter / websockets. -@app.route("/player//") -def player_simple(channel: int, command: str): +@app.route("/player//") +def player_simple(request, channel: int, command: str): simple_endpoints = ["play", "pause", "unpause", "stop", "unload", "clear"] if command in simple_endpoints: player_to_q[channel].put("UI:" + command.upper()) - return ui_status() + return redirect("/status") - return page_not_found() + abort(404) -@app.route("/player//seek/") -def player_seek(channel: int, pos: float): +@app.route("/player//seek/") +def player_seek(request, channel: int, pos: float): player_to_q[channel].put("UI:SEEK:" + str(pos)) - return ui_status() + return redirect("/status") -@app.route("/player//load/") -def player_load(channel: int, channel_weight: int): +@app.route("/player//load/") +def player_load(request, channel: int, channel_weight: int): player_to_q[channel].put("UI:LOAD:" + str(channel_weight)) - return ui_status() + return redirect("/status") -@app.route("/player//remove/") -def player_remove(channel: int, channel_weight: int): +@app.route("/player//remove/") +def player_remove(request, channel: int, channel_weight: int): player_to_q[channel].put("UI:REMOVE:" + str(channel_weight)) - return ui_status() + return redirect("/status") -@app.route("/player//output/") -def player_output(channel: int, name: Optional[str]): - player_to_q[channel].put("UI:OUTPUT:" + str(name)) - return ui_config_player() +@app.route("/player//output/") +def player_output(request, channel: int, name: Optional[str]): + player_to_q[channel].put("UI:OUTPUT:" + unquote(str(name))) + return redirect("/config/player") -@app.route("/player//autoadvance/") -def player_autoadvance(channel: int, state: int): +@app.route("/player//autoadvance/") +def player_autoadvance(request, channel: int, state: int): player_to_q[channel].put("UI:AUTOADVANCE:" + str(state)) - return ui_status() + return redirect("/status") -@app.route("/player//repeat/") -def player_repeat(channel: int, state: str): +@app.route("/player//repeat/") +def player_repeat(request, channel: int, state: str): player_to_q[channel].put("UI:REPEAT:" + state.upper()) - return ui_status() + return redirect("/status") -@app.route("/player//playonload/") -def player_playonload(channel: int, state: int): +@app.route("/player//playonload/") +def player_playonload(request, channel: int, state: int): player_to_q[channel].put("UI:PLAYONLOAD:" + str(state)) - return ui_status() + return redirect("/status") -@app.route("/player//status") -def player_status_json(channel: int): +@app.route("/player//status") +def player_status_json(request, channel: int): - return jsonify(status(channel)) + return resp_json(status(channel)) @app.route("/player/all/stop") -def player_all_stop(): +def player_all_stop(request): for channel in player_to_q: channel.put("UI:STOP") - return ui_status() + return redirect("/status") # Show Plan Functions @app.route("/plan/load/") -def plan_load(timeslotid: int): +def plan_load(request, timeslotid: int): for channel in player_to_q: channel.put("UI:GET_PLAN:" + str(timeslotid)) - return ui_status() + return redirect("/status") @app.route("/plan/clear") -def plan_clear(): +def plan_clear(request): for channel in player_to_q: channel.put("UI:CLEAR") - return ui_status() + return redirect("/status") # API Proxy Endpoints @app.route("/plan/list") -def api_list_showplans(): +def api_list_showplans(request): while not api_from_q.empty(): api_from_q.get() # Just waste any previous status responses. @@ -235,7 +252,7 @@ def api_list_showplans(): response = api_from_q.get_nowait() if response.startswith("LIST_PLANS:"): response = response[response.index(":") + 1:] - return response + return text(response) except Empty: pass @@ -244,7 +261,7 @@ def api_list_showplans(): @app.route("/library/search/") -def api_search_library(type: str): +def api_search_library(request, type: str): if type not in ["managed", "track"]: abort(404) @@ -253,17 +270,17 @@ def api_search_library(type: str): api_from_q.get() # Just waste any previous status responses. params = json.dumps( - {"title": request.args.get( - "title"), "artist": request.args.get("artist")} + {"title": request.args.get("title"), "artist": request.args.get("artist")} ) - api_to_q.put("SEARCH_TRACK:{}".format(params)) + command = "SEARCH_TRACK:{}".format(params) + api_to_q.put(command) while True: try: response = api_from_q.get_nowait() if response.startswith("SEARCH_TRACK:"): - response = response.split(":", 1)[1] - return response + response = response[len(command)+1:] + return text(response) except Empty: pass @@ -271,8 +288,8 @@ def api_search_library(type: str): sleep(0.02) -@app.route("/library/playlists/") -def api_get_playlists(type: str): +@app.route("/library/playlists/") +def api_get_playlists(request, type: str): if type not in ["music", "aux"]: abort(401) @@ -288,7 +305,7 @@ def api_get_playlists(type: str): response = api_from_q.get_nowait() if response.startswith(command): response = response.split(":", 1)[1] - return response + return text(response) except Empty: pass @@ -296,8 +313,8 @@ def api_get_playlists(type: str): sleep(0.02) -@app.route("/library/playlist//") -def api_get_playlist(type: str, library_id: str): +@app.route("/library/playlist//") +def api_get_playlist(request, type: str, library_id: str): if type not in ["music", "aux"]: abort(401) @@ -315,7 +332,7 @@ def api_get_playlist(type: str, library_id: str): response = response[len(command) + 1:] if response == "null": abort(401) - return response + return text(response) except Empty: pass @@ -327,43 +344,28 @@ def api_get_playlist(type: str, library_id: str): @app.route("/status-json") -def json_status(): +def json_status(request): channel_states = [] for i in range(server_state.state["num_channels"]): channel_states.append(status(i)) - return {"server": server_state.state, "channels": channel_states} + return resp_json({"server": server_state.state, "channels": channel_states}) # Get audio for UI to generate waveforms. -@app.route("/audiofile//") -def audio_file(type: str, id: int): +@app.route("/audiofile//") +async def audio_file(request, type: str, id: int): if type not in ["managed", "track"]: abort(404) - return send_from_directory("music-tmp", type + "-" + str(id) + ".mp3") + return await file("music-tmp/" + type + "-" + str(id) + ".mp3") # Static Files - -@app.route("/favicon.ico") -def serve_favicon(): - return send_from_directory("ui-static", "favicon.ico") - - -@app.route("/static/") -def serve_static(path: str): - return send_from_directory("ui-static", path) - - -@app.route("/presenter/") -def serve_presenter_index(): - return send_from_directory("presenter-build", "index.html") - - -@app.route("/presenter/") -def serve_presenter_static(path: str): - return send_from_directory("presenter-build", path) +app.static("/favicon.ico", "./ui-static/favicon.ico", name="ui-favicon") +app.static("/static", "./ui-static", name="ui-static") +app.static("/presenter/", "./presenter-build/index.html", strict_slashes=True) +app.static("/presenter", "./presenter-build") # Helper Functions @@ -398,7 +400,7 @@ def status(channel: int): @app.route("/quit") -def quit(): +def quit(request): # stopServer() return "Shutting down..." @@ -413,19 +415,29 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], api_to: Queue, a api_to_q = api_to server_state = state - process_title = "WebServer" + process_title = "Web Server" setproctitle(process_title) CORS(app, supports_credentials=True) # Allow ALL CORS!!! - if not isBundelled(): - log = logging.getLogger("werkzeug") - log.disabled = True + # if not isBundelled(): + # log = logging.getLogger("werkzeug") + # log.disabled = True - app.logger.disabled = True - app.run( - host=server_state.state["host"], - port=server_state.state["port"], - debug=True, - use_reloader=False, - threaded=False # While API handles are singlethreaded. - ) + #app.logger.disabled = True + + terminate = Terminator() + while not terminate.terminate: + try: + app.run( + host=server_state.state["host"], + port=server_state.state["port"], + debug=True, + workers=1, + auto_reload=False + + # use_reloader=False, + # threaded=False # While API handles are singlethreaded. + ) + except Exception: + break + app.stop()