WebStudio/stateserver.py

436 lines
15 KiB
Python
Raw Permalink Normal View History

#!flask/bin/python
## IMPORTANT ASSUMPTIONS MADE:
# show timestamps start on the hour.
# normal shows in real studios aren't currently a thing!!!
import subprocess
from typing import List, Any, Dict, Optional
from flask import Flask, jsonify, request
from flask_cors import CORS # type: ignore
import requests
import datetime
import random
from telnetlib import Telnet
import configparser
config = configparser.RawConfigParser()
config.read("serverconfig.ini")
api_key = config.get("stateserver", "myradio_key")
app = Flask(__name__)
CORS(app) # Enable Cors access-all
SUSTAINER_AUTONEWS = config.get("stateserver", "sustainer_autonews") == "True"
def do_ws_srv_telnet(source: str) -> None:
HOST = "localhost"
print(
"telnet {} {} SEL {}".format(
HOST, config.get("shittyserver", "telnet_port"), source
)
)
tn = Telnet(HOST, int(config.get("shittyserver", "telnet_port")))
tn.write(b"SEL " + str.encode(source) + b"\n")
try:
print(tn.read_until(b"\n").decode("utf-8"))
except EOFError:
pass
else:
tn.close()
def genFail(reason: str, code: int = 400) -> Any:
return jsonify({"status": "FAIL", "reason": reason})
def genPayload(payload: Any) -> Any:
return jsonify({"status": "OK", "payload": payload})
def myradioApiRequest(url: str) -> Any:
res = requests.get("https://ury.org.uk/api/v2/" + url + "?api_key=" + api_key)
if res.ok:
return res.json()["payload"]
else:
raise Exception("err {} {}".format(res.status_code, res.text))
def getNextHourTimestamp() -> int:
current = datetime.datetime.now()
currentPlusHour = current + datetime.timedelta(hours=1)
nextHourStart = currentPlusHour.replace(minute=0, second=0)
nextTimestamp = int(nextHourStart.timestamp())
return nextTimestamp
# sadly we're on python 3.7 so we can't use TypedDict
Connection = Dict[str, Any]
def getConnByID(connID: str) -> Optional[Connection]:
for conn in connections:
if conn["connid"] == connID:
return conn
return None
SOURCE_JUKEBOX = 3 # Set to 8 for testing.
SOURCE_OB = 4
SOURCE_WS = 5
SOURCE_OFFAIR = 8
SOURCES = [SOURCE_JUKEBOX, SOURCE_OB, SOURCE_WS, SOURCE_OFFAIR]
# This array will only hold connections we've validated to be authorised to broadcast.
connections: List[Connection] = []
wsSessions: Dict[str, Dict[str, str]] = {}
def getCurrentShowConnection() -> Optional[Connection]:
for connection in connections:
if (connection["startTimestamp"] <= datetime.datetime.now().timestamp()) and (
connection["endTimestamp"] >= getNextHourTimestamp()
):
return connection
return None
def getNextHourConnection() -> Optional[Connection]:
nextHourTimestamp = getNextHourTimestamp()
isConnectionEnding = False
for connection in connections:
if connection["startTimestamp"] == nextHourTimestamp:
return connection
if connection["endTimestamp"] == nextHourTimestamp:
isConnectionEnding = True
if not isConnectionEnding:
# There isn't a show that starts at the next hour, so we're returning the current connection.
return getCurrentShowConnection()
else:
# The show is ending, return no next show.
return None
def cleanOldConnections() -> None:
global connections
# Go backwards round the loop so that pop's don't interfere with the index.
for i in range(len(connections) - 1, -1, -1):
if connections[i]["endTimestamp"] < datetime.datetime.now().timestamp():
connections.pop(i)
def stateDecider() -> Dict[str, Any]:
currentConnection = getCurrentShowConnection()
nextConnection = getNextHourConnection()
print("currentConnection:", currentConnection)
print("nextConnection:", nextConnection)
willRunAutoNews = True
switchAudioAtMin = 2
newSelSource = None
newWSSource = None
if currentConnection != nextConnection:
print("Will be transitioning")
# The show is transitioning this hour.
if currentConnection:
print("There's a current connection.")
# Current show wants to end their show at 2 mins past
if currentConnection["autoNewsEnd"] == False:
print("This show doesn't want to end with news.")
willRunAutoNews = False
switchAudioAtMin = 2 # no real change here
if nextConnection:
print("There's a next connection.")
# next show wants to begin at 0 mins past hour.
if nextConnection["autoNewsBeginning"] == False:
print("The next connection doesn't want news at start.")
willRunAutoNews = False
switchAudioAtMin = 0
newSelSource = nextConnection["sourceid"]
newWSSource = nextConnection["wsid"] # None if show is not a WS.
else:
print("No next show, going back to jukebox.")
# There isn't a next show, go back to sustainer
newSelSource = SOURCE_JUKEBOX
else:
# Show/sustainer is continuing for another hour.
print("Show / Sustainer is continuing this hour.")
if currentConnection:
print("We're currently doing a show, so check if they want middle news.")
willRunAutoNews = currentConnection["autoNewsMiddle"]
print("(conclusion: {})".format("yes" if willRunAutoNews else "no"))
newSelSource = currentConnection["sourceid"]
newWSSource = currentConnection["wsid"]
elif SUSTAINER_AUTONEWS:
print(
"There's no show on currently, so we're going to AutoNEWS on sustainer"
)
# Jukebox -> NEWS -> Jukebox
newSelSource = SOURCE_JUKEBOX
else:
print(
"There's no show on currently, but AutoNews on sustainer is disabled, so don't do news"
)
# Jukebox -> Jukebox
newSelSource = SOURCE_JUKEBOX
switchAudioAtMin = 0
willRunAutoNews = False
nextState = {
"autoNews": willRunAutoNews,
"switchAudioAtMin": switchAudioAtMin,
"selSource": newSelSource,
"wsSource": newWSSource,
}
return nextState
@app.route("/api/v1/status", methods=["GET"])
def get_status() -> Any:
print(getNextHourTimestamp())
global connections
cleanOldConnections()
return genPayload({"connections": connections, "wsSessions": wsSessions})
@app.route("/api/v1/nextTransition", methods=["GET"])
def get_next_transition() -> Any:
cleanOldConnections()
return genPayload(stateDecider())
@app.route("/api/v1/cancelTimeslot", methods=["POST"])
def post_cancelCheck() -> Any:
global connections
content = request.json
if not content:
return genFail("No parameters provided.")
if not isinstance(content["connid"], int):
return genFail("Request missing valid connid.")
# We're gonna cancel their show.
currentShow = getCurrentShowConnection()
if currentShow and currentShow["connid"] == content["connid"]:
# this show is (at least supposed to be) live now.
# kill their show
# but don't kill it during the news, or after the end time, to avoid unexpected jukeboxing
2020-04-17 08:52:24 +00:00
now = datetime.datetime.now().timestamp()
2020-04-17 11:40:16 +00:00
if now < (currentShow["endTimestamp"] - 15):
print(
"Jukeboxing due to {}'s ({}, {}) cancellation".format(
currentShow["connid"],
currentShow["timeslotid"],
currentShow["wsid"],
)
)
2020-04-17 08:52:24 +00:00
do_ws_srv_telnet("NUL")
subprocess.Popen(["sel", str(SOURCE_JUKEBOX)])
# yeet the connection
for i in range(len(connections)):
if connections[i]["connid"] == content["connid"]:
connections.pop(i)
return genPayload("Connection cancelled.")
return genFail("Connection not found.")
@app.route("/api/v1/registerTimeslot", methods=["POST"])
def post_registerCheck() -> Any:
global connections
content = request.json
if not content:
return genFail("No parameters provided.")
if not isinstance(content["timeslotid"], int):
return genFail("Request missing valid timeslotid.")
if not isinstance(content["memberid"], int):
return genFail("Request missing valid memberid.")
if not isinstance(content["sourceid"], int):
return genFail("Request missing valid source.")
if not content["sourceid"] in SOURCES:
return genFail("Request missing valid source.")
2020-04-17 14:45:39 +00:00
if not isinstance(content["wsid"], str):
return genFail("Request missing valid wsID")
member = myradioApiRequest("user/" + str(content["memberid"]))
if not member:
return genFail("Could not get member.")
timeslot = myradioApiRequest("timeslot/" + str(content["timeslotid"]))
if not timeslot:
return genFail("Could not get tiemslot.")
found_credit = False
for credit in timeslot["credits"]:
if content["memberid"] == credit["memberid"]:
found_credit = True
break
if not found_credit:
return genFail("You are not authorised to broadcast for this timeslot.")
start_time = datetime.datetime.strptime(timeslot["start_time"], "%d/%m/%Y %H:%M")
duration = timeslot["duration"].split(":")
duration_time = datetime.timedelta(hours=int(duration[0]), minutes=int(duration[1]))
end_time = start_time + duration_time
now_time = datetime.datetime.now()
2020-04-17 08:10:40 +00:00
2020-04-17 08:17:06 +00:00
connection: Optional[Connection] = None
2020-04-17 08:10:40 +00:00
for conn in connections:
if content["timeslotid"] == conn["timeslotid"]:
# they've already registered, return the existing session
print(
"found existing connection {} for {}".format(
conn["connid"], conn["timeslotid"]
)
)
2020-04-17 08:17:06 +00:00
connection = conn
# make sure we update their wsID
if "wsid" in content:
connection["wsid"] = content["wsid"]
2020-04-17 08:17:06 +00:00
new_connection = False
2020-04-17 08:17:06 +00:00
if connection is None:
new_connection = True
2020-04-17 08:17:06 +00:00
if start_time - now_time > datetime.timedelta(hours=1):
return genFail(
"This show too far away, please try again within an hour of starting your show."
)
2020-04-17 08:17:06 +00:00
if start_time + duration_time < now_time:
return genFail("This show has already ended.")
if (
start_time - datetime.timedelta(minutes=1)
< now_time
< start_time + datetime.timedelta(minutes=2)
):
return genFail(
"You registered too late. Please re-register after the news."
)
2020-04-17 08:17:06 +00:00
random.seed(a=timeslot["timeslot_id"], version=2)
connection = {
"connid": random.randint(
0, 100000000
), # TODO: this is horrible. I'll sort this later.
2020-04-17 08:17:06 +00:00
"timeslotid": timeslot["timeslot_id"],
"startTimestamp": int(start_time.timestamp()),
"endTimestamp": int(end_time.timestamp()),
"sourceid": content["sourceid"],
"autoNewsBeginning": True,
"autoNewsMiddle": True,
"autoNewsEnd": True,
"wsid": content["wsid"],
2020-04-17 08:17:06 +00:00
}
2020-04-17 14:45:39 +00:00
if start_time + datetime.timedelta(minutes=2) < now_time:
2020-04-23 08:31:02 +00:00
if connection["wsid"] is not None:
# they're late, bring them live now
print(
"({}, {}) late, bringing on air now".format(
connection["connid"], connection["wsid"]
)
)
2020-04-23 08:31:02 +00:00
do_ws_srv_telnet(connection["wsid"])
subprocess.Popen(["sel", "5"])
2020-04-14 15:29:21 +00:00
2020-04-17 08:17:06 +00:00
assert connection is not None
if new_connection:
connections.append(connection)
print(connections)
return genPayload(connection)
@app.route("/api/v1/changeTimeslot", methods=["POST"])
def post_settingsCheck() -> Any:
global connections
content = request.json
if not content:
return genFail("No parameters provided.")
if not isinstance(content["connid"], int):
return genFail("Request missing valid connID.")
if not isinstance(content["beginning"], bool):
return genFail("Request missing valid beginning bool.")
if not isinstance(content["middle"], bool):
return genFail("Request missing valid middle bool.")
if not isinstance(content["end"], bool):
return genFail("Request missing valid end bool.")
if not isinstance(content["sourceid"], int):
return genFail("Request missing valid sourcid.")
for conn in connections:
if conn["connid"] == content["connid"]:
conn["autoNewsBeginning"] = content["beginning"]
conn["autoNewsMiddle"] = content["middle"]
conn["autoNewsEnd"] = content["end"]
conn["sourceid"] = content["sourceid"]
return genPayload(conn)
return genFail("No connection found.")
@app.route("/api/v1/updateWSSessions", methods=["POST"])
def post_wsSessions() -> Any:
global connections
global wsSessions
content = request.json
assert content is not None
# if not content:
# return genFail("No parameters provided.")
oldSessions = wsSessions
wsSessions = content
print("New wsSessions:", wsSessions)
wsids_to_remove = []
wsids_to_add = []
for session in oldSessions:
if not oldSessions[session]["connection_id"] in wsSessions:
wsids_to_remove.append(oldSessions[session]["connection_id"])
print("wsSessions which have disappeared:", wsids_to_remove)
for session in wsSessions:
if not wsSessions[session]["connection_id"] in oldSessions:
wsids_to_add.append(wsSessions[session]["connection_id"])
print("wsSessions which have appeared:", wsids_to_add)
for conn in connections:
2020-04-13 18:55:10 +00:00
if conn["wsid"] in wsids_to_add:
if conn["startTimestamp"] + 120 < datetime.datetime.now().timestamp():
2020-04-13 18:55:10 +00:00
# they're late, bring them on air now
print(
"({}, {}) late, bringing on air now".format(
conn["connid"], conn["wsid"]
)
)
do_ws_srv_telnet(conn["wsid"])
subprocess.Popen(["sel", "5"])
if conn["wsid"] in wsids_to_remove:
2020-04-14 08:11:02 +00:00
print("({}, {}) gone".format(conn["connid"], conn["wsid"]))
conn["wsid"] = None
currentShow = getCurrentShowConnection()
if currentShow and currentShow["connid"] == conn["connid"]:
# they should be on air now, but they've just died. go to jukebox.
# but don't kill it during the news, or after the end time, to avoid unexpected jukeboxing
# Also, avoid killing them if they're on a non-WS source
if currentShow["sourceid"] == SOURCE_WS:
now = datetime.datetime.now().timestamp()
if now < (currentShow["endTimestamp"] - 15):
print("jukeboxing due to their disappearance...")
subprocess.Popen(["sel", str(SOURCE_JUKEBOX)])
do_ws_srv_telnet("NUL")
return genPayload("Thx, K, bye.")
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")