docs + operation

This commit is contained in:
AF 2023-01-15 08:51:29 +00:00
parent 9ad6126838
commit 08192b5d93
28 changed files with 1300 additions and 369 deletions

View File

@ -9,5 +9,8 @@ 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
RUN mkdir ${v6root}
COPY v6d3musicbase v6d3musicbase
COPY v6d3music v6d3music COPY v6d3music v6d3music
RUN python3 -m v6d3music.main
CMD ["python3", "-m", "v6d3music.run-bot"] CMD ["python3", "-m", "v6d3music.run-bot"]

View File

@ -1,3 +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~=2022.11.11
typing_extensions~=4.4.0

View File

@ -1,2 +1,44 @@
For Administrators For Administrators
================== ==================
.. _volume-command:
:code:`?/volume` command
------------------------
command syntax::
?/volume
?/volume volume
Getting parrrate-music bot on your server
=========================================
Self-hosting
------------
See :doc:`operation`.
Developer-hosted
----------------
Ask parrrate-music's developers via Discord for the invite link.
Things to consider when using this option:
* Updates requiring bot restart (5~20 second outage) are quite frequent.
* All updates are tested live, i.e. the bot currently has no fallback stable version.
Guild (Discord server) data we store
====================================
Guild IDs
---------
Stored for queues and volume settings.
IDs of banned guild are also stored.
Volume settings
---------------
See :ref:`volume-command`.

View File

@ -1,2 +1,60 @@
For Users For Users
========= =========
:code:`?/play` command
----------------------
command syntax::
?/play url [- effects | + preset] [[[h] m] s] [tor|ignore]* ...
examples::
?/play http://127.0.0.1/audio.mp3 + bassboost tor
?/play http://127.0.0.1/audio.mp3 - "bass=g=10" 23 59 59 ignore
?/play http://127.0.0.1/audio.mp3 http://127.0.0.1/audio.mp3
:code:`?/skip` command
----------------------
command syntax::
?/skip
?/skip at
?/skip start end
examples::
?/skip
?/skip 0
?/skip 0 0
:code:`?/queue` commands
------------------------
command syntax::
?/queue resume
?/queue pause
?/queue [limit]
?/queue tail limit
User data we store
==================
Audio URLs, effects, user IDs
-----------------------------
Those are required for bot's functionality.
This data is stored only for the tracks that are currently in queue.
Audio contents
--------------
Persistent storage of audio is performed only for caching.
URLs aren't stored in a reversible form (only represented as hashes).
Tokens (for the web app)
------------------------
Revokable as any other Discord app token.

View File

@ -3,4 +3,4 @@ v6d0auth @ git+https://gitea.parrrate.ru/PTV/v6d0auth.git@c718d4d1422945a756213d
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@18001ff3403646db46f36175a824e571c5734fd6
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@0a0da256a3be72c76fbe6af4b941ff70881d3704 adaas @ git+https://gitea.parrrate.ru/PTV/adaas.git@0c7f974ec4955204b35f463749df138663c98550

12
setup.py Normal file
View File

@ -0,0 +1,12 @@
from setuptools import setup
setup(
name='v6d3music',
version='',
packages=['v6d3music', 'v6d3musicbase'],
url='',
license='',
author='PARRRATE T&V',
author_email='',
description=''
)

View File

