From 90d5020a8738d6e51e9683ad83ecc4dc2afd8b5f Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Sat, 17 Apr 2021 21:28:57 +0100 Subject: [PATCH] Shuffle flask into own file, change platypus options. --- .vscode/launch.json | 11 +- build/build-exe-config.template.json | 2 +- build/macos-platypus.sh | 10 +- launch_standalone.py => launch.py | 24 +- server.py | 486 +----------------- templates/base.html | 20 +- templates/{config.html => config_player.html} | 6 +- templates/{server.html => config_server.html} | 2 +- web_server.py | 431 ++++++++++++++++ 9 files changed, 485 insertions(+), 507 deletions(-) rename launch_standalone.py => launch.py (70%) rename templates/{config.html => config_player.html} (78%) rename templates/{server.html => config_server.html} (97%) create mode 100644 web_server.py diff --git a/.vscode/launch.json b/.vscode/launch.json index e644a23..e9c6420 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,17 +2,10 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Launch Player Standalone", + "name": "Python: Launch Server", "type": "python", "request": "launch", - "program": "./player.py", - "console": "integratedTerminal" - }, - { - "name": "Python: Launch Server Standalone", - "type": "python", - "request": "launch", - "program": "./launch_standalone.py", + "program": "./launch.py", "console": "integratedTerminal" }, { diff --git a/build/build-exe-config.template.json b/build/build-exe-config.template.json index a02ddfd..3876b03 100644 --- a/build/build-exe-config.template.json +++ b/build/build-exe-config.template.json @@ -7,7 +7,7 @@ }, { "optionDest": "filenames", - "value": "/launch_standalone.py" + "value": "/launch.py" }, { "optionDest": "onefile", diff --git a/build/macos-platypus.sh b/build/macos-platypus.sh index edef21b..1065d22 100755 --- a/build/macos-platypus.sh +++ b/build/macos-platypus.sh @@ -5,15 +5,13 @@ then echo "----" if curl --output /dev/null --silent --head --fail --max-time 1 "http://localhost:13500" then - echo "Status" - echo "Config" - echo "Logs" + echo "Presenter" + echo "Server" echo "----" echo "Stop Server" else - echo "DISABLED|Status" - echo "DISABLED|Config" - echo "DISABLED|Logs" + echo "DISABLED|Presenter" + echo "DISABLED|Server" echo "----" echo "Start Server" fi diff --git a/launch_standalone.py b/launch.py similarity index 70% rename from launch_standalone.py rename to launch.py index 22c64ec..1dcd666 100755 --- a/launch_standalone.py +++ b/launch.py @@ -7,18 +7,23 @@ from setproctitle import setproctitle from server import BAPSicleServer - -def startServer(): +def startServer(notifications = False): server = multiprocessing.Process(target=BAPSicleServer) server.start() + sent_start_notif = False try: while True: time.sleep(5) if server and server.is_alive(): + if notifications and not sent_start_notif: + print("NOTIFICATION:Welcome to BAPSicle!") + sent_start_notif = True pass else: print("Server dead. Exiting.") + if notifications: + print("NOTIFICATION:BAPSicle Server Stopped!") sys.exit(0) # Catch the handler being killed externally. except KeyboardInterrupt: @@ -37,20 +42,17 @@ if __name__ == "__main__": # If it's not here, multiprocessing just doesn't run in the package. # Freeze support refers to being packaged with Pyinstaller. multiprocessing.freeze_support() - setproctitle("BAPSicle - Standalone Launch") + setproctitle("BAPSicle Launcher") if len(sys.argv) > 1: # We got an argument! It's probably Platypus's UI. try: if (sys.argv[1]) == "Start Server": - print("NOTIFICATION:Welcome to BAPSicle!") webbrowser.open("http://localhost:13500/") - startServer() - if sys.argv[1] == "Status": - webbrowser.open("http://localhost:13500/status") - if sys.argv[1] == "Config": - webbrowser.open("http://localhost:13500/config") - if sys.argv[1] == "Logs": - webbrowser.open("http://localhost:13500/logs") + startServer(notifications=True) + if sys.argv[1] == "Server": + webbrowser.open("http://localhost:13500/") + if sys.argv[1] == "Presenter": + webbrowser.open("http://localhost:13500/presenter/") except Exception as e: print("ALERT:BAPSicle failed with exception:\n", e) sys.exit(1) diff --git a/server.py b/server.py index 17b019b..6243f3c 100644 --- a/server.py +++ b/server.py @@ -15,20 +15,14 @@ from api_handler import APIHandler from controllers.mattchbox_usb import MattchBox import multiprocessing -import queue +from multiprocessing.queues import Queue import time import player -from flask import Flask, render_template, send_from_directory, request, jsonify, abort -from flask_cors import CORS -from typing import Any, Optional +from typing import Any import json from setproctitle import setproctitle -import logging - -from player_handler import PlayerHandler from helpers.os_environment import isBundelled, isMacOS -from helpers.device_manager import DeviceManager if not isMacOS(): # Rip, this doesn't like threading on MacOS. @@ -42,8 +36,10 @@ from typing import Dict, List from helpers.state_manager import StateManager from helpers.logging_manager import LoggingManager from websocket_server import WebsocketServer +from web_server import WebServer +from player_handler import PlayerHandler -setproctitle("BAPSicleServer.py") +setproctitle("server.py") class BAPSicleServer: @@ -51,8 +47,8 @@ class BAPSicleServer: startServer() - def get_flask(self): - return app +# def get_flask(self): +# return app default_state = { @@ -71,444 +67,17 @@ default_state = { } -app = Flask(__name__, static_url_path="") - - -logger: LoggingManager -state: StateManager - -api_from_q: queue.Queue -api_to_q: queue.Queue - -channel_to_q: List[queue.Queue] = [] -channel_from_q: List[queue.Queue] = [] -ui_to_q: List[queue.Queue] = [] -websocket_to_q: List[queue.Queue] = [] -controller_to_q: List[queue.Queue] = [] +channel_to_q: List[Queue] = [] +channel_from_q: List[Queue] = [] +ui_to_q: List[Queue] = [] +websocket_to_q: List[Queue] = [] +controller_to_q: List[Queue] = [] channel_p: List[multiprocessing.Process] = [] websockets_server: multiprocessing.Process controller_handler: multiprocessing.Process webserver: multiprocessing.Process -# General Endpoints - - -@app.errorhandler(404) -def page_not_found(e: Any): - data = {"ui_page": "404", "ui_title": "404"} - return render_template("404.html", data=data), 404 - - -@app.route("/") -def ui_index(): - data = { - "ui_page": "index", - "ui_title": "", - "server_version": state.state["server_version"], - "server_build": state.state["server_build"], - "server_name": state.state["server_name"], - } - return render_template("index.html", data=data) - - -@app.route("/config") -def ui_config(): - channel_states = [] - for i in range(state.state["num_channels"]): - channel_states.append(status(i)) - - outputs = DeviceManager.getAudioOutputs() - - data = { - "channels": channel_states, - "outputs": outputs, - "ui_page": "config", - "ui_title": "Config", - } - return render_template("config.html", data=data) - - -@app.route("/status") -def ui_status(): - channel_states = [] - for i in range(state.state["num_channels"]): - channel_states.append(status(i)) - - data = {"channels": channel_states, - "ui_page": "status", "ui_title": "Status"} - return render_template("status.html", data=data) - - -@app.route("/status-json") -def json_status(): - channel_states = [] - for i in range(state.state["num_channels"]): - channel_states.append(status(i)) - return {"server": state.state, "channels": channel_states} - - -@app.route("/server") -def server_config(): - data = { - "ui_page": "server", - "ui_title": "Server Config", - "state": state.state, - "ser_ports": DeviceManager.getSerialPorts(), - } - return render_template("server.html", data=data) - - -@app.route("/server/update", methods=["POST"]) -def update_server(): - state.update("server_name", request.form["name"]) - state.update("host", request.form["host"]) - state.update("port", int(request.form["port"])) - state.update("num_channels", int(request.form["channels"])) - state.update("ws_port", int(request.form["ws_port"])) - state.update("serial_port", request.form["serial_port"]) - - # Because we're not showing the api key once it's set. - if "myradio_api_key" in request.form and request.form["myradio_api_key"] != "": - state.update("myradio_api_key", request.form["myradio_api_key"]) - - state.update("myradio_base_url", request.form["myradio_base_url"]) - state.update("myradio_api_url", request.form["myradio_api_url"]) - # stopServer() - return server_config() - - -# Get audio for UI to generate waveforms. - - -@app.route("/audiofile//") -def audio_file(type: str, id: int): - if type not in ["managed", "track"]: - abort(404) - return send_from_directory("music-tmp", type + "-" + str(id) + ".mp3") - - -# Channel Audio Options - - -@app.route("/player//play") -def play(channel: int): - - channel_to_q[channel].put("UI:PLAY") - - return ui_status() - - -@app.route("/player//pause") -def pause(channel: int): - - channel_to_q[channel].put("UI:PAUSE") - - return ui_status() - - -@app.route("/player//unpause") -def unPause(channel: int): - - channel_to_q[channel].put("UI:UNPAUSE") - - return ui_status() - - -@app.route("/player//stop") -def stop(channel: int): - - channel_to_q[channel].put("UI:STOP") - - return ui_status() - - -@app.route("/player//seek/") -def seek(channel: int, pos: float): - - channel_to_q[channel].put("UI:SEEK:" + str(pos)) - - return ui_status() - - -@app.route("/player//output/") -def output(channel: int, name: Optional[str]): - channel_to_q[channel].put("UI:OUTPUT:" + str(name)) - return ui_config() - - -@app.route("/player//autoadvance/") -def autoadvance(channel: int, state: int): - channel_to_q[channel].put("UI:AUTOADVANCE:" + str(state)) - return ui_status() - - -@app.route("/player//repeat/") -def repeat(channel: int, state: str): - channel_to_q[channel].put("UI:REPEAT:" + state.upper()) - return ui_status() - - -@app.route("/player//playonload/") -def playonload(channel: int, state: int): - channel_to_q[channel].put("UI:PLAYONLOAD:" + str(state)) - return ui_status() - - -# Channel Items - - -@app.route("/player//load/") -def load(channel: int, channel_weight: int): - channel_to_q[channel].put("UI:LOAD:" + str(channel_weight)) - return ui_status() - - -@app.route("/player//unload") -def unload(channel: int): - - channel_to_q[channel].put("UI:UNLOAD") - - return ui_status() - - -@app.route("/player//add", methods=["POST"]) -def add_to_plan(channel: int): - new_item: Dict[str, Any] = { - "channel_weight": int(request.form["channel_weight"]), - "filename": request.form["filename"], - "title": request.form["title"], - "artist": request.form["artist"], - } - - channel_to_q[channel].put("UI:ADD:" + json.dumps(new_item)) - - return new_item - - -# @app.route("/player//remove/") -def remove_plan(channel: int, channel_weight: int): - channel_to_q[channel].put("UI:REMOVE:" + str(channel_weight)) - - # TODO Return - return True - - -# @app.route("/player//clear") -def clear_channel_plan(channel: int): - channel_to_q[channel].put("UI:CLEAR") - - # TODO Return - return True - - -# General Channel Endpoints - - -@app.route("/player//status") -def channel_json(channel: int): - return jsonify(status(channel)) - - -@app.route("/plan/list") -def list_showplans(): - - while not api_from_q.empty(): - api_from_q.get() # Just waste any previous status responses. - - api_to_q.put("LIST_PLANS") - - while True: - try: - response = api_from_q.get_nowait() - if response.startswith("LIST_PLANS:"): - response = response[response.index(":") + 1:] - return response - - except queue.Empty: - pass - - time.sleep(0.02) - - -@app.route("/library/search/") -def search_library(type: str): - - if type not in ["managed", "track"]: - abort(404) - - while not api_from_q.empty(): - api_from_q.get() # Just waste any previous status responses. - - params = json.dumps( - {"title": request.args.get( - "title"), "artist": request.args.get("artist")} - ) - api_to_q.put("SEARCH_TRACK:{}".format(params)) - - while True: - try: - response = api_from_q.get_nowait() - if response.startswith("SEARCH_TRACK:"): - response = response.split(":", 1)[1] - return response - - except queue.Empty: - pass - - time.sleep(0.02) - - -@app.route("/library/playlists/") -def get_playlists(type: str): - - if type not in ["music", "aux"]: - abort(401) - - while not api_from_q.empty(): - api_from_q.get() # Just waste any previous status responses. - - command = "LIST_PLAYLIST_{}".format(type.upper()) - api_to_q.put(command) - - while True: - try: - response = api_from_q.get_nowait() - if response.startswith(command): - response = response.split(":", 1)[1] - return response - - except queue.Empty: - pass - - time.sleep(0.02) - - -@app.route("/library/playlist//") -def get_playlist(type: str, library_id: str): - - if type not in ["music", "aux"]: - abort(401) - - while not api_from_q.empty(): - api_from_q.get() # Just waste any previous status responses. - - command = "GET_PLAYLIST_{}:{}".format(type.upper(), library_id) - api_to_q.put(command) - - while True: - try: - response = api_from_q.get_nowait() - if response.startswith(command): - response = response[len(command) + 1:] - if response == "null": - abort(401) - return response - - except queue.Empty: - pass - - time.sleep(0.02) - - -@app.route("/plan/load/") -def load_showplan(timeslotid: int): - - for channel in channel_to_q: - channel.put("UI:GET_PLAN:" + str(timeslotid)) - - return ui_status() - - -def status(channel: int): - while not ui_to_q[channel].empty(): - ui_to_q[channel].get() # Just waste any previous status responses. - - channel_to_q[channel].put("UI:STATUS") - retries = 0 - while retries < 40: - try: - response = ui_to_q[channel].get_nowait() - if response.startswith("UI:STATUS:"): - response = response.split(":", 2)[2] - # TODO: Handle OKAY / FAIL - response = response[response.index(":") + 1:] - try: - response = json.loads(response) - except Exception as e: - raise e - return response - - except queue.Empty: - pass - - retries += 1 - - time.sleep(0.02) - - -@app.route("/quit") -def quit(): - stopServer() - return "Shutting down..." - - -@app.route("/player/all/stop") -def all_stop(): - for channel in channel_to_q: - channel.put("UI:STOP") - return ui_status() - - -@app.route("/player/all/clear") -def clear_all_channels(): - for channel in channel_to_q: - channel.put("UI:CLEAR") - return ui_status() - - -@app.route("/logs") -def list_logs(): - data = { - "ui_page": "logs", - "ui_title": "Logs", - "logs": ["BAPSicleServer"] - + ["Player{}".format(x) for x in range(state.state["num_channels"])], - } - return render_template("loglist.html", data=data) - - -@app.route("/logs/") -def send_logs(path): - log_file = open("logs/{}.log".format(path)) - data = { - "logs": log_file.read().splitlines(), - "ui_page": "logs", - "ui_title": "Logs - {}".format(path), - } - log_file.close() - return render_template("log.html", data=data) - - -@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) - def startServer(): process_title = "startServer" @@ -548,7 +117,7 @@ def startServer(): ) channel_p[channel].start() - global api_from_q, api_to_q, api_handler, player_handler, websockets_server, controller_handler + global api_from_q, api_to_q, api_handler, player_handler, websockets_server, controller_handler # , webserver api_to_q = multiprocessing.Queue() api_from_q = multiprocessing.Queue() api_handler = multiprocessing.Process( @@ -574,6 +143,11 @@ def startServer(): ) controller_handler.start() + webserver = multiprocessing.Process( + target=WebServer, args=(channel_to_q, ui_to_q, api_to_q, api_from_q, state) + ) + webserver.start() + # TODO Move this to player or installer. if False: if not isMacOS(): @@ -605,28 +179,8 @@ def startServer(): channel_to_q[0].put("LOAD:0") channel_to_q[0].put("PLAY") - # Don't use reloader, it causes Nested Processes! - def runWebServer(): - process_title = "WebServer" - setproctitle(process_title) - CORS(app, supports_credentials=True) # Allow ALL CORS!!! - - if not isBundelled(): - log = logging.getLogger("werkzeug") - log.disabled = True - - app.logger.disabled = True - app.run( - host=state.state["host"], - port=state.state["port"], - debug=True, - use_reloader=False, - threaded=False # While API handles are singlethreaded. - ) - - global webserver - webserver = multiprocessing.Process(runWebServer()) - webserver.start() + while True: + time.sleep(10000) def stopServer(): diff --git a/templates/base.html b/templates/base.html index fa69d87..b86c7e3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,10 +25,10 @@ - + - + diff --git a/templates/config.html b/templates/config_player.html similarity index 78% rename from templates/config.html rename to templates/config_player.html index ba1babb..f825bcd 100644 --- a/templates/config.html +++ b/templates/config_player.html @@ -1,10 +1,10 @@ {% extends 'base.html' %} {% block content_inner %}

