Merge pull request #284 from UniversityRadioYork/marks/upgrades

Replace pip with Poetry and upgrades for Python 3.10
This commit is contained in:
Matthew Stratford 2023-03-10 00:31:06 +00:00 committed by GitHub
commit 0d953961ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1944 additions and 166 deletions

10
Jenkinsfile vendored
View file

@ -15,8 +15,7 @@ pipeline {
}
stage('Python') {
steps {
sh '/usr/local/bin/python3.7 -m venv env'
sh 'env/bin/pip install -r requirements.ci.txt'
sh 'poetry install'
}
}
}
@ -31,12 +30,12 @@ pipeline {
}
stage('MyPy (stateserver)') {
steps {
sh 'env/bin/mypy stateserver.py'
sh 'poetry run mypy stateserver.py'
}
}
stage('MyPy (shittyserver)') {
steps {
sh 'env/bin/mypy shittyserver.py'
sh 'poetry run mypy shittyserver.py'
}
}
}
@ -118,7 +117,8 @@ pipeline {
sshagent(credentials: ['ury']) {
sh 'scp -v -o StrictHostKeyChecking=no stateserver.py liquidsoap@dolby.ury:/opt/webstudioserver/stateserver.py'
sh 'scp -v -o StrictHostKeyChecking=no shittyserver.py liquidsoap@dolby.ury:/opt/webstudioserver/shittyserver.py'
sh 'scp -v -o StrictHostKeyChecking=no requirements.txt liquidsoap@dolby.ury:/opt/webstudioserver/requirements.txt'
sh 'scp -v -o StrictHostKeyChecking=no pyproject.toml liquidsoap@dolby.ury:/opt/webstudioserver/pyproject.toml'
sh 'scp -v -o StrictHostKeyChecking=no poetry.lock liquidsoap@dolby.ury:/opt/webstudioserver/poetry.lock'
}
}
}

View file