@ -1,21 +1,21 @@
import asyncio import asyncio
import time import time
from typing import TypeAlias
import discord import discord
from typing_extensions import Self
from v6d3musicbase.responsetype import *
from v6d3musicbase.targets import *
from rainbowadn.instrument import Instrumentation
from v6d2ctx.context import * from v6d2ctx.context import *
from v6d3music.core.mainaudio import * from v6d3music.core.mainaudio import *
from v6d3music.core.mainservice import * from v6d3music.core.mainservice import *
ResponseType: TypeAlias = list | dict | float | str | None
__all__ = ('Api',) __all__ = ('Api',)
class Api: class Api:
class MisusedApi(KeyError): class MisusedApi(Exception):
def json(self) -> dict: def json(self) -> dict:
return {'error': list(map(str, self.args)), 'errormessage': str(self)} return {'error': list(map(str, self.args)), 'errormessage': str(self)}
@ -34,12 +34,40 @@ class Api:
self.mainservice = mainservice self.mainservice = mainservice
self.client = mainservice.client self.client = mainservice.client
self.roles = roles self.roles = roles
self.targets = mainservice.targets
self.targets.register_instance(self, 'api', Async)
self.targets.register_instrumentation('Count', lambda t, n: Count(t, n))
self.targets.register_instrumentation('Concurrency', lambda t, n: Concurrency(t, n), Async)
def user_id(self) -> int | None:
if self.client.user is None:
return None
else:
return self.client.user.id
def is_operator(self, user_id: int) -> bool: def is_operator(self, user_id: int) -> bool:
return '(operator)' in self.roles.get(f'roles{user_id}', '') return '(operator)' in self.roles.get(f'roles{user_id}', '')
async def api(self, request: dict, user_id: int) -> ResponseType: async def api(self, request: dict, user_id: int) -> ResponseType:
return await UserApi(self, request, user_id).api() response = await UserApi(ApiSession(self), request, user_id).api()
match response, request:
case {'time': _}, _:
pass
case dict() as d, {'time': _}:
response = d | {'time': time.time()}
return response
class ApiSession:
def __init__(self, api: Api) -> None:
self.__api = api
self.__complexity = 1000
def api(self):
if self.__complexity <= 0:
raise Api.MisusedApi('hit complexity limit')
self.__complexity -= 1
return self.__api
class UserApi: class UserApi:
@ -47,33 +75,52 @@ class UserApi:
def json(self) -> dict: def json(self) -> dict:
return super().json() | {'unknownmember': None} return super().json() | {'unknownmember': None}
def __init__(self, api: Api, request: dict, user_id: int) -> None: def __init__(self, session: ApiSession, request: dict, user_id: int) -> None:
self.pi = api self.session = session
self.client = api.client self.pi = session.api()
self.client = self.pi.client
self.request = request self.request = request
self.user_id = user_id self.user_id = user_id
self._parent: Self | None = None
self._key: int | str | None = None
async def subs(self, requests: list[dict] | dict[str, dict]) -> ResponseType: async def subs(self, requests: list[dict] | dict[str, dict]) -> ResponseType:
match self.request:
case {'idkey': str() as idkey}:
pass
case _:
idkey = 'type'
match self.request:
case {'idbase': dict() as base}:
pass
case _:
base = {}
match requests: match requests:
case list(): case list():
return list( return list(
await asyncio.gather( await asyncio.gather(
*(self.sub(request).api() for request in requests) *(self.sub(request, key).api() for (key, request) in enumerate(requests))
) )
) )
case dict(): case dict():
items = list(requests.items()) items = list(requests.items())
responses = await asyncio.gather( responses = await asyncio.gather(
*(self.sub(request if 'type' in request else request | {'type': key}).api() for key, request in items) *(self.sub({idkey: key} | base | request, key).api() for key, request in items)
) )
return dict((key, response) for (key, _), response in zip(items, responses)) return dict((key, response) for (key, _), response in zip(items, responses))
case _: case _:
raise Api.MisusedApi('that should not happen') raise Api.MisusedApi('that should not happen')
def sub(self, request: dict) -> 'UserApi': def _sub(self, request: dict) -> Self:
return UserApi(self.pi, request, self.user_id) return UserApi(self.session, request, self.user_id)
async def _guild_api(self, guild_id: int) -> 'GuildApi': def sub(self, request: dict, key: str | int) -> Self:
sub = self._sub(request)
sub._parent = self
sub._key = key
return sub
async def to_guild_api(self, guild_id: int) -> 'GuildApi':
guild = self.client.get_guild(guild_id) or await self.client.fetch_guild(guild_id) guild = self.client.get_guild(guild_id) or await self.client.fetch_guild(guild_id)
if guild is None: if guild is None:
raise UserApi.UnknownMember('unknown guild') raise UserApi.UnknownMember('unknown guild')
@ -82,19 +129,31 @@ class UserApi:
raise UserApi.UnknownMember('unknown member of a guild') raise UserApi.UnknownMember('unknown member of a guild')
return GuildApi(self, member) return GuildApi(self, member)
async def _operator_api(self) -> 'OperatorApi': async def to_operator_api(self) -> 'OperatorApi':
if not self.pi.is_operator(self.user_id): if not self.pi.is_operator(self.user_id):
raise UserApi.UnknownMember('not an operator') raise UserApi.UnknownMember('not an operator')
return OperatorApi(self.pi, self.request, self.user_id) return OperatorApi(self)
def _api_text(self) -> str:
return 'user api'
async def _fall_through_api(self) -> ResponseType:
match self.request:
case {'type': '?'}:
return f'this is {self._api_text()}'
case {'type': '*', 'requests': list() | dict() as requests}:
return await self.subs(requests)
case _:
raise Api.UnknownApi(f'unknown {self._api_text()}')
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'guild': str() as guild_id_str} if guild_id_str.isdecimal() and len(guild_id_str) < 100: case {'guild': str() as guild_id_str} if guild_id_str.isdecimal() and len(guild_id_str) < 100:
self.request.pop('guild') self.request.pop('guild')
return await (await self._guild_api(int(guild_id_str))).api() return await (await self.to_guild_api(int(guild_id_str))).api()
case {'operator': _}: case {'operator': _}:
self.request.pop('operator') self.request.pop('operator')
return await (await self._operator_api()).api() return await (await self.to_operator_api()).api()
case {'type': 'ping', 't': (float() | int()) as t}: case {'type': 'ping', 't': (float() | int()) as t}:
return time.time() - t return time.time() - t
case {'type': 'guilds'}: case {'type': 'guilds'}:
@ -103,14 +162,10 @@ class UserApi:
if guild.get_member(self.user_id) is not None: if guild.get_member(self.user_id) is not None:
guilds.append(str(guild.id)) guilds.append(str(guild.id))
return guilds return guilds
case {'type': '?'}:
return 'this is user api'
case {'type': '*', 'requests': list() | dict() as requests}:
return await self.subs(requests)
case _: case _:
raise Api.UnknownApi('unknown user api') return await self._fall_through_api()
async def api(self): async def api(self) -> ResponseType:
try: try:
try: try:
return await self._api() return await self._api()
@ -131,11 +186,11 @@ class GuildApi(UserApi):
return super().json() | {'notconnected': None} return super().json() | {'notconnected': None}
def __init__(self, api: UserApi, member: discord.Member) -> None: def __init__(self, api: UserApi, member: discord.Member) -> None:
super().__init__(api.pi, api.request, member.id) super().__init__(api.session, api.request, member.id)
self.member = member self.member = member
self.guild = member.guild self.guild = member.guild
async def voice_api(self) -> 'VoiceApi': async def to_voice_api(self) -> 'VoiceApi':
voice = self.member.voice voice = self.member.voice
if voice is None: if voice is None:
raise GuildApi.VoiceNotConnected('you are not connected to voice') raise GuildApi.VoiceNotConnected('you are not connected to voice')
@ -148,20 +203,19 @@ class GuildApi(UserApi):
raise GuildApi.VoiceNotConnected('bot not connected') raise GuildApi.VoiceNotConnected('bot not connected')
return VoiceApi(self, channel) return VoiceApi(self, channel)
def sub(self, request: dict) -> 'GuildApi': def _sub(self, request: dict) -> Self:
return GuildApi(super().sub(request), self.member) return GuildApi(super()._sub(request), self.member)
def _api_text(self) -> str:
return 'guild api'
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'voice': _}: case {'voice': _}:
self.request.pop('voice') self.request.pop('voice')
return await (await self.voice_api()).api() return await (await self.to_voice_api()).api()
case {'type': '?'}:
return 'this is guild api'
case {'type': '*', 'requests': list() | dict() as requests}:
return await self.subs(requests)
case _: case _:
raise Api.UnknownApi('unknown guild api') return await self._fall_through_api()
class VoiceApi(GuildApi): class VoiceApi(GuildApi):
@ -172,25 +226,24 @@ class VoiceApi(GuildApi):
self.channel = channel self.channel = channel
self.mainservice = self.pi.mainservice self.mainservice = self.pi.mainservice
async def _main_api(self) -> 'MainApi': async def to_main_api(self) -> 'MainApi':
vc = await self.mainservice.raw_vc_for_member(self.member) vc = await self.mainservice.raw_vc_for_member(self.member)
main = await self.mainservice.descriptor(create=False, force_play=False).main_for_raw_vc(vc) main = await self.mainservice.mode(create=False, force_play=False).main_for_raw_vc(vc)
return MainApi(self, vc, main) return MainApi(self, vc, main)
def sub(self, request: dict) -> 'VoiceApi': def _sub(self, request: dict) -> Self:
return VoiceApi(super().sub(request), self.channel) return VoiceApi(super()._sub(request), self.channel)
def _api_text(self) -> str:
return 'voice api'
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'main': _}: case {'main': _}:
self.request.pop('main') self.request.pop('main')
return await (await self._main_api()).api() return await (await self.to_main_api()).api()
case {'type': '?'}:
return 'this is voice api'
case {'type': '*', 'requests': list() | dict() as requests}:
return await self.subs(requests)
case _: case _:
raise Api.UnknownApi('unknown voice api') return await self._fall_through_api()
class MainApi(VoiceApi): class MainApi(VoiceApi):
@ -201,8 +254,11 @@ class MainApi(VoiceApi):
self.vc = vc self.vc = vc
self.main = main self.main = main
def sub(self, request: dict) -> 'MainApi': def _sub(self, request: dict) -> Self:
return MainApi(super().sub(request), self.vc, self.main) return MainApi(super()._sub(request), self.vc, self.main)
def _api_text(self) -> str:
return 'main api'
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
@ -216,23 +272,29 @@ class MainApi(VoiceApi):
return await self.main.queue.format() return await self.main.queue.format()
case {'type': 'queuejson'}: case {'type': 'queuejson'}:
return await self.main.queue.pubjson(self.member, self.request.get('limit', 1000)) return await self.main.queue.pubjson(self.member, self.request.get('limit', 1000))
case {'type': '?'}:
return 'this is main api'
case {'type': '*', 'requests': list() | dict() as requests}:
return await self.subs(requests)
case _: case _:
raise Api.UnknownApi('unknown main api') return await self._fall_through_api()
class OperatorApi(UserApi): class OperatorApi(UserApi):
def sub(self, request: dict) -> 'OperatorApi': def __init__(self, api: UserApi) -> None:
return OperatorApi(self.pi, request, self.user_id) super().__init__(api.session, api.request, api.user_id)
def _guild_visible(self, guild: discord.Guild) -> bool: def _guild_visible(self, guild: discord.Guild) -> bool:
return True return True
def _sub(self, request: dict) -> Self:
return OperatorApi(super()._sub(request))
def _api_text(self) -> str:
return 'operator api'
async def _api(self) -> ResponseType: async def _api(self) -> ResponseType:
match self.request: match self.request:
case {'target': str() as targetname}:
return await InstrumentationApi(self, targetname).api()
case {'type': 'resetmonitoring'}:
return self.pi.mainservice.pmonitoring.reset()
case {'type': 'guilds'}: case {'type': 'guilds'}:
guilds = [] guilds = []
for guild in self.client.guilds: for guild in self.client.guilds:
@ -245,9 +307,87 @@ class OperatorApi(UserApi):
} }
) )
return guilds return guilds
case {'type': '?'}: case {'type': 'sleep', 'duration': (float() | int()) as duration, 'echo': _ as echo}:
return 'this is operator api' await asyncio.sleep(duration)
case {'type': '*', 'requests': list() | dict() as requests}: return echo
return await self.subs(requests) case {'type': 'pool'}:
return self.pi.mainservice.pool_json()
case _: case _:
raise Api.UnknownApi('unknown operator api') return await self._fall_through_api()
class InstrumentationApi(OperatorApi):
class UnknownTarget(Api.UnknownApi):
def json(self) -> dict:
return super().json() | {'unknowntarget': None}
def __init__(self, api: OperatorApi, targetname: str) -> None:
super().__init__(api)
self.targets = self.pi.targets
self.targetname = targetname
target_tuple = self.targets.targets.get(targetname, None)
if target_tuple is None:
raise InstrumentationApi.UnknownTarget('unknown target', targetname)
self.target, self.methodname = target_tuple.value
def _sub(self, request: dict) -> Self:
return InstrumentationApi(super()._sub(request), self.targetname)
def _api_text(self) -> str:
return 'instrumentation api'
async def _api(self) -> ResponseType:
match self.request:
case {
'type': str() as instrumentationname
} if (
instrumentation_factory := self.targets.instrumentations.get(instrumentationname)
) is not None:
try:
instrumentation: Instrumentation = await self.pi.mainservice.pmonitoring.get(
self.targets.get_factory(
self.targetname,
self.target,
self.methodname,
instrumentationname,
instrumentation_factory.value,
)
)
except KeyError as e:
raise InstrumentationApi.UnknownTarget(
'binding failed', self.targetname, instrumentationname, str(e)
) from e
if not isinstance(instrumentation, JsonLike):
raise TypeError
return instrumentation.json()
case _:
return await self._fall_through_api()
class Count(Instrumentation, JsonLike):
def __init__(self, target, methodname: str):
super().__init__(target, methodname)
self.count = 0
def instrument(self, method, *args, **kwargs):
self.count += 1
return method(*args, **kwargs)
def json(self) -> ResponseType:
return self.count
class Concurrency(Instrumentation, JsonLike):
def __init__(self, target, methodname: str):
super().__init__(target, methodname)
self.concurrency = 0
async def instrument(self, method, *args, **kwargs):
self.concurrency += 1
try:
return await method(*args, **kwargs)
finally:
self.concurrency -= 1
def json(self) -> ResponseType:
return self.concurrency

