Shuffle flask into own file, change platypus options.

This commit is contained in:
Matthew Stratford 2021-04-17 21:28:57 +01:00
parent f0d6540e26
commit 90d5020a87
9 changed files with 485 additions and 507 deletions

11
.vscode/launch.json vendored
View file

@ -2,17 +2,10 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Launch Player Standalone", "name": "Python: Launch Server",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "./player.py", "program": "./launch.py",
"console": "integratedTerminal"
},
{
"name": "Python: Launch Server Standalone",
"type": "python",
"request": "launch",
"program": "./launch_standalone.py",
"console": "integratedTerminal" "console": "integratedTerminal"
}, },
{ {

View file

@ -7,7 +7,7 @@
}, },
{ {
"optionDest": "filenames", "optionDest": "filenames",
"value": "/launch_standalone.py" "value": "/launch.py"
}, },
{ {
"optionDest": "onefile", "optionDest": "onefile",

View file

@ -5,15 +5,13 @@ then
echo "----" echo "----"
if curl --output /dev/null --silent --head --fail --max-time 1 "http://localhost:13500" if curl --output /dev/null --silent --head --fail --max-time 1 "http://localhost:13500"
then then
echo "Status" echo "Presenter"
echo "Config" echo "Server"
echo "Logs"
echo "----" echo "----"
echo "Stop Server" echo "Stop Server"
else else
echo "DISABLED|Status" echo "DISABLED|Presenter"
echo "DISABLED|Config" echo "DISABLED|Server"
echo "DISABLED|Logs"
echo "----" echo "----"
echo "Start Server" echo "Start Server"
fi fi

View file

@ -7,18 +7,23 @@ from setproctitle import setproctitle
from server import BAPSicleServer from server import BAPSicleServer
def startServer(notifications = False):
def startServer():
server = multiprocessing.Process(target=BAPSicleServer) server = multiprocessing.Process(target=BAPSicleServer)
server.start() server.start()
sent_start_notif = False
try: try:
while True: while True:
time.sleep(5) time.sleep(5)
if server and server.is_alive(): if server and server.is_alive():
if notifications and not sent_start_notif:
print("NOTIFICATION:Welcome to BAPSicle!")
sent_start_notif = True
pass pass
else: else:
print("Server dead. Exiting.") print("Server dead. Exiting.")
if notifications:
print("NOTIFICATION:BAPSicle Server Stopped!")
sys.exit(0) sys.exit(0)
# Catch the handler being killed externally. # Catch the handler being killed externally.
except KeyboardInterrupt: except KeyboardInterrupt:
@ -37,20 +42,17 @@ if __name__ == "__main__":
# If it's not here, multiprocessing just doesn't run in the package. # If it's not here, multiprocessing just doesn't run in the package.
# Freeze support refers to being packaged with Pyinstaller. # Freeze support refers to being packaged with Pyinstaller.
multiprocessing.freeze_support() multiprocessing.freeze_support()
setproctitle("BAPSicle - Standalone Launch") setproctitle("BAPSicle Launcher")
if len(sys.argv) > 1: if len(sys.argv) > 1:
# We got an argument! It's probably Platypus's UI. # We got an argument! It's probably Platypus's UI.
try: try:
if (sys.argv[1]) == "Start Server": if (sys.argv[1]) == "Start Server":
print("NOTIFICATION:Welcome to BAPSicle!")
webbrowser.open("http://localhost:13500/") webbrowser.open("http://localhost:13500/")
startServer() startServer(notifications=True)
if sys.argv[1] == "Status": if sys.argv[1] == "Server":
webbrowser.open("http://localhost:13500/status") webbrowser.open("http://localhost:13500/")
if sys.argv[1] == "Config": if sys.argv[1] == "Presenter":
webbrowser.open("http://localhost:13500/config") webbrowser.open("http://localhost:13500/presenter/")
if sys.argv[1] == "Logs":
webbrowser.open("http://localhost:13500/logs")
except Exception as e: except Exception as e:
print("ALERT:BAPSicle failed with exception:\n", e) print("ALERT:BAPSicle failed with exception:\n", e)
sys.exit(1) sys.exit(1)

486
server.py
View file

@ -15,20 +15,14 @@
from api_handler import APIHandler from api_handler import APIHandler
from controllers.mattchbox_usb import MattchBox from controllers.mattchbox_usb import MattchBox
import multiprocessing import multiprocessing
import queue from multiprocessing.queues import Queue
import time import time
import player import player
from flask import Flask, render_template, send_from_directory, request, jsonify, abort from typing import Any
from flask_cors import CORS
from typing import Any, Optional
import json import json
from setproctitle import setproctitle from setproctitle import setproctitle
import logging
from player_handler import PlayerHandler
from helpers.os_environment import isBundelled, isMacOS from helpers.os_environment import isBundelled, isMacOS
from helpers.device_manager import DeviceManager
if not isMacOS(): if not isMacOS():
# Rip, this doesn't like threading on MacOS. # 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.state_manager import StateManager
from helpers.logging_manager import LoggingManager from helpers.logging_manager import LoggingManager
from websocket_server import WebsocketServer from websocket_server import WebsocketServer
from web_server import WebServer
from player_handler import PlayerHandler
setproctitle("BAPSicleServer.py") setproctitle("server.py")
class BAPSicleServer: class BAPSicleServer:
@ -51,8 +47,8 @@ class BAPSicleServer:
startServer() startServer()
def get_flask(self): # def get_flask(self):
return app # return app
default_state = { default_state = {
@ -71,444 +67,17 @@ default_state = {
} }
app = Flask(__name__, static_url_path="") channel_to_q: List[Queue] = []
channel_from_q: List[Queue] = []
ui_to_q: List[Queue] = []
logger: LoggingManager websocket_to_q: List[Queue] = []
state: StateManager controller_to_q: List[Queue] = []
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_p: List[multiprocessing.Process] = [] channel_p: List[multiprocessing.Process] = []
websockets_server: multiprocessing.Process websockets_server: multiprocessing.Process
controller_handler: multiprocessing.Process controller_handler: multiprocessing.Process
webserver: 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/<type>/<int:id>")
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/<int:channel>/play")
def play(channel: int):
channel_to_q[channel].put("UI:PLAY")
return ui_status()
@app.route("/player/<int:channel>/pause")
def pause(channel: int):
channel_to_q[channel].put("UI:PAUSE")
return ui_status()
@app.route("/player/<int:channel>/unpause")
def unPause(channel: int):
channel_to_q[channel].put("UI:UNPAUSE")
return ui_status()
@app.route("/player/<int:channel>/stop")
def stop(channel: int):
channel_to_q[channel].put("UI:STOP")
return ui_status()
@app.route("/player/<int:channel>/seek/<float:pos>")
def seek(channel: int, pos: float):
channel_to_q[channel].put("UI:SEEK:" + str(pos))
return ui_status()
@app.route("/player/<int:channel>/output/<name>")
def output(channel: int, name: Optional[str]):
channel_to_q[channel].put("UI:OUTPUT:" + str(name))
return ui_config()
@app.route("/player/<int:channel>/autoadvance/<int:state>")
def autoadvance(channel: int, state: int):
channel_to_q[channel].put("UI:AUTOADVANCE:" + str(state))
return ui_status()
@app.route("/player/<int:channel>/repeat/<state>")
def repeat(channel: int, state: str):
channel_to_q[channel].put("UI:REPEAT:" + state.upper())
return ui_status()
@app.route("/player/<int:channel>/playonload/<int:state>")
def playonload(channel: int, state: int):
channel_to_q[channel].put("UI:PLAYONLOAD:" + str(state))
return ui_status()
# Channel Items
@app.route("/player/<int:channel>/load/<int:channel_weight>")
def load(channel: int, channel_weight: int):
channel_to_q[channel].put("UI:LOAD:" + str(channel_weight))
return ui_status()
@app.route("/player/<int:channel>/unload")
def unload(channel: int):
channel_to_q[channel].put("UI:UNLOAD")
return ui_status()
@app.route("/player/<int:channel>/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/<int:channel>/remove/<int:channel_weight>")
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/<int:channel>/clear")
def clear_channel_plan(channel: int):
channel_to_q[channel].put("UI:CLEAR")
# TODO Return
return True
# General Channel Endpoints
@app.route("/player/<int:channel>/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/<type>")
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/<type>")
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/<type>/<library_id>")
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/<int:timeslotid>")
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/<path:path>")
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/<path:path>")
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/<path:path>")
def serve_presenter_static(path: str):
return send_from_directory("presenter-build", path)
def startServer(): def startServer():
process_title = "startServer" process_title = "startServer"
@ -548,7 +117,7 @@ def startServer():
) )
channel_p[channel].start() 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_to_q = multiprocessing.Queue()
api_from_q = multiprocessing.Queue() api_from_q = multiprocessing.Queue()
api_handler = multiprocessing.Process( api_handler = multiprocessing.Process(
@ -574,6 +143,11 @@ def startServer():
) )
controller_handler.start() 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. # TODO Move this to player or installer.
if False: if False:
if not isMacOS(): if not isMacOS():
@ -605,28 +179,8 @@ def startServer():
channel_to_q[0].put("LOAD:0") channel_to_q[0].put("LOAD:0")
channel_to_q[0].put("PLAY") channel_to_q[0].put("PLAY")
# Don't use reloader, it causes Nested Processes! while True:
def runWebServer(): time.sleep(10000)
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()
def stopServer(): def stopServer():

View file

@ -38,10 +38,10 @@
<a href="/status" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'status' %}active{% endif %}"> <a href="/status" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'status' %}active{% endif %}">
Status Status
</a> </a>
<a href="/config" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'config' %}active{% endif %}"> <a href="/config/player" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'config' %}active{% endif %}">
Channel Config Player Config
</a> </a>
<a href="/server" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'server' %}active{% endif %}"> <a href="/config/server" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'server' %}active{% endif %}">
Server Config Server Config
</a> </a>
<a href="/logs" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'logs' %}active{% endif %}"> <a href="/logs" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'logs' %}active{% endif %}">

