underrun detection + new architecture
This commit is contained in:
parent
b8b378a5bb
commit
4aa5a679c1
@ -9,8 +9,9 @@ COPY base.requirements.txt base.requirements.txt
|
|||||||
RUN pip install -r base.requirements.txt
|
RUN pip install -r base.requirements.txt
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
ENV v6port=80
|
||||||
|
ENV v6host=0.0.0.0
|
||||||
RUN mkdir ${v6root}
|
RUN mkdir ${v6root}
|
||||||
COPY v6d3musicbase v6d3musicbase
|
|
||||||
COPY v6d3music v6d3music
|
COPY v6d3music v6d3music
|
||||||
RUN python3 -m v6d3music.main
|
RUN python3 -m v6d3music.main
|
||||||
CMD ["python3", "-m", "v6d3music.run-bot"]
|
CMD ["python3", "-m", "v6d3music.run-bot"]
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# 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 update
|
||||||
RUN apt-get install -y python3-sphinx node.js npm
|
|
||||||
RUN npm install -g http-server
|
RUN apt-get install -y python3-sphinx
|
||||||
RUN pip install pydata-sphinx-theme
|
RUN pip install Sphinx==5.3.0 pydata-sphinx-theme==0.12.0
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
ENV v6root=/app/data/
|
ENV v6root=/app/data/
|
||||||
RUN mkdir ${v6root}
|
RUN mkdir ${v6root}
|
||||||
|
|
||||||
|
|
||||||
|
FROM compile-sphinx5.3.0-base as compile-latest
|
||||||
|
|
||||||
COPY base.requirements.txt base.requirements.txt
|
COPY base.requirements.txt base.requirements.txt
|
||||||
RUN pip install -r base.requirements.txt
|
RUN pip install -r base.requirements.txt
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
@ -16,5 +22,11 @@ COPY v6d3music v6d3music
|
|||||||
COPY docs/source docs/source
|
COPY docs/source docs/source
|
||||||
WORKDIR /app/docs/
|
WORKDIR /app/docs/
|
||||||
RUN make html
|
RUN make html
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:19
|
||||||
|
|
||||||
|
RUN npm install -g http-server
|
||||||
WORKDIR /app/docs/build/html/
|
WORKDIR /app/docs/build/html/
|
||||||
|
COPY --from=compile-latest /app/docs/build/html/ /app/docs/build/html/
|
||||||
CMD [ "http-server", "-p", "80" ]
|
CMD [ "http-server", "-p", "80" ]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
aiohttp>=3.7.4,<4
|
aiohttp>=3.7.4,<4
|
||||||
discord.py[voice]~=2.1.0
|
discord.py[voice]~=2.1.0
|
||||||
yt-dlp~=2022.11.11
|
yt-dlp~=2023.2.17
|
||||||
typing_extensions~=4.4.0
|
typing_extensions~=4.4.0
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
ptvp35 @ git+https://gitea.parrrate.ru/PTV/ptvp35.git@e760fca39e2070b9959aeb95b53e59e895f1ad57
|
ptvp35 @ git+https://gitea.parrrate.ru/PTV/ptvp35.git@e760fca39e2070b9959aeb95b53e59e895f1ad57
|
||||||
v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@c718d4d1422945a756213d22d9e26aa24babe0f6
|
v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@c718d4d1422945a756213d22d9e26aa24babe0f6
|
||||||
v6d1tokens @ git+https://gitea.parrrate.ru/PTV/v6d1tokens.git@9ada50f111bd6e9a49c9c6683fa7504fee030056
|
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
|
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
|
||||||
|
2
setup.py
2
setup.py
@ -3,7 +3,7 @@ from setuptools import setup
|
|||||||
setup(
|
setup(
|
||||||
name='v6d3music',
|
name='v6d3music',
|
||||||
version='',
|
version='',
|
||||||
packages=['v6d3music', 'v6d3musicbase'],
|
packages=['v6d3music'],
|
||||||
url='',
|
url='',
|
||||||
license='',
|
license='',
|
||||||
author='PARRRATE T&V',
|
author='PARRRATE T&V',
|
||||||
|
@ -3,8 +3,8 @@ import time
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
from v6d3musicbase.responsetype import *
|
from v6d2ctx.integration.responsetype import *
|
||||||
from v6d3musicbase.targets import *
|
from v6d2ctx.integration.targets import *
|
||||||
|
|
||||||
from rainbowadn.instrument import Instrumentation
|
from rainbowadn.instrument import Instrumentation
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import *
|
||||||
|
@ -56,8 +56,6 @@ class CachedDictionary(Generic[TKey, T]):
|
|||||||
|
|
||||||
|
|
||||||
class MusicAppFactory(AppFactory):
|
class MusicAppFactory(AppFactory):
|
||||||
htmlroot = Path(__file__).parent / 'html'
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
secret: str,
|
secret: str,
|
||||||
@ -81,13 +79,6 @@ class MusicAppFactory(AppFactory):
|
|||||||
return f'https://discord.com/api/oauth2/authorize?client_id={client_id}' \
|
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'
|
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:
|
async def code_token(self, code: str) -> dict:
|
||||||
client_id = self._api.user_id()
|
client_id = self._api.user_id()
|
||||||
assert client_id is not None
|
assert client_id is not None
|
||||||
@ -199,38 +190,26 @@ class MusicAppFactory(AppFactory):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def define_routes(self, routes: web.RouteTableDef) -> None:
|
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/')
|
@routes.get('/authlink/')
|
||||||
async def authlink(_request: web.Request) -> web.StreamResponse:
|
async def authlink(_request: web.Request) -> web.StreamResponse:
|
||||||
return web.Response(text=self.auth_link())
|
return web.Response(text=self.auth_link())
|
||||||
|
|
||||||
@routes.get('/auth/')
|
@routes.get('/auth/')
|
||||||
async def auth(request: web.Request) -> web.StreamResponse:
|
async def auth(request: web.Request) -> web.StreamResponse:
|
||||||
if 'session' in request.query:
|
session = request.query.get('session')
|
||||||
response = web.HTTPFound('/')
|
state = request.query.get('state')
|
||||||
session = str(request.query.get('session'))
|
code = request.query.get('code')
|
||||||
s_state = str(request.query.get('state'))
|
match session, state, code:
|
||||||
code = str(request.query.get('code'))
|
case str() as session, str() as state, str() as code:
|
||||||
if bytes_hash(session.encode()) != s_state:
|
if bytes_hash(session.encode()) != state:
|
||||||
raise web.HTTPBadRequest
|
raise web.HTTPBadRequest
|
||||||
data = self.session_data(session)
|
data = self.session_data(session)
|
||||||
data['code'] = code
|
data['code'] = code
|
||||||
data['token'] = await self.code_token(code)
|
data['token'] = await self.code_token(code)
|
||||||
await self.db.set(session, data)
|
await self.db.set(session, data)
|
||||||
return response
|
return web.HTTPFound('/')
|
||||||
else:
|
case _:
|
||||||
return web.FileResponse(self._path('auth.html'))
|
raise web.HTTPBadRequest
|
||||||
|
|
||||||
@routes.get('/state/')
|
@routes.get('/state/')
|
||||||
async def get_state(request: web.Request) -> web.Response:
|
async def get_state(request: web.Request) -> web.Response:
|
||||||
@ -246,29 +225,6 @@ class MusicAppFactory(AppFactory):
|
|||||||
data=await self.session_status(session)
|
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/')
|
@routes.post('/api/')
|
||||||
async def api(request: web.Request) -> web.Response:
|
async def api(request: web.Request) -> web.Response:
|
||||||
session = request.query.get('session')
|
session = request.query.get('session')
|
||||||
|
@ -8,9 +8,10 @@ from v6d3music.core.mainservice import *
|
|||||||
from v6d3music.utils.assert_admin import *
|
from v6d3music.utils.assert_admin import *
|
||||||
from v6d3music.utils.catch import *
|
from v6d3music.utils.catch import *
|
||||||
from v6d3music.utils.effects_for_preset import *
|
from v6d3music.utils.effects_for_preset import *
|
||||||
from v6d3music.utils.options_for_effects import *
|
|
||||||
from v6d3music.utils.presets import *
|
from v6d3music.utils.presets import *
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
__all__ = ('get_of',)
|
__all__ = ('get_of',)
|
||||||
|
|
||||||
|
|
||||||
@ -40,12 +41,23 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
''',
|
''',
|
||||||
(), 'help'
|
(), '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):
|
async with mainservice.lock_for(ctx.guild):
|
||||||
queue = await mainservice.context(ctx, create=True, force_play=False).queue()
|
queue = await mainservice.context(ctx, create=True, force_play=False).queue()
|
||||||
if ctx.message.attachments:
|
if attachments:
|
||||||
if len(ctx.message.attachments) > 1:
|
args = ['[[', *(attachment.url for attachment in attachments), ']]'] + args
|
||||||
raise Explicit('no more than one attachment')
|
|
||||||
args = [ctx.message.attachments[0].url] + args
|
|
||||||
async for audio in mainservice.yt_audios(ctx, args):
|
async for audio in mainservice.yt_audios(ctx, args):
|
||||||
queue.append(audio)
|
queue.append(audio)
|
||||||
await ctx.reply('done')
|
await ctx.reply('done')
|
||||||
@ -69,9 +81,7 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
|
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
|
||||||
pos0, pos1 = int(pos0), int(pos1)
|
pos0, pos1 = int(pos0), int(pos1)
|
||||||
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
||||||
for _ in range(pos0, pos1 + 1):
|
queue.skip_between(pos0, pos1, ctx.member)
|
||||||
if not queue.skip_at(pos0, ctx.member):
|
|
||||||
pos0 += 1
|
|
||||||
case _:
|
case _:
|
||||||
raise Explicit('misformatted')
|
raise Explicit('misformatted')
|
||||||
await ctx.reply('done')
|
await ctx.reply('done')
|
||||||
@ -120,10 +130,7 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
raise Explicit('misformatted')
|
raise Explicit('misformatted')
|
||||||
assert_admin(ctx.member)
|
assert_admin(ctx.member)
|
||||||
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
||||||
yta = queue.queue[0]
|
queue.queue[0].set_effects(effects)
|
||||||
seconds = yta.source_seconds()
|
|
||||||
yta.options = options_for_effects(effects)
|
|
||||||
yta.set_seconds(seconds)
|
|
||||||
|
|
||||||
@at('default')
|
@at('default')
|
||||||
async def default(ctx: Context, args: list[str]) -> None:
|
async def default(ctx: Context, args: list[str]) -> None:
|
||||||
@ -154,7 +161,7 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
@at('repeat')
|
@at('repeat')
|
||||||
async def repeat(ctx: Context, args: list[str]):
|
async def repeat(ctx: Context, args: list[str]):
|
||||||
match args:
|
match args:
|
||||||
case [n_, *args] if n_.isdecimal():
|
case ['x', n_, *args] if n_.isdecimal():
|
||||||
n = int(n_)
|
n = int(n_)
|
||||||
case [*args]:
|
case [*args]:
|
||||||
n = 1
|
n = 1
|
||||||
@ -206,16 +213,7 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
raise Explicit('misformatted')
|
raise Explicit('misformatted')
|
||||||
assert_admin(ctx.member)
|
assert_admin(ctx.member)
|
||||||
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
|
||||||
if not queue.queue:
|
queue.branch(effects)
|
||||||
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)
|
|
||||||
|
|
||||||
@at('//')
|
@at('//')
|
||||||
@at('queue')
|
@at('queue')
|
||||||
@ -321,4 +319,13 @@ presets: {shlex.join(allowed_presets)}
|
|||||||
vc = await mainservice.context(ctx, create=False, force_play=True).vc()
|
vc = await mainservice.context(ctx, create=False, force_play=True).vc()
|
||||||
vc.resume()
|
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
|
return of
|
||||||
|
@ -4,9 +4,9 @@ from contextlib import AsyncExitStack
|
|||||||
from typing import AsyncIterable, TypeVar
|
from typing import AsyncIterable, TypeVar
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from v6d3musicbase.event import *
|
from v6d2ctx.integration.event import *
|
||||||
from v6d3musicbase.responsetype import *
|
from v6d2ctx.integration.responsetype import *
|
||||||
from v6d3musicbase.targets import *
|
from v6d2ctx.integration.targets import *
|
||||||
|
|
||||||
import v6d3music.processing.pool
|
import v6d3music.processing.pool
|
||||||
from ptvp35 import *
|
from ptvp35 import *
|
||||||
@ -72,7 +72,7 @@ class MainService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
|
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
|
||||||
vc: discord.VoiceProtocol | None = member.guild.voice_client
|
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
|
vs: discord.VoiceState | None = member.voice
|
||||||
if vs is None:
|
if vs is None:
|
||||||
raise Explicit('not connected')
|
raise Explicit('not connected')
|
||||||
|
@ -94,6 +94,11 @@ class QueueAudio(discord.AudioSource):
|
|||||||
return True
|
return True
|
||||||
return False
|
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:
|
def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool:
|
||||||
if audio in self.queue:
|
if audio in self.queue:
|
||||||
if audio.can_be_skipped_by(member):
|
if audio.can_be_skipped_by(member):
|
||||||
@ -188,6 +193,16 @@ class QueueAudio(discord.AudioSource):
|
|||||||
print_exc()
|
print_exc()
|
||||||
self.update_sources()
|
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]):
|
class ForwardView(MutableSequence[YTAudio]):
|
||||||
def __init__(self, sequence: MutableSequence[YTAudio]) -> None:
|
def __init__(self, sequence: MutableSequence[YTAudio]) -> None:
|
||||||
|
@ -3,7 +3,7 @@ from collections import deque
|
|||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import AsyncIterable, Iterable
|
from typing import AsyncIterable, Iterable
|
||||||
|
|
||||||
from v6d3musicbase.responsetype import *
|
from v6d2ctx.integration.responsetype import *
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import *
|
||||||
from v6d3music.core.create_ytaudio import *
|
from v6d3music.core.create_ytaudio import *
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
@ -10,6 +12,7 @@ from v6d3music.core.real_url import *
|
|||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import *
|
||||||
from v6d3music.processing.abstractrunner import *
|
from v6d3music.processing.abstractrunner import *
|
||||||
from v6d3music.utils.fill import *
|
from v6d3music.utils.fill import *
|
||||||
|
from v6d3music.utils.options_for_effects import *
|
||||||
from v6d3music.utils.sparq import *
|
from v6d3music.utils.sparq import *
|
||||||
from v6d3music.utils.tor_prefix import *
|
from v6d3music.utils.tor_prefix import *
|
||||||
|
|
||||||
@ -47,6 +50,7 @@ class YTAudio(discord.AudioSource):
|
|||||||
self._duration_lock = asyncio.Lock()
|
self._duration_lock = asyncio.Lock()
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.stop_at: int | None = stop_at
|
self.stop_at: int | None = stop_at
|
||||||
|
self.attempts = 0
|
||||||
|
|
||||||
def _reduced_durations(self) -> dict[str, str]:
|
def _reduced_durations(self) -> dict[str, str]:
|
||||||
return {url: duration for url, duration in self._durations.items() if url == self.url}
|
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):
|
def set_seconds(self, seconds: float):
|
||||||
self.set_already_read(round(seconds / sparq(self.options)))
|
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:
|
def source_seconds(self) -> float:
|
||||||
return self.already_read * sparq(self.options)
|
return self.already_read * sparq(self.options)
|
||||||
|
|
||||||
@ -139,6 +148,24 @@ class YTAudio(discord.AudioSource):
|
|||||||
)
|
)
|
||||||
return before_options
|
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:
|
def read(self) -> bytes:
|
||||||
if self.regenerating:
|
if self.regenerating:
|
||||||
return FILL
|
return FILL
|
||||||
@ -146,10 +173,15 @@ class YTAudio(discord.AudioSource):
|
|||||||
return b''
|
return b''
|
||||||
self.already_read += 1
|
self.already_read += 1
|
||||||
ret: bytes = self.source.read()
|
ret: bytes = self.source.read()
|
||||||
if not ret and not self.source.droppable():
|
if not ret and (not (droppable := self.source.droppable()) or self.underran()):
|
||||||
if random.random() > .1:
|
if self.attempts < 5 or random.random() > .1:
|
||||||
|
self.attempts += 1
|
||||||
self.regenerating = True
|
self.regenerating = True
|
||||||
self.loop.create_task(self.regenerate())
|
self.loop.create_task(
|
||||||
|
self.regenerate(
|
||||||
|
'underran' if droppable else 'not droppable'
|
||||||
|
)
|
||||||
|
)
|
||||||
return FILL
|
return FILL
|
||||||
else:
|
else:
|
||||||
print(f'dropped {self.origin}')
|
print(f'dropped {self.origin}')
|
||||||
@ -216,9 +248,9 @@ class YTAudio(discord.AudioSource):
|
|||||||
audio._durations |= respawn.get('durations', {})
|
audio._durations |= respawn.get('durations', {})
|
||||||
return audio
|
return audio
|
||||||
|
|
||||||
async def regenerate(self):
|
async def regenerate(self, reason: str):
|
||||||
try:
|
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)
|
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor)
|
||||||
if hasattr(self, 'source'):
|
if hasattr(self, 'source'):
|
||||||
self.source.cleanup()
|
self.source.cleanup()
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
@ -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;
|
|
||||||
}
|
|
@ -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>
|
|
@ -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;
|
|
||||||
};
|
|
@ -6,8 +6,8 @@ import time
|
|||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from v6d3musicbase.event import *
|
from v6d2ctx.integration.event import *
|
||||||
from v6d3musicbase.targets import *
|
from v6d2ctx.integration.targets import *
|
||||||
|
|
||||||
from ptvp35 import *
|
from ptvp35 import *
|
||||||
from rainbowadn.instrument import Instrumentation
|
from rainbowadn.instrument import Instrumentation
|
||||||
|
@ -3,8 +3,8 @@ __all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged')
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable, Coroutine, TypeVar
|
from typing import Any, Callable, Coroutine, TypeVar
|
||||||
|
|
||||||
from v6d3musicbase.event import *
|
from v6d2ctx.integration.event import *
|
||||||
from v6d3musicbase.responsetype import *
|
from v6d2ctx.integration.responsetype import *
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ __all__ = ('Job', 'Pool', 'JobUnit', 'JobContext', 'JobStatusChanged', 'PoolEven
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
|
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
|
||||||
|
|
||||||
from v6d3musicbase.event import *
|
from v6d2ctx.integration.event import *
|
||||||
from v6d3musicbase.responsetype import *
|
from v6d2ctx.integration.responsetype import *
|
||||||
|
|
||||||
from .abstractrunner import *
|
from .abstractrunner import *
|
||||||
|
|
||||||
|
@ -12,15 +12,26 @@ from v6d3music.utils.sparq import *
|
|||||||
__all__ = ('InfoCtx', 'BoundCtx', 'UrlCtx', 'ArgCtx',)
|
__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:
|
class InfoCtx:
|
||||||
def __init__(
|
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:
|
) -> None:
|
||||||
self.info = info
|
self.info = info
|
||||||
self.effects = effects
|
self.post = post
|
||||||
self.already_read = already_read
|
self.effects = post.effects
|
||||||
self.tor = tor
|
self.already_read = post.already_read
|
||||||
self.ignore = ignore
|
self.tor = post.tor
|
||||||
|
self.ignore = post.ignore
|
||||||
|
|
||||||
def bind(self, ctx: Context) -> 'BoundCtx':
|
def bind(self, ctx: Context) -> 'BoundCtx':
|
||||||
return BoundCtx(self, ctx)
|
return BoundCtx(self, ctx)
|
||||||
@ -52,17 +63,18 @@ class BoundCtx:
|
|||||||
|
|
||||||
|
|
||||||
class UrlCtx:
|
class UrlCtx:
|
||||||
def __init__(self, url: str, effects: str | None) -> None:
|
def __init__(self, url: str, post: PostCtx) -> None:
|
||||||
self.url = url
|
self.url = url
|
||||||
self.effects = effects
|
self.post = post
|
||||||
self.already_read = 0
|
self.effects: str | None = post.effects
|
||||||
self.tor = False
|
self.already_read = post.already_read
|
||||||
self.ignore = False
|
self.tor = post.tor
|
||||||
|
self.ignore = post.ignore
|
||||||
|
|
||||||
async def entries(self) -> AsyncIterable[InfoCtx]:
|
async def entries(self) -> AsyncIterable[InfoCtx]:
|
||||||
try:
|
try:
|
||||||
async for info in entries_for_url(self.url, self.tor):
|
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:
|
except Exception:
|
||||||
if not self.ignore:
|
if not self.ignore:
|
||||||
raise
|
raise
|
||||||
@ -73,22 +85,38 @@ class ArgCtx:
|
|||||||
self.sources: list[UrlCtx] = []
|
self.sources: list[UrlCtx] = []
|
||||||
while args:
|
while args:
|
||||||
match args:
|
match args:
|
||||||
case [url, '-', effects, *args]:
|
case ['[[', *args]:
|
||||||
pass
|
try:
|
||||||
case [url, '+', preset, *args]:
|
close_ix = args.index(']]')
|
||||||
effects = effects_for_preset(preset)
|
except ValueError:
|
||||||
case [url, *args]:
|
raise Explicit('expected closing `]]`, not found')
|
||||||
effects = default_effects
|
urls = args[:close_ix]
|
||||||
|
args = args[close_ix + 1:]
|
||||||
|
case [']]', *args]:
|
||||||
|
raise Explicit('unexpected `]]`')
|
||||||
|
case [_url, *args]:
|
||||||
|
urls = [_url]
|
||||||
case _:
|
case _:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
for url in urls:
|
||||||
if url in presets:
|
if url in presets:
|
||||||
raise Explicit('expected url, got preset. maybe you are missing `+`?')
|
raise Explicit('expected url, got preset. maybe you are missing `+`?')
|
||||||
if url in {'+', '-'}:
|
if url in {'+', '-'}:
|
||||||
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?')
|
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?')
|
||||||
if url.startswith('+') or url.startswith('-"') or url.startswith('-\''):
|
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?')
|
raise Explicit(
|
||||||
ctx = UrlCtx(url, effects)
|
'expected url, got `+` or `-"` or `-\'`. maybe you forgot to separate control symbol from the effects?'
|
||||||
seconds = 0
|
)
|
||||||
|
match args:
|
||||||
|
case ['-', effects, *args]:
|
||||||
|
pass
|
||||||
|
case ['+', preset, *args]:
|
||||||
|
effects = effects_for_preset(preset)
|
||||||
|
case [*args]:
|
||||||
|
effects = default_effects
|
||||||
|
case _:
|
||||||
|
raise RuntimeError
|
||||||
|
post = PostCtx(effects)
|
||||||
match args:
|
match args:
|
||||||
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
|
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
|
||||||
seconds = 3600 * int(h) + 60 * int(m) + int(s)
|
seconds = 3600 * int(h) + 60 * int(m) + int(s)
|
||||||
@ -97,18 +125,19 @@ class ArgCtx:
|
|||||||
case [s, *args] if s.isdecimal():
|
case [s, *args] if s.isdecimal():
|
||||||
seconds = int(s)
|
seconds = int(s)
|
||||||
case [*args]:
|
case [*args]:
|
||||||
pass
|
seconds = 0
|
||||||
ctx.already_read = round(seconds / sparq(options_for_effects(effects)))
|
post.already_read = round(seconds / sparq(options_for_effects(effects)))
|
||||||
while True:
|
while True:
|
||||||
match args:
|
match args:
|
||||||
case ['tor', *args]:
|
case ['tor', *args]:
|
||||||
if ctx.tor:
|
if post.tor:
|
||||||
raise Explicit('duplicate tor')
|
raise Explicit('duplicate tor')
|
||||||
ctx.tor = True
|
post.tor = True
|
||||||
case ['ignore', *args]:
|
case ['ignore', *args]:
|
||||||
if ctx.ignore:
|
if post.ignore:
|
||||||
raise Explicit('duplicate ignore')
|
raise Explicit('duplicate ignore')
|
||||||
ctx.ignore = True
|
post.ignore = True
|
||||||
case [*args]:
|
case [*args]:
|
||||||
break
|
break
|
||||||
self.sources.append(ctx)
|
for url in urls:
|
||||||
|
self.sources.append(UrlCtx(url, post))
|
||||||
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
Loading…
Reference in New Issue
Block a user