View File

@ -1,13 +1,11 @@
import asyncio import asyncio
import functools import functools
import os
import urllib.parse import urllib.parse
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Coroutine, Generic, Hashable, TypeVar from typing import Any, Callable, Coroutine, Generic, Hashable, TypeVar
import aiohttp import aiohttp
import discord
from aiohttp import web from aiohttp import web
from ptvp35 import * from ptvp35 import *
@ -16,7 +14,6 @@ from v6d0auth.run_app import *
from v6d1tokens.client import * from v6d1tokens.client import *
from v6d3music.api import * from v6d3music.api import *
from v6d3music.config import auth_redirect, myroot from v6d3music.config import auth_redirect, myroot
from v6d3music.core.mainservice import *
from v6d3music.utils.bytes_hash import * from v6d3music.utils.bytes_hash import *
__all__ = ('AppContext',) __all__ = ('AppContext',)
@ -64,14 +61,12 @@ class MusicAppFactory(AppFactory):
def __init__( def __init__(
self, self,
secret: str, secret: str,
client: discord.Client,
api: Api, api: Api,
db: DbConnection db: DbConnection
): ):
self.secret = secret self.secret = secret
self.redirect = auth_redirect self.redirect = auth_redirect
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.client = client
self._api = api self._api = api
self.db = db self.db = db
self._token_clients: CachedDictionary[str, dict | None] = CachedDictionary( self._token_clients: CachedDictionary[str, dict | None] = CachedDictionary(
@ -79,10 +74,11 @@ class MusicAppFactory(AppFactory):
) )
def auth_link(self) -> str: def auth_link(self) -> str:
if self.client.user is None: client_id = self._api.user_id()
if client_id is None:
return '' return ''
else: else:
return f'https://discord.com/api/oauth2/authorize?client_id={self.client.user.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): def _path(self, file: str):
@ -93,9 +89,10 @@ class MusicAppFactory(AppFactory):
return f.read() return f.read()
async def code_token(self, code: str) -> dict: async def code_token(self, code: str) -> dict:
assert self.client.user is not None client_id = self._api.user_id()
assert client_id is not None
data = { data = {
'client_id': str(self.client.user.id), 'client_id': str(client_id),
'client_secret': self.secret, 'client_secret': self.secret,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': code, 'code': code,
@ -205,6 +202,10 @@ class MusicAppFactory(AppFactory):
@routes.get('/') @routes.get('/')
async def home(_request: web.Request) -> web.StreamResponse: async def home(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('home.html')) 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/') @routes.get('/login/')
async def login(_request: web.Request) -> web.StreamResponse: async def login(_request: web.Request) -> web.StreamResponse:
@ -256,10 +257,18 @@ class MusicAppFactory(AppFactory):
async def mainjs(_request: web.Request) -> web.StreamResponse: async def mainjs(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('main.js')) 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') @routes.get('/main.css')
async def maincss(_request: web.Request) -> web.StreamResponse: async def maincss(_request: web.Request) -> web.StreamResponse:
return web.FileResponse(self._path('main.css')) 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')
@ -289,18 +298,14 @@ class MusicAppFactory(AppFactory):
class AppContext: class AppContext:
def __init__(self, mainservice: MainService) -> None: def __init__(self, api: Api) -> None:
self.mainservice = mainservice self.api = api
async def start(self) -> tuple[web.Application, asyncio.Task[None]] | None: async def start(self) -> tuple[web.Application, asyncio.Task[None]] | None:
try: try:
factory = MusicAppFactory( factory = MusicAppFactory(
await request_token('music-client', 'token'), await request_token('music-client', 'token'),
self.mainservice.client, self.api,
Api(
self.mainservice,
{key: value for key, value in os.environ.items() if key.startswith('roles')},
),
self.__db self.__db
) )
except aiohttp.ClientConnectorError: except aiohttp.ClientConnectorError:
@ -313,10 +318,10 @@ class AppContext:
async def __aenter__(self) -> 'AppContext': async def __aenter__(self) -> 'AppContext':
async with AsyncExitStack() as es: async with AsyncExitStack() as es:
self.__db = await es.enter_async_context(DbFactory(myroot / 'session.db', kvfactory=KVJson())) self.__db = await es.enter_async_context(DbFactory(myroot / 'session.db', kvfactory=KVJson()))
self.__es = es.pop_all()
self.__task: asyncio.Task[ self.__task: asyncio.Task[
tuple[web.Application, asyncio.Task[None]] | None tuple[web.Application, asyncio.Task[None]] | None
] = asyncio.create_task(self.start()) ] = asyncio.create_task(self.start())
self.__es = es.pop_all()
return self return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):

View File

@ -32,11 +32,11 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
await catch( await catch(
ctx, args, ctx, args,
f''' f'''
`play ...args` `play ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args` `play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args`
`pause` `pause`
`resume` `resume`
presets: {shlex.join(allowed_presets)} presets: {shlex.join(allowed_presets)}
''', ''',
(), 'help' (), 'help'
) )
@ -54,8 +54,8 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def skip(ctx: Context, args: list[str]) -> None: async def skip(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`skip [first] [last]` `skip [first] [last]`
''', 'help' ''', 'help'
) )
assert ctx.member is not None assert ctx.member is not None
match args: match args:
@ -69,7 +69,7 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
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 i in range(pos0, pos1 + 1): for _ in range(pos0, pos1 + 1):
if not queue.skip_at(pos0, ctx.member): if not queue.skip_at(pos0, ctx.member):
pos0 += 1 pos0 += 1
case _: case _:
@ -80,28 +80,36 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def skip_to(ctx: Context, args: list[str]) -> None: async def skip_to(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`to [[h]] [m] s` `to [[h]] [m] s`
''', 'help' ''', 'help'
) )
match args: match args:
case [h, m, s] 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)
case [m, s] if m.isdecimal() and s.isdecimal(): case [m, s, *args] if m.isdecimal() and s.isdecimal():
seconds = 60 * int(m) + int(s) seconds = 60 * int(m) + int(s)
case [s] if s.isdecimal(): case [s, *args] if s.isdecimal():
seconds = int(s) seconds = int(s)
case _: case _:
raise Explicit('misformatted') raise Explicit('misformatted, expected time')
match args:
case ['at', spos] if spos.isdecimal():
pos = int(spos)
case []:
pos = 0
case _:
raise Explicit('misformatted, expected position')
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()
queue.queue[0].set_seconds(seconds) queue.queue[pos].set_seconds(seconds)
@at('effects') @at('effects')
async def effects_(ctx: Context, args: list[str]) -> None: async def effects_(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`effects - effects` `effects - effects`
`effects + preset` `effects + preset`
''', 'help' ''', 'help'
) )
match args: match args:
case ['-', effects]: case ['-', effects]:
@ -121,9 +129,9 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def default(ctx: Context, args: list[str]) -> None: async def default(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`default - effects` `default - effects`
`default + preset` `default + preset`
`default none` `default none`
''', 'help' ''', 'help'
) )
assert ctx.guild is not None assert ctx.guild is not None
@ -214,49 +222,53 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def queue_(ctx: Context, args: list[str]) -> None: async def queue_(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`queue` `queue`
`queue clear` `queue clear`
`queue resume` `queue resume`
`queue pause` `queue pause`
''', 'help' ''', 'help'
) )
assert ctx.member is not None assert ctx.member is not None
limit = 100
match args:
case [lstr, *args] if lstr.isdecimal():
limit = int(lstr)
case [*args]:
pass
match args: match args:
case []: case []:
await ctx.long( limit = 24
( case [lstr] if lstr.isdecimal():
await ( limit = int(lstr)
await mainservice.context(ctx, create=True, force_play=False).queue() case ['tail', lstr] if lstr.isdecimal():
).format(limit) limit = -int(lstr)
).strip() or 'no queue' if limit >= 0:
) raise Explicit('limit of at least `1` required')
case ['clear']: case ['clear']:
(await mainservice.context(ctx, create=False, force_play=False).queue()).clear(ctx.member) (await mainservice.context(ctx, create=False, force_play=False).queue()).clear(ctx.member)
await ctx.reply('done') await ctx.reply('done')
return
case ['resume']: case ['resume']:
async with mainservice.lock_for(ctx.guild): async with mainservice.lock_for(ctx.guild):
await mainservice.context(ctx, create=True, force_play=True).vc() await mainservice.context(ctx, create=True, force_play=True).vc()
await ctx.reply('done') await ctx.reply('done')
return
case ['pause']: case ['pause']:
async with mainservice.lock_for(ctx.guild): async with mainservice.lock_for(ctx.guild):
vc = await mainservice.context(ctx, create=True, force_play=False).vc() vc = await mainservice.context(ctx, create=True, force_play=False).vc()
vc.pause() vc.pause()
await ctx.reply('done') await ctx.reply('done')
return
case _: case _:
raise Explicit('misformatted') raise Explicit('misformatted')
await ctx.long(
(
await (
await mainservice.context(ctx, create=True, force_play=False).queue()
).format(limit)
).strip() or 'no queue'
)
@at('swap') @at('swap')
async def swap(ctx: Context, args: list[str]) -> None: async def swap(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`swap a b` `swap a b`
''', 'help' ''', 'help'
) )
assert ctx.member is not None assert ctx.member is not None
match args: match args:
@ -270,8 +282,8 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def move(ctx: Context, args: list[str]) -> None: async def move(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`move a b` `move a b`
''', 'help' ''', 'help'
) )
assert ctx.member is not None assert ctx.member is not None
match args: match args:
@ -285,14 +297,17 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
async def volume_(ctx: Context, args: list[str]) -> None: async def volume_(ctx: Context, args: list[str]) -> None:
await catch( await catch(
ctx, args, ''' ctx, args, '''
`volume volume` `volume volume`
''', 'help' ''', 'help'
) )
assert ctx.member is not None assert ctx.member is not None
match args: match args:
case [volume]: case [svolume]:
volume = float(volume) volume = float(svolume)
await (await mainservice.context(ctx, create=True, force_play=False).main()).set(volume, ctx.member) await (await mainservice.context(ctx, create=True, force_play=False).main()).set(volume, ctx.member)
case []:
volume = (await mainservice.context(ctx, create=True, force_play=False).main()).get()
await ctx.reply(f'volume is {volume}')
case _: case _:
raise Explicit('misformatted') raise Explicit('misformatted')