@ -10,11 +10,11 @@ The clientside is written in TypeScript using React and Redux, the serverside is
Client:
* Node.js and Yarn 1.x
- Node.js and Yarn 1.x
Server:
* Python 3.7-3.9 (note: Python 3.10 is not supported)
- Python 3.7-3.10
### Installing
@ -22,12 +22,10 @@ Clone the repo and run `yarn`.
You'll probably want to change the values in `.env` to reflect the MyRadio environment and/or where the server is running (e.g. if you're running the server locally, change `REACT_APP_WS_URL` to `ws://localhost:8079/stream`).
If you want to hack on the server, create a virtualenv and install Python packages:
If you want to hack on the server, use [Poetry](https://python-poetry.org/docs/) create a virtualenv and install Python packages:
```sh
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install -r requirements.txt
$ poetry install
```
### Versions
@ -65,6 +63,7 @@ This is done via the BAPSicle project by updating the `/presenter` submodule, si
If you want to demo build a BAPS Presenter release, run `npm run build-baps` and the result will be in the `build` directory.
## Screenshots
![Mic Live With Main Screen](images/HomeWithMic.png?raw=true "Mic Live on Main Screen")
![Home Page of webstudio](images/Home.png?raw=true "Home Page of WebStudio")

1767
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

29
pyproject.toml Normal file
View file

@ -0,0 +1,29 @@
[tool.poetry]
name = "webstudio"
version = "1.6.0"
description = ""
authors = ["Marks Polakovs <marks.polakovs@ury.org.uk>", "Matthew Stratford <matthew.stratford@ury.org.uk>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.7"
aiohttp = "^3.8.4"
av = "^10.0.0"
jack-client = "^0.5.4"
websockets = "^10.4"
aiortc = "^1.4.0"
sentry-sdk = "^1.16.0"
requests = "^2.28.2"
flask = "^2.2.3"
flask-cors = "^3.0.10"
[tool.poetry.group.dev.dependencies]
black = "^23.1.0"
mypy = "^1.1.1"
types-jack-client = "^0.5.10.7"
types-requests = "^2.28.11.15"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,21 +0,0 @@
aiohttp==3.7.4
async-timeout==3.0.1
attrs==19.3.0
certifi==2020.4.5.1
chardet==3.0.4
click==7.1.2
Flask==1.1.2
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.3
MarkupSafe==1.1.1
multidict==5.1.0
mypy==0.770
mypy-extensions==0.4.3
requests==2.26.0
typed-ast==1.4.1
typing-extensions==3.7.4.2
urllib3==1.26.5
websockets==9.1
Werkzeug==1.0.1
yarl==1.4.2

View file

@ -1,56 +0,0 @@
aiohttp==3.7.4
aioice==0.6.18
aiortc==0.9.27
appdirs==1.4.4
async-timeout==3.0.1
attrs==19.3.0
av==7.0.1
black==20.8b1
blinker==1.4
certifi==2020.4.5.1
cffi==1.14.0
chardet==3.0.4
click==7.1.2
crc32c==2.0
cryptography==3.3.2
expiringdict==1.2.1
Flask==1.1.2
Flask-Cors==3.0.9
gunicorn==20.0.4
idna==2.9
itsdangerous==1.1.0
JACK-Client==0.5.2
jedi==0.15.2
Jinja2==2.11.3
jsonpickle==1.3
MarkupSafe==1.1.1
multidict==5.1.0
mypy==0.770
mypy-extensions==0.4.3
netifaces==0.10.9
parso==0.6.2
pathspec==0.8.1
pluggy==0.13.1
pycparser==2.20
pyee==7.0.1
PyJWT==1.7.1
pylibsrtp==0.6.6
pyls==0.1.6
pyls-mypy==0.1.8
python-dateutil==2.8.1
python-jsonrpc-server==0.3.4
python-language-server==0.31.9
pytz==2019.3
regex==2020.11.13
requests==2.26.0
sentry-sdk==1.0.0
six==1.14.0
toml==0.10.2
twilio==6.38.1
typed-ast==1.4.1
typing-extensions==3.7.4.2
ujson==1.35
urllib3==1.26.5
websockets==9.1
Werkzeug==1.0.1
yarl==1.4.2

View file

@ -11,20 +11,18 @@ from typing import Optional, Any, Type, Dict, List
import aiohttp
import av # type: ignore
import jack as Jack # type: ignore
import websockets
import jack as Jack
from jack import OwnPort
import websockets.exceptions, websockets.server, websockets.connection
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription # type: ignore
from aiortc.mediastreams import MediaStreamError # type: ignore
import sentry_sdk # type: ignore
import sentry_sdk
config = configparser.RawConfigParser()
config.read("serverconfig.ini")
if config.get("sentry", "enable") == "True":
sentry_sdk.init(
config.get("sentry", "dsn"),
traces_sample_rate=1.0
)
sentry_sdk.init(config.get("sentry", "dsn"), traces_sample_rate=1.0)
file_contents_ex = re.compile(r"^ws=\d$")
@ -53,7 +51,10 @@ def get_turn_credentials() -> TurnCredentials:
provider = config.get("shittyserver", "turn_provider")
if provider == "twilio":
from twilio.rest import Client # type: ignore
client = Client(config.get("twilio", "account_sid"), config.get("twilio", "auth_token"))
client = Client(
config.get("twilio", "account_sid"), config.get("twilio", "auth_token")
)
token = client.tokens.create()
# Twilio's typedef is wrong, reee
@ -65,19 +66,19 @@ def get_turn_credentials() -> TurnCredentials:
raise Exception("unknown provider " + provider)
@Jack.set_error_function # type: ignore
@Jack.set_error_function
def error(msg: str) -> None:
print("Error:", msg)
@Jack.set_info_function # type: ignore
@Jack.set_info_function
def info(msg: str) -> None:
print("Info:", msg)
jack = Jack.Client("webstudio")
out1 = jack.outports.register("out_0")
out2 = jack.outports.register("out_1")
out1: OwnPort = jack.outports.register("out_0") # type: ignore
out2: OwnPort = jack.outports.register("out_1") # type: ignore
transfer_buffer1: Any = None
transfer_buffer2: Any = None
@ -92,19 +93,19 @@ def init_buffers() -> None:
init_buffers()
@jack.set_process_callback # type: ignore
@jack.set_process_callback
def process(frames: int) -> None:
buf1 = out1.get_buffer()
if transfer_buffer1.read_space == 0:
for i in range(len(buf1)):
buf1[i] = b'\x00'
buf1[i] = b"\x00" # type: ignore
else:
piece1 = transfer_buffer1.read(len(buf1))
buf1[: len(piece1)] = piece1
buf2 = out2.get_buffer()
if transfer_buffer2.read_space == 0:
for i in range(len(buf2)):
buf2[i] = b'\x00'
buf2[i] = b"\x00" # type: ignore
else:
piece2 = transfer_buffer2.read(len(buf2))
buf2[: len(piece2)] = piece2
@ -119,7 +120,9 @@ async def notify_mattserver_about_sessions() -> None:
data: Dict[str, Dict[str, str]] = {}
for sid, sess in active_sessions.items():
data[sid] = sess.to_dict()
async with session.post(config.get("shittyserver", "notify_url"), json=data) as response:
async with session.post(
config.get("shittyserver", "notify_url"), json=data
) as response:
print("Mattserver response", response)
@ -128,7 +131,7 @@ class NotReadyException(BaseException):
class Session(object):
websocket: Optional[websockets.WebSocketServerProtocol]
websocket: Optional[websockets.server.WebSocketServerProtocol]
connection_state: Optional[str]
pc: Optional[Any]
connection_id: str
@ -153,7 +156,7 @@ class Session(object):
def to_dict(self) -> Dict[str, str]:
return {
"connection_id": self.connection_id,
"connected_at": self.connected_at.strftime("%Y-%m-%dT%H:%M:%S%z")
"connected_at": self.connected_at.strftime("%Y-%m-%dT%H:%M:%S%z"),
}
async def activate(self) -> None:
@ -169,7 +172,9 @@ class Session(object):
try:
await self.websocket.send(json.dumps({"kind": "DEACTIVATED"}))
except websockets.exceptions.ConnectionClosed:
print(self.connection_id, "not sending DEACTIVATED as it's already closed")
print(
self.connection_id, "not sending DEACTIVATED as it's already closed"
)
pass
async def end(self) -> None:
@ -188,14 +193,16 @@ class Session(object):
await self.pc.close()
if (
self.websocket is not None
and self.websocket.state == websockets.protocol.State.OPEN
self.websocket is not None
and self.websocket.state == websockets.connection.State.OPEN
):
try:
await self.websocket.send(json.dumps({"kind": "DIED"}))
await self.websocket.close(1008)
except websockets.exceptions.ConnectionClosed:
print(self.connection_id, "socket already closed, no died message")
print(
self.connection_id, "socket already closed, no died message"
)
if self.connection_id in active_sessions:
print(self.connection_id, "removing from active_sessions")
@ -253,7 +260,10 @@ class Session(object):
@track.on("ended") # type: ignore
async def on_ended() -> None:
print(self.connection_id, "Ending due to {} track end".format(track.kind))
print(
self.connection_id,
"Ending due to {} track end".format(track.kind),
)
await self.end()
write_ob_status(True)
@ -314,7 +324,9 @@ class Session(object):
),
)
async def connect(self, websocket: websockets.WebSocketServerProtocol) -> None:
async def connect(
self, websocket: websockets.server.WebSocketServerProtocol
) -> None:
global active_sessions
active_sessions[self.connection_id] = self
@ -326,7 +338,13 @@ class Session(object):
print(self.connection_id, "Obtained ICE")
sentry_sdk.set_context("session", {"session_id": self.connection_id})
await websocket.send(
json.dumps({"kind": "HELLO", "connectionId": self.connection_id, "iceServers": ice_config})
json.dumps(
{
"kind": "HELLO",
"connectionId": self.connection_id,
"iceServers": ice_config,
}
)
)
try:
@ -350,7 +368,9 @@ class Session(object):
await self.end()
async def serve(websocket: websockets.WebSocketServerProtocol, path: str) -> None:
async def serve(
websocket: websockets.server.WebSocketServerProtocol, path: str
) -> None:
if path == "/stream":
session = Session()
await session.connect(websocket)
@ -358,15 +378,19 @@ async def serve(websocket: websockets.WebSocketServerProtocol, path: str) -> Non
pass
start_server = websockets.serve(
start_server = websockets.server.serve(
serve, host=None, port=int(config.get("shittyserver", "websocket_port"))
)
print("Shittyserver WS starting on port {}.".format(config.get("shittyserver", "websocket_port")))
print(
"Shittyserver WS starting on port {}.".format(
config.get("shittyserver", "websocket_port")
)
)
async def telnet_server(
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
global active_sessions, live_session
while True:
@ -387,15 +411,15 @@ async def telnet_server(
result[sid] = sess.to_dict()
writer.write(
(
json.dumps(
{
"live": live_session.to_dict()
if live_session is not None
else None,
"active": result,
}
)
+ "\r\n"
json.dumps(
{
"live": live_session.to_dict()
if live_session is not None
else None,
"active": result,
}
)
+ "\r\n"
).encode("utf-8")
)
@ -444,7 +468,11 @@ async def run_telnet_server() -> None:
jack.activate()
print("Shittyserver TELNET starting on port {}".format(config.get("shittyserver", "telnet_port")))
print(
"Shittyserver TELNET starting on port {}".format(
config.get("shittyserver", "telnet_port")
)
)
asyncio.get_event_loop().run_until_complete(notify_mattserver_about_sessions())
asyncio.get_event_loop().run_until_complete(

View file

@ -6,7 +6,7 @@ import subprocess
from typing import List, Any, Dict, Optional
from flask import Flask, jsonify, request
from flask_cors import CORS # type: ignore
from flask_cors import CORS # type: ignore
import requests
import datetime
import random
@ -25,7 +25,11 @@ 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))
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:
@ -45,7 +49,7 @@ def genPayload(payload: Any) -> Any:
def myradioApiRequest(url: str) -> Any:
res = requests.get('https://ury.org.uk/api/v2/' + url + '?api_key=' + api_key)
res = requests.get("https://ury.org.uk/api/v2/" + url + "?api_key=" + api_key)
if res.ok:
return res.json()["payload"]
else:
@ -85,7 +89,8 @@ 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()):
connection["endTimestamp"] >= getNextHourTimestamp()
):
return connection
return None
@ -109,8 +114,8 @@ def getNextHourConnection() -> Optional[Connection]:
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):
# 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)
@ -159,11 +164,15 @@ def stateDecider() -> Dict[str, Any]:
newSelSource = currentConnection["sourceid"]
newWSSource = currentConnection["wsid"]
elif SUSTAINER_AUTONEWS:
print("There's no show on currently, so we're going to AutoNEWS on sustainer")
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")
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
@ -173,32 +182,27 @@ def stateDecider() -> Dict[str, Any]:
"autoNews": willRunAutoNews,
"switchAudioAtMin": switchAudioAtMin,
"selSource": newSelSource,
"wsSource": newWSSource
"wsSource": newWSSource,
}
return nextState
@app.route('/api/v1/status', methods=['GET'])
@app.route("/api/v1/status", methods=["GET"])
def get_status() -> Any:
print(getNextHourTimestamp())
global connections
cleanOldConnections()
return genPayload(
{
"connections": connections,
"wsSessions": wsSessions
}
)
return genPayload({"connections": connections, "wsSessions": wsSessions})
@app.route('/api/v1/nextTransition', methods=['GET'])
@app.route("/api/v1/nextTransition", methods=["GET"])
def get_next_transition() -> Any:
cleanOldConnections()
return genPayload(stateDecider())
@app.route('/api/v1/cancelTimeslot', methods=['POST'])
@app.route("/api/v1/cancelTimeslot", methods=["POST"])
def post_cancelCheck() -> Any:
global connections
content = request.json
@ -215,7 +219,13 @@ def post_cancelCheck() -> Any:
# 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"]))
print(
"Jukeboxing due to {}'s ({}, {}) cancellation".format(
currentShow["connid"],
currentShow["timeslotid"],
currentShow["wsid"],
)
)
do_ws_srv_telnet("NUL")
subprocess.Popen(["sel", str(SOURCE_JUKEBOX)])
@ -227,7 +237,7 @@ def post_cancelCheck() -> Any:
return genFail("Connection not found.")
@app.route('/api/v1/registerTimeslot', methods=['POST'])
@app.route("/api/v1/registerTimeslot", methods=["POST"])
def post_registerCheck() -> Any:
global connections
@ -274,45 +284,62 @@ def post_registerCheck() -> Any:
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"]))
print(
"found existing connection {} for {}".format(
conn["connid"], conn["timeslotid"]
)
)
connection = conn
# make sure we update their wsID
# 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.")
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.")
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.
"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"]
"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"]))
print(
"({}, {}) late, bringing on air now".format(
connection["connid"], connection["wsid"]
)
)
do_ws_srv_telnet(connection["wsid"])
subprocess.Popen(['sel', '5'])
subprocess.Popen(["sel", "5"])
assert connection is not None
if new_connection:
@ -322,7 +349,7 @@ def post_registerCheck() -> Any:
return genPayload(connection)
@app.route('/api/v1/changeTimeslot', methods=['POST'])
@app.route("/api/v1/changeTimeslot", methods=["POST"])
def post_settingsCheck() -> Any:
global connections
content = request.json
@ -349,11 +376,12 @@ def post_settingsCheck() -> Any:
return genFail("No connection found.")
@app.route('/api/v1/updateWSSessions', methods=['POST'])
@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
@ -378,9 +406,13 @@ def post_wsSessions() -> Any:
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"]))
print(
"({}, {}) late, bringing on air now".format(
conn["connid"], conn["wsid"]
)
)
do_ws_srv_telnet(conn["wsid"])
subprocess.Popen(['sel', '5'])
subprocess.Popen(["sel", "5"])
if conn["wsid"] in wsids_to_remove:
print("({}, {}) gone".format(conn["connid"], conn["wsid"]))
@ -394,10 +426,10 @@ def post_wsSessions() -> Any:
now = datetime.datetime.now().timestamp()
if now < (currentShow["endTimestamp"] - 15):
print("jukeboxing due to their disappearance...")
subprocess.Popen(['sel', str(SOURCE_JUKEBOX)])
subprocess.Popen(["sel", str(SOURCE_JUKEBOX)])
do_ws_srv_telnet("NUL")
return genPayload("Thx, K, bye.")
if __name__ == '__main__':
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")