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