docs + operation
This commit is contained in:
parent
9ad6126838
commit
08192b5d93
@ -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"]
|
||||||
|
@ -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
|
||||||
|
@ -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`.
|
||||||
|
@ -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.
|
||||||
|
@ -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
12
setup.py
Normal 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=''
|
||||||
|
)
|
256
v6d3music/api.py
256
v6d3music/api.py
@ -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
|
||||||
|
@ -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,
|
||||||
@ -206,6 +203,10 @@ class MusicAppFactory(AppFactory):
|
|||||||
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:
|
||||||
return web.FileResponse(self._path('login.html'))
|
return web.FileResponse(self._path('login.html'))
|
||||||
@ -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):
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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()
|
||||||
|
117
v6d3music/core/monitoring.py
Normal file
117
v6d3music/core/monitoring.py
Normal 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
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
@ -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 += (
|
||||||
|
@ -44,3 +44,7 @@ body,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background: #050505;
|
background: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#homeroot {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
@ -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);
|
|
||||||
|
22
v6d3music/html/operator.css
Normal file
22
v6d3music/html/operator.css
Normal 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;
|
||||||
|
}
|
24
v6d3music/html/operator.html
Normal file
24
v6d3music/html/operator.html
Normal 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>
|
69
v6d3music/html/operator.js
Normal file
69
v6d3music/html/operator.js
Normal 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
176
v6d3music/main.py
Normal 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)
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
60
v6d3musicbase/event.py
Normal 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)
|
17
v6d3musicbase/responsetype.py
Normal file
17
v6d3musicbase/responsetype.py
Normal 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
76
v6d3musicbase/targets.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user