View File

@ -10,10 +10,10 @@ __all__ = ('MainAudio',)
class MainAudio(discord.PCMVolumeTransformer): class MainAudio(discord.PCMVolumeTransformer):
def __init__(self, db: DbConnection, queue: QueueAudio, volume: float): def __init__(self, db: DbConnection, queue: QueueAudio):
self.db = db self.db = db
self.queue = queue self.queue = queue
super().__init__(self.queue, volume=volume) super().__init__(self.queue, volume=self.get())
async def set(self, volume: float, member: discord.Member): async def set(self, volume: float, member: discord.Member):
assert_admin(member) assert_admin(member)
@ -24,6 +24,9 @@ class MainAudio(discord.PCMVolumeTransformer):
self.volume = volume self.volume = volume
await self.db.set(member.guild.id, volume) await self.db.set(member.guild.id, volume)
def get(self) -> float:
return self.db.get(self.queue.guild.id, 0.2)
@classmethod @classmethod
async def create(cls, servicing: YTAServicing, db: DbConnection, queues: DbConnection, guild: discord.Guild) -> 'MainAudio': async def create(cls, servicing: YTAServicing, db: DbConnection, queues: DbConnection, guild: discord.Guild) -> 'MainAudio':
return cls(db, await QueueAudio.create(servicing, queues, guild), volume=db.get(guild.id, 0.2)) return cls(db, await QueueAudio.create(servicing, queues, guild))

View File

@ -4,10 +4,19 @@ from contextlib import AsyncExitStack
from typing import AsyncIterable, TypeVar from typing import AsyncIterable, TypeVar
import discord import discord
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
from v6d3musicbase.targets import *
import v6d3music.processing.pool
from ptvp35 import *
from v6d2ctx.context import *
from v6d2ctx.lock_for import *
from v6d3music.config import myroot from v6d3music.config import myroot
from v6d3music.core.caching import * from v6d3music.core.caching import *
from v6d3music.core.default_effects import * from v6d3music.core.default_effects import *
from v6d3music.core.mainaudio import * from v6d3music.core.mainaudio import *
from v6d3music.core.monitoring import *
from v6d3music.core.queueaudio import * from v6d3music.core.queueaudio import *
from v6d3music.core.ystate import * from v6d3music.core.ystate import *
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import *
@ -15,22 +24,50 @@ from v6d3music.core.ytaudio import *
from v6d3music.processing.pool import * from v6d3music.processing.pool import *
from v6d3music.utils.argctx import * from v6d3music.utils.argctx import *
from ptvp35 import * __all__ = ('MainService', 'MainMode', 'MainContext', 'MainEvent')
from v6d2ctx.context import *
from v6d2ctx.lock_for import *
__all__ = ('MainService', 'MainDescriptor', 'MainContext')
T = TypeVar('T') T = TypeVar('T')
class MainEvent(Event):
pass
class _PMEvent(MainEvent):
def __init__(self, event: PoolEvent, /) -> None:
self.event = event
def json(self) -> ResponseType:
return {'pool': self.event.json()}
class _PMSendable(SendableEvents[PoolEvent]):
def __init__(self, sendable: SendableEvents[MainEvent], /) -> None:
self.sendable = sendable
def send(self, event: PoolEvent, /) -> None:
return self.sendable.send(_PMEvent(event))
class MainService: class MainService:
def __init__(self, defaulteffects: DefaultEffects, client: discord.Client) -> None: def __init__(
self,
targets: Targets,
defaulteffects: DefaultEffects,
client: discord.Client,
events: SendableEvents[MainEvent],
) -> None:
self.targets = targets
self.defaulteffects = defaulteffects self.defaulteffects = defaulteffects
self.client = client self.client = client
self.mains: dict[discord.Guild, MainAudio] = {} self.mains: dict[discord.Guild, MainAudio] = {}
self.restore_lock = asyncio.Lock() self.restore_lock = asyncio.Lock()
self.__events: SendableEvents[MainEvent] = events
self.__pool_events: SendableEvents[PoolEvent] = _PMSendable(self.__events)
def register_instrumentation(self):
self.targets.register_type(v6d3music.processing.pool.UnitJob, 'run', Async)
@staticmethod @staticmethod
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient: async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
@ -58,11 +95,11 @@ class MainService:
raise Explicit('not in a guild') raise Explicit('not in a guild')
return await self.raw_vc_for_member(ctx.member) return await self.raw_vc_for_member(ctx.member)
def descriptor(self, *, create: bool, force_play: bool) -> 'MainDescriptor': def mode(self, *, create: bool, force_play: bool) -> 'MainMode':
return MainDescriptor(self, create=create, force_play=force_play) return MainMode(self, create=create, force_play=force_play)
def context(self, ctx: Context, *, create: bool, force_play: bool) -> 'MainContext': def context(self, ctx: Context, *, create: bool, force_play: bool) -> 'MainContext':
return self.descriptor(create=create, force_play=force_play).context(ctx) return self.mode(create=create, force_play=force_play).context(ctx)
async def create(self, guild: discord.Guild) -> MainAudio: async def create(self, guild: discord.Guild) -> MainAudio:
return await MainAudio.create(self.__servicing, self.__volumes, self.__queues, guild) return await MainAudio.create(self.__servicing, self.__volumes, self.__queues, guild)
@ -73,10 +110,13 @@ class MainService:
self.__volumes = await es.enter_async_context(DbFactory(myroot / 'volume.db', kvfactory=KVJson())) self.__volumes = await es.enter_async_context(DbFactory(myroot / 'volume.db', kvfactory=KVJson()))
self.__queues = await es.enter_async_context(DbFactory(myroot / 'queue.db', kvfactory=KVJson())) self.__queues = await es.enter_async_context(DbFactory(myroot / 'queue.db', kvfactory=KVJson()))
self.__caching = await es.enter_async_context(Caching()) self.__caching = await es.enter_async_context(Caching())
self.__pool = await es.enter_async_context(Pool(5)) self.__pool = await es.enter_async_context(Pool(5, self.__pool_events))
self.__servicing = YTAServicing(self.__caching, self.__pool) self.__servicing = YTAServicing(self.__caching, self.__pool)
self.__vcs_restored: asyncio.Future[None] = asyncio.Future() self.__vcs_restored: asyncio.Future[None] = asyncio.Future()
self.__save_task = asyncio.create_task(self.save_daemon()) self.__save_task = asyncio.create_task(self.save_daemon())
self.monitoring = await es.enter_async_context(Monitoring())
self.pmonitoring = es.enter_context(PersistentMonitoring(self.monitoring))
self.register_instrumentation()
self.__es = es.pop_all() self.__es = es.pop_all()
return self return self
@ -153,7 +193,7 @@ class MainService:
vp: discord.VoiceProtocol = await channel.connect() vp: discord.VoiceProtocol = await channel.connect()
assert isinstance(vp, discord.VoiceClient) assert isinstance(vp, discord.VoiceClient)
vc = vp vc = vp
await self.descriptor(create=True, force_play=True).main_for_raw_vc(vc) await self.mode(create=True, force_play=True).main_for_raw_vc(vc)
if vc_is_paused: if vc_is_paused:
vc.pause() vc.pause()
@ -193,8 +233,11 @@ class MainService:
async for audio in YState(self.__servicing, self.__pool, ctx, argctx.sources).iterate(): async for audio in YState(self.__servicing, self.__pool, ctx, argctx.sources).iterate():
yield audio yield audio
def pool_json(self) -> ResponseType:
return self.__pool.json()
class MainDescriptor:
class MainMode:
def __init__(self, service: MainService, *, create: bool, force_play: bool) -> None: def __init__(self, service: MainService, *, create: bool, force_play: bool) -> None:
self.mainservice = service self.mainservice = service
self.mains = service.mains self.mains = service.mains
@ -220,14 +263,14 @@ class MainDescriptor:
class MainContext: class MainContext:
def __init__(self, descriptor: MainDescriptor, ctx: Context) -> None: def __init__(self, mode: MainMode, ctx: Context) -> None:
self.mainservice = descriptor.mainservice self.mainservice = mode.mainservice
self.descriptor = descriptor self.mode = mode
self.ctx = ctx self.ctx = ctx
async def vc_main(self) -> tuple[discord.VoiceClient, MainAudio]: async def vc_main(self) -> tuple[discord.VoiceClient, MainAudio]:
vc = await self.mainservice.raw_vc_for(self.ctx) vc = await self.mainservice.raw_vc_for(self.ctx)
return vc, await self.descriptor.main_for_raw_vc(vc) return vc, await self.mode.main_for_raw_vc(vc)
async def vc(self) -> discord.VoiceClient: async def vc(self) -> discord.VoiceClient:
vc, _ = await self.vc_main() vc, _ = await self.vc_main()