View file

@ -1,10 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content_inner %} {% block content_inner %}
<h3 class="h5">Audio Outputs</h3> <h3 class="h5">Audio Outputs</h3>
<p><strong>Please note: Currently BAPSicle does not support choosing which Host API is used. Only supported options can be selected.</strong></p> <p>
<strong>Please note: Currently BAPSicle does not support choosing which Host API is used. Only supported options can be selected.</strong>
</p>
{% for host_api in data.outputs %} {% for host_api in data.outputs %}
<hr>
{{host_api.name}} {{host_api.name}}
<br> <br>
<code> <code>

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content_inner %} {% block content_inner %}
{% if data %} {% if data %}
<form action="/server/update" method="POST"> <form action="/config/server/update" method="POST">
<label for="host">Server Host:</label> <label for="host">Server Host:</label>
<input type="text" id="host" name="host" class="form-control" value="{{data.state.host}}"> <input type="text" id="host" name="host" class="form-control" value="{{data.state.host}}">
<br> <br>

431
web_server.py Normal file
View file

@ -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/<path:path>")
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/<int:channel>/<command>")
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/<int:channel>/seek/<float:pos>")
def player_seek(channel: int, pos: float):
player_to_q[channel].put("UI:SEEK:" + str(pos))
return ui_status()
@app.route("/player/<int:channel>/load/<int:channel_weight>")
def player_load(channel: int, channel_weight: int):
player_to_q[channel].put("UI:LOAD:" + str(channel_weight))
return ui_status()
@app.route("/player/<int:channel>/remove/<int:channel_weight>")
def player_remove(channel: int, channel_weight: int):
player_to_q[channel].put("UI:REMOVE:" + str(channel_weight))
return ui_status()
@app.route("/player/<int:channel>/output/<name>")
def player_output(channel: int, name: Optional[str]):
player_to_q[channel].put("UI:OUTPUT:" + str(name))
return ui_config_player()
@app.route("/player/<int:channel>/autoadvance/<int:state>")
def player_autoadvance(channel: int, state: int):
player_to_q[channel].put("UI:AUTOADVANCE:" + str(state))
return ui_status()
@app.route("/player/<int:channel>/repeat/<state>")
def player_repeat(channel: int, state: str):
player_to_q[channel].put("UI:REPEAT:" + state.upper())
return ui_status()
@app.route("/player/<int:channel>/playonload/<int:state>")
def player_playonload(channel: int, state: int):
player_to_q[channel].put("UI:PLAYONLOAD:" + str(state))
return ui_status()
@app.route("/player/<int:channel>/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/<int:timeslotid>")
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/<type>")
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/<type>")
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/<type>/<library_id>")
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/<type>/<int:id>")
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/<path:path>")
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/<path:path>")
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.
)