Merge pull request #9 from UniversityRadioYork/michaelg-server-state
Server State and Logs
This commit is contained in:
commit
afb63dbfe2
12 changed files with 180 additions and 44 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -3,7 +3,6 @@
|
|||
|
||||
__pycache__/
|
||||
|
||||
state/
|
||||
|
||||
*.egg-info/
|
||||
|
||||
|
@ -31,8 +30,6 @@ dev/welcome.mp3
|
|||
|
||||
build/build-exe-pyinstaller-command.sh
|
||||
|
||||
logs/
|
||||
|
||||
*.mp3
|
||||
|
||||
*.oga
|
||||
|
|
|
@ -1,6 +1,2 @@
|
|||
# Flask Details
|
||||
HOST: str = "localhost"
|
||||
PORT: int = 13500
|
||||
|
||||
# BAPSicle Details
|
||||
VERSION: float = 1.0
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
1
logs/.gitignore
vendored
Normal file
1
logs/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.log
|
136
server.py
136
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/<int:channel>/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/<int:channel>/autoadvance/<int:state>")
|
||||
def autoadvance(channel: int, state: int):
|
||||
channel_to_q[channel].put("AUTOADVANCE:" + str(state))
|
||||
return ui_status()
|
||||
|
||||
|
||||
@app.route("/player/<int:channel>/repeat/<state>")
|
||||
def repeat(channel: int, state):
|
||||
channel_to_q[channel].put("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("PLAYONLOAD:" + str(state))
|
||||
return ui_status()
|
||||
|
||||
### Channel Items
|
||||
# Channel Items
|
||||
|
||||
|
||||
@app.route("/player/<int:channel>/load/<int:timeslotitemid>")
|
||||
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/<int:channel>/unload")
|
||||
def unload(channel):
|
||||
|
||||
|
@ -185,6 +239,7 @@ def unload(channel):
|
|||
|
||||
return ui_status()
|
||||
|
||||
|
||||
@app.route("/player/<int:channel>/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/<int:channel>/move/<int:timeslotitemid>/<int:position>")
|
||||
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/<int:channel>/remove/<int:timeslotitemid>")
|
||||
def remove_plan(channel: int, timeslotitemid: int):
|
||||
channel_to_q[channel].put("REMOVE:" + timeslotitemid)
|
||||
|
||||
#TODO Return
|
||||
# TODO Return
|
||||
return True
|
||||
|
||||
|
||||
@app.route("/player/<int:channel>/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/<int:channel>/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/<path:path>")
|
||||
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__":
|
||||
|
|
1
state/.gitignore
vendored
Normal file
1
state/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.json
|
|
@ -8,7 +8,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
{% block head %}
|
||||
<meta http-equiv="refresh" content="5;" />
|
||||
{% endblock %}
|
||||
|
||||
<title>BAPSicle {% if data.ui_title %} | {{data.ui_title}}{% endif %}</title>
|
||||
|
@ -40,7 +39,10 @@
|
|||
Status
|
||||
</a>
|
||||
<a href="/config" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'config' %}active{% endif %}">
|
||||
Config
|
||||
Channel Config
|
||||
</a>
|
||||
<a href="/server" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'server' %}active{% endif %}">
|
||||
Server Config
|
||||
</a>
|
||||
<a href="/logs" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'logs' %}active{% endif %}">
|
||||
Logs
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
Open WebStudio
|
||||
</a>
|
||||
<hr>
|
||||
<p>Version: X | Server Name: Studio X</p>
|
||||
<p>Version: {{data.server_version}} | Server Name: {{data.server_name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
11
templates/log.html
Normal file
11
templates/log.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content_inner %}
|
||||
{% if data %}
|
||||
{% for log in data.logs %}
|
||||
<code>
|
||||
{{log}}
|
||||
</code>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
9
templates/loglist.html
Normal file
9
templates/loglist.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content_inner %}
|
||||
{% if data %}
|
||||
{% for log in data.logs %}
|
||||
<a href="/logs/{{log}}">{{log}}</a>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
20
templates/server.html
Normal file
20
templates/server.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block content_inner %}
|
||||
{% if data %}
|
||||
<form action="/restart" method="POST">
|
||||
<label for="host">Server Host:</label>
|
||||
<input type="text" id="host" name="host" class="form-control" value="{{data.state.host}}">
|
||||
<br>
|
||||
<label for="port">Server Port:</label>
|
||||
<input type="number" id="port" name="port" class="form-control" value="{{data.state.port}}">
|
||||
<br>
|
||||
<label for="name">Server Name:</label>
|
||||
<input type="text" id="name" name="name" class="form-control" value="{{data.state.server_name}}">
|
||||
<br>
|
||||
<label for="channels">Number of Channels:</label>
|
||||
<input type="number" id="channels" name="channels" class="form-control" value="{{data.state.num_channels}}">
|
||||
<br>
|
||||
<input type="submit" class="btn btn-primary" value="Restart Server">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
Loading…
Reference in a new issue