diff --git a/.gitignore b/.gitignore index 09936eb..d7ee597 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ __pycache__/ -state/ *.egg-info/ @@ -31,8 +30,6 @@ dev/welcome.mp3 build/build-exe-pyinstaller-command.sh -logs/ - *.mp3 *.oga diff --git a/config.py.example b/config.py.example index f50f5ad..9925970 100644 --- a/config.py.example +++ b/config.py.example @@ -1,6 +1,2 @@ -# Flask Details -HOST: str = "localhost" -PORT: int = 13500 - # BAPSicle Details VERSION: float = 1.0 \ No newline at end of file diff --git a/helpers/logging_manager.py b/helpers/logging_manager.py index 8df6d65..0a11e25 100644 --- a/helpers/logging_manager.py +++ b/helpers/logging_manager.py @@ -1,5 +1,6 @@ import logging from helpers.os_environment import resolve_external_file_path +import os class LoggingManager(): @@ -9,8 +10,18 @@ class LoggingManager(): def __init__(self, name): self.logger = logging.getLogger(name) + filename: str = resolve_external_file_path("/logs/" + name + ".log") + + if not os.path.isfile(filename): + try: + # Try creating the file. + open(filename, "x") + except: + print("Failed to create log file") + return + logging.basicConfig( - filename=resolve_external_file_path("/logs/" + name + ".log"), + filename=filename, format='%(asctime)s | %(levelname)s | %(message)s', level=logging.INFO, filemode='a' diff --git a/helpers/state_manager.py b/helpers/state_manager.py index 40257b5..8e1b28c 100644 --- a/helpers/state_manager.py +++ b/helpers/state_manager.py @@ -19,8 +19,7 @@ class StateManager: __rate_limit_params_until = {} __rate_limit_period_s = 0 - - def __init__(self, name, logger: LoggingManager, default_state=None, rate_limit_params=[], rate_limit_period_s = 5): + def __init__(self, name, logger: LoggingManager, default_state=None, rate_limit_params=[], rate_limit_period_s=5): self.logger = logger self.filepath = resolve_external_file_path("/state/" + name + ".json") @@ -47,9 +46,10 @@ class StateManager: file_state = json.loads(file_state) # Turn from JSON -> PlanObject - file_state["loaded_item"] = PlanObject(file_state["loaded_item"]) if file_state["loaded_item"] else None - - file_state["show_plan"] = [PlanObject(obj) for obj in file_state["show_plan"]] + if "channel" in file_state: + file_state["loaded_item"] = PlanObject( + file_state["loaded_item"]) if file_state["loaded_item"] else None + file_state["show_plan"] = [PlanObject(obj) for obj in file_state["show_plan"]] # Now feed the loaded state into the initialised state manager. self.state = file_state @@ -73,7 +73,7 @@ class StateManager: def state(self, state): self.__state = copy(state) - def write_to_file(self,state): + def write_to_file(self, state): if self.__state_in_file == state: # No change to be updated. return @@ -89,8 +89,9 @@ class StateManager: state_to_json["last_updated"] = current_time # Not the biggest fan of this, but maybe I'll get a better solution for this later - state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__ if state_to_json["loaded_item"] else None - state_to_json["show_plan"] = [repr.__dict__ for repr in state_to_json["show_plan"]] + if "channel" in state_to_json: # If its a channel object + state_to_json["loaded_item"] = state_to_json["loaded_item"].__dict__ if state_to_json["loaded_item"] else None + state_to_json["show_plan"] = [repr.__dict__ for repr in state_to_json["show_plan"]] try: state_json = json.dumps(state_to_json, indent=2, sort_keys=True) except: @@ -109,7 +110,6 @@ class StateManager: else: self.__rate_limit_params_until[key] = self._currentTimeS + self.__rate_limit_period_s - state_to_update = self.state if key in state_to_update and state_to_update[key] == value: diff --git a/logs/.gitignore b/logs/.gitignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/logs/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/server.py b/server.py index b1c0e54..b108652 100644 --- a/server.py +++ b/server.py @@ -28,9 +28,19 @@ if not isMacOS(): import config from typing import Dict, List +from helpers.state_manager import StateManager +from helpers.logging_manager import LoggingManager setproctitle.setproctitle("BAPSicle - Server") +default_state = { + "server_version": 0, + "server_name": "URY BAPSicle", + "host": "localhost", + "port": 13500, + "num_channels": 3 +} + class BAPSicleServer(): @@ -46,6 +56,11 @@ class BAPSicleServer(): stopServer() +logger = LoggingManager("BAPSicleServer") + +state = StateManager("BAPSicleServer", logger, default_state) +state.update("server_version", config.VERSION) + app = Flask(__name__, static_url_path='') log = logging.getLogger('werkzeug') @@ -59,7 +74,7 @@ channel_p = [] stopping = False -### General Endpoints +# General Endpoints @app.errorhandler(404) def page_not_found(e): @@ -74,7 +89,9 @@ def page_not_found(e): def ui_index(): data = { 'ui_page': "index", - "ui_title": "" + "ui_title": "", + "server_version": config.VERSION, + "server_name": state.state["server_name"] } return render_template('index.html', data=data) @@ -82,7 +99,7 @@ def ui_index(): @app.route("/config") def ui_config(): channel_states = [] - for i in range(3): + for i in range(state.state["num_channels"]): channel_states.append(status(i)) outputs = DeviceManager.getOutputs() @@ -99,7 +116,7 @@ def ui_config(): @app.route("/status") def ui_status(): channel_states = [] - for i in range(3): + for i in range(state.state["num_channels"]): channel_states.append(status(i)) data = { @@ -109,7 +126,39 @@ def ui_status(): } return render_template('status.html', data=data) -### Channel Audio Options + +@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 + } + return render_template("server.html", data=data) + + +@app.route("/restart", methods=["POST"]) +def restart_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"])) + stopServer(restart=True) + startServer() + +# Channel Audio Options + @app.route("/player//play") def play(channel): @@ -156,28 +205,33 @@ def output(channel, name): channel_to_q[channel].put("OUTPUT:" + name) return ui_status() + @app.route("/player//autoadvance/") def autoadvance(channel: int, state: int): channel_to_q[channel].put("AUTOADVANCE:" + str(state)) return ui_status() + @app.route("/player//repeat/") def repeat(channel: int, state): channel_to_q[channel].put("REPEAT:" + state.upper()) return ui_status() + @app.route("/player//playonload/") def playonload(channel: int, state: int): channel_to_q[channel].put("PLAYONLOAD:" + str(state)) return ui_status() -### Channel Items +# Channel Items + @app.route("/player//load/") -def load(channel:int, timeslotitemid: int): +def load(channel: int, timeslotitemid: int): channel_to_q[channel].put("LOAD:" + str(timeslotitemid)) return ui_status() + @app.route("/player//unload") def unload(channel): @@ -185,6 +239,7 @@ def unload(channel): return ui_status() + @app.route("/player//add", methods=["POST"]) def add_to_plan(channel: int): new_item: Dict[str, any] = { @@ -198,28 +253,32 @@ def add_to_plan(channel: int): return new_item + @app.route("/player//move//") def move_plan(channel: int, timeslotitemid: int, position: int): channel_to_q[channel].put("MOVE:" + json.dumps({"timeslotitemid": timeslotitemid, "position": position})) - #TODO Return + # TODO Return return True + @app.route("/player//remove/") def remove_plan(channel: int, timeslotitemid: int): channel_to_q[channel].put("REMOVE:" + timeslotitemid) - #TODO Return + # TODO Return return True + @app.route("/player//clear") def clear_channel_plan(channel: int): channel_to_q[channel].put("CLEAR") - #TODO Return + # TODO Return return True -### General Channel Endpoints +# General Channel Endpoints + @app.route("/player//status") def status(channel): @@ -263,10 +322,32 @@ def send_static(path): return send_from_directory('ui-static', path) +@app.route("/logs") +def list_logs(): + data = { + "ui_page": "loglist", + "ui_title": "Logs", + "logs": ["BAPSicleServer"] + ["channel{}".format(x) for x in range(state.state["num_channels"])] + } + return render_template("loglist.html", data=data) + + +@app.route("/logs/") +def send_logs(path): + l = open("logs/{}.log".format(path)) + data = { + "logs": l.read().splitlines(), + 'ui_page': "log", + "ui_title": "Logs - {}".format(path) + } + l.close() + return render_template('log.html', data=data) + + def startServer(): if isMacOS(): multiprocessing.set_start_method("spawn", True) - for channel in range(3): + for channel in range(state.state["num_channels"]): channel_to_q.append(multiprocessing.Queue()) channel_from_q.append(multiprocessing.Queue()) @@ -287,14 +368,13 @@ def startServer(): text_to_speach = pyttsx3.init() text_to_speach.save_to_file( - """Thank-you for installing BAPSicle - the play-out server from the broadcasting and presenting suite. - This server is accepting connections on port {0} - The version of the server service is {1} + """Thank-you for installing BAPSicle - the play-out server from the broadcasting and presenting suite. + By default, this server is accepting connections on port 13500 + The version of the server service is {} Please refer to the documentation included with this application for further assistance.""".format( - config.PORT, - config.VERSION - ), - "dev/welcome.mp3" + config.VERSION + ), + "dev/welcome.mp3" ) text_to_speach.runAndWait() @@ -306,14 +386,17 @@ def startServer(): } channel_to_q[0].put("ADD:" + json.dumps(new_item)) - #channel_to_q[0].put("LOAD:0") - #channel_to_q[0].put("PLAY") + # channel_to_q[0].put("LOAD:0") + # channel_to_q[0].put("PLAY") # Don't use reloader, it causes Nested Processes! + app.run(host=state.state["host"], port=state.state["port"], debug=True, use_reloader=False) - app.run(host='0.0.0.0', port=13500, debug=True, use_reloader=False) -def stopServer(): +def stopServer(restart=False): + global channel_p + global channel_from_q + global channel_to_q print("Stopping server.py") for q in channel_to_q: q.put("QUIT") @@ -322,6 +405,10 @@ def stopServer(): player.join() except: pass + finally: + channel_p = [] + channel_from_q = [] + channel_to_q = [] print("Stopped all players.") global stopping if stopping == False: @@ -332,7 +419,8 @@ def stopServer(): else: print("Shutting down Flask.") - shutdown() + if not restart: + shutdown() if __name__ == "__main__": diff --git a/state/.gitignore b/state/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/state/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3b4a82d..fa69d87 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,7 +8,6 @@ {% block head %} - {% endblock %} BAPSicle {% if data.ui_title %} | {{data.ui_title}}{% endif %} @@ -40,7 +39,10 @@ Status - Config + Channel Config + + + Server Config Logs diff --git a/templates/index.html b/templates/index.html index a999a62..ac2e7d4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -17,7 +17,7 @@ Open WebStudio
-

Version: X | Server Name: Studio X

+

Version: {{data.server_version}} | Server Name: {{data.server_name}}

diff --git a/templates/log.html b/templates/log.html new file mode 100644 index 0000000..5651a39 --- /dev/null +++ b/templates/log.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content_inner %} + {% if data %} + {% for log in data.logs %} + + {{log}} + +
+ {% endfor %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/loglist.html b/templates/loglist.html new file mode 100644 index 0000000..7ae36b9 --- /dev/null +++ b/templates/loglist.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content_inner %} + {% if data %} + {% for log in data.logs %} + {{log}} +
+ {% endfor %} + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/server.html b/templates/server.html new file mode 100644 index 0000000..c006006 --- /dev/null +++ b/templates/server.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% block content_inner %} + {% if data %} +
+ + +
+ + +
+ + +
+ + +
+ +
+ {% endif %} +{% endblock %} \ No newline at end of file