servicing

This commit is contained in:
AF 2022-12-25 02:55:21 +00:00
parent b2dc8c5edb
commit 872271fc6f
19 changed files with 201 additions and 99 deletions

View File

@ -280,6 +280,10 @@ class MusicAppFactory(AppFactory):
except Api.MisusedApi as e:
return web.json_response(e.json(), status=404)
@routes.get('/whaturl/')
async def whaturl(request: web.Request) -> web.StreamResponse:
return web.json_response(str(request.url))
class AppContext:
def __init__(self, mainservice: MainService) -> None:

View File

@ -3,19 +3,20 @@ from typing import Callable
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
from v6d3music.utils.effects_for_preset import effects_for_preset
from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.catch import *
from v6d3music.utils.effects_for_preset import *
from v6d3music.utils.options_for_effects import *
from v6d3music.utils.presets import allowed_presets
from v6d2ctx.at_of import AtOf
from v6d2ctx.context import Context, Explicit, command_type
from v6d2ctx.lock_for import lock_for
__all__ = ('get_of',)
def get_of(mainservice: MainService, defaulteffects: DefaultEffects) -> Callable[[str], command_type]:
def get_of(mainservice: MainService) -> Callable[[str], command_type]:
at_of: AtOf[str, command_type] = AtOf()
at, of = at_of()
@ -47,7 +48,7 @@ def get_of(mainservice: MainService, defaulteffects: DefaultEffects) -> Callable
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):
async for audio in mainservice.yt_audios(ctx, args):
queue.append(audio)
await ctx.reply('done')
@ -136,12 +137,12 @@ def get_of(mainservice: MainService, defaulteffects: DefaultEffects) -> Callable
case ['none']:
effects = None
case []:
await ctx.reply(f'current default effects: {defaulteffects.get(ctx.guild.id)}')
await ctx.reply(f'current default effects: {mainservice.defaulteffects.get(ctx.guild.id)}')
return
case _:
raise Explicit('misformatted')
assert_admin(ctx.member)
await defaulteffects.set(ctx.guild.id, effects)
await mainservice.defaulteffects.set(ctx.guild.id, effects)
await ctx.reply(f'effects set to `{effects}`')
@at('repeat')
@ -155,14 +156,13 @@ def get_of(mainservice: MainService, defaulteffects: DefaultEffects) -> Callable
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())
queue.update_sources()
queue.repeat(n)
@at('shuffle')
async def shuffle(ctx: Context, args: list[str]):
assert_admin(ctx.member)
queue = await mainservice.context(ctx, create=False, force_play=False).queue()
queue.shuffle()
@at('branch')
async def branch(ctx: Context, args: list[str]):

View File

@ -1,19 +1,18 @@
import string
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.ytaservicing import *
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.argctx import InfoCtx
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.presets import allowed_effects
from v6d3music.utils.argctx import InfoCtx
from v6d2ctx.context import Context, Explicit, escape
async def create_ytaudio(
caching: Caching, ctx: Context, it: InfoCtx
servicing: YTAServicing, ctx: Context, it: InfoCtx
) -> YTAudio:
assert ctx.member is not None
if it.effects:
@ -25,8 +24,8 @@ async def create_ytaudio(
else:
options = None
return YTAudio(
caching,
await real_url(caching, it.info['url'], False, it.tor),
servicing,
await real_url(servicing.caching, it.info['url'], False, it.tor),
it.info['url'],
f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}',
options,

View File

@ -1,11 +1,13 @@
import discord
from v6d3music.core.caching import Caching
from v6d3music.core.queueaudio import QueueAudio
from v6d3music.core.ytaservicing import *
from v6d3music.core.queueaudio import *
from v6d3music.utils.assert_admin import assert_admin
from ptvp35 import *
from v6d2ctx.context import Explicit
__all__ = ('MainAudio',)
class MainAudio(discord.PCMVolumeTransformer):
def __init__(self, db: DbConnection, queue: QueueAudio, volume: float):
@ -23,5 +25,5 @@ class MainAudio(discord.PCMVolumeTransformer):
await self.db.set(member.guild.id, volume)
@classmethod
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))
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))

View File