Audio Outputs

-

Please note: Currently BAPSicle does not support choosing which Host API is used. Only supported options can be selected.

- +

+ Please note: Currently BAPSicle does not support choosing which Host API is used. Only supported options can be selected. +

{% for host_api in data.outputs %} -
{{host_api.name}}
diff --git a/templates/server.html b/templates/config_server.html similarity index 97% rename from templates/server.html rename to templates/config_server.html index f6daf67..f29735c 100644 --- a/templates/server.html +++ b/templates/config_server.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block content_inner %} {% if data %} -
+
diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000..3187254 --- /dev/null +++ b/web_server.py @@ -0,0 +1,431 @@ +from flask import Flask, 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 +from multiprocessing.queues import Queue +from queue import Empty +from time import sleep +import json + +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 + +app = Flask(__name__, static_url_path="") + + +logger: LoggingManager +server_state: StateManager + +api_from_q: Queue +api_to_q: Queue + +player_to_q: List[Queue] = [] +player_from_q: List[Queue] = [] + +# General UI Endpoints + + +@app.errorhandler(404) +def page_not_found(e: Any): + data = {"ui_page": "404", "ui_title": "404"} + return render_template("404.html", data=data), 404 + + +@app.route("/") +def ui_index(): + data = { + "ui_page": "index", + "ui_title": "", + "server_version": server_state.state["server_version"], + "server_build": server_state.state["server_build"], + "server_name": server_state.state["server_name"], + } + return render_template("index.html", data=data) + + +@app.route("/status") +def ui_status(): + channel_states = [] + for i in range(server_state.state["num_channels"]): + channel_states.append(status(i)) + + data = {"channels": channel_states, + "ui_page": "status", "ui_title": "Status"} + return render_template("status.html", data=data) + + +@app.route("/config/player") +def ui_config_player(): + channel_states = [] + for i in range(server_state.state["num_channels"]): + channel_states.append(status(i)) + + outputs = DeviceManager.getAudioOutputs() + + data = { + "channels": channel_states, + "outputs": outputs, + "ui_page": "config", + "ui_title": "Player Config", + } + return render_template("config_player.html", data=data) + + +@app.route("/config/server") +def ui_config_server(): + data = { + "ui_page": "server", + "ui_title": "Server Config", + "state": server_state.state, + "ser_ports": DeviceManager.getSerialPorts(), + } + return render_template("config_server.html", data=data) + + +@app.route("/config/server/update", methods=["POST"]) +def ui_config_server_update(): + server_state.update("server_name", request.form["name"]) + server_state.update("host", request.form["host"]) + server_state.update("port", int(request.form["port"])) + server_state.update("num_channels", int(request.form["channels"])) + server_state.update("ws_port", int(request.form["ws_port"])) + server_state.update("serial_port", request.form["serial_port"]) + + # Because we're not showing the api key once it's set. + if "myradio_api_key" in request.form and request.form["myradio_api_key"] != "": + server_state.update("myradio_api_key", request.form["myradio_api_key"]) + + 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() + + +@app.route("/logs") +def ui_logs_list(): + data = { + "ui_page": "logs", + "ui_title": "Logs", + "logs": ["BAPSicleServer"] + + ["Player{}".format(x) for x in range(server_state.state["num_channels"])], + } + return render_template("loglist.html", data=data) + + +@app.route("/logs/") +def ui_logs_render(path): + log_file = open("logs/{}.log".format(path)) + data = { + "logs": log_file.read().splitlines(), + "ui_page": "logs", + "ui_title": "Logs - {}".format(path), + } + log_file.close() + return render_template("log.html", data=data) + + +# Player Audio Control Endpoints +# Just useful for messing arround without presenter / websockets. + + +@app.route("/player//") +def player_simple(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 page_not_found() + + +@app.route("/player//seek/") +def player_seek(channel: int, pos: float): + + player_to_q[channel].put("UI:SEEK:" + str(pos)) + + return ui_status() + + +@app.route("/player//load/") +def player_load(channel: int, channel_weight: int): + + player_to_q[channel].put("UI:LOAD:" + str(channel_weight)) + return ui_status() + + +@app.route("/player//remove/") +def player_remove(channel: int, channel_weight: int): + player_to_q[channel].put("UI:REMOVE:" + str(channel_weight)) + + return ui_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//autoadvance/") +def player_autoadvance(channel: int, state: int): + player_to_q[channel].put("UI:AUTOADVANCE:" + str(state)) + return ui_status() + + +@app.route("/player//repeat/") +def player_repeat(channel: int, state: str): + player_to_q[channel].put("UI:REPEAT:" + state.upper()) + return ui_status() + + +@app.route("/player//playonload/") +def player_playonload(channel: int, state: int): + player_to_q[channel].put("UI:PLAYONLOAD:" + str(state)) + return ui_status() + + +@app.route("/player//status") +def player_status_json(channel: int): + + return jsonify(status(channel)) + + +@app.route("/player/all/stop") +def player_all_stop(): + + for channel in player_to_q: + channel.put("UI:STOP") + return ui_status() + + +# Show Plan Functions + +@app.route("/plan/load/") +def plan_load(timeslotid: int): + + for channel in player_to_q: + channel.put("UI:GET_PLAN:" + str(timeslotid)) + + return ui_status() + + +@app.route("/plan/clear") +def plan_clear(): + for channel in player_to_q: + channel.put("UI:CLEAR") + return ui_status() + + +# API Proxy Endpoints + +@app.route("/plan/list") +def api_list_showplans(): + + while not api_from_q.empty(): + api_from_q.get() # Just waste any previous status responses. + + api_to_q.put("LIST_PLANS") + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith("LIST_PLANS:"): + response = response[response.index(":") + 1:] + return response + + except Empty: + pass + + sleep(0.02) + + +@app.route("/library/search/") +def api_search_library(type: str): + + if type not in ["managed", "track"]: + abort(404) + + while not api_from_q.empty(): + api_from_q.get() # Just waste any previous status responses. + + params = json.dumps( + {"title": request.args.get( + "title"), "artist": request.args.get("artist")} + ) + api_to_q.put("SEARCH_TRACK:{}".format(params)) + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith("SEARCH_TRACK:"): + response = response.split(":", 1)[1] + return response + + except Empty: + pass + + sleep(0.02) + + +@app.route("/library/playlists/") +def api_get_playlists(type: str): + + if type not in ["music", "aux"]: + abort(401) + + while not api_from_q.empty(): + api_from_q.get() # Just waste any previous status responses. + + command = "LIST_PLAYLIST_{}".format(type.upper()) + api_to_q.put(command) + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith(command): + response = response.split(":", 1)[1] + return response + + except Empty: + pass + + sleep(0.02) + + +@app.route("/library/playlist//") +def api_get_playlist(type: str, library_id: str): + + if type not in ["music", "aux"]: + abort(401) + + while not api_from_q.empty(): + api_from_q.get() # Just waste any previous status responses. + + command = "GET_PLAYLIST_{}:{}".format(type.upper(), library_id) + api_to_q.put(command) + + while True: + try: + response = api_from_q.get_nowait() + if response.startswith(command): + response = response[len(command) + 1:] + if response == "null": + abort(401) + return response + + except Empty: + pass + + sleep(0.02) + + +# JSON Outputs + + +@app.route("/status-json") +def json_status(): + channel_states = [] + for i in range(server_state.state["num_channels"]): + channel_states.append(status(i)) + return {"server": server_state.state, "channels": channel_states} + + +# Get audio for UI to generate waveforms. + + +@app.route("/audiofile//") +def audio_file(type: str, id: int): + if type not in ["managed", "track"]: + abort(404) + return send_from_directory("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) + + +# Helper Functions + +def status(channel: int): + while not player_from_q[channel].empty(): + player_from_q[channel].get() # Just waste any previous status responses. + + player_to_q[channel].put("UI:STATUS") + retries = 0 + while retries < 40: + try: + response = player_from_q[channel].get_nowait() + if response.startswith("UI:STATUS:"): + response = response.split(":", 2)[2] + # TODO: Handle OKAY / FAIL + response = response[response.index(":") + 1:] + try: + response = json.loads(response) + except Exception as e: + raise e + return response + + except Empty: + pass + + retries += 1 + + sleep(0.02) + +# WebServer Start / Stop Functions + + +@app.route("/quit") +def quit(): + # stopServer() + return "Shutting down..." + + +# Don't use reloader, it causes Nested Processes! +def WebServer(player_to: List[Queue], player_from: List[Queue], api_to: Queue, api_from: Queue, state: StateManager): + + global player_to_q, player_from_q, api_to_q, api_from_q, server_state + player_to_q = player_to + player_from_q = player_from + api_from_q = api_from + api_to_q = api_to + server_state = state + + process_title = "WebServer" + setproctitle(process_title) + CORS(app, supports_credentials=True) # Allow ALL CORS!!! + + 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. + )