underrun detection + new architecture

This commit is contained in:
AF 2023-02-27 11:38:01 +00:00
parent b8b378a5bb
commit 4aa5a679c1
27 changed files with 188 additions and 695 deletions

View File

@ -9,8 +9,9 @@ COPY base.requirements.txt base.requirements.txt
RUN pip install -r base.requirements.txt
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
ENV v6port=80
ENV v6host=0.0.0.0
RUN mkdir ${v6root}
COPY v6d3musicbase v6d3musicbase
COPY v6d3music v6d3music
RUN python3 -m v6d3music.main
CMD ["python3", "-m", "v6d3music.run-bot"]

View File

@ -1,12 +1,18 @@
# syntax=docker/dockerfile:1
FROM python:3.10
FROM python:3.10 as compile-sphinx5.3.0-base
RUN apt-get update
RUN apt-get install -y python3-sphinx node.js npm
RUN npm install -g http-server
RUN pip install pydata-sphinx-theme
RUN apt-get install -y python3-sphinx
RUN pip install Sphinx==5.3.0 pydata-sphinx-theme==0.12.0
WORKDIR /app/
ENV v6root=/app/data/
RUN mkdir ${v6root}
FROM compile-sphinx5.3.0-base as compile-latest
COPY base.requirements.txt base.requirements.txt
RUN pip install -r base.requirements.txt
COPY requirements.txt requirements.txt
@ -16,5 +22,11 @@ COPY v6d3music v6d3music
COPY docs/source docs/source
WORKDIR /app/docs/
RUN make html
FROM node:19
RUN npm install -g http-server
WORKDIR /app/docs/build/html/
COPY --from=compile-latest /app/docs/build/html/ /app/docs/build/html/
CMD [ "http-server", "-p", "80" ]

View File

@ -1,4 +1,4 @@
aiohttp>=3.7.4,<4
discord.py[voice]~=2.1.0
yt-dlp~=2022.11.11
yt-dlp~=2023.2.17
typing_extensions~=4.4.0

View File

@ -1,6 +1,6 @@
ptvp35 @ git+https://gitea.parrrate.ru/PTV/ptvp35.git@e760fca39e2070b9959aeb95b53e59e895f1ad57
v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@c718d4d1422945a756213d22d9e26aa24babe0f6
v6d1tokens @ git+https://gitea.parrrate.ru/PTV/v6d1tokens.git@9ada50f111bd6e9a49c9c6683fa7504fee030056
v6d2ctx @ git+https://gitea.parrrate.ru/PTV/v6d2ctx.git@18001ff3403646db46f36175a824e571c5734fd6
v6d2ctx @ git+https://gitea.parrrate.ru/PTV/v6d2ctx.git@c9f3f5ac5c7feb2165fc4fae4eb998a0fe4f5f00
rainbowadn @ git+https://gitea.parrrate.ru/PTV/rainbowadn.git@fc1d11f4b53ac4653ffac1bbcad130855e1b7f10
adaas @ git+https://gitea.parrrate.ru/PTV/adaas.git@0c7f974ec4955204b35f463749df138663c98550
adaas @ git+https://gitea.parrrate.ru/PTV/adaas.git@8093665489901098f92d5a4001f1782dab6ddcf9

View File