@ -1,22 +1,33 @@
import asyncio
import traceback
from contextlib import AsyncExitStack
from typing import AsyncIterable, TypeVar
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 v6d3music.core.caching import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainaudio import *
from v6d3music.core.queueaudio import *
from v6d3music.core.ystate import *
from v6d3music.core.ytaservicing import *
from v6d3music.core.ytaudio import *
from v6d3music.processing.pool import *
from v6d3music.utils.argctx import *
from ptvp35 import *
from v6d2ctx.context import Context, Explicit
from v6d2ctx.lock_for import lock_for
T = TypeVar('T')
class MainService:
def __init__(self, client: discord.Client) -> None:
def __init__(self, defaulteffects: DefaultEffects, client: discord.Client) -> None:
self.defaulteffects = defaulteffects
self.client = client
self.mains: dict[discord.Guild, MainAudio] = {}
self.restore_lock = asyncio.Lock()
@staticmethod
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
@ -51,16 +62,18 @@ class MainService:
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)
return await MainAudio.create(self.__servicing, 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.__queues = await es.enter_async_context(DbFactory(myroot / 'queue.db', kvfactory=KVJson()))
self.__caching = await es.enter_async_context(Caching())
self.__pool = await es.enter_async_context(Pool(5))
self.__servicing = YTAServicing(self.__caching, self.__pool)
self.__vcs_restored: asyncio.Future[None] = asyncio.Future()
self.__es = es.pop_all()
self.__save_task = asyncio.create_task(self.save_daemon())
self.__es = es.pop_all()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
@ -83,10 +96,10 @@ class MainService:
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)
self.__queues.set_nowait('vcs', vcs)
async def save_commit(self) -> None:
await self.queues.commit()
await self.__queues.commit()
async def _save_all(self, delay: bool, save_playing: bool) -> None:
await self.save_queues(delay)
@ -152,7 +165,7 @@ class MainService:
print(f'vc restored {vcgid} {vccid}')
async def restore_vcs(self) -> None:
vcs: list[tuple[int, int, bool]] = self.queues.get('vcs', [])
vcs: list[tuple[int, int, bool]] = self.__queues.get('vcs', [])
try:
tasks = []
for vcgid, vccid, vc_is_paused in vcs:
@ -163,10 +176,16 @@ class MainService:
self.__vcs_restored.set_result(None)
async def restore(self) -> None:
async with lock_for('vcs_restored', '...'):
async with self.restore_lock:
if not self.__vcs_restored.done():
await self.restore_vcs()
async def yt_audios(self, ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
assert ctx.guild is not None
argctx = ArgCtx(self.defaulteffects.get(ctx.guild.id), args)
async for audio in YState(self.__servicing, self.__pool, ctx, argctx.sources).iterate():
yield audio
class MainDescriptor:
def __init__(self, service: MainService, *, create: bool, force_play: bool) -> None:

View File

@ -1,15 +1,21 @@
import asyncio
import random
from collections import deque
from io import StringIO
from typing import MutableSequence
import discord
from v6d3music.core.caching import Caching
from v6d2ctx.context import Explicit
from v6d3music.core.ytaservicing import *
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.fill import FILL
from ptvp35 import *
__all__ = ('QueueAudio',)
PRE_SET_LENGTH = 24
@ -30,12 +36,12 @@ class QueueAudio(discord.AudioSource):
return
@staticmethod
async def respawned(caching: Caching, db: DbConnection, guild: discord.Guild) -> list[YTAudio]:
async def respawned(servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> list[YTAudio]:
respawned = []
try:
for audio_respawn in db.get(guild.id, []):
try:
respawned.append(await YTAudio.respawn(caching, guild, audio_respawn))
respawned.append(await YTAudio.respawn(servicing, guild, audio_respawn))
except Exception as e:
print('audio respawn failed', e)
raise
@ -44,8 +50,8 @@ class QueueAudio(discord.AudioSource):
return respawned
@classmethod
async def create(cls, caching: Caching, db: DbConnection, guild: discord.Guild) -> 'QueueAudio':
return cls(db, guild, await cls.respawned(caching, db, guild))
async def create(cls, servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> 'QueueAudio':
return cls(db, guild, await cls.respawned(servicing, db, guild))
async def save(self, delay: bool) -> None:
hybernated = []
@ -143,3 +149,41 @@ class QueueAudio(discord.AudioSource):
import random
audios = list(self.queue)
return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))]
def repeat(self, n: int) -> None:
if not self.queue:
raise Explicit('empty queue')
if n > 99:
raise Explicit('too long')
audio = self.queue[0]
for _ in range(n):
self.queue.insert(1, audio.copy())
self.update_sources()
def shuffle(self) -> None:
try:
random.shuffle(ForwardView(self.queue))
except:
from traceback import print_exc
print_exc()
self.update_sources()
class ForwardView(MutableSequence[YTAudio]):
def __init__(self, sequence: MutableSequence[YTAudio]) -> None:
self.sequence = sequence
def __len__(self) -> int:
return max(0, self.sequence.__len__() - 1)
def __setitem__(self, index: int, value: YTAudio) -> None:
self.sequence.__setitem__(index + 1, value)
def __getitem__(self, index: int) -> YTAudio:
return self.sequence.__getitem__(index + 1)
def __delitem__(self, index: int | slice) -> None:
self.sequence.__delitem__(index)
def insert(self, index: int, value: YTAudio) -> None:
self.sequence.insert(index, value)

