Files
AWCharacterCreator/backend/app/sounds.py
T

105 lines
4.1 KiB
Python

"""Lobby sounds: the DM can replace the dice sound and add sounds for critical successes / failures (MP3, per lobby)."""
import json
from pathlib import Path
from fastapi import Body, Depends, HTTPException, Request
from fastapi.responses import Response
MAX_SOUND = 1_500_000 # bytes per file
SLOTS = ("roll", "crit_success", "crit_failure", "ping")
FLAGS = ("result", "crit_success", "crit_failure", "ping") # result = the short tones for success / failure of a check
def schema(con):
if "sounds" not in [r["name"] for r in con.execute("PRAGMA table_info(lobbies)")]:
con.execute("ALTER TABLE lobbies ADD COLUMN sounds TEXT NOT NULL DEFAULT '{}'")
def settings(raw) -> dict:
try:
s = json.loads(raw or "{}")
except ValueError:
s = {}
return {"result": s.get("result", True), "crit_success": s.get("crit_success", True), "crit_failure": s.get("crit_failure", True), "ping": s.get("ping", True),
"custom": {k: bool(s.get("custom", {}).get(k)) for k in SLOTS}, "v": int(s.get("v", 0))}
def install(api, db, current_user, lobby_for, dm_only, hub, store_dir: Path):
store_dir.mkdir(parents=True, exist_ok=True)
def path(lid: str, slot: str) -> Path:
if slot not in SLOTS:
raise HTTPException(404, "Unknown sound")
return store_dir / f"{lid}_{slot}.mp3"
def save(con, lid, s):
s["v"] += 1
con.execute("UPDATE lobbies SET sounds=? WHERE id=?", (json.dumps(s), lid))
@api.put("/lobbies/{lid}/sounds")
def set_flags(lid: str, body: dict = Body(...), user: dict = Depends(current_user)):
with db() as con:
lobby = lobby_for(con, lid, user)
dm_only(lobby, user)
s = settings(lobby["sounds"])
for k in FLAGS:
if k in body:
s[k] = bool(body[k])
save(con, lid, s)
hub.notify(lid, "lobby")
return {"ok": True}
@api.post("/lobbies/{lid}/sounds/{slot}")
async def upload_sound(lid: str, slot: str, request: Request, user: dict = Depends(current_user)):
"""Raw MP3 bytes in the body."""
p = path(lid, slot)
with db() as con:
dm_only(lobby_for(con, lid, user), user)
if int(request.headers.get("content-length") or 0) > MAX_SOUND:
raise HTTPException(413, f"The file is too large (at most {MAX_SOUND // 1_000_000} MB)")
chunks, size = [], 0
async for c in request.stream():
size += len(c)
if size > MAX_SOUND:
raise HTTPException(413, f"The file is too large (at most {MAX_SOUND // 1_000_000} MB)")
chunks.append(c)
data = b"".join(chunks)
if not (data[:3] == b"ID3" or (len(data) > 4 and data[0] == 0xFF and data[1] & 0xE0 == 0xE0)):
raise HTTPException(422, "Please upload an MP3 file")
p.write_bytes(data)
with db() as con:
lobby = lobby_for(con, lid, user)
s = settings(lobby["sounds"])
s["custom"][slot] = True
save(con, lid, s)
hub.notify(lid, "lobby")
return {"ok": True}
@api.delete("/lobbies/{lid}/sounds/{slot}")
def reset_sound(lid: str, slot: str, user: dict = Depends(current_user)):
p = path(lid, slot)
with db() as con:
lobby = lobby_for(con, lid, user)
dm_only(lobby, user)
s = settings(lobby["sounds"])
s["custom"][slot] = False
save(con, lid, s)
p.unlink(missing_ok=True)
hub.notify(lid, "lobby")
return {"ok": True}
@api.get("/lobbies/{lid}/sounds/{slot}")
def get_sound(lid: str, slot: str, v: int = 0, user: dict = Depends(current_user)):
p = path(lid, slot)
with db() as con:
lobby_for(con, lid, user)
if not p.exists():
raise HTTPException(404, "No custom sound")
return Response(p.read_bytes(), media_type="audio/mpeg", headers={"Cache-Control": "private, max-age=86400"})
def purge(lid: str):
for slot in SLOTS:
path(lid, slot).unlink(missing_ok=True)
return purge