@ -3,7 +3,7 @@ from setuptools import setup
setup(
name='v6d3music',
version='',
packages=['v6d3music', 'v6d3musicbase'],
packages=['v6d3music'],
url='',
license='',
author='PARRRATE T&V',

View File

@ -3,8 +3,8 @@ import time
import discord
from typing_extensions import Self
from v6d3musicbase.responsetype import *
from v6d3musicbase.targets import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.targets import *
from rainbowadn.instrument import Instrumentation
from v6d2ctx.context import *

View File

@ -56,8 +56,6 @@ class CachedDictionary(Generic[TKey, T]):
class MusicAppFactory(AppFactory):
htmlroot = Path(__file__).parent / 'html'
def __init__(
self,
secret: str,
@ -81,13 +79,6 @@ class MusicAppFactory(AppFactory):
return f'https://discord.com/api/oauth2/authorize?client_id={client_id}' \
f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
def _path(self, file: str):
return self.htmlroot / file
def _file(self, file: str):
with open(self.htmlroot / file) as f:
return f.read()
async def code_token(self, code: str) -> dict:
client_id = self._api.user_id()
assert client_id is not None
@ -199,38 +190,26 @@ class MusicAppFactory(AppFactory):
return data
def define_routes(self, routes: web.RouteTableDef) -> None:
@routes.get('/')
async def home(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('home.html'))
@routes.get('/operator/')
async def operatorhome(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('operator.html'))
@routes.get('/login/')
async def login(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('login.html'))
@routes.get('/authlink/')
async def authlink(_request: web.Request) -> web.StreamResponse:
return web.Response(text=self.auth_link())
@routes.get('/auth/')
async def auth(request: web.Request) -> web.StreamResponse:
if 'session' in request.query:
response = web.HTTPFound('/')
session = str(request.query.get('session'))
s_state = str(request.query.get('state'))
code = str(request.query.get('code'))
if bytes_hash(session.encode()) != s_state:
session = request.query.get('session')
state = request.query.get('state')
code = request.query.get('code')
match session, state, code:
case str() as session, str() as state, str() as code:
if bytes_hash(session.encode()) != state:
raise web.HTTPBadRequest
data = self.session_data(session)
data['code'] = code
data['token'] = await self.code_token(code)
await self.db.set(session, data)
return web.HTTPFound('/')
case _:
raise web.HTTPBadRequest
data = self.session_data(session)
data['code'] = code
data['token'] = await self.code_token(code)
await self.db.set(session, data)
return response
else:
return web.FileResponse(self._path('auth.html'))
@routes.get('/state/')
async def get_state(request: web.Request) -> web.Response:
@ -246,29 +225,6 @@ class MusicAppFactory(AppFactory):
data=await self.session_status(session)
)
@routes.get('/queue/')
async def api_queue(request: web.Request) -> web.Response:
session = str(request.query.get('session'))
return web.json_response(
data=await self.session_queue(session)
)
@routes.get('/main.js')
async def mainjs(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('main.js'))
@routes.get('/operator.js')
async def operatorjs(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('operator.js'))
@routes.get('/main.css')
async def maincss(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('main.css'))
@routes.get('/operator.css')
async def operatorcss(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('operator.css'))
@routes.post('/api/')
async def api(request: web.Request) -> web.Response:
session = request.query.get('session')

View File

@ -8,9 +8,10 @@ from v6d3music.core.mainservice import *
from v6d3music.utils.assert_admin import *
from v6d3music.utils.catch import *
from v6d3music.utils.effects_for_preset import *
from v6d3music.utils.options_for_effects import *
from v6d3music.utils.presets import *
import discord
__all__ = ('get_of',)
@ -40,12 +41,23 @@ presets: {shlex.join(allowed_presets)}
''',
(), 'help'
)
match args:
case ['this', *args]:
reference = ctx.message.reference
if reference is None:
raise Explicit('use reply')
resolved = reference.resolved
if not isinstance(resolved, discord.Message):
raise Explicit('reference message is either deleted or cannot be found')
attachments = resolved.attachments
case [*args]:
attachments = ctx.message.attachments
case _:
raise RuntimeError
async with mainservice.lock_for(ctx.guild):
queue = await mainservice.context(ctx, create=True, force_play=False).queue()
if ctx.message.attachments:
if len(ctx.message.attachments) > 1:
raise Explicit('no more than one attachment')
args = [ctx.message.attachments[0].url] + args
if attachments:
args = ['[[', *(attachment.url for attachment in attachments), ']]'] + args
async for audio in mainservice.yt_audios(ctx, args):
queue.append(audio)
await ctx.reply('done')
@ -69,9 +81,7 @@ presets: {shlex.join(allowed_presets)}
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
pos0, pos1 = int(pos0), int(pos1)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
for _ in range(pos0, pos1 + 1):
if not queue.skip_at(pos0, ctx.member):
pos0 += 1
queue.skip_between(pos0, pos1, ctx.member)
case _:
raise Explicit('misformatted')
await ctx.reply('done')
@ -120,10 +130,7 @@ presets: {shlex.join(allowed_presets)}
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
yta = queue.queue[0]
seconds = yta.source_seconds()
yta.options = options_for_effects(effects)
yta.set_seconds(seconds)
queue.queue[0].set_effects(effects)
@at('default')
async def default(ctx: Context, args: list[str]) -> None:
@ -154,7 +161,7 @@ presets: {shlex.join(allowed_presets)}
@at('repeat')
async def repeat(ctx: Context, args: list[str]):
match args:
case [n_, *args] if n_.isdecimal():
case ['x', n_, *args] if n_.isdecimal():
n = int(n_)
case [*args]:
n = 1
@ -206,16 +213,7 @@ presets: {shlex.join(allowed_presets)}
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
if not queue.queue:
raise Explicit('empty queue')
audio = queue.queue[0].branch()
if effects is not None:
seconds = audio.source_seconds()
audio.options = options_for_effects(effects or None)
audio.set_seconds(seconds)
else:
audio.set_source()
queue.queue.insert(1, audio)
queue.branch(effects)
@at('//')
@at('queue')
@ -321,4 +319,13 @@ presets: {shlex.join(allowed_presets)}
vc = await mainservice.context(ctx, create=False, force_play=True).vc()
vc.resume()
@at('leave')
async def leave(ctx: Context, _args: list[str]) -> None:
async with mainservice.lock_for(ctx.guild):
vc, main = await mainservice.context(ctx, create=False, force_play=False).vc_main()
queue = main.queue
if queue.queue:
raise Explicit('queue not empty')
await vc.disconnect()
return of

View File

@ -4,9 +4,9 @@ from contextlib import AsyncExitStack
from typing import AsyncIterable, TypeVar
import discord
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
from v6d3musicbase.targets import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.targets import *
import v6d3music.processing.pool
from ptvp35 import *
@ -72,7 +72,7 @@ class MainService:
@staticmethod
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
vc: discord.VoiceProtocol | None = member.guild.voice_client
if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
if vc is None or vc.channel is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState | None = member.voice
if vs is None:
raise Explicit('not connected')

View File

@ -94,6 +94,11 @@ class QueueAudio(discord.AudioSource):
return True
return False
def skip_between(self, pos0: int, pos1: int, member: discord.Member) -> None:
for _ in range(pos0, min(pos1 + 1, len(self.queue))):
if not self.skip_at(pos0, member):
pos0 += 1
def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool:
if audio in self.queue:
if audio.can_be_skipped_by(member):
@ -188,6 +193,16 @@ class QueueAudio(discord.AudioSource):
print_exc()
self.update_sources()
def branch(self, effects: str | None) -> None:
if not self.queue:
raise Explicit('empty queue')
audio = self.queue[0].branch()
if effects is not None:
audio.set_effects(effects or None)
else:
audio.set_source()
self.queue.insert(1, audio)
class ForwardView(MutableSequence[YTAudio]):
def __init__(self, sequence: MutableSequence[YTAudio]) -> None:

View File

@ -3,7 +3,7 @@ from collections import deque
from contextlib import AsyncExitStack
from typing import AsyncIterable, Iterable
from v6d3musicbase.responsetype import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.context import *
from v6d3music.core.create_ytaudio import *

View File

@ -1,5 +1,7 @@
import asyncio
import random
import re
import traceback
from typing import Optional
import discord
@ -10,6 +12,7 @@ from v6d3music.core.real_url import *
from v6d3music.core.ytaservicing import *
from v6d3music.processing.abstractrunner import *
from v6d3music.utils.fill import *
from v6d3music.utils.options_for_effects import *
from v6d3music.utils.sparq import *
from v6d3music.utils.tor_prefix import *
@ -47,6 +50,7 @@ class YTAudio(discord.AudioSource):
self._duration_lock = asyncio.Lock()
self.loop = asyncio.get_running_loop()
self.stop_at: int | None = stop_at
self.attempts = 0
def _reduced_durations(self) -> dict[str, str]:
return {url: duration for url, duration in self._durations.items() if url == self.url}
@ -71,6 +75,11 @@ class YTAudio(discord.AudioSource):
def set_seconds(self, seconds: float):
self.set_already_read(round(seconds / sparq(self.options)))
def set_effects(self, effects: str | None) -> None:
seconds = self.source_seconds()
self.options = options_for_effects(effects or None)
self.set_seconds(seconds)
def source_seconds(self) -> float:
return self.already_read * sparq(self.options)
@ -139,6 +148,24 @@ class YTAudio(discord.AudioSource):
)
return before_options
def estimated_seconds_duration(self) -> float:
duration = self.duration()
_m = re.match(r'(\d+):(\d+):(\d+)', duration)
if _m is None:
return 0.0
else:
try:
hs, ms, ss = _m.groups()
h, m, s = int(hs), int(ms), int(ss)
return float(h * 3600 + m * 60 + s)
except Exception:
traceback.print_exc()
return 0.0
def underran(self) -> bool:
to_end = self.estimated_seconds_duration() - self.source_seconds()
return to_end > 1.0
def read(self) -> bytes:
if self.regenerating:
return FILL
@ -146,10 +173,15 @@ class YTAudio(discord.AudioSource):
return b''
self.already_read += 1
ret: bytes = self.source.read()
if not ret and not self.source.droppable():
if random.random() > .1:
if not ret and (not (droppable := self.source.droppable()) or self.underran()):
if self.attempts < 5 or random.random() > .1:
self.attempts += 1
self.regenerating = True
self.loop.create_task(self.regenerate())
self.loop.create_task(
self.regenerate(
'underran' if droppable else 'not droppable'
)
)
return FILL
else:
print(f'dropped {self.origin}')
@ -216,9 +248,9 @@ class YTAudio(discord.AudioSource):
audio._durations |= respawn.get('durations', {})
return audio
async def regenerate(self):
async def regenerate(self, reason: str):
try:
print(f'regenerating {self.origin}')
print(f'regenerating {self.origin} {reason=}')
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor)
if hasattr(self, 'source'):
self.source.cleanup()

View File

@ -1,8 +0,0 @@
<link rel="stylesheet" href="/main.css">
<div id="root"></div>
<script src="/main.js"></script>
<script>
(async () => {
window.location = window.location + `&session=${sessionStr()}`;
})();
</script>

View File

@ -1,17 +0,0 @@
<!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>

View File

@ -1,12 +0,0 @@
<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>

View File

@ -1,50 +0,0 @@
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: auto;
min-width: min(40em, 100%);
flex: auto;
overflow-y: scroll;
}
.sidebars {
width: 100%;
background: #050505;
}
#homeroot {
padding: 1em;
}

View File

@ -1,204 +0,0 @@
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;
};

View File

@ -1,22 +0,0 @@
#operatorroot {
height: 10em;
width: 100%;
}
#operation {
width: 100%;
}
#workerpool {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1em;
padding: 1em;
height: 5em;
overflow: hidden;
}
.workerview {
background: #0f0f0f;
overflow: hidden;
}

View File

@ -1,24 +0,0 @@
<!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>

View File

@ -1,69 +0,0 @@
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(elJob(worker.job));
workerview.append(`qsize: ${worker.qsize}`);
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;
};

View File

@ -6,8 +6,8 @@ import time
from traceback import print_exc
import discord
from v6d3musicbase.event import *
from v6d3musicbase.targets import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.targets import *
from ptvp35 import *
from rainbowadn.instrument import Instrumentation

View File

@ -3,8 +3,8 @@ __all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged')
from abc import ABC, abstractmethod
from typing import Any, Callable, Coroutine, TypeVar
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
T = TypeVar('T')

View File

@ -3,8 +3,8 @@ __all__ = ('Job', 'Pool', 'JobUnit', 'JobContext', 'JobStatusChanged', 'PoolEven
import asyncio
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
from .abstractrunner import *

View File

@ -12,15 +12,26 @@ from v6d3music.utils.sparq import *
__all__ = ('InfoCtx', 'BoundCtx', 'UrlCtx', 'ArgCtx',)
class PostCtx:
def __init__(
self, effects: str | None
) -> None:
self.effects: str | None = effects
self.already_read: int = 0
self.tor: bool = False
self.ignore: bool = False
class InfoCtx:
def __init__(
self, info: dict[str, Any], effects: str | None, already_read: int, tor: bool, ignore: bool
self, info: dict[str, Any], post: PostCtx
) -> None:
self.info = info
self.effects = effects
self.already_read = already_read
self.tor = tor
self.ignore = ignore
self.post = post
self.effects = post.effects
self.already_read = post.already_read
self.tor = post.tor
self.ignore = post.ignore
def bind(self, ctx: Context) -> 'BoundCtx':
return BoundCtx(self, ctx)
@ -52,17 +63,18 @@ class BoundCtx:
class UrlCtx:
def __init__(self, url: str, effects: str | None) -> None:
def __init__(self, url: str, post: PostCtx) -> None:
self.url = url
self.effects = effects
self.already_read = 0
self.tor = False
self.ignore = False
self.post = post
self.effects: str | None = post.effects
self.already_read = post.already_read
self.tor = post.tor
self.ignore = post.ignore
async def entries(self) -> AsyncIterable[InfoCtx]:
try:
async for info in entries_for_url(self.url, self.tor):
yield InfoCtx(info, self.effects, self.already_read, self.tor, self.ignore)
yield InfoCtx(info, self.post)
except Exception:
if not self.ignore:
raise
@ -73,22 +85,38 @@ class ArgCtx:
self.sources: list[UrlCtx] = []
while args:
match args:
case [url, '-', effects, *args]:
case ['[[', *args]:
try:
close_ix = args.index(']]')
except ValueError:
raise Explicit('expected closing `]]`, not found')
urls = args[:close_ix]
args = args[close_ix + 1:]
case [']]', *args]:
raise Explicit('unexpected `]]`')
case [_url, *args]:
urls = [_url]
case _:
raise RuntimeError
for url in urls:
if url in presets:
raise Explicit('expected url, got preset. maybe you are missing `+`?')
if url in {'+', '-'}:
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?')
if url.startswith('+') or url.startswith('-"') or url.startswith('-\''):
raise Explicit(
'expected url, got `+` or `-"` or `-\'`. maybe you forgot to separate control symbol from the effects?'
)
match args:
case ['-', effects, *args]:
pass
case [url, '+', preset, *args]:
case ['+', preset, *args]:
effects = effects_for_preset(preset)
case [url, *args]:
case [*args]:
effects = default_effects
case _:
raise RuntimeError
if url in presets:
raise Explicit('expected url, got preset. maybe you are missing `+`?')
if url in {'+', '-'}:
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?')
if url.startswith('+') or url.startswith('-"') or url.startswith('-\''):
raise Explicit('expected url, got `+` or `-"` or `-\'`. maybe you forgot to separate control symbol from the effects?')
ctx = UrlCtx(url, effects)
seconds = 0
post = PostCtx(effects)
match args:
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
seconds = 3600 * int(h) + 60 * int(m) + int(s)
@ -97,18 +125,19 @@ class ArgCtx:
case [s, *args] if s.isdecimal():
seconds = int(s)
case [*args]:
pass
ctx.already_read = round(seconds / sparq(options_for_effects(effects)))
seconds = 0
post.already_read = round(seconds / sparq(options_for_effects(effects)))
while True:
match args:
case ['tor', *args]:
if ctx.tor:
if post.tor:
raise Explicit('duplicate tor')
ctx.tor = True
post.tor = True
case ['ignore', *args]:
if ctx.ignore:
if post.ignore:
raise Explicit('duplicate ignore')
ctx.ignore = True
post.ignore = True
case [*args]:
break
self.sources.append(ctx)
for url in urls:
self.sources.append(UrlCtx(url, post))

View File

@ -1,60 +0,0 @@
__all__ = ('Event', 'SendableEvents', 'ReceivableEvents', 'Events', 'Receiver')
import asyncio
from typing import Callable, Generic, TypeVar
from typing_extensions import Self
from .responsetype import ResponseType
class Event:
def json(self) -> ResponseType:
raise NotImplementedError
T = TypeVar('T', bound=Event)
T_co = TypeVar('T_co', bound=Event, covariant=True)
T_contra = TypeVar('T_contra', bound=Event, contravariant=True)
class Receiver(Generic[T_contra]):
def __init__(self, receive: Callable[[T_contra], None], receivers: set[Self], /) -> None:
self.__receive = receive
self.__receivers = receivers
self.__receiving = False
def __enter__(self) -> None:
self.__receivers.add(self)
self.__receiving = True
def __exit__(self, exc_type, exc_val, exc_tb):
self.__receiving = False
self.__receivers.remove(self)
def receive(self, event: T_contra, /) -> None:
if self.__receiving:
self.__receive(event)
class SendableEvents(Generic[T_contra]):
def send(self, event: T_contra, /) -> None:
raise NotImplementedError
class ReceivableEvents(Generic[T_co]):
def receive(self, receive: Callable[[T_co], None], /) -> Receiver[T_co]:
raise NotImplementedError
class Events(Generic[T], SendableEvents[T], ReceivableEvents[T]):
def __init__(self) -> None:
self.__receivers: set[Receiver[T]] = set()
self.__loop = asyncio.get_running_loop()
def send(self, event: T, /) -> None:
for receiver in self.__receivers:
self.__loop.call_soon(receiver.receive, event)
def receive(self, receive: Callable[[T], None], /) -> Receiver[T]:
return Receiver(receive, self.__receivers)

View File

@ -1,17 +0,0 @@
__all__ = ('ResponseType', 'cast_to_response')
from typing import Any, TypeAlias
ResponseType: TypeAlias = list['ResponseType'] | dict[str, 'ResponseType'] | float | int | bool | str | None
def cast_to_response(target: Any) -> ResponseType:
match target:
case str() | int() | float() | bool() | None:
return target
case list() | tuple():
return list(map(cast_to_response, target))
case dict():
return {str(key): cast_to_response(value) for key, value in target.items()}
case _:
return str(target)

View File

@ -1,76 +0,0 @@
__all__ = ('Targets', 'JsonLike', 'Async')
import abc
from typing import Any, Callable, Generic, TypeVar
from rainbowadn.instrument import Instrumentation
from .responsetype import *
def qualname(t: type) -> str:
return f'{t.__module__}.{t.__qualname__}'
T = TypeVar('T')
class Flagful(Generic[T]):
def __init__(self, value: T, flags: set[object]) -> None:
self.value = value
self.flags = flags
class Targets:
def __init__(self) -> None:
self.targets: dict[str, Flagful[tuple[Any, str]]] = {}
self.instrumentations: dict[str, Flagful[Callable[[Any, str], Instrumentation]]] = {}
self.factories: dict[tuple[str, str], Callable[[], Instrumentation]] = {}
def register_target(self, targetname: str, target: Any, methodname: str, /, *flags: object) -> None:
self.targets[targetname] = Flagful((target, methodname), set(flags))
print(f'registered target: {targetname}')
def register_type(self, target: type, methodname: str, /, *flags: object) -> None:
self.register_target(f'{qualname(target)}.{methodname}', target, methodname, *flags)
def register_instance(self, target: object, methodname: str, /, *flags: object) -> None:
self.register_target(f'{qualname(target.__class__)}().{methodname}', target, methodname, *flags)
def register_instrumentation(
self,
instrumentationname: str,
instrumentation_factory: Callable[[Any, str], Instrumentation],
/,
*flags: object,
) -> None:
self.instrumentations[instrumentationname] = Flagful(instrumentation_factory, set(flags))
print(f'registered instrumentation: {instrumentationname}')
def get_factory(
self,
targetname: str,
target: Any,
methodname: str,
instrumentationname: str,
instrumentation_factory: Callable[[Any, str], Instrumentation],
/
) -> Callable[[], Instrumentation]:
if (targetname, instrumentationname) not in self.factories:
flags_required = self.instrumentations[instrumentationname].flags
flags_present = self.targets[targetname].flags
if not flags_required.issubset(flags_present):
raise KeyError('target lacks flags required by instrumentation')
self.factories[targetname, instrumentationname] = (
lambda: instrumentation_factory(target, methodname)
)
return self.factories[targetname, instrumentationname]
class JsonLike(abc.ABC):
@abc.abstractmethod
def json(self) -> ResponseType:
raise NotImplementedError
Async = object()