service hierarchy

This commit is contained in:
AF 2022-12-24 16:37:05 +00:00
parent 45fee6d593
commit 77c0f16da3
19 changed files with 725 additions and 653 deletions

View File

@ -3,7 +3,7 @@ import time
from typing import TypeAlias
import discord
from v6d3music.core.mainasrc import main_for_raw_vc, raw_vc_for_member
from v6d3music.core.mainservice import MainService
from v6d3music.core.mainaudio import MainAudio
from v6d2ctx.context import Explicit
@ -27,8 +27,9 @@ class Api:
def json(self) -> dict:
return super().json() | {'explicit': None}
def __init__(self, client: discord.Client, roles: dict[str, str]) -> None:
self.client = client
def __init__(self, mainservice: MainService, roles: dict[str, str]) -> None:
self.mainservice = mainservice
self.client = mainservice.client
self.roles = roles
def is_operator(self, user_id: int) -> bool:
@ -166,10 +167,11 @@ class VoiceApi(GuildApi):
) -> None:
super().__init__(api, api.member)
self.channel = channel
self.mainservice = self.pi.mainservice
async def _main_api(self) -> 'MainApi':
vc = await raw_vc_for_member(self.member)
main = await main_for_raw_vc(vc, create=False, force_play=False)
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)
return MainApi(self, vc, main)
def sub(self, request: dict) -> 'VoiceApi':
@ -210,7 +212,7 @@ class MainApi(VoiceApi):
case {'type': 'queueformat'}:
return await self.main.queue.format()
case {'type': 'queuejson'}:
return await self.main.queue.pubjson(self.member)
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}:

View File

