From 4aa5a679c186a066a79e75817b21f7d9d37477e0 Mon Sep 17 00:00:00 2001 From: timofey Date: Mon, 27 Feb 2023 11:38:01 +0000 Subject: [PATCH] underrun detection + new architecture --- Dockerfile | 3 +- Docs.Dockerfile | 20 ++- base.requirements.txt | 2 +- requirements.txt | 4 +- setup.py | 2 +- v6d3music/api.py | 4 +- v6d3music/app.py | 70 ++------- v6d3music/commands.py | 53 ++++--- v6d3music/core/mainservice.py | 8 +- v6d3music/core/queueaudio.py | 15 ++ v6d3music/core/ystate.py | 2 +- v6d3music/core/ytaudio.py | 42 ++++- v6d3music/html/auth.html | 8 - v6d3music/html/home.html | 17 --- v6d3music/html/login.html | 12 -- v6d3music/html/main.css | 50 ------ v6d3music/html/main.js | 204 ------------------------- v6d3music/html/operator.css | 22 --- v6d3music/html/operator.html | 24 --- v6d3music/html/operator.js | 69 --------- v6d3music/main.py | 4 +- v6d3music/processing/abstractrunner.py | 4 +- v6d3music/processing/pool.py | 4 +- v6d3music/utils/argctx.py | 87 +++++++---- v6d3musicbase/event.py | 60 -------- v6d3musicbase/responsetype.py | 17 --- v6d3musicbase/targets.py | 76 --------- 27 files changed, 188 insertions(+), 695 deletions(-) delete mode 100644 v6d3music/html/auth.html delete mode 100644 v6d3music/html/home.html delete mode 100644 v6d3music/html/login.html delete mode 100644 v6d3music/html/main.css delete mode 100644 v6d3music/html/main.js delete mode 100644 v6d3music/html/operator.css delete mode 100644 v6d3music/html/operator.html delete mode 100644 v6d3music/html/operator.js delete mode 100644 v6d3musicbase/event.py delete mode 100644 v6d3musicbase/responsetype.py delete mode 100644 v6d3musicbase/targets.py diff --git a/Dockerfile b/Dockerfile index 350faae..98d95d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/Docs.Dockerfile b/Docs.Dockerfile index a08ec12..7e2afea 100644 --- a/Docs.Dockerfile +++ b/Docs.Dockerfile @@ -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" ] diff --git a/base.requirements.txt b/base.requirements.txt index 921ee1d..f5cd17c 100644 --- a/base.requirements.txt +++ b/base.requirements.txt @@ -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 diff --git a/requirements.txt b/requirements.txt index 1fbb143..0e7e626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index 9990419..c28d08b 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup( name='v6d3music', version='', - packages=['v6d3music', 'v6d3musicbase'], + packages=['v6d3music'], url='', license='', author='PARRRATE T&V', diff --git a/v6d3music/api.py b/v6d3music/api.py index 3d8db24..613fdcd 100644 --- a/v6d3music/api.py +++ b/v6d3music/api.py @@ -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 * diff --git a/v6d3music/app.py b/v6d3music/app.py index 0e9ceb7..edae2f6 100644 --- a/v6d3music/app.py +++ b/v6d3music/app.py @@ -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') diff --git a/v6d3music/commands.py b/v6d3music/commands.py index 227cb3b..fabcc40 100644 --- a/v6d3music/commands.py +++ b/v6d3music/commands.py @@ -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 diff --git a/v6d3music/core/mainservice.py b/v6d3music/core/mainservice.py index 1bac940..a11574c 100644 --- a/v6d3music/core/mainservice.py +++ b/v6d3music/core/mainservice.py @@ -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') diff --git a/v6d3music/core/queueaudio.py b/v6d3music/core/queueaudio.py index a562a82..e812642 100644 --- a/v6d3music/core/queueaudio.py +++ b/v6d3music/core/queueaudio.py @@ -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: diff --git a/v6d3music/core/ystate.py b/v6d3music/core/ystate.py index f15968c..11a5d96 100644 --- a/v6d3music/core/ystate.py +++ b/v6d3music/core/ystate.py @@ -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 * diff --git a/v6d3music/core/ytaudio.py b/v6d3music/core/ytaudio.py index 94c948d..de1fd2c 100644 --- a/v6d3music/core/ytaudio.py +++ b/v6d3music/core/ytaudio.py @@ -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() diff --git a/v6d3music/html/auth.html b/v6d3music/html/auth.html deleted file mode 100644 index e9ee04f..0000000 --- a/v6d3music/html/auth.html +++ /dev/null @@ -1,8 +0,0 @@ - -
- - diff --git a/v6d3music/html/home.html b/v6d3music/html/home.html deleted file mode 100644 index 4fa9d5c..0000000 --- a/v6d3music/html/home.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - -
- -
- -
- - - diff --git a/v6d3music/html/login.html b/v6d3music/html/login.html deleted file mode 100644 index 43055a2..0000000 --- a/v6d3music/html/login.html +++ /dev/null @@ -1,12 +0,0 @@ - -
- - diff --git a/v6d3music/html/main.css b/v6d3music/html/main.css deleted file mode 100644 index 93c7531..0000000 --- a/v6d3music/html/main.css +++ /dev/null @@ -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; -} diff --git a/v6d3music/html/main.js b/v6d3music/html/main.js deleted file mode 100644 index 6b1772b..0000000 --- a/v6d3music/html/main.js +++ /dev/null @@ -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; -}; diff --git a/v6d3music/html/operator.css b/v6d3music/html/operator.css deleted file mode 100644 index 64b8b6f..0000000 --- a/v6d3music/html/operator.css +++ /dev/null @@ -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; -} diff --git a/v6d3music/html/operator.html b/v6d3music/html/operator.html deleted file mode 100644 index 2ed4b38..0000000 --- a/v6d3music/html/operator.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - -
- -
- -
- - - - - diff --git a/v6d3music/html/operator.js b/v6d3music/html/operator.js deleted file mode 100644 index 2457b6c..0000000 --- a/v6d3music/html/operator.js +++ /dev/null @@ -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; -}; diff --git a/v6d3music/main.py b/v6d3music/main.py index 2e6deba..329e776 100644 --- a/v6d3music/main.py +++ b/v6d3music/main.py @@ -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 diff --git a/v6d3music/processing/abstractrunner.py b/v6d3music/processing/abstractrunner.py index 448eef5..1b03f29 100644 --- a/v6d3music/processing/abstractrunner.py +++ b/v6d3music/processing/abstractrunner.py @@ -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') diff --git a/v6d3music/processing/pool.py b/v6d3music/processing/pool.py index c70ac95..317c27b 100644 --- a/v6d3music/processing/pool.py +++ b/v6d3music/processing/pool.py @@ -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 * diff --git a/v6d3music/utils/argctx.py b/v6d3music/utils/argctx.py index 2659c04..03d1176 100644 --- a/v6d3music/utils/argctx.py +++ b/v6d3music/utils/argctx.py @@ -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)) diff --git a/v6d3musicbase/event.py b/v6d3musicbase/event.py deleted file mode 100644 index 7f8b4fb..0000000 --- a/v6d3musicbase/event.py +++ /dev/null @@ -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) diff --git a/v6d3musicbase/responsetype.py b/v6d3musicbase/responsetype.py deleted file mode 100644 index 2181c0d..0000000 --- a/v6d3musicbase/responsetype.py +++ /dev/null @@ -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) diff --git a/v6d3musicbase/targets.py b/v6d3musicbase/targets.py deleted file mode 100644 index eb58130..0000000 --- a/v6d3musicbase/targets.py +++ /dev/null @@ -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()