View File

@ -1,7 +1,7 @@
import asyncio
import os
from v6d3music.core.caching import Caching
from v6d3music.core.caching import *
from v6d3music.utils.bytes_hash import bytes_hash
from v6d3music.utils.tor_prefix import tor_prefix

View File

@ -3,6 +3,7 @@ from collections import deque
from contextlib import AsyncExitStack
from typing import AsyncIterable, Iterable
from v6d3music.core.ytaservicing import *
from v6d3music.core.create_ytaudio import *
from v6d3music.core.ytaudio import *
from v6d3music.processing.pool import *
@ -14,8 +15,8 @@ __all__ = ('YState',)
class YState:
def __init__(self, caching: Caching, pool: Pool, ctx: Context, sources: Iterable[UrlCtx]) -> None:
self.caching = caching
def __init__(self, servicing: YTAServicing, pool: Pool, ctx: Context, sources: Iterable[UrlCtx]) -> None:
self.servicing = servicing
self.pool = pool
self.ctx = ctx
self.sources: deque[UrlCtx] = deque(sources)
@ -53,7 +54,7 @@ class YState:
async def result(self, entry: InfoCtx) -> YTAudio | None:
try:
return await create_ytaudio(self.caching, self.ctx, entry)
return await create_ytaudio(self.servicing, self.ctx, entry)
except Exception:
if not entry.ignore:
raise

View File