@ -2,6 +2,7 @@ import asyncio
import functools
import os
import urllib.parse
from contextlib import AsyncExitStack
from pathlib import Path
from typing import Any, Callable, Coroutine, Generic, Hashable, TypeVar
@ -10,15 +11,14 @@ import discord
from aiohttp import web
from v6d3music.api import Api
from v6d3music.config import auth_redirect, myroot
from v6d3music.core.mainservice import MainService
from v6d3music.utils.bytes_hash import bytes_hash
from ptvp35 import Db, KVJson
from ptvp35 import *
from v6d0auth.appfactory import AppFactory
from v6d0auth.run_app import start_app
from v6d1tokens.client import request_token
session_db = Db(myroot / 'session.db', kvfactory=KVJson())
T = TypeVar('T')
TKey = TypeVar('TKey', bound=Hashable)
@ -64,13 +64,15 @@ class MusicAppFactory(AppFactory):
self,
secret: str,
client: discord.Client,
api: Api
api: Api,
db: DbConnection
):
self.secret = secret
self.redirect = auth_redirect
self.loop = asyncio.get_running_loop()
self.client = client
self._api = api
self.db = db
self._token_clients: CachedDictionary[str, dict | None] = CachedDictionary(
self._token_client
)
@ -190,11 +192,10 @@ class MusicAppFactory(AppFactory):
cid = self.user_id(user)
return cid
@classmethod
def session_data(cls, session: str | None) -> dict:
def session_data(self, session: str | None) -> dict:
if session is None:
return {}
data = session_db.get(session, {})
data = self.db.get(session, {})
if not isinstance(data, dict):
return {}
return data
@ -224,7 +225,7 @@ class MusicAppFactory(AppFactory):
data = self.session_data(session)
data['code'] = code
data['token'] = await self.code_token(code)
await session_db.set(session, data)
await self.db.set(session, data)
return response
else:
return web.FileResponse(self._path('auth.html'))
@ -279,15 +280,47 @@ class MusicAppFactory(AppFactory):
except Api.MisusedApi as e:
return web.json_response(e.json(), status=404)
@classmethod
async def start(cls, client: discord.Client):
class AppContext:
def __init__(self, mainservice: MainService) -> None:
self.mainservice = mainservice
async def start(self) -> tuple[web.Application, asyncio.Task[None]] | None:
try:
factory = cls(
factory = MusicAppFactory(
await request_token('music-client', 'token'),
client,
Api(client, {key: value for key, value in os.environ.items() if key.startswith('roles')})
self.mainservice.client,
Api(
self.mainservice,
{key: value for key, value in os.environ.items() if key.startswith('roles')},
),
self.__db
)
except aiohttp.ClientConnectorError:
print('no web app (likely due to no token)')
else:
await start_app(factory.app())
app = factory.app()
task = asyncio.create_task(start_app(app))
return app, task
async def __aenter__(self) -> 'AppContext':
async with AsyncExitStack() as es:
self.__db = await es.enter_async_context(DbFactory(myroot / 'session.db', kvfactory=KVJson()))
self.__es = es.pop_all()
self.__task: asyncio.Task[
tuple[web.Application, asyncio.Task[None]] | None
] = asyncio.create_task(self.start())
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es:
if self.__task.done():
result = await self.__task
if result is not None:
app, task = result
await task
await app.shutdown()
await app.cleanup()
else:
self.__task.cancel()
del self.__es

View File

@ -1,8 +1,8 @@
import shlex
from typing import Callable
from v6d3music.core.entries_effects_for_args import *
from v6d3music.core.mainasrc import main_for, queue_for, vc_for
from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import MainService
from v6d3music.core.yt_audios import yt_audios
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.catch import catch
@ -14,276 +14,270 @@ from v6d2ctx.at_of import AtOf
from v6d2ctx.context import Context, Explicit, command_type
from v6d2ctx.lock_for import lock_for
at_of: AtOf[str, command_type] = AtOf()
at, of = at_of()
def get_of(mainservice: MainService, defaulteffects: DefaultEffects) -> Callable[[str], command_type]:
at_of: AtOf[str, command_type] = AtOf()
at, of = at_of()
@at('help')
async def help_(ctx: Context, args: list[str]) -> None:
match args:
case []:
await ctx.reply('music bot')
case [name]:
await ctx.reply(f'help for {name}: `{name} help`')
@at('help')
async def help_(ctx: Context, args: list[str]) -> None:
match args:
case []:
await ctx.reply('music bot')
case [name]:
await ctx.reply(f'help for {name}: `{name} help`')
@at('/')
@at('play')
async def play(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args,
f'''
`play ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args`
`pause`
`resume`
presets: {shlex.join(allowed_presets)}
''',
(), 'help'
)
async with lock_for(ctx.guild, 'not in a guild'):
queue = await mainservice.context(ctx, create=True, force_play=False).queue()
if ctx.message.attachments:
if len(ctx.message.attachments) > 1:
raise Explicit('no more than one attachment')
args = [ctx.message.attachments[0].url] + args
async for audio in yt_audios(mainservice.caching, defaulteffects, ctx, args):
queue.append(audio)
await ctx.reply('done')
@at('/')
@at('play')
async def play(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args,
f'''
`play ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args`
`pause`
`resume`
presets: {shlex.join(allowed_presets)}
''',
(), 'help'
)
async with lock_for(ctx.guild, 'not in a guild'):
queue = await queue_for(ctx, create=True, force_play=False)
if ctx.message.attachments:
if len(ctx.message.attachments) > 1:
raise Explicit('no more than one attachment')
args = [ctx.message.attachments[0].url] + args
async for audio in yt_audios(ctx, args):
queue.append(audio)
@at('skip')
async def skip(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`skip [first] [last]`
''', 'help'
)
assert ctx.member is not None
match args:
case []:
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
queue.skip_at(0, ctx.member)
case [pos] if pos.isdecimal():
pos = int(pos)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
queue.skip_at(pos, ctx.member)
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
pos0, pos1 = int(pos0), int(pos1)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
for i in range(pos0, pos1 + 1):
if not queue.skip_at(pos0, ctx.member):
pos0 += 1
case _:
raise Explicit('misformatted')
await ctx.reply('done')
@at('to')
async def skip_to(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`to [[h]] [m] s`
''', 'help'
)
match args:
case [h, m, s] if h.isdecimal() and m.isdecimal() and s.isdecimal():
seconds = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s] if m.isdecimal() and s.isdecimal():
seconds = 60 * int(m) + int(s)
case [s] if s.isdecimal():
seconds = int(s)
case _:
raise Explicit('misformatted')
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
queue.queue[0].set_seconds(seconds)
@at('skip')
async def skip(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`skip [first] [last]`
''', 'help'
)
assert ctx.member is not None
match args:
case []:
queue = await queue_for(ctx, create=False, force_play=False)
queue.skip_at(0, ctx.member)
case [pos] if pos.isdecimal():
pos = int(pos)
queue = await queue_for(ctx, create=False, force_play=False)
queue.skip_at(pos, ctx.member)
case [pos0, pos1] if pos0.isdecimal() and pos1.isdecimal():
pos0, pos1 = int(pos0), int(pos1)
queue = await queue_for(ctx, create=False, force_play=False)
for i in range(pos0, pos1 + 1):
if not queue.skip_at(pos0, ctx.member):
pos0 += 1
case _:
raise Explicit('misformatted')
await ctx.reply('done')
@at('effects')
async def effects_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`effects - effects`
`effects + preset`
''', 'help'
)
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
yta = queue.queue[0]
seconds = yta.source_seconds()
yta.options = options_for_effects(effects)
yta.set_seconds(seconds)
@at('default')
async def default(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`default - effects`
`default + preset`
`default none`
''', 'help'
)
assert ctx.guild is not None
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case ['none']:
effects = None
case []:
await ctx.reply(f'current default effects: {defaulteffects.get(ctx.guild.id)}')
return
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
await defaulteffects.set(ctx.guild.id, effects)
await ctx.reply(f'effects set to `{effects}`')
@at('to')
async def skip_to(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`to [[h]] [m] s`
''', 'help'
)
match args:
case [h, m, s] if h.isdecimal() and m.isdecimal() and s.isdecimal():
seconds = 3600 * int(h) + 60 * int(m) + int(s)
case [m, s] if m.isdecimal() and s.isdecimal():
seconds = 60 * int(m) + int(s)
case [s] if s.isdecimal():
seconds = int(s)
case _:
raise Explicit('misformatted')
queue = await queue_for(ctx, create=False, force_play=False)
queue.queue[0].set_seconds(seconds)
@at('repeat')
async def repeat(ctx: Context, args: list[str]):
match args:
case []:
n = 1
case [n_] if n_.isdecimal():
n = int(n_)
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
if not queue.queue:
raise Explicit('empty queue')
if n > 99:
raise Explicit('too long')
audio = queue.queue[0]
for _ in range(n):
queue.queue.insert(1, audio.copy())
@at('branch')
async def branch(ctx: Context, args: list[str]):
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case ['none']:
effects = ''
case []:
effects = None
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
if not queue.queue:
raise Explicit('empty queue')
audio = queue.queue[0].branch()
if effects is not None:
seconds = audio.source_seconds()
audio.options = options_for_effects(effects or None)
audio.set_seconds(seconds)
else:
audio.set_source()
queue.queue.insert(1, audio)
@at('effects')
async def effects_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`effects - effects`
`effects + preset`
''', 'help'
)
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await queue_for(ctx, create=False, force_play=False)
yta = queue.queue[0]
seconds = yta.source_seconds()
yta.options = options_for_effects(effects)
yta.set_seconds(seconds)
@at('default')
async def default(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`default - effects`
`default + preset`
`default none`
''', 'help'
)
assert ctx.guild is not None
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case ['none']:
effects = None
case []:
await ctx.reply(f'current default effects: {default_effects(ctx.guild.id)}')
return
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
await set_default_effects(ctx.guild.id, effects)
await ctx.reply(f'effects set to `{effects}`')
@at('repeat')
async def repeat(ctx: Context, args: list[str]):
match args:
case []:
n = 1
case [n_] if n_.isdecimal():
n = int(n_)
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await queue_for(ctx, create=False, force_play=False)
if not queue.queue:
raise Explicit('empty queue')
if n > 99:
raise Explicit('too long')
audio = queue.queue[0]
for _ in range(n):
queue.queue.insert(1, audio.copy())
@at('branch')
async def branch(ctx: Context, args: list[str]):
match args:
case ['-', effects]:
pass
case ['+', preset]:
effects = effects_for_preset(preset)
case ['none']:
effects = ''
case []:
effects = None
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
queue = await queue_for(ctx, create=False, force_play=False)
if not queue.queue:
raise Explicit('empty queue')
audio = queue.queue[0].branch()
if effects is not None:
seconds = audio.source_seconds()
audio.options = options_for_effects(effects or None)
audio.set_seconds(seconds)
else:
audio.set_source()
queue.queue.insert(1, audio)
@at('//')
@at('queue')
async def queue_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`queue`
`queue clear`
`queue resume`
`queue pause`
''', 'help'
)
assert ctx.member is not None
match args:
case []:
await ctx.long(
(await (await queue_for(ctx, create=True, force_play=False)).format()).strip() or 'no queue'
)
case ['clear']:
(await queue_for(ctx, create=False, force_play=False)).clear(ctx.member)
await ctx.reply('done')
case ['resume']:
async with lock_for(ctx.guild, 'not in a guild'):
await queue_for(ctx, create=True, force_play=True)
@at('//')
@at('queue')
async def queue_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`queue`
`queue clear`
`queue resume`
`queue pause`
''', 'help'
)
assert ctx.member is not None
match args:
case []:
await ctx.long(
(
await (
await mainservice.context(ctx, create=True, force_play=False).queue()
).format()
).strip() or 'no queue'
)
case ['clear']:
(await mainservice.context(ctx, create=False, force_play=False).queue()).clear(ctx.member)
await ctx.reply('done')
case ['pause']:
async with lock_for(ctx.guild, 'not in a guild'):
vc = await vc_for(ctx, create=True, force_play=False)
vc.pause()
await ctx.reply('done')
case _:
raise Explicit('misformatted')
case ['resume']:
async with lock_for(ctx.guild, 'not in a guild'):
await mainservice.context(ctx, create=True, force_play=True).vc()
await ctx.reply('done')
case ['pause']:
async with lock_for(ctx.guild, 'not in a guild'):
vc = await mainservice.context(ctx, create=True, force_play=False).vc()
vc.pause()
await ctx.reply('done')
case _:
raise Explicit('misformatted')
@at('swap')
async def swap(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`swap a b`
''', 'help'
)
assert ctx.member is not None
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await mainservice.context(ctx, create=False, force_play=False).queue()).swap(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('swap')
async def swap(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`swap a b`
''', 'help'
)
assert ctx.member is not None
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await queue_for(ctx, create=False, force_play=False)).swap(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('move')
async def move(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`move a b`
''', 'help'
)
assert ctx.member is not None
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await mainservice.context(ctx, create=False, force_play=False).queue()).move(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('volume')
async def volume_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`volume volume`
''', 'help'
)
assert ctx.member is not None
match args:
case [volume]:
volume = float(volume)
await (await mainservice.context(ctx, create=True, force_play=False).main()).set(volume, ctx.member)
case _:
raise Explicit('misformatted')
@at('move')
async def move(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`move a b`
''', 'help'
)
assert ctx.member is not None
match args:
case [a, b] if a.isdecimal() and b.isdecimal():
a, b = int(a), int(b)
(await queue_for(ctx, create=False, force_play=False)).move(ctx.member, a, b)
case _:
raise Explicit('misformatted')
@at('pause')
async def pause(ctx: Context, _args: list[str]) -> None:
vc = await mainservice.context(ctx, create=False, force_play=False).vc()
vc.pause()
@at('resume')
async def resume(ctx: Context, _args: list[str]) -> None:
vc = await mainservice.context(ctx, create=False, force_play=True).vc()
vc.resume()
@at('volume')
async def volume_(ctx: Context, args: list[str]) -> None:
await catch(
ctx, args, '''
`volume volume`
''', 'help'
)
assert ctx.member is not None
match args:
case [volume]:
volume = float(volume)
await (await main_for(ctx, create=True, force_play=False)).set(volume, ctx.member)
case _:
raise Explicit('misformatted')
@at('pause')
async def pause(ctx: Context, _args: list[str]) -> None:
vc = await vc_for(ctx, create=False, force_play=False)
vc.pause()
@at('resume')
async def resume(ctx: Context, _args: list[str]) -> None:
vc = await vc_for(ctx, create=False, force_play=True)
vc.resume()
return of

View File

@ -1,44 +0,0 @@
import asyncio
from ptvp35 import Db, KVJson
from v6d2ctx.lock_for import lock_for
from v6d3music.config import myroot
from v6d3music.utils.tor_prefix import tor_prefix
cache_root = myroot / 'cache'
cache_root.mkdir(exist_ok=True)
cache_db = Db(myroot / 'cache.db', kvfactory=KVJson())
async def cache_url(hurl: str, rurl: str, override: bool, tor: bool) -> None:
async with lock_for(('cache', hurl), 'cache failed'):
if not override and cache_db.get(f'url:{hurl}', None) is not None:
return
cachable: bool = cache_db.get(f'cachable:{hurl}', False)
if cachable:
print('caching', hurl)
path = cache_root / f'{hurl}.opus'
tmp_path = cache_root / f'{hurl}.tmp.opus'
args = []
if tor:
args.extend(tor_prefix())
args.extend(
[
'ffmpeg', '-hide_banner', '-loglevel', 'warning',
'-reconnect', '1', '-reconnect_at_eof', '0',
'-reconnect_streamed', '1', '-reconnect_delay_max', '10', '-copy_unknown',
'-y', '-i', rurl, '-b:a', '128k', str(tmp_path)
]
)
ap = await asyncio.create_subprocess_exec(*args)
code = await ap.wait()
if code:
print(f'caching {hurl} failed with {code}')
return
await asyncio.to_thread(tmp_path.rename, path)
await cache_db.set(f'url:{hurl}', str(path))
print('cached', hurl)
# await cache_db.set(f'cachable:{hurl}', False)
else:
await cache_db.set(f'cachable:{hurl}', True)

67
v6d3music/core/caching.py Normal file
View File

@ -0,0 +1,67 @@
import asyncio
from contextlib import AsyncExitStack
from v6d3music.config import myroot
from v6d3music.utils.tor_prefix import tor_prefix
from ptvp35 import *
from v6d2ctx.lock_for import lock_for
__all__ = ('Caching',)
cache_root = myroot / 'cache'
cache_root.mkdir(exist_ok=True)
class Caching:
async def cache_url(self, hurl: str, rurl: str, override: bool, tor: bool) -> None:
async with lock_for(('cache', hurl), 'cache failed'):
if not override and self.__db.get(f'url:{hurl}', None) is not None:
return
cachable: bool = self.__db.get(f'cachable:{hurl}', False)
if cachable:
print('caching', hurl)
path = cache_root / f'{hurl}.opus'
tmp_path = cache_root / f'{hurl}.tmp.opus'
args = []
if tor:
args.extend(tor_prefix())
args.extend(
[
'ffmpeg', '-hide_banner', '-loglevel', 'warning',
'-reconnect', '1', '-reconnect_at_eof', '0',
'-reconnect_streamed', '1', '-reconnect_delay_max', '10', '-copy_unknown',
'-y', '-i', rurl, '-b:a', '128k', str(tmp_path)
]
)
ap = await asyncio.create_subprocess_exec(*args)
code = await ap.wait()
if code:
print(f'caching {hurl} failed with {code}')
return
await asyncio.to_thread(tmp_path.rename, path)
await self.__db.set(f'url:{hurl}', str(path))
print('cached', hurl)
# await cache_db.set(f'cachable:{hurl}', False)
else:
await self.__db.set(f'cachable:{hurl}', True)
def get(self, hurl: str) -> str | None:
return self.__db.get(f'url:{hurl}', None)
async def __aenter__(self) -> 'Caching':
es = AsyncExitStack()
async with es:
self.__db = await es.enter_async_context(DbFactory(myroot / 'cache.db', kvfactory=KVJson()))
self.__tasks = set()
self.__es = es.pop_all()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es:
del self.__es
def schedule_cache(self, hurl: str, rurl: str, override: bool, tor: bool):
task = asyncio.create_task(self.cache_url(hurl, rurl, override, tor))
self.__tasks.add(task)
task.add_done_callback(self.__tasks.discard)

View File

@ -4,6 +4,7 @@ from typing import Any, Optional
from v6d2ctx.context import Context, Explicit, escape
from v6d3music.core.real_url import real_url
from v6d3music.core.caching import Caching
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.options_for_effects import options_for_effects
@ -12,7 +13,7 @@ from v6d3music.utils.argctx import InfoCtx
async def create_ytaudio(
ctx: Context, it: InfoCtx
caching: Caching, ctx: Context, it: InfoCtx
) -> YTAudio:
assert ctx.member is not None
if it.effects:
@ -24,7 +25,8 @@ async def create_ytaudio(
else:
options = None
return YTAudio(
await real_url(it.info['url'], False, it.tor),
caching,
await real_url(caching, it.info['url'], False, it.tor),
it.info['url'],
f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}',
options,

View File

@ -1,21 +0,0 @@
import asyncio
from typing import AsyncIterable
from v6d2ctx.context import Context
from v6d3music.core.create_ytaudio import create_ytaudio
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.argctx import InfoCtx
async def create_ytaudios(ctx: Context, infos: list[InfoCtx]) -> AsyncIterable[YTAudio]:
for audio in await asyncio.gather(
*[
create_ytaudio(ctx, it)
for
it
in
infos
]
):
yield audio

View File

@ -0,0 +1,33 @@
from contextlib import AsyncExitStack
from v6d3music.config import myroot
from v6d3music.utils.presets import allowed_effects
from ptvp35 import *
from v6d2ctx.context import Explicit
__all__ = ('DefaultEffects',)
class DefaultEffects:
def get(self, gid: int) -> str | None:
effects = self.__db.get(gid, None)
if effects in allowed_effects:
return effects
else:
return None
async def set(self, gid: int, effects: str | None) -> None:
if effects is not None and effects not in allowed_effects:
raise Explicit('these effects are not allowed')
await self.__db.set(gid, effects)
async def __aenter__(self) -> 'DefaultEffects':
async with AsyncExitStack() as es:
self.__db = await es.enter_async_context(DbFactory(myroot / 'effects.db', kvfactory=KVJson()))
self.__es = es.pop_all()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es:
del self.__es

View File

@ -1,34 +0,0 @@
from typing import AsyncIterable
from ptvp35 import Db, KVJson
from v6d2ctx.context import Explicit
from v6d3music.utils.argctx import ArgCtx, InfoCtx
from v6d3music.config import myroot
from v6d3music.utils.presets import allowed_effects
__all__ = ('effects_db', 'default_effects', 'set_default_effects', 'entries_effects_for_args',)
effects_db = Db(myroot / 'effects.db', kvfactory=KVJson())
def default_effects(gid: int) -> str | None:
effects = effects_db.get(gid, None)
if effects in allowed_effects:
return effects
else:
return None
async def set_default_effects(gid: int, effects: str | None) -> None:
if effects is not None and effects not in allowed_effects:
raise Explicit('these effects are not allowed')
await effects_db.set(gid, effects)
async def entries_effects_for_args(args: list[str], gid: int) -> AsyncIterable[InfoCtx]:
for ctx in ArgCtx(default_effects(gid), args).sources:
async for it in ctx.entries():
yield it

View File

@ -1,69 +0,0 @@
import discord
from v6d2ctx.context import Context, Explicit
from v6d3music.core.mainaudio import MainAudio
from v6d3music.core.queueaudio import QueueAudio
mainasrcs: dict[discord.Guild, MainAudio] = {}
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
vc: discord.VoiceProtocol | None = member.guild.voice_client
if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState | None = member.voice
if vs is None:
raise Explicit('not connected')
vch: discord.VoiceChannel | None = vs.channel # type: ignore
if vch is None:
raise Explicit('not connected')
try:
vc = await vch.connect()
except discord.ClientException:
vc = member.guild.voice_client
assert vc is not None
await member.guild.fetch_channels()
await vc.disconnect(force=True)
raise Explicit('try again later')
assert isinstance(vc, discord.VoiceClient)
return vc
async def raw_vc_for(ctx: Context) -> discord.VoiceClient:
if ctx.member is None:
raise Explicit('not in a guild')
return await raw_vc_for_member(ctx.member)
async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool, force_play: bool) -> MainAudio:
if vc.guild in mainasrcs:
source = mainasrcs[vc.guild]
else:
if create:
source = mainasrcs.setdefault(
vc.guild,
await MainAudio.create(vc.guild)
)
else:
raise Explicit('not playing, use `queue pause` or `queue resume`')
if vc.source != source or create and not vc.is_playing() and (force_play or not vc.is_paused()):
vc.play(source)
return source
async def vc_main_for(ctx: Context, *, create: bool, force_play: bool) -> tuple[discord.VoiceClient, MainAudio]:
vc = await raw_vc_for(ctx)
return vc, await main_for_raw_vc(vc, create=create, force_play=force_play)
async def vc_for(ctx: Context, *, create: bool, force_play: bool) -> discord.VoiceClient:
vc, source = await vc_main_for(ctx, create=create, force_play=force_play)
return vc
async def main_for(ctx: Context, *, create: bool, force_play: bool) -> MainAudio:
vc, source = await vc_main_for(ctx, create=create, force_play=force_play)
return source
async def queue_for(ctx: Context, *, create: bool, force_play: bool) -> QueueAudio:
return (await main_for(ctx, create=create, force_play=force_play)).queue

View File

@ -1,16 +1,15 @@
import discord
from ptvp35 import Db, KVJson
from v6d2ctx.context import Explicit
from v6d3music.config import myroot
from v6d3music.core.caching import Caching
from v6d3music.core.queueaudio import QueueAudio
from v6d3music.utils.assert_admin import assert_admin
volume_db = Db(myroot / 'volume.db', kvfactory=KVJson())
from ptvp35 import *
from v6d2ctx.context import Explicit
class MainAudio(discord.PCMVolumeTransformer):
def __init__(self, queue: QueueAudio, volume: float):
def __init__(self, db: DbConnection, queue: QueueAudio, volume: float):
self.db = db
self.queue = queue
super().__init__(self.queue, volume=volume)
@ -21,8 +20,8 @@ class MainAudio(discord.PCMVolumeTransformer):
if volume > 1:
raise Explicit('volume too big')
self.volume = volume
await volume_db.set(member.guild.id, volume)
await self.db.set(member.guild.id, volume)
@classmethod
async def create(cls, guild: discord.Guild) -> 'MainAudio':
return cls(await QueueAudio.create(guild), volume=volume_db.get(guild.id, 0.2))
async def create(cls, caching: Caching, db: DbConnection, queues: DbConnection, guild: discord.Guild) -> 'MainAudio':
return cls(db, await QueueAudio.create(caching, queues, guild), volume=db.get(guild.id, 0.2))

View File

@ -0,0 +1,215 @@
import asyncio
import traceback
from contextlib import AsyncExitStack
import discord
from v6d3music.config import myroot
from v6d3music.core.caching import Caching
from v6d3music.core.mainaudio import MainAudio
from v6d3music.core.queueaudio import QueueAudio
from ptvp35 import *
from v6d2ctx.context import Context, Explicit
from v6d2ctx.lock_for import lock_for
class MainService:
def __init__(self, client: discord.Client) -> None:
self.client = client
self.mains: dict[discord.Guild, MainAudio] = {}
@staticmethod
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
vc: discord.VoiceProtocol | None = member.guild.voice_client
if vc is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState | None = member.voice
if vs is None:
raise Explicit('not connected')
vch: discord.abc.Connectable | None = vs.channel
if vch is None:
raise Explicit('not connected')
try:
vc = await vch.connect()
except discord.ClientException:
vc = member.guild.voice_client
assert vc is not None
await member.guild.fetch_channels()
await vc.disconnect(force=True)
raise Explicit('try again later')
assert isinstance(vc, discord.VoiceClient)
return vc
async def raw_vc_for(self, ctx: Context) -> discord.VoiceClient:
if ctx.member is None:
raise Explicit('not in a guild')
return await self.raw_vc_for_member(ctx.member)
def descriptor(self, *, create: bool, force_play: bool) -> 'MainDescriptor':
return MainDescriptor(self, create=create, force_play=force_play)
def context(self, ctx: Context, *, create: bool, force_play: bool) -> 'MainContext':
return self.descriptor(create=create, force_play=force_play).context(ctx)
async def create(self, guild: discord.Guild) -> MainAudio:
return await MainAudio.create(self.caching, self.__volumes, self.queues, guild)
async def __aenter__(self) -> 'MainService':
async with AsyncExitStack() as es:
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.caching = await es.enter_async_context(Caching())
self.__vcs_restored: asyncio.Future[None] = asyncio.Future()
self.__es = es.pop_all()
self.__save_task = asyncio.create_task(self.save_daemon())
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
async with self.__es:
await self.final_save()
del self.__es
async def save_queues(self, delay: bool) -> None:
for mainasrc in list(self.mains.values()):
if delay:
await asyncio.sleep(0.01)
await mainasrc.queue.save(delay)
async def save_vcs(self, delay: bool) -> None:
vcs = []
vc: discord.VoiceClient
for vc in (vcc for vcc in self.client.voice_clients if isinstance(vcc, discord.VoiceClient)):
if delay:
await asyncio.sleep(0.01)
if vc.is_playing():
if vc.guild is not None and vc.channel is not None:
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
self.queues.set_nowait('vcs', vcs)
async def save_commit(self) -> None:
await self.queues.commit()
async def _save_all(self, delay: bool, save_playing: bool) -> None:
await self.save_queues(delay)
if save_playing:
await self.save_vcs(delay)
await self.save_commit()
async def save_all(self, delay: bool, save_playing: bool) -> None:
await self._save_all(delay, save_playing)
async def save_job(self):
await self.__vcs_restored
print('starting saving')
while True:
await asyncio.sleep(1)
await self.save_all(True, not self.client.is_closed())
async def save_daemon(self):
try:
await self.save_job()
except asyncio.CancelledError:
pass
async def final_save(self):
self.__save_task.cancel()
if not self.__vcs_restored.done():
self.__vcs_restored.cancel()
else:
try:
await self.save_all(False, False)
print('saved')
except Exception:
traceback.print_exc()
async def _restore_vc(self, guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None:
channels = await guild.fetch_channels()
channel: discord.VoiceChannel
channel, = [
ch for ch in
(
chc for chc in channels
if
isinstance(chc, discord.VoiceChannel)
)
if ch.id == vccid
]
vp: discord.VoiceProtocol = await channel.connect()
assert isinstance(vp, discord.VoiceClient)
vc = vp
await self.descriptor(create=True, force_play=True).main_for_raw_vc(vc)
if vc_is_paused:
vc.pause()
async def restore_vc(self, vcgid: int, vccid: int, vc_is_paused: bool) -> None:
try:
print(f'vc restoring {vcgid}')
guild: discord.Guild = await self.client.fetch_guild(vcgid)
async with lock_for(guild, 'not in a guild'):
await self._restore_vc(guild, vccid, vc_is_paused)
except Exception as e:
print(f'vc {vcgid} {vccid} {vc_is_paused} failed', e)
else:
print(f'vc restored {vcgid} {vccid}')
async def restore_vcs(self) -> None:
vcs: list[tuple[int, int, bool]] = self.queues.get('vcs', [])
try:
tasks = []
for vcgid, vccid, vc_is_paused in vcs:
tasks.append(asyncio.create_task(self.restore_vc(vcgid, vccid, vc_is_paused)))
for task in tasks:
await task
finally:
self.__vcs_restored.set_result(None)
async def restore(self) -> None:
async with lock_for('vcs_restored', '...'):
if not self.__vcs_restored.done():
await self.restore_vcs()
class MainDescriptor:
def __init__(self, service: MainService, *, create: bool, force_play: bool) -> None:
self.mainservice = service
self.mains = service.mains
self.create = create
self.force_play = force_play
async def main_for_raw_vc(self, vc: discord.VoiceClient) -> MainAudio:
if vc.guild in self.mains:
source = self.mains[vc.guild]
elif self.create:
source = self.mains.setdefault(
vc.guild,
await self.mainservice.create(vc.guild)
)
else:
raise Explicit('not playing, use `queue pause` or `queue resume`')
if vc.source != source or self.create and not vc.is_playing() and (self.force_play or not vc.is_paused()):
vc.play(source)
return source
def context(self, ctx: Context) -> 'MainContext':
return MainContext(self, ctx)
class MainContext:
def __init__(self, descriptor: MainDescriptor, ctx: Context) -> None:
self.mainservice = descriptor.mainservice
self.descriptor = descriptor
self.ctx = ctx
async def vc_main(self) -> tuple[discord.VoiceClient, MainAudio]:
vc = await self.mainservice.raw_vc_for(self.ctx)
return vc, await self.descriptor.main_for_raw_vc(vc)
async def vc(self) -> discord.VoiceClient:
vc, _ = await self.vc_main()
return vc
async def main(self) -> MainAudio:
_, source = await self.vc_main()
return source
async def queue(self) -> QueueAudio:
return (await self.main()).queue

View File

@ -3,19 +3,19 @@ from collections import deque
from io import StringIO
import discord
from ptvp35 import Db, KVJson
from v6d3music.config import myroot
from v6d3music.core.caching import Caching
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.fill import FILL
queue_db = Db(myroot / 'queue.db', kvfactory=KVJson())
from ptvp35 import *
PRE_SET_LENGTH = 24
class QueueAudio(discord.AudioSource):
def __init__(self, guild: discord.Guild, respawned: list[YTAudio]):
def __init__(self, db: DbConnection, guild: discord.Guild, respawned: list[YTAudio]):
self.db = db
self.queue: deque[YTAudio] = deque()
for audio in respawned:
self.append(audio)
@ -30,12 +30,12 @@ class QueueAudio(discord.AudioSource):
return
@staticmethod
async def respawned(guild: discord.Guild) -> list[YTAudio]:
async def respawned(caching: Caching, db: DbConnection, guild: discord.Guild) -> list[YTAudio]:
respawned = []
try:
for audio_respawn in queue_db.get(guild.id, []):
for audio_respawn in db.get(guild.id, []):
try:
respawned.append(await YTAudio.respawn(guild, audio_respawn))
respawned.append(await YTAudio.respawn(caching, guild, audio_respawn))
except Exception as e:
print('audio respawn failed', e)
raise
@ -44,16 +44,16 @@ class QueueAudio(discord.AudioSource):
return respawned
@classmethod
async def create(cls, guild: discord.Guild):
return cls(guild, await cls.respawned(guild))
async def create(cls, caching: Caching, db: DbConnection, guild: discord.Guild) -> 'QueueAudio':
return cls(db, guild, await cls.respawned(caching, db, guild))
async def save(self, delay: bool):
async def save(self, delay: bool) -> None:
hybernated = []
for audio in list(self.queue):
if delay:
await asyncio.sleep(0.01)
hybernated.append(audio.hybernate())
queue_db.set_nowait(self.guild.id, hybernated)
self.db.set_nowait(self.guild.id, hybernated)
def append(self, audio: YTAudio):
if len(self.queue) < PRE_SET_LENGTH:
@ -139,6 +139,7 @@ class QueueAudio(discord.AudioSource):
except ValueError:
pass
async def pubjson(self, member: discord.Member) -> list:
async def pubjson(self, member: discord.Member, limit: int) -> list:
import random
audios = list(self.queue)
return [await audio.pubjson(member) for audio in audios]
return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))]

View File

@ -1,27 +1,17 @@
import asyncio
import os
from typing import Optional
from adaas.cachedb import RemoteCache
from v6d3music.core.cache_url import cache_db, cache_url
from v6d3music.core.caching import Caching
from v6d3music.utils.bytes_hash import bytes_hash
from v6d3music.utils.tor_prefix import tor_prefix
from adaas.cachedb import RemoteCache
adaas_available = bool(os.getenv('adaasurl'))
if adaas_available:
print('running real_url through adaas')
_tasks = set()
def _schedule_cache(hurl: str, rurl: str, override: bool, tor: bool):
task = asyncio.create_task(cache_url(hurl, rurl, override, tor))
_tasks.add(task)
task.add_done_callback(_tasks.discard)
async def _resolve_url(url: str, tor: bool) -> str:
args = []
if tor:
@ -39,15 +29,15 @@ async def _resolve_url(url: str, tor: bool) -> str:
return (await ap.stdout.readline()).decode()[:-1]
async def real_url(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:
return await RemoteCache().real_url(url, override, tor)
hurl: str = bytes_hash(url.encode())
if not override:
curl: Optional[str] = cache_db.get(f'url:{hurl}', None)
curl: str | None = caching.get(hurl)
if curl is not None:
print('using cached', hurl)
return curl
rurl: str = await _resolve_url(url, tor)
_schedule_cache(hurl, rurl, override, tor)
caching.schedule_cache(hurl, rurl, override, tor)
return rurl

View File

@ -14,7 +14,8 @@ __all__ = ('YState',)
class YState:
def __init__(self, pool: Pool, ctx: Context, sources: Iterable[UrlCtx]) -> None:
def __init__(self, caching: Caching, pool: Pool, ctx: Context, sources: Iterable[UrlCtx]) -> None:
self.caching = caching
self.pool = pool
self.ctx = ctx
self.sources: deque[UrlCtx] = deque(sources)
@ -52,7 +53,7 @@ class YState:
async def result(self, entry: InfoCtx) -> YTAudio | None:
try:
return await create_ytaudio(self.ctx, entry)
return await create_ytaudio(self.caching, self.ctx, entry)
except Exception:
if not entry.ignore:
raise

View File

@ -1,17 +1,18 @@
from typing import AsyncIterable
from v6d3music.core.entries_effects_for_args import *
from v6d3music.core.default_effects import *
from v6d3music.core.ystate import *
from v6d3music.core.ytaudio import YTAudio
from v6d3music.core.ytaudio import *
from v6d3music.core.caching import Caching
from v6d3music.processing.pool import *
from v6d3music.utils.argctx import *
from v6d2ctx.context import Context
async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
async def yt_audios(caching: Caching, defaulteffects: DefaultEffects, ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
assert ctx.guild is not None
argctx = ArgCtx(default_effects(ctx.guild.id), args)
argctx = ArgCtx(defaulteffects.get(ctx.guild.id), args)
async with Pool(5) as pool:
async for audio in YState(pool, ctx, argctx.sources).iterate():
async for audio in YState(caching, pool, ctx, argctx.sources).iterate():
yield audio

View File

@ -5,6 +5,7 @@ from typing import Optional
import discord
from v6d3music.core.ffmpegnormalaudio import FFmpegNormalAudio
from v6d3music.core.real_url import real_url
from v6d3music.core.caching import Caching
from v6d3music.utils.fill import FILL
from v6d3music.utils.sparq import sparq
from v6d3music.utils.tor_prefix import tor_prefix
@ -22,6 +23,7 @@ class YTAudio(discord.AudioSource):
def __init__(
self,
caching: Caching,
url: str,
origin: str,
description: str,
@ -33,6 +35,7 @@ class YTAudio(discord.AudioSource):
*,
stop_at: int | None = None
):
self.caching = caching
self.url = url
self.origin = origin
self.description = description
@ -181,7 +184,7 @@ class YTAudio(discord.AudioSource):
}
@classmethod
async def respawn(cls, guild: discord.Guild, respawn: dict) -> 'YTAudio':
async def respawn(cls, caching: Caching, guild: discord.Guild, respawn: dict) -> 'YTAudio':
member_id: int | None = respawn['rby']
if member_id is None:
member = None
@ -194,6 +197,7 @@ class YTAudio(discord.AudioSource):
except discord.NotFound:
member = None
audio = YTAudio(
caching,
respawn['url'],
respawn['origin'],
respawn['description'],
@ -209,7 +213,7 @@ class YTAudio(discord.AudioSource):
async def regenerate(self):
try:
print(f'regenerating {self.origin}')
self.url = await real_url(self.origin, True, self.tor)
self.url = await real_url(self.caching, self.origin, True, self.tor)
if hasattr(self, 'source'):
self.source.cleanup()
self.set_source()
@ -228,6 +232,7 @@ class YTAudio(discord.AudioSource):
def copy(self) -> 'YTAudio':
return YTAudio(
self.caching,
self.url,
self.origin,
self.description,
@ -242,6 +247,7 @@ class YTAudio(discord.AudioSource):
raise Explicit('already branched')
self.stop_at = stop_at = self.already_read + 50
audio = YTAudio(
self.caching,
self.url,
self.origin,
self.description,

View File

@ -3,22 +3,19 @@ import contextlib
import os
import sys
import time
import traceback
import discord
from v6d3music.app import MusicAppFactory, session_db
from v6d3music.commands import of
from v6d3music.app import AppContext
from v6d3music.commands import get_of
from v6d3music.config import prefix
from v6d3music.core.cache_url import cache_db
from v6d3music.core.entries_effects_for_args import effects_db
from v6d3music.core.mainasrc import main_for_raw_vc, mainasrcs
from v6d3music.core.mainaudio import volume_db
from v6d3music.core.queueaudio import queue_db
from v6d3music.core.caching import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import MainService
from ptvp35 import *
from rainbowadn.instrument import Instrumentation
from v6d1tokens.client import request_token
from v6d2ctx.handle_content import handle_content
from v6d2ctx.lock_for import lock_for
from v6d2ctx.pain import ABlockMonitor
from v6d2ctx.serve import serve
@ -27,14 +24,7 @@ asyncio.set_event_loop(loop)
class MusicClient(discord.Client):
async def close(self) -> None:
save_task.cancel()
await super().close()
try:
await save_all(False, False)
print('saved')
except Exception:
traceback.print_exc()
pass
client = MusicClient(
@ -49,69 +39,9 @@ client = MusicClient(
reactions=True,
message_content=True,
),
loop=loop
loop=loop,
)
vcs_restored = False
async def _restore_vc(guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None:
channels = await guild.fetch_channels()
channel: discord.VoiceChannel
channel, = [
ch for ch in
(
chc for chc in channels
if
isinstance(chc, discord.VoiceChannel)
)
if ch.id == vccid
]
vp: discord.VoiceProtocol = await channel.connect()
assert isinstance(vp, discord.VoiceClient)
vc = vp
await main_for_raw_vc(vc, create=True, force_play=True)
if vc_is_paused:
vc.pause()
async def restore_vc(vcgid: int, vccid: int, vc_is_paused: bool) -> None:
try:
print(f'vc restoring {vcgid}')
guild: discord.Guild = await client.fetch_guild(vcgid)
async with lock_for(guild, 'not in a guild'):
await _restore_vc(guild, vccid, vc_is_paused)
except Exception as e:
print(f'vc {vcgid} {vccid} {vc_is_paused} failed', e)
else:
print(f'vc restored {vcgid} {vccid}')
async def restore_vcs():
global vcs_restored
vcs: list[tuple[int, int, bool]] = queue_db.get('vcs', [])
try:
tasks = []
for vcgid, vccid, vc_is_paused in vcs:
tasks.append(asyncio.create_task(restore_vc(vcgid, vccid, vc_is_paused)))
for task in tasks:
await task
finally:
vcs_restored = True
@client.event
async def on_ready():
print('ready')
await client.change_presence(
activity=discord.Game(
name='феноменально',
)
)
async with lock_for('vcs_restored', '...'):
if not vcs_restored:
await restore_vcs()
banned_guilds = set(map(int, map(str.strip, os.getenv('banned_guilds', '').split(':'))))
@ -124,64 +54,23 @@ def message_allowed(message: discord.Message) -> bool:
return guild_allowed(message.guild)
@client.event
async def on_message(message: discord.Message) -> None:
if message_allowed(message):
await handle_content(of, message, message.content, prefix, client)
def register_handlers(mainservice: MainService, defaulteffects: DefaultEffects):
of = get_of(mainservice, defaulteffects)
@client.event
async def on_message(message: discord.Message) -> None:
if message_allowed(message):
await handle_content(of, message, message.content, prefix, client)
async def save_queues(delay: bool):
for mainasrc in list(mainasrcs.values()):
if delay:
await asyncio.sleep(0.01)
await mainasrc.queue.save(delay)
async def save_vcs(delay: bool):
if vcs_restored:
vcs = []
vc: discord.VoiceClient
for vc in (vcc for vcc in client.voice_clients if isinstance(vcc, discord.VoiceClient)):
if delay:
await asyncio.sleep(0.01)
if vc.is_playing():
if vc.guild is not None and vc.channel is not None:
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
queue_db.set_nowait('vcs', vcs)
async def save_commit():
await queue_db.commit()
async def save_all(delay: bool, save_playing: bool):
await save_queues(delay)
if save_playing:
await save_vcs(delay)
await save_commit()
async def save_job():
while True:
await asyncio.sleep(1)
await save_all(True, True)
async def save_daemon():
try:
await save_job()
except asyncio.CancelledError:
pass
async def start_app():
await MusicAppFactory.start(client)
async def setup_tasks():
global save_task
save_task = loop.create_task(save_daemon())
loop.create_task(start_app())
@client.event
async def on_ready():
print('ready')
await client.change_presence(
activity=discord.Game(
name='феноменально',
)
)
await mainservice.restore()
class UpgradeABMInit(Instrumentation):
@ -223,8 +112,37 @@ def _upgrade_abm() -> contextlib.ExitStack:
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():
async with volume_db, queue_db, cache_db, session_db, effects_db, ABlockMonitor(delta=0.5):
async with (
DefaultEffects() as defaulteffects,
MainService(client) as mainservice,
AppContext(mainservice),
ABlockMonitor(delta=0.5)
):
register_handlers(mainservice, defaulteffects)
if 'guerilla' in sys.argv:
from pathlib import Path
tokenpath = Path('.token.txt')
@ -238,7 +156,6 @@ async def main():
else:
token = await request_token('music', 'token')
await client.login(token)
loop.create_task(setup_tasks())
if os.getenv('v6tor', None) is None:
print('no tor')
await client.connect()
@ -246,5 +163,5 @@ async def main():
if __name__ == '__main__':
with _upgrade_abm():
with _upgrade_abm(), _db_ee():
serve(main(), client, loop)

View File

@ -1,24 +1,3 @@
__all__ = ('tor_prefix',)
import os
if (address := os.getenv('v6tor', None)) is not None:
print('tor through torsocks')
try:
import socket
address = socket.gethostbyname_ex(address)[2][0]
except Exception:
print('failed tor resolution')
_tor_prefix = None
else:
print('tor successfully resolved')
_tor_prefix = ['torsocks', '--address', address]
else:
print('tor unavailable')
_tor_prefix = None
def tor_prefix():
if _tor_prefix is None:
raise ValueError('tor unavailable')
return _tor_prefix
from adaas.tor_prefix import tor_prefix