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()