View File

@ -0,0 +1,117 @@
__all__ = ('Monitoring', 'PersistentMonitoring')
import asyncio
from contextlib import AsyncExitStack, ExitStack
from typing import Any, Callable, Generic, TypeVar
from rainbowadn.instrument import Instrumentation
T = TypeVar('T', bound=Instrumentation, covariant=True)
class Provider(Generic[T]):
def __init__(self, provider: Callable[[], T], /) -> None:
self.provider = provider
self.__count = 0
self.__empty = asyncio.Event()
self.__empty.set()
self.__closed = False
def __enter__(self) -> T:
if self.__closed:
raise RuntimeError('the provider is closed')
if self.__count < 0:
raise RuntimeError
if self.__count == 0:
self.__instrumentation = self.provider().__enter__()
self.__empty.clear()
self.__count += 1
return self.__instrumentation
def __exit__(self, exc_type, exc_val, exc_tb):
if self.__count <= 0:
raise RuntimeError
self.__count -= 1
if self.__count == 0:
self.__empty.set()
try:
self.__instrumentation.__exit__(exc_type, exc_val, exc_tb)
except:
self.__closed = True
raise
finally:
del self.__instrumentation
async def stop(self) -> None:
while self.__count:
await self.__empty.wait()
self.__closed = True
class ProviderManager(Generic[T]):
def __init__(self, provider: Callable[[], T], /) -> None:
self.provider = Provider(provider)
async def __aenter__(self) -> Provider:
return self.provider
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.provider.stop()
class Monitoring:
async def get(self, provider: Callable[[], T]) -> Provider[T]:
if provider not in self.__providers:
self.__providers[provider] = asyncio.create_task(
self.__es.enter_async_context(ProviderManager(provider))
)
return await self.__providers[provider]
async def __aenter__(self) -> 'Monitoring':
async with AsyncExitStack() as es:
self.__providers: dict[
Callable[[], Instrumentation],
asyncio.Future[Provider]
] = {}
self.__es = es.pop_all()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es:
del self.__es
class PersistentMonitoring:
def __init__(self, monitoring: Monitoring) -> None:
self.__monitoring = monitoring
async def _get(self, provider: Callable[[], T]) -> T:
return self.__es.enter_context(await self.__monitoring.get(provider))
async def get(self, provider: Callable[[], T]) -> T:
if provider not in self.__instrumentations:
self.__instrumentations[provider] = asyncio.create_task(
self._get(provider)
)
return await self.__instrumentations[provider]
def __enter__(self) -> 'PersistentMonitoring':
self.__instrumentations: dict[
Callable[[], Instrumentation],
asyncio.Future
] = {}
self.__es = ExitStack()
return self
def reset(self) -> int:
with self.__es:
self.__es = ExitStack()
length = len(self.__instrumentations)
self.__instrumentations.clear()
return length
raise RuntimeError
def __exit__(self, exc_type, exc_val, exc_tb):
with self.__es:
del self.__instrumentations
del self.__es

View File

@ -130,22 +130,34 @@ class QueueAudio(discord.AudioSource):
self.update_sources() self.update_sources()
async def format(self, limit=100) -> str: async def format(self, limit=100) -> str:
if limit > 100: if limit > 100 or limit < -100:
raise Explicit('queue limit is too large') raise Explicit('queue limit is too large')
stream = StringIO() stream = StringIO()
for i, audio in enumerate(lst := list(self.queue)): lst = list(self.queue)
if i >= limit: llst = len(lst)
stream.write(f'cutting queue at {limit} results, {len(lst) - limit} remaining.\n')
break def write():
stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n') stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n')
if limit >= 0:
for i, audio in enumerate(lst):
if i >= limit:
stream.write(f'cutting queue at {limit} results, {llst - limit} remaining.\n')
break
write()
else:
for i_, audio in enumerate(lst[limit:]):
i = llst + limit + i_
write()
return stream.getvalue() return stream.getvalue()
def cleanup(self): def cleanup(self):
for audio in self.queue: pass
try: # for audio in self.queue:
audio.cleanup() # try:
except ValueError: # audio.cleanup()
pass # except ValueError:
# pass
async def pubjson(self, member: discord.Member, limit: int) -> list: async def pubjson(self, member: discord.Member, limit: int) -> list:
import random import random

View File

@ -32,7 +32,7 @@ async def _resolve_url(url: str, tor: bool) -> str:
async def real_url(caching: Caching, url: str, override: bool, tor: bool) -> str: async def real_url(caching: Caching, url: str, override: bool, tor: bool) -> str:
if adaas_available and not tor: if adaas_available and not tor:
return await RemoteCache().real_url(url, override, tor) return await RemoteCache().real_url(url, override, tor, True)
hurl: str = bytes_hash(url.encode()) hurl: str = bytes_hash(url.encode())
if not override: if not override:
curl: str | None = caching.get(hurl) curl: str | None = caching.get(hurl)

View File

