#!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 now = datetime.datetime.now().timestamp() if now < (currentShow["endTimestamp"] - 15): print("Jukeboxing due to {}'s ({}, {}) cancellation".format(currentShow["connid"], currentShow["timeslotid"], currentShow["wsid"])) 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.") 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() connection: Optional[Connection] = None 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"])) connection = conn # make sure we update their wsID if "wsid" in content: connection["wsid"] = content["wsid"] new_connection = False if connection is None: new_connection = True 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.") 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.") 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': content["wsid"] } if start_time + datetime.timedelta(minutes=2) < now_time: if connection["wsid"] is not None: # 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']) 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 # 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["wsid"] in wsids_to_add: if conn["startTimestamp"] + 120 < datetime.datetime.now().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 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 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")