@ -1,18 +0,0 @@
from typing import AsyncIterable
from v6d3music.core.default_effects import *
from v6d3music.core.ystate import *
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(caching: Caching, defaulteffects: DefaultEffects, ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
assert ctx.guild is not None
argctx = ArgCtx(defaulteffects.get(ctx.guild.id), args)
async with Pool(5) as pool:
async for audio in YState(caching, pool, ctx, argctx.sources).iterate():
yield audio

View File

@ -0,0 +1,11 @@
from v6d3music.core.caching import *
from v6d3music.processing.abstractrunner import *
__all__ = ('YTAServicing',)
class YTAServicing:
def __init__(self, caching: Caching, runner: AbstractRunner) -> None:
self.caching = caching
self.runner = runner

View File

@ -5,25 +5,22 @@ 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
from v6d3music.core.ytaservicing import *
from v6d2ctx.context import Explicit
__all__ = ('YTAudio',)
semaphore = asyncio.BoundedSemaphore(5)
class YTAudio(discord.AudioSource):
source: FFmpegNormalAudio
def __init__(
self,
caching: Caching,
servicing: YTAServicing,
url: str,
origin: str,
description: str,
@ -35,7 +32,7 @@ class YTAudio(discord.AudioSource):
*,
stop_at: int | None = None
):
self.caching = caching
self.servicing = servicing
self.url = url
self.origin = origin
self.description = description
@ -112,8 +109,7 @@ class YTAudio(discord.AudioSource):
self._durations[url] = (await ap.stdout.read()).decode().strip().split('.')[0]
async def update_duration(self):
async with semaphore:
await self._update_duration()
await self.servicing.runner.run(self._update_duration())
def duration(self) -> str:
duration = self._durations.get(self.url)
@ -184,7 +180,7 @@ class YTAudio(discord.AudioSource):
}
@classmethod
async def respawn(cls, caching: Caching, guild: discord.Guild, respawn: dict) -> 'YTAudio':
async def respawn(cls, servicing: YTAServicing, guild: discord.Guild, respawn: dict) -> 'YTAudio':
member_id: int | None = respawn['rby']
if member_id is None:
member = None
@ -197,7 +193,7 @@ class YTAudio(discord.AudioSource):
except discord.NotFound:
member = None
audio = YTAudio(
caching,
servicing,
respawn['url'],
respawn['origin'],
respawn['description'],
@ -213,7 +209,7 @@ class YTAudio(discord.AudioSource):
async def regenerate(self):
try:
print(f'regenerating {self.origin}')
self.url = await real_url(self.caching, self.origin, True, self.tor)
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor)
if hasattr(self, 'source'):
self.source.cleanup()
self.set_source()
@ -232,7 +228,7 @@ class YTAudio(discord.AudioSource):
def copy(self) -> 'YTAudio':
return YTAudio(
self.caching,
self.servicing,
self.url,
self.origin,
self.description,
@ -247,7 +243,7 @@ class YTAudio(discord.AudioSource):
raise Explicit('already branched')
self.stop_at = stop_at = self.already_read + 50
audio = YTAudio(
self.caching,
self.servicing,
self.url,
self.origin,
self.description,

View File

@ -166,7 +166,7 @@ const aUpdateQueueSetup = async (el) => {
while (true) {
await sleep(2);
if (queue !== null && queue.queuejson.length > 100) {
await sleep(queue.queuejson.length / 100);
await sleep((queue.queuejson.length - 100) / 200);
}
const newQueue = await aQueue();
await aUpdateQueueOnce(newQueue, el);

View File

@ -0,0 +1,13 @@
from typing import Any, Coroutine, TypeVar
from abc import ABC, abstractmethod
T = TypeVar('T')
__all__ = ('AbstractRunner',)
class AbstractRunner(ABC):
@abstractmethod
async def run(self, coro: Coroutine[Any, Any, T]) -> T:
raise NotImplementedError

View File

@ -1,7 +1,7 @@
import asyncio
from typing import Union
from typing import Any, Coroutine, Generic, TypeVar, Union
from v6d3music.processing.yprocess import *
from .abstractrunner import AbstractRunner
__all__ = ('Job', 'Pool', 'JobDescriptor',)
@ -164,7 +164,23 @@ class Working:
return self.__worker.busy()
class Pool:
T = TypeVar('T')
class CoroJD(JobDescriptor, Generic[T]):
def __init__(self, coro: Coroutine[Any, Any, T]) -> None:
self.future = asyncio.Future()
self.coro = coro
async def run(self) -> JobDescriptor | None:
try:
self.future.set_result(await self.coro)
except BaseException as e:
self.future.set_exception(e)
return None
class Pool(AbstractRunner):
def __init__(self, workers: int) -> None:
if workers < 1:
raise ValueError('non-positive number of workers')
@ -203,6 +219,11 @@ class Pool:
def workers(self) -> int:
return self.__workers
async def run(self, coro: Coroutine[Any, Any, T]) -> T:
job = CoroJD(coro)
self.submit(job.wrap())
return await job.future
class JDC:
def __init__(self, descriptor: JobDescriptor, pool: Pool) -> None:

View File

@ -6,7 +6,7 @@ import time
import discord
from v6d3music.app import AppContext
from v6d3music.commands import get_of
from v6d3music.commands import *
from v6d3music.config import prefix
from v6d3music.core.caching import *
from v6d3music.core.default_effects import *
@ -54,8 +54,8 @@ def message_allowed(message: discord.Message) -> bool:
return guild_allowed(message.guild)
def register_handlers(mainservice: MainService, defaulteffects: DefaultEffects):
of = get_of(mainservice, defaulteffects)
def register_handlers(mainservice: MainService):
of = get_of(mainservice)
@client.event
async def on_message(message: discord.Message) -> None:
@ -138,11 +138,11 @@ def _db_ee() -> contextlib.ExitStack:
async def main():
async with (
DefaultEffects() as defaulteffects,
MainService(client) as mainservice,
MainService(defaulteffects, client) as mainservice,
AppContext(mainservice),
ABlockMonitor(delta=0.5)
):
register_handlers(mainservice, defaulteffects)
register_handlers(mainservice)
if 'guerilla' in sys.argv:
from pathlib import Path
tokenpath = Path('.token.txt')

View File

@ -4,6 +4,7 @@ from v6d3music.utils.effects_for_preset import effects_for_preset
from v6d3music.utils.entries_for_url import entries_for_url
from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.sparq import sparq
from v6d2ctx.context import Explicit
__all__ = ('InfoCtx', 'UrlCtx', 'ArgCtx',)
@ -61,14 +62,16 @@ class ArgCtx:
case [*args]:
pass
ctx.already_read = round(seconds / sparq(options_for_effects(effects)))
match args:
case ['tor', *args]:
ctx.tor = True
case [*args]:
pass
match args:
case ['ignore', *args]:
ctx.ignore = True
case [*args]:
pass
while True:
match args:
case ['tor', *args]:
if ctx.tor:
raise Explicit('duplicate tor')
ctx.tor = True
case ['ignore', *args]:
if ctx.ignore:
raise Explicit('duplicate ignore')
ctx.ignore = True
case [*args]:
break
self.sources.append(ctx)

View File

@ -2,6 +2,8 @@ from typing import Iterable
from v6d2ctx.context import Context, Implicit
__all__ = ('catch',)
async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str), attachments_ok=True):
if ctx.message.attachments and attachments_ok:

View File

@ -3,6 +3,9 @@ from v6d2ctx.context import Explicit
from v6d3music.utils.presets import presets
__all__ = ('effects_for_preset',)
def effects_for_preset(preset: str) -> str:
if preset in presets:
return presets[preset]

View File

@ -1,6 +1,8 @@
import shlex
from typing import Optional
__all__ = ('options_for_effects',)
def options_for_effects(effects: str | None) -> Optional[str]:
return f'-af {shlex.quote(effects)}' if effects else None