@ -3,14 +3,15 @@ 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.context import *
from v6d3music.core.create_ytaudio import * from v6d3music.core.create_ytaudio import *
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import *
from v6d3music.core.ytaudio import * from v6d3music.core.ytaudio import *
from v6d3music.processing.pool import * from v6d3music.processing.pool import *
from v6d3music.utils.argctx import * from v6d3music.utils.argctx import *
from v6d2ctx.context import *
__all__ = ('YState',) __all__ = ('YState',)
@ -38,7 +39,7 @@ class YState:
async def _start_workers(self) -> None: async def _start_workers(self) -> None:
for _ in range(self.pool.workers()): for _ in range(self.pool.workers()):
await self.es.enter_async_context(YJD(self).at(self.pool)) await self.es.enter_async_context(YStream(self).at(self.pool))
async def _next_audio(self) -> YTAudio | None | _Stop: async def _next_audio(self) -> YTAudio | None | _Stop:
future = await self.results.get() future = await self.results.get()
@ -82,9 +83,11 @@ class YState:
return None return None
class YJD(JobDescriptor): class YStream(JobUnit):
def __init__(self, state: YState) -> None: def __init__(self, state: YState) -> None:
self.state = state self.state = state
self.__running = False
self.__details: dict[str, ResponseType] = {'status': 'stopped'}
def _unpack_playlists(self) -> None: def _unpack_playlists(self) -> None:
while self.state.playlists and self.state.playlists[0].done(): while self.state.playlists and self.state.playlists[0].done():
@ -97,8 +100,9 @@ class YJD(JobDescriptor):
for entry in playlist.result(): for entry in playlist.result():
self.state.entries.append(entry) self.state.entries.append(entry)
async def run(self) -> JobDescriptor | None: async def _run(self, context: JobContext, /) -> JobUnit | None:
if self.state.empty_processing(): if self.state.empty_processing():
self.__details = {'status': 'stopping'}
if self.state.results.empty(): if self.state.results.empty():
self.state.results.put_nowait(_Stop()) self.state.results.put_nowait(_Stop())
return None return None
@ -106,9 +110,21 @@ class YJD(JobDescriptor):
entry = self.state.entries.popleft() entry = self.state.entries.popleft()
audiotask: asyncio.Future[YTAudio | None] audiotask: asyncio.Future[YTAudio | None]
if isinstance(entry, BaseException): if isinstance(entry, BaseException):
self._set_details(context, {'status': 'breaking downstream audio creation'})
audiotask = asyncio.Future() audiotask = asyncio.Future()
audiotask.set_exception(entry) audiotask.set_exception(entry)
else: else:
self._set_details(
context,
{
'status': 'creating audio',
'info': cast_to_response(entry.info),
'effects': entry.effects,
'already_read': entry.already_read,
'tor': entry.tor,
'ignore': entry.ignore,
}
)
audiotask = asyncio.create_task(self.state.result(entry)) audiotask = asyncio.create_task(self.state.result(entry))
self.state.results.put_nowait(audiotask) self.state.results.put_nowait(audiotask)
try: try:
@ -117,9 +133,21 @@ class YJD(JobDescriptor):
self.state.entries.clear() self.state.entries.clear()
self.state.playlists.clear() self.state.playlists.clear()
self.state.sources.clear() self.state.sources.clear()
self._set_details(context, {'status': 'rescheduling self from entries'})
return self return self
elif self.state.sources: elif self.state.sources:
source = self.state.sources.popleft() source = self.state.sources.popleft()
self._set_details(
context,
{
'status': 'parsing playlist',
'url': source.url,
'effects': source.effects,
'already_read': source.already_read,
'tor': source.tor,
'ignore': source.ignore,
}
)
playlisttask = asyncio.create_task(self.state.playlist(source)) playlisttask = asyncio.create_task(self.state.playlist(source))
self.state.playlists.append(playlisttask) self.state.playlists.append(playlisttask)
try: try:
@ -131,9 +159,27 @@ class YJD(JobDescriptor):
self._unpack_playlists() self._unpack_playlists()
rescheduled = self.state.descheduled rescheduled = self.state.descheduled
self.state.descheduled = 0 self.state.descheduled = 0
self._set_details(context, {'status': 'rescheduling others', 'rescheduling': rescheduled})
for _ in range(rescheduled): for _ in range(rescheduled):
await self.state.es.enter_async_context(YJD(self.state).at(self.state.pool)) await self.state.es.enter_async_context(YStream(self.state).at(self.state.pool))
self._set_details(context, {'status': 'rescheduling self from sources'})
return self return self
else: else:
self._set_details(context, {'status': 'descheduling'})
self.state.descheduled += 1 self.state.descheduled += 1
return None return None
def _set_details(self, context: JobContext, details: dict[str, ResponseType], /) -> None:
self.__details = details
context.events.send(JobStatusChanged(self))
async def run(self, context: JobContext, /) -> JobUnit | None:
try:
self.__running = True
return await self._run(context)
finally:
self.__running = False
self.__details = {'status': 'stopped'}
def json(self) -> ResponseType:
return {'type': 'ystream', 'details': self.__details, 'running': self.__running}

View File

@ -8,6 +8,7 @@ from v6d2ctx.context import *
from v6d3music.core.ffmpegnormalaudio import * from v6d3music.core.ffmpegnormalaudio import *
from v6d3music.core.real_url import * from v6d3music.core.real_url import *
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import *
from v6d3music.processing.abstractrunner import *
from v6d3music.utils.fill import * from v6d3music.utils.fill import *
from v6d3music.utils.sparq import * from v6d3music.utils.sparq import *
from v6d3music.utils.tor_prefix import * from v6d3music.utils.tor_prefix import *
@ -43,6 +44,7 @@ class YTAudio(discord.AudioSource):
self.regenerating = False self.regenerating = False
# self.set_source() # self.set_source()
self._durations: dict[str, str] = {} self._durations: dict[str, str] = {}
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
@ -84,7 +86,7 @@ class YTAudio(discord.AudioSource):
def schedule_duration_update(self): def schedule_duration_update(self):
self.loop.call_soon_threadsafe(self._schedule_duration_update) self.loop.call_soon_threadsafe(self._schedule_duration_update)
async def _update_duration(self): async def _do_update_duration(self):
url: str = self.url url: str = self.url
if url in self._durations: if url in self._durations:
return return
@ -108,6 +110,14 @@ class YTAudio(discord.AudioSource):
assert ap.stdout is not None assert ap.stdout is not None
self._durations[url] = (await ap.stdout.read()).decode().strip().split('.')[0] self._durations[url] = (await ap.stdout.read()).decode().strip().split('.')[0]
async def _update_duration(self):
async with self._duration_lock:
await self._do_update_duration()
async def _update_duration_context(self, context: CoroContext):
context.events.send(CoroStatusChanged({'ytaudio': 'duration'}))
await self._update_duration()
async def update_duration(self): async def update_duration(self):
await self.servicing.runner.run(self._update_duration()) await self.servicing.runner.run(self._update_duration())
@ -121,7 +131,7 @@ class YTAudio(discord.AudioSource):
before_options = '' before_options = ''
if 'https' in self.url: if 'https' in self.url:
before_options += ( before_options += (
'-reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 60 -rw_timeout 5000000 -copy_unknown' '-reconnect 1 -reconnect_at_eof 0 -reconnect_streamed 1 -reconnect_delay_max 60 -copy_unknown'
) )
if self.already_read: if self.already_read:
before_options += ( before_options += (

View File

@ -44,3 +44,7 @@ body,
width: 100%; width: 100%;
background: #050505; background: #050505;
} }
#homeroot {
padding: 1em;
}

View File

@ -191,17 +191,14 @@ const aQueueWidget = async () => {
return el; return el;
}; };
const pageHome = async () => { const pageHome = async () => {
return baseEl( const el = document.createElement("div");
"div", el.append(
baseEl("div", aLogin()), baseEl("div", aLogin()),
baseEl("div", await userAvatarImg()), baseEl("div", await userAvatarImg()),
baseEl("div", await userId()), baseEl("div", await userId()),
baseEl("div", await userUsername()), baseEl("div", await userUsername()),
baseEl("div", await aQueueWidget()) baseEl("div", await aQueueWidget())
); );
el.id = "homeroot";
return el;
}; };
aApi({
type: "guilds",
operator: null,
catches: { "not an operator": null, "*": null },
}).then(console.log);

View File

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

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/main.css" />
<link rel="stylesheet" href="/operator.css" />
</head>
<body>
<div id="root-container">
<div class="sidebars"></div>
<div id="root"><div id="operatorroot"></div></div>
<div class="sidebars"></div>
</div>
<script src="/main.js"></script>
<script>
(async () => {
root.append(await pageHome());
})();
</script>
<script src="/operator.js"></script>
<script>
(async () => {
operatorroot.append(await pageOperator());
})();
</script>
</body>

View File

