386 lines
14 KiB
Python
Executable file
386 lines
14 KiB
Python
Executable file
#!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 pytz
|
|
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"
|
|
|
|
LOCAL_TIME = pytz.timezone(config.get("time", "local_timezone"))
|
|
|
|
|
|
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.utcnow()
|
|
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]] = {}
|
|
lastConnectionIDToRegister = -1
|
|
|
|
|
|
def getCurrentShowConnection() -> Optional[Connection]:
|
|
for connection in connections:
|
|
if (connection["startTimestamp"] <= datetime.datetime.utcnow().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
|
|
for i in range(len(connections)):
|
|
if connections[i]["endTimestamp"] < datetime.datetime.utcnow().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"]
|
|
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
|
|
do_ws_srv_telnet("NUL")
|
|
switch_proc = subprocess.Popen(["sel", str(SOURCE_JUKEBOX)])
|
|
pass
|
|
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
|
|
global lastConnectionIDToRegister
|
|
|
|
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.")
|
|
|
|
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.")
|
|
|
|
for conn in connections:
|
|
if content["timeslotid"] == conn["timeslotid"]:
|
|
# they've already registered, return the existing session
|
|
return genPayload(conn)
|
|
|
|
start_time = datetime.datetime.strptime(timeslot["start_time"], "%d/%m/%Y %H:%M")
|
|
start_time = LOCAL_TIME.localize(start_time).astimezone(pytz.utc)
|
|
|
|
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.utcnow().replace(tzinfo=pytz.utc)
|
|
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.")
|
|
|
|
if start_time + duration_time < now_time:
|
|
return genFail("This show has already ended.")
|
|
|
|
random.seed(a=timeslot["timeslot_id"], version=2)
|
|
connection = {
|
|
"connid": random.randint(0, 100000000), # TODO: this is horrible. I'll sort this later.
|
|
"timeslotid": timeslot["timeslot_id"],
|
|
"startTimestamp": int(start_time.timestamp()),
|
|
"endTimestamp": int(end_time.timestamp()),
|
|
"sourceid": content["sourceid"],
|
|
'autoNewsBeginning': True,
|
|
'autoNewsMiddle': True,
|
|
'autoNewsEnd': True,
|
|
'wsid': None
|
|
}
|
|
if "wsid" in content:
|
|
print("got wsid from client! {}".format(content["wsid"]))
|
|
connection["wsid"] = content["wsid"]
|
|
if start_time > now_time + datetime.timedelta(minutes=2):
|
|
# they're late, bring them live now
|
|
print("({}, {}) late, bringing on air now".format(connection["connid"], connection["wsid"]))
|
|
do_ws_srv_telnet(connection["wsid"])
|
|
subprocess.Popen(['sel', '5'])
|
|
|
|
connections.append(connection)
|
|
print(connections)
|
|
|
|
lastConnectionIDToRegister = connection["connid"]
|
|
|
|
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
|
|
global lastConnectionIDToRegister
|
|
content = request.json
|
|
# 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:
|
|
if conn["connid"] == lastConnectionIDToRegister:
|
|
if conn["wsid"] is None and len(wsids_to_add) == 1:
|
|
conn["wsid"] = wsids_to_add[0]
|
|
print("({}, {}) hello".format(conn["connid"], conn["wsid"]))
|
|
|
|
if conn["wsid"] in wsids_to_add:
|
|
if conn["startTimestamp"] + 120 < datetime.datetime.utcnow().timestamp():
|
|
# 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:
|
|
print("({}, {}) gone".format(conn["connid"], conn["wsid"]))
|
|
conn["wsid"] = None
|
|
# TODO Make this actually do a disconnect sequence if this is the current show.
|
|
# time.sleep(5)
|
|
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")
|