basic proxy implementation
This commit is contained in:
parent
28a5c527b6
commit
9e688b76b4
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
.git*
|
212
.gitignore
vendored
Normal file
212
.gitignore
vendored
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
WORKDIR /code/
|
||||||
|
|
||||||
|
COPY requirements.txt requirements.txt
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
COPY app app
|
||||||
|
|
||||||
|
RUN python3 -m app.main
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
126
app/main.py
Normal file
126
app/main.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Coroutine, TypeVar
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.responses import (
|
||||||
|
FileResponse,
|
||||||
|
JSONResponse,
|
||||||
|
PlainTextResponse,
|
||||||
|
RedirectResponse,
|
||||||
|
Response,
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
root = Path(__file__).parent / "static"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def home():
|
||||||
|
return FileResponse(root / "home.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/operator/")
|
||||||
|
async def operatorhome():
|
||||||
|
return FileResponse(root / "operator.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/login/")
|
||||||
|
async def login():
|
||||||
|
return FileResponse(root / "login.html")
|
||||||
|
|
||||||
|
|
||||||
|
base = "http://v6d3music/"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/authlink/")
|
||||||
|
async def authlink():
|
||||||
|
async with aiohttp.ClientSession() as s, s.get(f"{base}authlink/") as response:
|
||||||
|
return PlainTextResponse(await response.text())
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
async def url_get(
|
||||||
|
url: str, params: dict[str, str], handle: Callable[[aiohttp.ClientResponse], Coroutine[Any, Any, T]]
|
||||||
|
) -> T:
|
||||||
|
async with aiohttp.ClientSession() as s, s.get(url, params=params) as response:
|
||||||
|
return await handle(response)
|
||||||
|
|
||||||
|
|
||||||
|
async def url_post(
|
||||||
|
url: str, params: dict[str, str], json: dict, handle: Callable[[aiohttp.ClientResponse], Coroutine[Any, Any, T]]
|
||||||
|
) -> T:
|
||||||
|
async with aiohttp.ClientSession() as s, s.post(url, params=params, json=json) as response:
|
||||||
|
print(await response.read())
|
||||||
|
return await handle(response)
|
||||||
|
|
||||||
|
|
||||||
|
async def repeat(repeated: Callable[[], Coroutine[Any, Any, T]]) -> T:
|
||||||
|
for _ in range(60):
|
||||||
|
try:
|
||||||
|
return await repeated()
|
||||||
|
except aiohttp.ClientConnectorError:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
raise HTTPException(504)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_auth(response: aiohttp.ClientResponse):
|
||||||
|
if 300 <= response.status <= 399:
|
||||||
|
return RedirectResponse("/")
|
||||||
|
else:
|
||||||
|
return Response(content=await response.read(), media_type=response.content_type, status_code=response.status)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/auth/")
|
||||||
|
async def auth(session: str | None = None, state: str | None = None, code: str | None = None):
|
||||||
|
match session, state, code:
|
||||||
|
case str() as session, str() as state, str() as code:
|
||||||
|
params: dict[str, str] = {"session": session, "state": state, "code": code}
|
||||||
|
return await repeat(lambda: url_get(f"{base}auth/", params, handle_auth))
|
||||||
|
case None, None, None:
|
||||||
|
return FileResponse(root / "auth.html")
|
||||||
|
case _:
|
||||||
|
raise HTTPException(400)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_json(response: aiohttp.ClientResponse):
|
||||||
|
return JSONResponse(await response.json())
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/state/")
|
||||||
|
async def state(session: str):
|
||||||
|
return await repeat(lambda: url_get(f"{base}state/", {"session": session}, handle_json))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/status/")
|
||||||
|
async def status(session: str):
|
||||||
|
return await repeat(lambda: url_get(f"{base}status/", {"session": session}, handle_json))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/main.js")
|
||||||
|
async def mainjs():
|
||||||
|
return FileResponse(root / "main.js")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/operator.js")
|
||||||
|
async def operatorjs():
|
||||||
|
return FileResponse(root / "operator.js")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/main.css")
|
||||||
|
async def maincss():
|
||||||
|
return FileResponse(root / "main.css")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/operator.css")
|
||||||
|
async def operatorcss():
|
||||||
|
return FileResponse(root / "operator.css")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/")
|
||||||
|
async def api(json: dict, session: str):
|
||||||
|
return await repeat(lambda: url_post(f"{base}api/", {"session": session}, json, handle_json))
|
8
app/static/auth.html
Normal file
8
app/static/auth.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<link rel="stylesheet" href="/main.css">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/main.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
window.location = window.location + `&session=${sessionStr()}`;
|
||||||
|
})();
|
||||||
|
</script>
|
17
app/static/home.html
Normal file
17
app/static/home.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root-container">
|
||||||
|
<div class="sidebars"></div>
|
||||||
|
<div id="root"></div>
|
||||||
|
<div class="sidebars"></div>
|
||||||
|
</div>
|
||||||
|
<script src="/main.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
root.append(await pageHome());
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
12
app/static/login.html
Normal file
12
app/static/login.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<link rel="stylesheet" href="/main.css">
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="/main.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
const a = await aAuth();
|
||||||
|
root.append(a);
|
||||||
|
logEl(sessionStr());
|
||||||
|
logEl(await sessionState());
|
||||||
|
a.click();
|
||||||
|
})();
|
||||||
|
</script>
|
50
app/static/main.css
Normal file
50
app/static/main.css
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
input {
|
||||||
|
color: white;
|
||||||
|
background: black;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 0%;
|
||||||
|
min-width: min(40em, 100%);
|
||||||
|
flex: auto;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebars {
|
||||||
|
width: 100%;
|
||||||
|
background: #050505;
|
||||||
|
}
|
||||||
|
|
||||||
|
#homeroot {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
204
app/static/main.js
Normal file
204
app/static/main.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
const genRanHex = (size) =>
|
||||||
|
[...Array(size)]
|
||||||
|
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||||
|
.join("");
|
||||||
|
const sessionStr = () => {
|
||||||
|
if (!localStorage.getItem("session"))
|
||||||
|
localStorage.setItem("session", genRanHex(64));
|
||||||
|
return localStorage.getItem("session");
|
||||||
|
};
|
||||||
|
const sessionState = async () => {
|
||||||
|
const response = await fetch(`/state/?session=${sessionStr()}`);
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
const sessionStatus = (() => {
|
||||||
|
let task;
|
||||||
|
return async () => {
|
||||||
|
if (task === undefined) {
|
||||||
|
task = (async () => {
|
||||||
|
const response = await fetch(`/status/?session=${sessionStr()}`);
|
||||||
|
return await response.json();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return await task;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
const root = document.querySelector("#root");
|
||||||
|
const logEl = (msg) => {
|
||||||
|
const el = document.createElement("pre");
|
||||||
|
el.innerText = msg;
|
||||||
|
root.append(el);
|
||||||
|
};
|
||||||
|
const sessionClient = async () => {
|
||||||
|
const session = await sessionStatus();
|
||||||
|
return session && session["client"];
|
||||||
|
};
|
||||||
|
const sessionUser = async () => {
|
||||||
|
const client = await sessionClient();
|
||||||
|
return client && client["user"];
|
||||||
|
};
|
||||||
|
const userAvatarUrl = async () => {
|
||||||
|
const user = await sessionUser();
|
||||||
|
return user && user["avatar"];
|
||||||
|
};
|
||||||
|
const userUsername = async () => {
|
||||||
|
const user = await sessionUser();
|
||||||
|
return user && user["username"];
|
||||||
|
};
|
||||||
|
const userAvatarImg = async () => {
|
||||||
|
const avatar = await userAvatarUrl();
|
||||||
|
if (avatar) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = avatar;
|
||||||
|
img.width = 64;
|
||||||
|
img.height = 64;
|
||||||
|
img.alt = await userUsername();
|
||||||
|
return img;
|
||||||
|
} else {
|
||||||
|
return baseEl("span");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const userId = async () => {
|
||||||
|
const user = await sessionUser();
|
||||||
|
return user && user["id"];
|
||||||
|
};
|
||||||
|
const baseEl = (tag, ...appended) => {
|
||||||
|
const element = document.createElement(tag);
|
||||||
|
element.append(...appended);
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
const aLogin = () => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "/login/";
|
||||||
|
a.innerText = "login";
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
const aAuthLink = async () => {
|
||||||
|
const response = await fetch("/authlink/");
|
||||||
|
return await response.text();
|
||||||
|
};
|
||||||
|
const aAuth = async () => {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const [authlink, sessionstate] = await Promise.all([
|
||||||
|
aAuthLink(),
|
||||||
|
sessionState(),
|
||||||
|
]);
|
||||||
|
a.href = authlink + "&state=" + sessionstate;
|
||||||
|
a.innerText = "auth";
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
const aApi = async (request) => {
|
||||||
|
const response = await fetch(`/api/?session=${sessionStr()}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
const aGuilds = async () => {
|
||||||
|
return await aApi({ type: "guilds" });
|
||||||
|
};
|
||||||
|
const aQueue = async () => {
|
||||||
|
const requests = {};
|
||||||
|
for (const guild of await aGuilds()) {
|
||||||
|
requests[guild] = {
|
||||||
|
type: "*",
|
||||||
|
guild,
|
||||||
|
voice: null,
|
||||||
|
main: null,
|
||||||
|
catches: { "you are not connected to voice": null, "*": null },
|
||||||
|
requests: { volume: {}, playing: {}, queueformat: {}, queuejson: {} },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const responses = await aApi({ type: "*", requests });
|
||||||
|
for (const [guild, response] of Object.entries(responses)) {
|
||||||
|
if (response !== null && response.error === undefined) {
|
||||||
|
response.guild = guild;
|
||||||
|
response.time = Date.now() / 1000;
|
||||||
|
response.delta = () => Date.now() / 1000 - response.time;
|
||||||
|
let index = 0;
|
||||||
|
for (const audio of response.queuejson) {
|
||||||
|
audio.playing = response.playing && index === 0;
|
||||||
|
audio.delta = () => (audio.playing ? response.delta() : 0);
|
||||||
|
audio.now = () => audio.seconds + audio.delta();
|
||||||
|
audio.ts = () => {
|
||||||
|
const seconds_total = Math.round(audio.now());
|
||||||
|
const seconds = seconds_total % 60;
|
||||||
|
const minutes_total = (seconds_total - seconds) / 60;
|
||||||
|
const minutes = minutes_total % 60;
|
||||||
|
const hours = (minutes_total - minutes) / 60;
|
||||||
|
return `${hours}:${("00" + minutes).slice(-2)}:${(
|
||||||
|
"00" + seconds
|
||||||
|
).slice(-2)}`;
|
||||||
|
};
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const sleep = (s) => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 1000 * s));
|
||||||
|
};
|
||||||
|
const audioWidget = (audio) => {
|
||||||
|
const description = baseEl("span", audio.description);
|
||||||
|
const timecode = baseEl("span", audio.timecode);
|
||||||
|
const duration = baseEl("span", audio.duration);
|
||||||
|
audio.tce = timecode;
|
||||||
|
return baseEl("div", "audio", " ", timecode, "/", duration, " ", description);
|
||||||
|
};
|
||||||
|
const aUpdateQueueOnce = async (queue, el) => {
|
||||||
|
el.innerHTML = "";
|
||||||
|
if (queue !== null) {
|
||||||
|
for (const audio of queue.queuejson) {
|
||||||
|
el.append(audioWidget(audio));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const aUpdateQueueSetup = async (el) => {
|
||||||
|
let queue = await aQueue();
|
||||||
|
await aUpdateQueueOnce(queue, el);
|
||||||
|
(async () => {
|
||||||
|
while (true) {
|
||||||
|
await sleep(2);
|
||||||
|
if (queue !== null && queue.queuejson.length > 100) {
|
||||||
|
await sleep((queue.queuejson.length - 100) / 200);
|
||||||
|
}
|
||||||
|
const newQueue = await aQueue();
|
||||||
|
await aUpdateQueueOnce(newQueue, el);
|
||||||
|
queue = newQueue;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
(async () => {
|
||||||
|
while (true) {
|
||||||
|
await sleep(0.25);
|
||||||
|
if (queue !== null) {
|
||||||
|
for (const audio of queue.queuejson) {
|
||||||
|
audio.tce.innerText = audio.ts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
const aQueueWidget = async () => {
|
||||||
|
const el = baseEl("div");
|
||||||
|
if (await sessionUser()) await aUpdateQueueSetup(el);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
const pageHome = async () => {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.append(
|
||||||
|
baseEl("div", aLogin()),
|
||||||
|
baseEl("div", await userAvatarImg()),
|
||||||
|
baseEl("div", await userId()),
|
||||||
|
baseEl("div", await userUsername()),
|
||||||
|
baseEl("div", await aQueueWidget())
|
||||||
|
);
|
||||||
|
el.id = "homeroot";
|
||||||
|
return el;
|
||||||
|
};
|
20
app/static/operator.css
Normal file
20
app/static/operator.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#operatorroot {
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #operation {
|
||||||
|
} */
|
||||||
|
|
||||||
|
#workerpool {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
height: 5em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workerview {
|
||||||
|
background: #0f0f0f;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
24
app/static/operator.html
Normal file
24
app/static/operator.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/main.css" />
|
||||||
|
<link rel="stylesheet" href="/operator.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root-container">
|
||||||
|
<div class="sidebars"></div>
|
||||||
|
<div id="root"><div id="operatorroot"></div></div>
|
||||||
|
<div class="sidebars"></div>
|
||||||
|
</div>
|
||||||
|
<script src="/main.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
root.append(await pageHome());
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script src="/operator.js"></script>
|
||||||
|
<script>
|
||||||
|
(async () => {
|
||||||
|
operatorroot.append(await pageOperator());
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
69
app/static/operator.js
Normal file
69
app/static/operator.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
aApi({
|
||||||
|
type: "guilds",
|
||||||
|
operator: null,
|
||||||
|
catches: { "not an operator": null, "*": null },
|
||||||
|
}).then(console.log);
|
||||||
|
aApi({
|
||||||
|
type: "sleep",
|
||||||
|
operator: null,
|
||||||
|
duration: 1,
|
||||||
|
echo: {},
|
||||||
|
time: null,
|
||||||
|
catches: { "not an operator": null, "*": null },
|
||||||
|
}).then(console.log);
|
||||||
|
aApi({
|
||||||
|
type: "*",
|
||||||
|
idkey: "target",
|
||||||
|
idbase: {
|
||||||
|
type: "*",
|
||||||
|
requests: {
|
||||||
|
Count: {},
|
||||||
|
Concurrency: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
operator: null,
|
||||||
|
requests: {
|
||||||
|
"v6d3music.api.Api().api": {},
|
||||||
|
"v6d3music.processing.pool.UnitJob.run": {},
|
||||||
|
},
|
||||||
|
catches: { "not an operator": null, "*": null },
|
||||||
|
time: null,
|
||||||
|
}).then((value) => console.log(JSON.stringify(value, undefined, 2)));
|
||||||
|
aApi({
|
||||||
|
type: "pool",
|
||||||
|
operator: null,
|
||||||
|
catches: { "not an operator": null, "*": null },
|
||||||
|
}).then((value) => console.log(JSON.stringify(value, undefined, 2)));
|
||||||
|
const elJob = (job) => {
|
||||||
|
const jobview = document.createElement("div");
|
||||||
|
jobview.classList.add("jobview");
|
||||||
|
jobview.innerText = JSON.stringify(job);
|
||||||
|
return jobview;
|
||||||
|
};
|
||||||
|
const elWorker = (worker) => {
|
||||||
|
const workerview = document.createElement("div");
|
||||||
|
workerview.classList.add("workerview");
|
||||||
|
workerview.append(`qsize: ${worker.qsize}`);
|
||||||
|
workerview.append(elJob(worker.job));
|
||||||
|
return workerview;
|
||||||
|
};
|
||||||
|
const elPool = async () => {
|
||||||
|
const pool = document.createElement("div");
|
||||||
|
pool.id = "workerpool";
|
||||||
|
const workers = await aApi({
|
||||||
|
type: "pool",
|
||||||
|
operator: null,
|
||||||
|
catches: { "not an operator": null, "*": null },
|
||||||
|
});
|
||||||
|
if (workers === null || workers.error !== undefined) return null;
|
||||||
|
for (const worker of workers) {
|
||||||
|
pool.append(elWorker(worker));
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
};
|
||||||
|
const pageOperator = async () => {
|
||||||
|
const operation = document.createElement("div");
|
||||||
|
operation.id = "operation";
|
||||||
|
operation.append(await elPool());
|
||||||
|
return operation;
|
||||||
|
};
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
aiohttp==3.8.4
|
||||||
|
fastapi==0.92.0
|
||||||
|
uvicorn[standard]==0.20.0
|
Loading…
Reference in New Issue
Block a user