@ -0,0 +1,69 @@
aApi({
type: "guilds",
operator: null,
catches: { "not an operator": null, "*": null },
}).then(console.log);
aApi({
type: "sleep",
operator: null,
duration: 1,
echo: {},
time: null,
catches: { "not an operator": null, "*": null },
}).then(console.log);
aApi({
type: "*",
idkey: "target",
idbase: {
type: "*",
requests: {
Count: {},
Concurrency: {},
},
},
operator: null,
requests: {
"v6d3music.api.Api().api": {},
"v6d3music.processing.pool.UnitJob.run": {},
},
catches: { "not an operator": null, "*": null },
time: null,
}).then((value) => console.log(JSON.stringify(value, undefined, 2)));
aApi({
type: "pool",
operator: null,
catches: { "not an operator": null, "*": null },
}).then((value) => console.log(JSON.stringify(value, undefined, 2)));
const elJob = (job) => {
const jobview = document.createElement("div");
jobview.classList.add("jobview");
jobview.innerText = JSON.stringify(job);
return jobview;
};
const elWorker = (worker) => {
const workerview = document.createElement("div");
workerview.classList.add("workerview");
workerview.append(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;
};

176
v6d3music/main.py Normal file
View File

@ -0,0 +1,176 @@
import asyncio
import contextlib
import os
import sys
import time
from traceback import print_exc
import discord
from v6d3musicbase.event import *
from v6d3musicbase.targets import *
from ptvp35 import *
from rainbowadn.instrument import Instrumentation
from v6d1tokens.client import *
from v6d2ctx.handle_content import *
from v6d2ctx.pain import *
from v6d2ctx.serve import *
from v6d3music.api import *
from v6d3music.app import *
from v6d3music.commands import *
from v6d3music.config import prefix
from v6d3music.core.caching import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import *
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
class MusicClient(discord.Client):
pass
_client = MusicClient(
intents=discord.Intents(
members=True,
guilds=True,
bans=True,
emojis=True,
invites=True,
voice_states=True,
guild_messages=True,
reactions=True,
message_content=True,
),
loop=loop,
)
banned_guilds = set(map(int, filter(bool, map(str.strip, os.getenv('banned_guilds', '').split(':')))))
def guild_allowed(guild: discord.Guild | None) -> bool:
return guild is not None and guild.id not in banned_guilds
def message_allowed(message: discord.Message) -> bool:
return guild_allowed(message.guild)
def register_handlers(client: discord.Client, mainservice: MainService):
of = get_of(mainservice)
@client.event
async def on_message(message: discord.Message) -> None:
if message_allowed(message):
try:
await handle_content(of, message, message.content, prefix, client)
except:
print_exc()
@client.event
async def on_ready():
print('ready')
await client.change_presence(
activity=discord.Game(
name='феноменально',
)
)
await mainservice.restore()
class UpgradeABMInit(Instrumentation):
def __init__(self):
super().__init__(ABlockMonitor, '__init__')
def instrument(self, method, abm, *, threshold=0.0, delta=10.0, interval=0.0):
print('created upgraded')
method(abm, threshold=threshold, delta=delta, interval=interval)
abm.threshold = threshold
class UpgradeABMTask(Instrumentation):
def __init__(self):
super().__init__(ABlockMonitor, '_monitor')
async def instrument(self, _, abm):
print('started upgraded')
while True:
delta = abm.delta
t = time.time()
await asyncio.sleep(delta)
spent = time.time() - t
delay = spent - delta
if delay > abm.threshold:
abm.threshold = delay
print(
f'upgraded block monitor reached new peak delay {delay:.4f}')
interval = abm.interval
if interval > 0:
await asyncio.sleep(interval)
def _upgrade_abm() -> contextlib.ExitStack:
with contextlib.ExitStack() as es:
es.enter_context(UpgradeABMInit())
es.enter_context(UpgradeABMTask())
return es.pop_all()
raise RuntimeError
class PathPrint(Instrumentation):
def __init__(self, methodname: str, pref: str):
super().__init__(DbConnection, methodname)
self.pref = pref
async def instrument(self, method, db: DbConnection, *args, **kwargs):
result = await method(db, *args, **kwargs)
try:
print(self.pref, db._DbConnection__path) # type: ignore
except Exception:
from traceback import print_exc
print_exc()
return result
def _db_ee() -> contextlib.ExitStack:
with contextlib.ExitStack() as es:
es.enter_context(PathPrint('_initialize', 'open :'))
es.enter_context(PathPrint('aclose', 'close:'))
return es.pop_all()
raise RuntimeError
async def amain(client: discord.Client):
roles = {key: value for key, value in os.environ.items() if key.startswith('roles')}
async with (
client,
DefaultEffects() as defaulteffects,
MainService(Targets(), defaulteffects, client, Events()) as mainservice,
AppContext(Api(mainservice, roles)),
ABlockMonitor(delta=0.5)
):
register_handlers(client, mainservice)
if 'guerilla' in sys.argv:
from pathlib import Path
tokenpath = Path('.token.txt')
if tokenpath.exists():
token = tokenpath.read_text()
else:
token = input('token:')
tokenpath.write_text(token)
elif (token_ := os.getenv('trial_token')):
token = token_
else:
token = await request_token('music', 'token')
await client.login(token)
if os.getenv('v6tor', None) is None:
print('no tor')
await client.connect()
print('exited')
def main() -> None:
with _upgrade_abm(), _db_ee():
serve(amain(_client), _client, loop)

View File

@ -1,13 +1,36 @@
from typing import Any, Coroutine, TypeVar __all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged')
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Callable, Coroutine, TypeVar
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
T = TypeVar('T') T = TypeVar('T')
__all__ = ('AbstractRunner',) class CoroEvent(Event):
pass
class CoroContext:
def __init__(self, events: SendableEvents[CoroEvent]) -> None:
self.events = events
class CoroStatusChanged(CoroEvent):
def __init__(self, status: ResponseType) -> None:
self.status = status
def json(self) -> ResponseType:
return {'status': self.status}
class AbstractRunner(ABC): class AbstractRunner(ABC):
@abstractmethod @abstractmethod
async def run(self, coro: Coroutine[Any, Any, T]) -> T: async def run(self, coro: Coroutine[Any, Any, T], /) -> T:
raise NotImplementedError
@abstractmethod
async def runctx(self, ctxcoro: Callable[[CoroContext], Coroutine[Any, Any, T]], /) -> T:
raise NotImplementedError raise NotImplementedError

View File

@ -1,56 +1,115 @@
__all__ = ('Job', 'Pool', 'JobUnit', 'JobContext', 'JobStatusChanged', 'PoolEvent')
import asyncio import asyncio
from typing import Any, Coroutine, Generic, TypeVar, Union from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
from v6d3musicbase.event import *
from v6d3musicbase.responsetype import *
from .abstractrunner import * from .abstractrunner import *
__all__ = ('Job', 'Pool', 'JobDescriptor',)
class JobEvent(Event):
pass
class JobContext:
def __init__(self, events: SendableEvents[JobEvent]) -> None:
self.events = events
class Job: class Job:
def __init__(self, future: asyncio.Future[None]) -> None: def __init__(self, future: asyncio.Future[None]) -> None:
self.future = future self.future = future
async def run(self) -> Union['Job', None]: async def run(self, context: JobContext, /) -> Union['Job', None]:
raise NotImplementedError raise NotImplementedError
def json(self) -> ResponseType:
return {'type': 'unknown'}
class JobDescriptor:
async def run(self) -> Union['JobDescriptor', None]: class JobUnit:
async def run(self, context: JobContext, /) -> Union['JobUnit', None]:
raise NotImplementedError raise NotImplementedError
def wrap(self) -> Job: def wrap(self) -> Job:
return DescriptorJob(asyncio.Future(), self) return UnitJob(asyncio.Future(), self)
def at(self, pool: 'Pool') -> 'JDC': def at(self, pool: 'Pool') -> 'JDC':
return JDC(self, pool) return JDC(self, pool)
def json(self) -> ResponseType:
return {'type': 'unknown'}
class DescriptorJob(Job):
def __init__(self, future: asyncio.Future[None], descriptor: JobDescriptor) -> None: class JobStatusChanged(JobEvent):
def __init__(self, job: Job | JobUnit) -> None:
self.job = job
def json(self) -> ResponseType:
return {'status': self.job.json()}
class UnitJob(Job):
def __init__(self, future: asyncio.Future[None], unit: JobUnit) -> None:
super().__init__(future) super().__init__(future)
self.__descriptor = descriptor self.__unit = unit
async def run(self) -> Job | None: async def run(self, context: JobContext, /) -> Job | None:
next_descriptor = await self.__descriptor.run() next_unit = await self.__unit.run(context)
if next_descriptor is None: if next_unit is None:
return None return None
else: else:
return DescriptorJob(self.future, next_descriptor) return UnitJob(self.future, next_unit)
def json(self) -> ResponseType:
return self.__unit.json()
class WorkerEvent(Event):
pass
class _JWEvent(WorkerEvent):
def __init__(self, event: JobEvent, /) -> None:
self.event = event
def json(self) -> ResponseType:
return {'job': self.event.json()}
class _JWSendable(SendableEvents[JobEvent]):
def __init__(self, sendable: SendableEvents[WorkerEvent]) -> None:
self.sendable = sendable
def send(self, event: JobEvent, /) -> None:
return self.sendable.send(_JWEvent(event))
class Worker: class Worker:
def __init__(self) -> None: def __init__(self, events: SendableEvents[WorkerEvent], /) -> None:
self.__queue: asyncio.Queue[Job | None] = asyncio.Queue() self.__queue: asyncio.Queue[Job | None] = asyncio.Queue()
self.__working = False self.__working = False
self.__busy = 0 self.__busy = 0
self.__job = None
self.__events = events
self.__job_events = _JWSendable(self.__events)
def _put_nowait(self, job: Job | None) -> None: def _put_nowait(self, job: Job | None, /) -> None:
self.__queue.put_nowait(job) self.__queue.put_nowait(job)
self.__busy += 1 self.__busy += 1
async def _run(self, job: Job, /) -> Job | None:
try:
self.__job = job
return await job.run(JobContext(self.__job_events))
finally:
self.__job = None
async def _handle(self, job: Job) -> None: async def _handle(self, job: Job) -> None:
try: try:
next_job = await job.run() next_job = await self._run(job)
except BaseException as e: except BaseException as e:
job.future.set_exception(e) job.future.set_exception(e)
else: else:
@ -138,6 +197,18 @@ class Worker:
def busy(self) -> int: def busy(self) -> int:
return self.__busy return self.__busy
def _job_json(self) -> ResponseType:
if self.__job is None:
return None
else:
return self.__job.json()
def json(self) -> ResponseType:
return {
'job': self._job_json(),
'qsize': self.__queue.qsize(),
}
class Working: class Working:
def __init__(self, worker: Worker, task: asyncio.Future[None]) -> None: def __init__(self, worker: Worker, task: asyncio.Future[None]) -> None:
@ -152,8 +223,8 @@ class Working:
self.__worker.submit(job) self.__worker.submit(job)
@classmethod @classmethod
def start(cls) -> 'Working': def start(cls, events: SendableEvents[WorkerEvent], /) -> 'Working':
worker = Worker() worker = Worker(events)
task = worker.start() task = worker.start()
return cls(worker, task) return cls(worker, task)
@ -163,30 +234,76 @@ class Working:
def busy(self) -> int: def busy(self) -> int:
return self.__worker.busy() return self.__worker.busy()
def json(self) -> ResponseType:
return self.__worker.json()
T = TypeVar('T') T = TypeVar('T')
class CoroJD(JobDescriptor, Generic[T]): class CoroJD(JobUnit, Generic[T]):
def __init__(self, coro: Coroutine[Any, Any, T]) -> None: def __init__(self, ctxcoro: Callable[[CoroContext], Coroutine[Any, Any, T]], /) -> None:
self.future = asyncio.Future() self.future = asyncio.Future()
self.coro = coro self.ctxcoro = ctxcoro
self.status: ResponseType = None
async def run(self) -> JobDescriptor | None: async def run(self, context: JobContext, /) -> JobUnit | None:
try: try:
self.future.set_result(await self.coro) self.future.set_result(await self.ctxcoro(CoroContext(_CJSendable(context.events, self))))
except BaseException as e: except BaseException as e:
self.future.set_exception(e) self.future.set_exception(e)
return None return None
def json(self) -> ResponseType:
return {'coroutine': self.status}
class _CJSendable(SendableEvents[CoroEvent]):
def __init__(self, sendable: SendableEvents[JobEvent], corojd: CoroJD) -> None:
self.sendable = sendable
self.corojd = corojd
def send(self, event: CoroEvent, /) -> None:
match event:
case CoroStatusChanged() as csc:
self.corojd.status = csc.status
self.sendable.send(JobStatusChanged(self.corojd))
case _:
pass
class PoolEvent(Event):
pass
class _WPEvent(PoolEvent):
def __init__(self, event: WorkerEvent, /) -> None:
self.event = event
def json(self) -> ResponseType:
return {'worker': self.event.json()}
class _WPSendable(SendableEvents[WorkerEvent]):
def __init__(self, sendable: SendableEvents[PoolEvent], /) -> None:
self.sendable = sendable
def send(self, event: WorkerEvent, /) -> None:
return self.sendable.send(_WPEvent(event))
class Pool(AbstractRunner): class Pool(AbstractRunner):
def __init__(self, workers: int) -> None: def __init__(self, workers: int, events: SendableEvents[PoolEvent], /) -> None:
if workers < 1: if workers < 1:
raise ValueError('non-positive number of workers') raise ValueError('non-positive number of workers')
self.__workers = workers self.__workers = workers
self.__working = False self.__working = False
self.__open = False self.__open = False
self.__events: SendableEvents[PoolEvent] = events
self.__worker_events: SendableEvents[WorkerEvent] = _WPSendable(self.__events)
def _start_worker(self) -> Working:
return Working.start(self.__worker_events)
async def __aenter__(self) -> 'Pool': async def __aenter__(self) -> 'Pool':
if self.__open: if self.__open:
@ -194,7 +311,7 @@ class Pool(AbstractRunner):
if self.__working: if self.__working:
raise RuntimeError('starting an already running pool') raise RuntimeError('starting an already running pool')
self.__working = True self.__working = True
self.__pool = set(Working.start() for _ in range(self.__workers)) self.__pool: set[Working] = set(self._start_worker() for _ in range(self.__workers))
self.__open = True self.__open = True
return self return self
@ -219,19 +336,25 @@ class Pool(AbstractRunner):
def workers(self) -> int: def workers(self) -> int:
return self.__workers return self.__workers
async def run(self, coro: Coroutine[Any, Any, T]) -> T: async def run(self, coro: Coroutine[Any, Any, T], /) -> T:
job = CoroJD(coro) return await self.runctx(lambda _: coro)
async def runctx(self, ctxcoro: Callable[[CoroContext], Coroutine[Any, Any, T]], /) -> T:
job = CoroJD(ctxcoro)
self.submit(job.wrap()) self.submit(job.wrap())
return await job.future return await job.future
def json(self) -> ResponseType:
return [working.json() for working in self.__pool]
class JDC: class JDC:
def __init__(self, descriptor: JobDescriptor, pool: Pool) -> None: def __init__(self, unit: JobUnit, pool: Pool) -> None:
self.__descriptor = descriptor self.__unit = unit
self.__pool = pool self.__pool = pool
async def __aenter__(self) -> 'JDC': async def __aenter__(self) -> 'JDC':
job = self.__descriptor.wrap() job = self.__unit.wrap()
self.__future = job.future self.__future = job.future
self.__pool.submit(job) self.__pool.submit(job)
return self return self

View File

@ -1,172 +1,5 @@
import asyncio from .main import main
import contextlib
import os
import sys
import time
from traceback import print_exc
import discord
from ptvp35 import *
from rainbowadn.instrument import Instrumentation
from v6d1tokens.client import *
from v6d2ctx.handle_content import *
from v6d2ctx.pain import *
from v6d2ctx.serve import *
from v6d3music.app import *
from v6d3music.commands import *
from v6d3music.config import prefix
from v6d3music.core.caching import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import *
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
class MusicClient(discord.Client):
pass
_client = MusicClient(
intents=discord.Intents(
members=True,
guilds=True,
bans=True,
emojis=True,
invites=True,
voice_states=True,
guild_messages=True,
reactions=True,
message_content=True,
),
loop=loop,
)
banned_guilds = set(map(int, map(str.strip, os.getenv('banned_guilds', '').split(':'))))
def guild_allowed(guild: discord.Guild | None) -> bool:
return guild is not None and guild.id not in banned_guilds
def message_allowed(message: discord.Message) -> bool:
return guild_allowed(message.guild)
def register_handlers(client: discord.Client, mainservice: MainService):
of = get_of(mainservice)
@client.event
async def on_message(message: discord.Message) -> None:
if message_allowed(message):
try:
await handle_content(of, message, message.content, prefix, client)
except:
print_exc()
@client.event
async def on_ready():
print('ready')
await client.change_presence(
activity=discord.Game(
name='феноменально',
)
)
await mainservice.restore()
class UpgradeABMInit(Instrumentation):
def __init__(self):
super().__init__(ABlockMonitor, '__init__')
def instrument(self, method, abm, *, threshold=0.0, delta=10.0, interval=0.0):
print('created upgraded')
method(abm, threshold=threshold, delta=delta, interval=interval)
abm.threshold = threshold
class UpgradeABMTask(Instrumentation):
def __init__(self):
super().__init__(ABlockMonitor, '_monitor')
async def instrument(self, _, abm):
print('started upgraded')
while True:
delta = abm.delta
t = time.time()
await asyncio.sleep(delta)
spent = time.time() - t
delay = spent - delta
if delay > abm.threshold:
abm.threshold = delay
print(
f'upgraded block monitor reached new peak delay {delay:.4f}')
interval = abm.interval
if interval > 0:
await asyncio.sleep(interval)
def _upgrade_abm() -> contextlib.ExitStack:
with contextlib.ExitStack() as es:
es.enter_context(UpgradeABMInit())
es.enter_context(UpgradeABMTask())
return es.pop_all()
raise RuntimeError
class PathPrint(Instrumentation):
def __init__(self, methodname: str, pref: str):
super().__init__(DbConnection, methodname)
self.pref = pref
async def instrument(self, method, db: DbConnection, *args, **kwargs):
result = await method(db, *args, **kwargs)
try:
print(self.pref, db._DbConnection__path) # type: ignore
except Exception:
from traceback import print_exc
print_exc()
return result
def _db_ee() -> contextlib.ExitStack:
with contextlib.ExitStack() as es:
es.enter_context(PathPrint('_initialize', 'open :'))
es.enter_context(PathPrint('aclose', 'close:'))
return es.pop_all()
raise RuntimeError
async def main(client: discord.Client):
async with (
client,
DefaultEffects() as defaulteffects,
MainService(defaulteffects, client) as mainservice,
AppContext(mainservice),
ABlockMonitor(delta=0.5)
):
register_handlers(client, mainservice)
if 'guerilla' in sys.argv:
from pathlib import Path
tokenpath = Path('.token.txt')
if tokenpath.exists():
token = tokenpath.read_text()
else:
token = input('token:')
tokenpath.write_text(token)
elif (token_ := os.getenv('trial_token')):
token = token_
else:
token = await request_token('music', 'token')
await client.login(token)
if os.getenv('v6tor', None) is None:
print('no tor')
await client.connect()
print('exited')
if __name__ == '__main__': if __name__ == '__main__':
with _upgrade_abm(), _db_ee(): main()
serve(main(_client), _client, loop)

60
v6d3musicbase/event.py Normal file
View File

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

View File

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

76
v6d3musicbase/targets.py Normal file
View File

@ -0,0 +1,76 @@
__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()