move extract, remove tor

This commit is contained in:
AF 2023-05-28 21:53:54 +00:00
parent 8e52889d8f
commit 715d4fbf61
35 changed files with 280 additions and 697 deletions

View File

@ -1 +0,0 @@
trial_token=paste here

View File

@ -4,7 +4,6 @@ WORKDIR /v6
ENV v6root=/v6data ENV v6root=/v6data
RUN apt-get update RUN apt-get update
RUN apt-get install -y libopus0 opus-tools ffmpeg RUN apt-get install -y libopus0 opus-tools ffmpeg
RUN apt-get install -y tor
COPY base.requirements.txt base.requirements.txt COPY base.requirements.txt base.requirements.txt
RUN pip install -r base.requirements.txt RUN pip install -r base.requirements.txt
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt

View File

@ -1,29 +1 @@
# music bot # music bot
## try for yourself
default prefix is `?/`
### install docker compose
follow instructions at https://docs.docker.com/compose/install/
from https://docs.docker.com/compose/install/linux/ :
```sh
sudo apt-get update
sudo apt-get install docker-compose-plugin
```
or
```sh
sudo yum update
sudo yum install docker-compose-plugin
```
### clone repo
```sh
git clone https://gitea.parrrate.ru/PTV/v6d3music.git
cd v6d3music
```
### set token
```sh
cp .trial_token_example.env .trial_token.env
vim .trial_token.env
```
### start or update
```sh
docker compose up -d --build
```

View File

@ -1,4 +1,3 @@
aiohttp>=3.7.4,<4 aiohttp>=3.7.4,<4
discord.py[voice]~=2.2.2 discord.py[voice]~=2.2.2
yt-dlp~=2023.2.17
typing_extensions~=4.4.0 typing_extensions~=4.4.0

View File

@ -1,14 +0,0 @@
services:
music-bot-trial:
build: .
volumes:
- "/v6data"
env_file:
- .trial_token.env
deploy:
resources:
limits:
cpus: '2'
memory: 2G
tty: true
stop_signal: SIGINT

View File

@ -6,11 +6,11 @@ For Users
command syntax:: command syntax::
?/play url [- effects | + preset] [[[h] m] s] [tor|ignore]* ... ?/play url [- effects | + preset] [[[h] m] s] [ignore]* ...
examples:: examples::
?/play http://127.0.0.1/audio.mp3 + bassboost tor ?/play http://127.0.0.1/audio.mp3 + bassboost
?/play http://127.0.0.1/audio.mp3 - "bass=g=10" 23 59 59 ignore ?/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 ?/play http://127.0.0.1/audio.mp3 http://127.0.0.1/audio.mp3

View File

@ -5,11 +5,11 @@ import discord
from typing_extensions import Self from typing_extensions import Self
from rainbowadn.instrument import Instrumentation from rainbowadn.instrument import Instrumentation
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d2ctx.integration.responsetype import * from v6d2ctx.integration.responsetype import ResponseType
from v6d2ctx.integration.targets import * from v6d2ctx.integration.targets import Async, JsonLike, Targets
from v6d3music.core.mainaudio import * from v6d3music.core.mainaudio import MainAudio
from v6d3music.core.mainservice import * from v6d3music.core.mainservice import MainService
__all__ = ("Api",) __all__ = ("Api",)

View File

@ -4,13 +4,13 @@ from typing import Callable
import discord import discord
from v6d2ctx.at_of import AtOf from v6d2ctx.at_of import AtOf
from v6d2ctx.context import * from v6d2ctx.context import Context, Explicit, command_type
from v6d3music.core.default_effects import * from v6d3music.core.default_effects import DefaultEffects
from v6d3music.core.mainservice import * from v6d3music.core.mainservice import MainService
from v6d3music.utils.assert_admin import * from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.catch import * from v6d3music.utils.catch import catch
from v6d3music.utils.effects_for_preset import * from v6d3music.utils.effects_for_preset import effects_for_preset
from v6d3music.utils.presets import * from v6d3music.utils.presets import allowed_presets
__all__ = ("get_of",) __all__ = ("get_of",)
@ -37,7 +37,7 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
args, args,
f""" f"""
`play ...args` `play ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args` `play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [ignore] ...args`
`pause` `pause`
`resume` `resume`
presets: {shlex.join(allowed_presets)} presets: {shlex.join(allowed_presets)}

View File

@ -1,75 +0,0 @@
import asyncio
from contextlib import AsyncExitStack
from ptvp35 import *
from v6d2ctx.lock_for import *
from v6d3music.config import myroot
from v6d3music.utils.tor_prefix import *
__all__ = ('Caching',)
cache_root = myroot / 'cache'
cache_root.mkdir(exist_ok=True)
class Caching:
async def _do_cache(self, hurl: str, rurl: str, tor: bool) -> None:
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))
async def _cache_logged(self, hurl: str, rurl: str, tor: bool) -> None:
print('caching', hurl)
await self._do_cache(hurl, rurl, tor)
print('cached', hurl)
async def _cache_url(self, hurl: str, rurl: str, override: bool, tor: bool) -> None:
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:
await self._cache_logged(hurl, rurl, tor)
else:
await self.__db.set(f'cachable:{hurl}', True)
async def cache_url(self, hurl: str, rurl: str, override: bool, tor: bool) -> None:
async with self.__locks.lock_for(('cache', hurl), 'cache failed'):
await self._cache_url(hurl, rurl, override, tor)
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.__locks = Locks()
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

@ -1,29 +1,24 @@
from v6d2ctx.context import * from v6d2ctx.context import Context
from v6d3music.core.real_url import * from v6d3music.core.real_url import real_url
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaudio import * from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.argctx import * from v6d3music.utils.argctx import BoundCtx, InfoCtx
__all__ = ('create_ytaudio',) __all__ = ("create_ytaudio",)
async def _create_ytaudio( async def _create_ytaudio(servicing: YTAServicing, bound: BoundCtx) -> YTAudio:
servicing: YTAServicing, bound: BoundCtx
) -> YTAudio:
return YTAudio( return YTAudio(
servicing, servicing,
await real_url(servicing.caching, bound.url, False, bound.tor), await real_url(bound.url, False),
bound.url, bound.url,
bound.description, bound.description,
bound.options, bound.options,
bound.member, bound.member,
bound.already_read, bound.already_read,
bound.tor
) )
async def create_ytaudio( async def create_ytaudio(servicing: YTAServicing, ctx: Context, it: InfoCtx) -> YTAudio:
servicing: YTAServicing, ctx: Context, it: InfoCtx
) -> YTAudio:
bound = it.bind(ctx) bound = it.bind(ctx)
return await _create_ytaudio(servicing, bound) return await _create_ytaudio(servicing, bound)

View File

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

View File

@ -6,37 +6,37 @@ from typing import Optional
import discord import discord
from v6d3music.utils.fill import * from v6d3music.utils.fill import FILL
from v6d3music.utils.tor_prefix import *
__all__ = ('FFmpegNormalAudio',) __all__ = ("FFmpegNormalAudio",)
class FFmpegNormalAudio(discord.FFmpegAudio): class FFmpegNormalAudio(discord.FFmpegAudio):
def __init__( def __init__(
self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None, self,
tor: bool source,
*,
executable="ffmpeg",
pipe=False,
stderr=None,
before_options=None,
options=None,
): ):
self.source = source self.source = source
args = [] args = []
if tor: subprocess_kwargs = {"stdin": source if pipe else subprocess.DEVNULL, "stderr": stderr}
_tor_prefix = tor_prefix()
args.extend([*_tor_prefix[1:], executable])
executable = _tor_prefix[0]
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
if isinstance(before_options, str): if isinstance(before_options, str):
args.extend(shlex.split(before_options)) args.extend(shlex.split(before_options))
args.append('-i') args.append("-i")
args.append('-' if pipe else source) args.append("-" if pipe else source)
args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning')) args.extend(("-f", "s16le", "-ar", "48000", "-ac", "2", "-loglevel", "warning"))
if isinstance(options, str): if isinstance(options, str):
args.extend(shlex.split(options)) args.extend(shlex.split(options))
args.append('pipe:1') args.append("pipe:1")
super().__init__(source, executable=executable, args=args, **subprocess_kwargs) super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
@ -85,9 +85,9 @@ class FFmpegNormalAudio(discord.FFmpegAudio):
ret = self._raw_read() ret = self._raw_read()
if len(ret) != discord.opus.Encoder.FRAME_SIZE: if len(ret) != discord.opus.Encoder.FRAME_SIZE:
if self._process.poll() is None: if self._process.poll() is None:
print('poll') print("poll")
return FILL return FILL
return b'' return b""
self.loaded_at = time.time() self.loaded_at = time.time()
self._loaded = True self._loaded = True
return ret return ret

View File

@ -1,12 +1,12 @@
import discord import discord
from ptvp35 import * from ptvp35 import DbConnection
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d3music.core.queueaudio import * from v6d3music.core.queueaudio import QueueAudio
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.utils.assert_admin import * from v6d3music.utils.assert_admin import assert_admin
__all__ = ('MainAudio',) __all__ = ("MainAudio",)
class MainAudio(discord.PCMVolumeTransformer): class MainAudio(discord.PCMVolumeTransformer):
@ -18,9 +18,9 @@ class MainAudio(discord.PCMVolumeTransformer):
async def set(self, volume: float, member: discord.Member): async def set(self, volume: float, member: discord.Member):
assert_admin(member) assert_admin(member)
if volume < 0.01: if volume < 0.01:
raise Explicit('volume too small') raise Explicit("volume too small")
if volume > 1: if volume > 1:
raise Explicit('volume too big') raise Explicit("volume too big")
self.volume = volume self.volume = volume
await self.db.set(member.guild.id, volume) await self.db.set(member.guild.id, volume)
@ -28,5 +28,7 @@ class MainAudio(discord.PCMVolumeTransformer):
return self.db.get(self.queue.guild.id, 0.2) 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)) return cls(db, await QueueAudio.create(servicing, queues, guild))

View File

@ -6,29 +6,28 @@ from typing import AsyncIterable, Callable, TypeVar
import discord import discord
import v6d3music.processing.pool import v6d3music.processing.pool
from ptvp35 import * from ptvp35 import DbFactory, KVJson
from v6d2ctx.context import * from v6d2ctx.context import Context, Explicit
from v6d2ctx.integration.event import * from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import * from v6d2ctx.integration.responsetype import ResponseType
from v6d2ctx.integration.targets import * from v6d2ctx.integration.targets import Async, Targets
from v6d2ctx.lock_for import * from v6d2ctx.lock_for import Locks
from v6d3music.config import myroot from v6d3music.config import myroot
from v6d3music.core.caching import * from v6d3music.core.default_effects import DefaultEffects
from v6d3music.core.default_effects import * from v6d3music.core.mainaudio import MainAudio
from v6d3music.core.mainaudio import * from v6d3music.core.monitoring import Monitoring, PersistentMonitoring
from v6d3music.core.monitoring import * from v6d3music.core.queueaudio import QueueAudio
from v6d3music.core.queueaudio import * from v6d3music.core.ystate import YState
from v6d3music.core.ystate import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaudio import YTAudio
from v6d3music.core.ytaudio import * from v6d3music.processing.pool import Pool, PoolEvent
from v6d3music.processing.pool import * from v6d3music.utils.argctx import ArgCtx
from v6d3music.utils.assert_admin import assert_admin from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.argctx import *
__all__ = ('MainService', 'MainMode', 'MainContext', 'MainEvent') __all__ = ("MainService", "MainMode", "MainContext", "MainEvent")
T = TypeVar('T') T = TypeVar("T")
class MainEvent(Event): class MainEvent(Event):
@ -40,7 +39,7 @@ class _PMEvent(MainEvent):
self.event = event self.event = event
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'pool': self.event.json()} return {"pool": self.event.json()}
class _PMSendable(SendableEvents[PoolEvent]): class _PMSendable(SendableEvents[PoolEvent]):
@ -69,7 +68,7 @@ class MainService:
self.__ystates: dict[discord.Guild, YState] = {} self.__ystates: dict[discord.Guild, YState] = {}
def register_instrumentation(self): def register_instrumentation(self):
self.targets.register_type(v6d3music.processing.pool.UnitJob, 'run', Async) 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:
@ -77,10 +76,10 @@ class MainService:
if vc is None or vc.channel is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected(): if vc is None or vc.channel is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
vs: discord.VoiceState | None = member.voice vs: discord.VoiceState | None = member.voice
if vs is None: if vs is None:
raise Explicit('not connected') raise Explicit("not connected")
vch: discord.abc.Connectable | None = vs.channel vch: discord.abc.Connectable | None = vs.channel
if vch is None: if vch is None:
raise Explicit('not connected') raise Explicit("not connected")
try: try:
vc = await vch.connect() vc = await vch.connect()
except discord.ClientException as e: except discord.ClientException as e:
@ -89,32 +88,31 @@ class MainService:
assert vc is not None assert vc is not None
await member.guild.fetch_channels() await member.guild.fetch_channels()
await vc.disconnect(force=True) await vc.disconnect(force=True)
raise Explicit('try again later') from e raise Explicit("try again later") from e
assert isinstance(vc, discord.VoiceClient) assert isinstance(vc, discord.VoiceClient)
return vc return vc
async def raw_vc_for(self, ctx: Context) -> discord.VoiceClient: async def raw_vc_for(self, ctx: Context) -> discord.VoiceClient:
if ctx.member is None: if ctx.member is None:
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 mode(self, *, create: bool, force_play: bool) -> 'MainMode': def mode(self, *, create: bool, force_play: bool) -> "MainMode":
return MainMode(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.mode(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)
async def __aenter__(self) -> 'MainService': async def __aenter__(self) -> "MainService":
async with AsyncExitStack() as es: async with AsyncExitStack() as es:
self.__locks = Locks() self.__locks = Locks()
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.__pool = await es.enter_async_context(Pool(5, self.__pool_events)) self.__pool = await es.enter_async_context(Pool(5, self.__pool_events))
self.__servicing = YTAServicing(self.__caching, self.__pool) self.__servicing = YTAServicing(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.monitoring = await es.enter_async_context(Monitoring())
@ -143,7 +141,7 @@ class MainService:
if vc.is_playing(): if vc.is_playing():
if vc.guild is not None and vc.channel is not None: if vc.guild is not None and vc.channel is not None:
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused())) 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: async def save_commit(self) -> None:
await self.__queues.commit() await self.__queues.commit()
@ -159,7 +157,7 @@ class MainService:
async def save_job(self): async def save_job(self):
await self.__vcs_restored await self.__vcs_restored
print('starting saving') print("starting saving")
while True: while True:
await asyncio.sleep(1) await asyncio.sleep(1)
await self.save_all(True, not self.client.is_closed()) await self.save_all(True, not self.client.is_closed())
@ -177,22 +175,14 @@ class MainService:
else: else:
try: try:
await self.save_all(False, False) await self.save_all(False, False)
print('saved') print("saved")
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
async def _restore_vc(self, guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None: async def _restore_vc(self, guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None:
channels = await guild.fetch_channels() channels = await guild.fetch_channels()
channel: discord.VoiceChannel channel: discord.VoiceChannel
channel, = [ (channel,) = [ch for ch in (chc for chc in channels if isinstance(chc, discord.VoiceChannel)) if ch.id == vccid]
ch for ch in
(
chc for chc in channels
if
isinstance(chc, discord.VoiceChannel)
)
if ch.id == vccid
]
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
@ -201,22 +191,22 @@ class MainService:
vc.pause() vc.pause()
def lock_for(self, guild: discord.Guild | None) -> asyncio.Lock: def lock_for(self, guild: discord.Guild | None) -> asyncio.Lock:
return self.__locks.lock_for(guild, 'not in a guild') return self.__locks.lock_for(guild, "not in a guild")
async def restore_vc(self, vcgid: int, vccid: int, vc_is_paused: bool) -> None: async def restore_vc(self, vcgid: int, vccid: int, vc_is_paused: bool) -> None:
try: try:
print(f'vc restoring {vcgid}') print(f"vc restoring {vcgid}")
guild: discord.Guild = await self.client.fetch_guild(vcgid) guild: discord.Guild = await self.client.fetch_guild(vcgid)
async with self.lock_for(guild): async with self.lock_for(guild):
await self._restore_vc(guild, vccid, vc_is_paused) await self._restore_vc(guild, vccid, vc_is_paused)
except Exception as e: except Exception as e:
print(f'vc {vcgid} {vccid} {vc_is_paused} failed') print(f"vc {vcgid} {vccid} {vc_is_paused} failed")
traceback.print_exc() traceback.print_exc()
else: else:
print(f'vc restored {vcgid} {vccid}') print(f"vc restored {vcgid} {vccid}")
async def restore_vcs(self) -> None: 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: try:
tasks = [] tasks = []
for vcgid, vccid, vc_is_paused in vcs: for vcgid, vccid, vc_is_paused in vcs:
@ -266,17 +256,14 @@ class MainMode:
if vc.guild in self.mains: if vc.guild in self.mains:
source = self.mains[vc.guild] source = self.mains[vc.guild]
elif self.create: elif self.create:
source = self.mains.setdefault( source = self.mains.setdefault(vc.guild, await self.mainservice.create(vc.guild))
vc.guild,
await self.mainservice.create(vc.guild)
)
else: else:
raise Explicit('not playing, use `queue pause` or `queue resume`') 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()): if vc.source != source or self.create and not vc.is_playing() and (self.force_play or not vc.is_paused()):
vc.play(source) vc.play(source)
return source return source
def context(self, ctx: Context) -> 'MainContext': def context(self, ctx: Context) -> "MainContext":
return MainContext(self, ctx) return MainContext(self, ctx)

View File

@ -7,14 +7,14 @@ from typing import MutableSequence
import discord import discord
from ptvp35 import * from ptvp35 import DbConnection
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaudio import * from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.assert_admin import * from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.fill import * from v6d3music.utils.fill import FILL
__all__ = ('QueueAudio',) __all__ = ("QueueAudio",)
PRE_SET_LENGTH = 6 PRE_SET_LENGTH = 6
@ -45,15 +45,15 @@ class QueueAudio(discord.AudioSource):
try: try:
respawned.append(await YTAudio.respawn(servicing, guild, audio_respawn)) respawned.append(await YTAudio.respawn(servicing, guild, audio_respawn))
except Exception as e: except Exception as e:
print('audio respawn failed', e) print("audio respawn failed", e)
raise raise
except Exception as e: except Exception as e:
print('queue respawn failed', e) print("queue respawn failed", e)
traceback.print_exc() traceback.print_exc()
return respawned return respawned
@classmethod @classmethod
async def create(cls, servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> 'QueueAudio': async def create(cls, servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> "QueueAudio":
return cls(db, guild, await cls.respawned(servicing, db, guild)) return cls(db, guild, await cls.respawned(servicing, db, guild))
async def save(self, delay: bool) -> None: async def save(self, delay: bool) -> None:
@ -139,18 +139,18 @@ class QueueAudio(discord.AudioSource):
async def format(self, limit=100) -> str: async def format(self, limit=100) -> str:
if limit > 100 or 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()
lst = list(self.queue) lst = list(self.queue)
llst = len(lst) llst = len(lst)
def write(): 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: if limit >= 0:
for i, audio in enumerate(lst): for i, audio in enumerate(lst):
if i >= limit: if i >= limit:
stream.write(f'cutting queue at {limit} results, {llst - limit} remaining.\n') stream.write(f"cutting queue at {limit} results, {llst - limit} remaining.\n")
break break
write() write()
else: else:
@ -169,18 +169,19 @@ class QueueAudio(discord.AudioSource):
async def pubjson(self, member: discord.Member, limit: int) -> list: async def pubjson(self, member: discord.Member, limit: int) -> list:
import random import random
audios = list(self.queue) audios = list(self.queue)
return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))] return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))]
def repeat(self, n: int, p: int, t: int | None) -> None: def repeat(self, n: int, p: int, t: int | None) -> None:
if not self.queue: if not self.queue:
raise Explicit('empty queue') raise Explicit("empty queue")
if n > 99: if n > 99:
raise Explicit('too long') raise Explicit("too long")
try: try:
audio = self.queue[p] audio = self.queue[p]
except IndexError: except IndexError:
raise Explicit('no track at that index') raise Explicit("no track at that index")
for _ in range(n): for _ in range(n):
if t is None: if t is None:
self.queue.append(audio.copy()) self.queue.append(audio.copy())
@ -196,7 +197,7 @@ class QueueAudio(discord.AudioSource):
def branch(self, effects: str | None) -> None: def branch(self, effects: str | None) -> None:
if not self.queue: if not self.queue:
raise Explicit('empty queue') raise Explicit("empty queue")
audio = self.queue[0].branch() audio = self.queue[0].branch()
if effects is not None: if effects is not None:
audio.set_effects(effects or None) audio.set_effects(effects or None)

View File

@ -1,44 +1,22 @@
import asyncio import aiohttp
import os
from adaas.cachedb import * from adaas.cachedb import RemoteCache
from v6d3music.core.caching import *
from v6d3music.utils.bytes_hash import *
from v6d3music.utils.tor_prefix import *
__all__ = ('real_url',) __all__ = ("real_url",)
adaas_available = bool(os.getenv('adaasurl'))
if adaas_available:
print('running real_url through adaas')
async def _resolve_url(url: str, tor: bool) -> str: async def real_url(url: str, override: bool) -> str:
args = [] base = RemoteCache()
if tor: async with aiohttp.ClientSession() as session:
args.extend(tor_prefix()) async with session.post(
args.extend( f"{base}/resolve",
[ json={
'yt-dlp', '--no-playlist', '-f', 'bestaudio', '-g', '--', url, "base": base,
] "url": url,
) "uncached": override,
ap = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE) "x": True,
code = await ap.wait() },
if code: ) as resp:
raise RuntimeError(code) if resp.status != 200:
assert ap.stdout is not None raise IOError(resp.status)
return (await ap.stdout.readline()).decode()[:-1] return await resp.text()
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, True)
hurl: str = bytes_hash(url.encode())
if not override:
curl: str | None = caching.get(hurl)
if curl is not None:
print('using cached', hurl)
return curl
rurl: str = await _resolve_url(url, tor)
caching.schedule_cache(hurl, rurl, override, tor)
return rurl

View File

@ -3,13 +3,13 @@ from collections import deque
from contextlib import AsyncExitStack from contextlib import AsyncExitStack
from typing import Any, AsyncIterable, Callable, Coroutine, Iterable from typing import Any, AsyncIterable, Callable, Coroutine, Iterable
from v6d2ctx.context import * from v6d2ctx.context import Context, Explicit
from v6d2ctx.integration.responsetype import * from v6d2ctx.integration.responsetype import ResponseType, cast_to_response
from v6d3music.core.create_ytaudio import * from v6d3music.core.create_ytaudio import create_ytaudio
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaudio import * from v6d3music.core.ytaudio import YTAudio
from v6d3music.processing.pool import * from v6d3music.processing.pool import JobContext, JobStatusChanged, JobUnit, Pool
from v6d3music.utils.argctx import * from v6d3music.utils.argctx import InfoCtx, UrlCtx
__all__ = ("YState",) __all__ = ("YState",)
@ -130,7 +130,6 @@ class YStream(JobUnit):
"info": cast_to_response(entry.info), "info": cast_to_response(entry.info),
"effects": entry.effects, "effects": entry.effects,
"already_read": entry.already_read, "already_read": entry.already_read,
"tor": entry.tor,
"ignore": entry.ignore, "ignore": entry.ignore,
}, },
) )
@ -155,7 +154,6 @@ class YStream(JobUnit):
"url": source.url, "url": source.url,
"effects": source.effects, "effects": source.effects,
"already_read": source.already_read, "already_read": source.already_read,
"tor": source.tor,
"ignore": source.ignore, "ignore": source.ignore,
}, },
) )

View File

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

View File

@ -6,15 +6,14 @@ from typing import Optional
import discord import discord
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d3music.core.ffmpegnormalaudio import * from v6d3music.core.ffmpegnormalaudio import FFmpegNormalAudio
from v6d3music.core.real_url import * from v6d3music.core.real_url import real_url
from v6d3music.core.ytaservicing import * from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.processing.abstractrunner import * from v6d3music.processing.abstractrunner import CoroContext, CoroStatusChanged
from v6d3music.utils.fill import * from v6d3music.utils.fill import FILL
from v6d3music.utils.options_for_effects import * from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.sparq import * from v6d3music.utils.sparq import sparq
from v6d3music.utils.tor_prefix import *
__all__ = ("YTAudio",) __all__ = ("YTAudio",)
@ -31,7 +30,6 @@ class YTAudio(discord.AudioSource):
options: Optional[str], options: Optional[str],
rby: discord.Member | None, rby: discord.Member | None,
already_read: int, already_read: int,
tor: bool,
/, /,
*, *,
stop_at: int | None = None, stop_at: int | None = None,
@ -46,7 +44,6 @@ class YTAudio(discord.AudioSource):
self.options = options self.options = options
self.rby = rby self.rby = rby
self.already_read = already_read self.already_read = already_read
self.tor = tor
self.regenerating = False self.regenerating = False
# self.set_source() # self.set_source()
self._durations: dict[str, str] = {} self._durations: dict[str, str] = {}
@ -74,9 +71,7 @@ class YTAudio(discord.AudioSource):
def set_source(self): def set_source(self):
self.schedule_duration_update() self.schedule_duration_update()
self.source = FFmpegNormalAudio( self.source = FFmpegNormalAudio(self.url, options=self.options, before_options=self.before_options())
self.url, options=self.options, before_options=self.before_options(), tor=self.tor
)
def set_already_read(self, already_read: int): def set_already_read(self, already_read: int):
self.already_read = already_read self.already_read = already_read
@ -110,9 +105,6 @@ class YTAudio(discord.AudioSource):
if url in self._durations: if url in self._durations:
return return
self._durations.setdefault(url, "") self._durations.setdefault(url, "")
if self.tor:
args = [*tor_prefix()]
else:
args = [] args = []
args += [ args += [
"ffprobe", "ffprobe",
@ -225,7 +217,6 @@ class YTAudio(discord.AudioSource):
"options": self.options, "options": self.options,
"rby": None if self.rby is None else self.rby.id, "rby": None if self.rby is None else self.rby.id,
"already_read": self.already_read, "already_read": self.already_read,
"tor": self.tor,
"stop_at": self.stop_at, "stop_at": self.stop_at,
"durations": self._reduced_durations(), "durations": self._reduced_durations(),
} }
@ -251,7 +242,6 @@ class YTAudio(discord.AudioSource):
respawn["options"], respawn["options"],
member, member,
respawn["already_read"], respawn["already_read"],
respawn.get("tor", False),
stop_at=respawn.get("stop_at", None), stop_at=respawn.get("stop_at", None),
) )
audio._durations |= respawn.get("durations", {}) audio._durations |= respawn.get("durations", {})
@ -260,7 +250,7 @@ class YTAudio(discord.AudioSource):
async def regenerate(self, reason: str): async def regenerate(self, reason: str):
try: try:
print(f"regenerating {self.origin} {reason=}") print(f"regenerating {self.origin} {reason=}")
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor) self.url = await real_url(self.origin, True)
if hasattr(self, "source"): if hasattr(self, "source"):
self.source.cleanup() self.source.cleanup()
self.set_source() self.set_source()
@ -286,7 +276,6 @@ class YTAudio(discord.AudioSource):
self.options, self.options,
self.rby, self.rby,
0, 0,
self.tor,
) )
def branch(self) -> "YTAudio": def branch(self) -> "YTAudio":
@ -301,6 +290,5 @@ class YTAudio(discord.AudioSource):
self.options, self.options,
self.rby, self.rby,
stop_at, stop_at,
self.tor,
) )
return audio return audio

View File

@ -1,29 +1,26 @@
import asyncio import asyncio
import contextlib import contextlib
import os import os
import struct
import sys import sys
import time import time
from traceback import print_exc from traceback import print_exc
from typing import Any
import discord import discord
from ptvp35 import * from ptvp35 import DbConnection
from rainbowadn.instrument import Instrumentation from rainbowadn.instrument import Instrumentation
from v6d1tokens.client import * from v6d1tokens.client import request_token
from v6d2ctx.handle_content import * from v6d2ctx.handle_content import handle_content
from v6d2ctx.integration.event import * from v6d2ctx.integration.event import Events
from v6d2ctx.integration.targets import * from v6d2ctx.integration.targets import Targets
from v6d2ctx.pain import ABlockMonitor, ALog from v6d2ctx.pain import ABlockMonitor
from v6d2ctx.serve import * from v6d2ctx.serve import serve
from v6d3music.api import * from v6d3music.api import Api
from v6d3music.app import * from v6d3music.app import AppContext
from v6d3music.commands import * from v6d3music.commands import get_of
from v6d3music.config import prefix from v6d3music.config import prefix
from v6d3music.core.caching import * from v6d3music.core.default_effects import DefaultEffects
from v6d3music.core.default_effects import * from v6d3music.core.mainservice import MainService
from v6d3music.core.mainservice import *
from v6d3music.core.set_config import set_config from v6d3music.core.set_config import set_config
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
@ -169,13 +166,9 @@ async def amain(client: discord.Client):
else: else:
token = input("token:") token = input("token:")
tokenpath.write_text(token) tokenpath.write_text(token)
elif token_ := os.getenv("trial_token"):
token = token_
else: else:
token = await request_token("music", "token") token = await request_token("music", "token")
await client.login(token) await client.login(token)
if os.getenv("v6tor", None) is None:
print("no tor")
await client.connect() await client.connect()
print("exited") print("exited")

View File

@ -1,12 +1,12 @@
__all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged') __all__ = ("AbstractRunner", "CoroEvent", "CoroContext", "CoroStatusChanged")
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Callable, Coroutine, TypeVar from typing import Any, Callable, Coroutine, TypeVar
from v6d2ctx.integration.event import * from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import * from v6d2ctx.integration.responsetype import ResponseType
T = TypeVar('T') T = TypeVar("T")
class CoroEvent(Event): class CoroEvent(Event):
@ -23,7 +23,7 @@ class CoroStatusChanged(CoroEvent):
self.status = status self.status = status
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'status': self.status} return {"status": self.status}
class AbstractRunner(ABC): class AbstractRunner(ABC):

View File

@ -1,12 +1,12 @@
__all__ = ('Job', 'Pool', 'JobUnit', 'JobContext', 'JobStatusChanged', 'PoolEvent') __all__ = ("Job", "Pool", "JobUnit", "JobContext", "JobStatusChanged", "PoolEvent")
import asyncio import asyncio
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
from v6d2ctx.integration.event import * from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import * from v6d2ctx.integration.responsetype import ResponseType
from .abstractrunner import * from .abstractrunner import AbstractRunner, CoroContext, CoroEvent, CoroStatusChanged
class JobEvent(Event): class JobEvent(Event):
@ -22,25 +22,25 @@ 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, context: JobContext, /) -> Union['Job', None]: async def run(self, context: JobContext, /) -> Union["Job", None]:
raise NotImplementedError raise NotImplementedError
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'type': 'unknown'} return {"type": "unknown"}
class JobUnit: class JobUnit:
async def run(self, context: JobContext, /) -> Union['JobUnit', None]: async def run(self, context: JobContext, /) -> Union["JobUnit", None]:
raise NotImplementedError raise NotImplementedError
def wrap(self) -> Job: def wrap(self) -> Job:
return UnitJob(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: def json(self) -> ResponseType:
return {'type': 'unknown'} return {"type": "unknown"}
class JobStatusChanged(JobEvent): class JobStatusChanged(JobEvent):
@ -48,7 +48,7 @@ class JobStatusChanged(JobEvent):
self.job = job self.job = job
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'status': self.job.json()} return {"status": self.job.json()}
class UnitJob(Job): class UnitJob(Job):
@ -76,7 +76,7 @@ class _JWEvent(WorkerEvent):
self.event = event self.event = event
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'job': self.event.json()} return {"job": self.event.json()}
class _JWSendable(SendableEvents[JobEvent]): class _JWSendable(SendableEvents[JobEvent]):
@ -154,7 +154,7 @@ class Worker:
def _cancel_job(self, job: Job) -> None: def _cancel_job(self, job: Job) -> None:
try: try:
job.future.set_exception(RuntimeError('task left in the worker after shutdown')) job.future.set_exception(RuntimeError("task left in the worker after shutdown"))
except asyncio.InvalidStateError: except asyncio.InvalidStateError:
pass pass
@ -176,19 +176,19 @@ class Worker:
async def _task(self) -> None: async def _task(self) -> None:
await self._work() await self._work()
if self.__working: if self.__working:
raise RuntimeError('worker left seemingly running after shutdown') raise RuntimeError("worker left seemingly running after shutdown")
if not self.__queue.empty(): if not self.__queue.empty():
raise RuntimeError('worker failed to finish all its jobs') raise RuntimeError("worker failed to finish all its jobs")
def start(self) -> asyncio.Future[None]: def start(self) -> asyncio.Future[None]:
if self.__working: if self.__working:
raise RuntimeError('starting an already running worker') raise RuntimeError("starting an already running worker")
self.__working = True self.__working = True
return asyncio.create_task(self._task()) return asyncio.create_task(self._task())
def submit(self, job: Job | None) -> None: def submit(self, job: Job | None) -> None:
if not self.__working: if not self.__working:
raise RuntimeError('submitting to a non-working worker') raise RuntimeError("submitting to a non-working worker")
self._put_nowait(job) self._put_nowait(job)
def working(self) -> bool: def working(self) -> bool:
@ -205,8 +205,8 @@ class Worker:
def json(self) -> ResponseType: def json(self) -> ResponseType:
return { return {
'job': self._job_json(), "job": self._job_json(),
'qsize': self.__queue.qsize(), "qsize": self.__queue.qsize(),
} }
@ -223,7 +223,7 @@ class Working:
self.__worker.submit(job) self.__worker.submit(job)
@classmethod @classmethod
def start(cls, events: SendableEvents[WorkerEvent], /) -> 'Working': def start(cls, events: SendableEvents[WorkerEvent], /) -> "Working":
worker = Worker(events) worker = Worker(events)
task = worker.start() task = worker.start()
return cls(worker, task) return cls(worker, task)
@ -238,7 +238,7 @@ class Working:
return self.__worker.json() return self.__worker.json()
T = TypeVar('T') T = TypeVar("T")
class CoroJD(JobUnit, Generic[T]): class CoroJD(JobUnit, Generic[T]):
@ -255,7 +255,7 @@ class CoroJD(JobUnit, Generic[T]):
return None return None
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'coroutine': self.status} return {"coroutine": self.status}
class _CJSendable(SendableEvents[CoroEvent]): class _CJSendable(SendableEvents[CoroEvent]):
@ -281,7 +281,7 @@ class _WPEvent(PoolEvent):
self.event = event self.event = event
def json(self) -> ResponseType: def json(self) -> ResponseType:
return {'worker': self.event.json()} return {"worker": self.event.json()}
class _WPSendable(SendableEvents[WorkerEvent]): class _WPSendable(SendableEvents[WorkerEvent]):
@ -295,7 +295,7 @@ class _WPSendable(SendableEvents[WorkerEvent]):
class Pool(AbstractRunner): class Pool(AbstractRunner):
def __init__(self, workers: int, events: SendableEvents[PoolEvent], /) -> 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
@ -305,11 +305,11 @@ class Pool(AbstractRunner):
def _start_worker(self) -> Working: def _start_worker(self) -> Working:
return Working.start(self.__worker_events) return Working.start(self.__worker_events)
async def __aenter__(self) -> 'Pool': async def __aenter__(self) -> "Pool":
if self.__open: if self.__open:
raise RuntimeError('starting an already open pool') raise RuntimeError("starting an already open pool")
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] = set(self._start_worker() for _ in range(self.__workers)) self.__pool: set[Working] = set(self._start_worker() for _ in range(self.__workers))
self.__open = True self.__open = True
@ -317,9 +317,9 @@ class Pool(AbstractRunner):
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
if not self.__working: if not self.__working:
raise RuntimeError('stopping a non-working pool') raise RuntimeError("stopping a non-working pool")
if not self.__open: if not self.__open:
raise RuntimeError('stopping a closed pool') raise RuntimeError("stopping a closed pool")
self.__open = False self.__open = False
for working in self.__pool: for working in self.__pool:
await working.close() await working.close()
@ -328,9 +328,9 @@ class Pool(AbstractRunner):
def submit(self, job: Job) -> None: def submit(self, job: Job) -> None:
if not self.__working: if not self.__working:
raise RuntimeError('submitting to a non-working pool') raise RuntimeError("submitting to a non-working pool")
if not self.__open: if not self.__open:
raise RuntimeError('submitting to a closed pool') raise RuntimeError("submitting to a closed pool")
min(self.__pool, key=lambda working: working.busy()).submit(job) min(self.__pool, key=lambda working: working.busy()).submit(job)
def workers(self) -> int: def workers(self) -> int:
@ -353,7 +353,7 @@ class JDC:
self.__unit = unit self.__unit = unit
self.__pool = pool self.__pool = pool
async def __aenter__(self) -> 'JDC': async def __aenter__(self) -> "JDC":
job = self.__unit.wrap() job = self.__unit.wrap()
self.__future = job.future self.__future = job.future
self.__pool.submit(job) self.__pool.submit(job)

View File

@ -1,159 +0,0 @@
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# why
# what in the fuck am I doing
# I don't want to actor model
# 3.10 has no task groups ffs
# whaaeeee
import asyncio
from typing import AsyncIterable, Generic, TypeVar
T = TypeVar('T')
async def future_join(futures: AsyncIterable[asyncio.Future[T]]) -> AsyncIterable[T]:
async for future in futures:
yield await future
TMessage = TypeVar('TMessage', contravariant=True)
class YProcess(Generic[TMessage]):
__queue: asyncio.Queue[TMessage]
__task: asyncio.Future[None]
def __init__(self) -> None:
super().__init__()
async def _handle(self, message: TMessage) -> bool:
raise NotImplementedError
async def _task(self) -> None:
while True:
message = await self.__queue.get()
try:
if await self._handle(message):
return
finally:
self.__queue.task_done()
async def _initialize(self) -> None:
self.__task = asyncio.create_task(self._task())
async def __aenter__(self) -> 'YProcess':
await self._initialize()
return self
def send(self, message: TMessage):
self.__queue.put_nowait(message)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.__task
TElement = TypeVar('TElement')
TResult = TypeVar('TResult')
class Push(Generic[TElement, TResult]):
def __init__(self, element: TElement) -> None:
self.element = element
self.push: asyncio.Future[None] = asyncio.Future()
self.pull: asyncio.Future[asyncio.Future[TResult]] = asyncio.Future()
class Reader(YProcess[Push[TElement, TResult] | None], Generic[TElement, TResult]):
__queue: asyncio.Queue[Push[TElement, TResult] | None]
async def _handle(self, message: Push[TElement, TResult] | None) -> bool:
self.__queue.put_nowait(message)
match message:
case Push() as push:
return False
case None:
return True
case _:
raise TypeError
async def _iterate(self) -> AsyncIterable[TResult]:
while True:
message = await self.__queue.get()
match message:
case Push() as push:
yield await (await push.pull)
case None:
return
case _:
raise TypeError
async def iterate(self) -> AsyncIterable[TResult]:
async with self:
async for element in self._iterate():
yield element
async def close(self):
self.send(None)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
return await super().__aexit__(exc_type, exc_val, exc_tb)
class Pushable(YProcess[Push[TElement, TResult] | None], Generic[TElement, TResult]):
def __init__(self, reader: YProcess[Push[TElement, TResult] | None]) -> None:
self.reader = reader
async def _on_push(self, push: Push[TElement, TResult]) -> None:
raise NotImplementedError
async def _result(self, element: TElement) -> TResult:
raise NotImplementedError
async def result(self, element: TElement) -> TResult:
try:
return await self._result(element)
except Exception as e:
await self.close()
raise
def _schedule(self, push: Push[TElement, TResult]) -> None:
push.pull.set_result(asyncio.create_task(self.result(push.element)))
async def _handle(self, message: Push[TElement, TResult] | None) -> bool:
match message:
case Push() as push:
await self._on_push(push)
return False
case None:
return True
case _:
raise TypeError
async def push(self, element: TElement) -> None:
push = Push(element)
self.send(push)
self.reader.send(push)
await push.push
async def close(self):
self.send(None)
self.reader.send(None)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
return await super().__aexit__(exc_type, exc_val, exc_tb)

View File

@ -1,8 +0,0 @@
import json
from v6d3music.utils.extract import *
params = json.loads(input())
url = json.loads(input())
kwargs = json.loads(input())
print(json.dumps(extract(params, url, kwargs)))

View File

@ -1,17 +1,14 @@
import asyncio import aiohttp
from concurrent.futures import ThreadPoolExecutor
from v6d3music.utils.extract import * from adaas.cachedb import RemoteCache
__all__ = ('aextract',) __all__ = ("aextract",)
async def aextract(params: dict, url: str, **kwargs): async def aextract(params: dict, url: str, **kwargs):
with ThreadPoolExecutor() as pool: async with aiohttp.ClientSession() as session:
return await asyncio.get_running_loop().run_in_executor( base = RemoteCache().base
pool, async with session.post(f"{base}/extract", json=dict(params=params, url=url, **kwargs)) as resp:
extract, if resp.status != 200:
params, raise IOError(resp.status)
url, return await resp.json()
kwargs
)

View File

@ -1,51 +1,49 @@
import string import string
from typing import Any, AsyncIterable from typing import Any, AsyncIterable
from v6d2ctx.context import * from v6d2ctx.context import Context, Explicit, escape
from v6d3music.utils.assert_admin import * from v6d3music.utils.assert_admin import assert_admin
from v6d3music.utils.effects_for_preset import * from v6d3music.utils.effects_for_preset import effects_for_preset
from v6d3music.utils.entries_for_url import * from v6d3music.utils.entries_for_url import entries_for_url
from v6d3music.utils.options_for_effects import * from v6d3music.utils.options_for_effects import options_for_effects
from v6d3music.utils.presets import * from v6d3music.utils.presets import allowed_effects, presets
from v6d3music.utils.sparq import * from v6d3music.utils.sparq import sparq
__all__ = ('InfoCtx', 'BoundCtx', 'UrlCtx', 'ArgCtx',) __all__ = (
"InfoCtx",
"BoundCtx",
"UrlCtx",
"ArgCtx",
)
class PostCtx: class PostCtx:
def __init__( def __init__(self, effects: str | None) -> None:
self, effects: str | None
) -> None:
self.effects: str | None = effects self.effects: str | None = effects
self.already_read: int = 0 self.already_read: int = 0
self.tor: bool = False
self.ignore: bool = False self.ignore: bool = False
class InfoCtx: class InfoCtx:
def __init__( def __init__(self, info: dict[str, Any], post: PostCtx) -> None:
self, info: dict[str, Any], post: PostCtx
) -> None:
self.info = info self.info = info
self.post = post self.post = post
self.effects = post.effects self.effects = post.effects
self.already_read = post.already_read self.already_read = post.already_read
self.tor = post.tor
self.ignore = post.ignore self.ignore = post.ignore
def bind(self, ctx: Context) -> 'BoundCtx': def bind(self, ctx: Context) -> "BoundCtx":
return BoundCtx(self, ctx) return BoundCtx(self, ctx)
class BoundCtx: class BoundCtx:
def __init__(self, it: InfoCtx, ctx: Context, /) -> None: def __init__(self, it: InfoCtx, ctx: Context, /) -> None:
if ctx.member is None: if ctx.member is None:
raise Explicit('not in a guild') raise Explicit("not in a guild")
self.member = ctx.member self.member = ctx.member
self.ctx = ctx self.ctx = ctx
self.url = it.info['url'] self.url = it.info["url"]
self.description = f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}' self.description = f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}'
self.tor = it.tor
self.effects = it.effects self.effects = it.effects
self.already_read = it.already_read self.already_read = it.already_read
self.options = self._options() self.options = self._options()
@ -55,8 +53,8 @@ class BoundCtx:
if self.effects: if self.effects:
if self.effects not in allowed_effects: if self.effects not in allowed_effects:
assert_admin(self.ctx.member) assert_admin(self.ctx.member)
if not set(self.effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'): if not set(self.effects) <= set(string.ascii_letters + string.digits + "*,=+-/()|.^:_"):
raise Explicit('malformed effects') raise Explicit("malformed effects")
return options_for_effects(self.effects) return options_for_effects(self.effects)
else: else:
return None return None
@ -68,12 +66,11 @@ class UrlCtx:
self.post = post self.post = post
self.effects: str | None = post.effects self.effects: str | None = post.effects
self.already_read = post.already_read self.already_read = post.already_read
self.tor = post.tor
self.ignore = post.ignore self.ignore = post.ignore
async def entries(self) -> AsyncIterable[InfoCtx]: async def entries(self) -> AsyncIterable[InfoCtx]:
try: try:
async for info in entries_for_url(self.url, self.tor): async for info in entries_for_url(self.url):
yield InfoCtx(info, self.post) yield InfoCtx(info, self.post)
except Exception: except Exception:
if not self.ignore: if not self.ignore:
@ -85,32 +82,32 @@ class ArgCtx:
self.sources: list[UrlCtx] = [] self.sources: list[UrlCtx] = []
while args: while args:
match args: match args:
case ['[[', *args]: case ["[[", *args]:
try: try:
close_ix = args.index(']]') close_ix = args.index("]]")
except ValueError: except ValueError:
raise Explicit('expected closing `]]`, not found') raise Explicit("expected closing `]]`, not found")
urls = args[:close_ix] urls = args[:close_ix]
args = args[close_ix + 1:] args = args[close_ix + 1 :]
case [']]', *args]: case ["]]", *args]:
raise Explicit('unexpected `]]`') raise Explicit("unexpected `]]`")
case [_url, *args]: case [_url, *args]:
urls = [_url] urls = [_url]
case _: case _:
raise RuntimeError raise RuntimeError
for url in urls: for url in urls:
if url in presets: if url in presets:
raise Explicit('expected url, got preset. maybe you are missing `+`?') raise Explicit("expected url, got preset. maybe you are missing `+`?")
if url in {'+', '-'}: if url in {"+", "-"}:
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?') raise Explicit("expected url, got `+` or `-`. maybe you tried to use multiple effects?")
if url.startswith('+') or url.startswith('-"') or url.startswith('-\''): if url.startswith("+") or url.startswith('-"') or url.startswith("-'"):
raise Explicit( raise Explicit(
'expected url, got `+` or `-"` or `-\'`. maybe you forgot to separate control symbol from the effects?' "expected url, got `+` or `-\"` or `-'`. maybe you forgot to separate control symbol from the effects?"
) )
match args: match args:
case ['-', effects, *args]: case ["-", effects, *args]:
pass pass
case ['+', preset, *args]: case ["+", preset, *args]:
effects = effects_for_preset(preset) effects = effects_for_preset(preset)
case [*args]: case [*args]:
effects = default_effects effects = default_effects
@ -129,13 +126,11 @@ class ArgCtx:
post.already_read = round(seconds / sparq(options_for_effects(effects))) post.already_read = round(seconds / sparq(options_for_effects(effects)))
while True: while True:
match args: match args:
case ['tor', *args]: case ["tor", *args]:
if post.tor: raise Explicit("tor support is temporarily suspended")
raise Explicit('duplicate tor') case ["ignore", *args]:
post.tor = True
case ['ignore', *args]:
if post.ignore: if post.ignore:
raise Explicit('duplicate ignore') raise Explicit("duplicate ignore")
post.ignore = True post.ignore = True
case [*args]: case [*args]:
break break

View File

@ -1,12 +1,12 @@
import discord import discord
from v6d2ctx.context import * from v6d2ctx.context import Explicit
__all__ = ('assert_admin',) __all__ = ("assert_admin",)
def assert_admin(member: discord.Member | None): def assert_admin(member: discord.Member | None):
assert isinstance(member, discord.Member) assert isinstance(member, discord.Member)
permissions: discord.Permissions = member.guild_permissions permissions: discord.Permissions = member.guild_permissions
if not permissions.administrator: if not permissions.administrator:
raise Explicit('not an administrator') raise Explicit("not an administrator")

View File

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

View File

@ -1,11 +1,11 @@
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d3music.utils.presets import * from v6d3music.utils.presets import presets
__all__ = ('effects_for_preset',) __all__ = ("effects_for_preset",)
def effects_for_preset(preset: str) -> str: def effects_for_preset(preset: str) -> str:
if preset in presets: if preset in presets:
return presets[preset] return presets[preset]
else: else:
raise Explicit('unknown preset') raise Explicit("unknown preset")

View File

@ -1,30 +1,17 @@
from typing import Any, AsyncIterable from typing import Any, AsyncIterable
from v6d2ctx.context import * from v6d2ctx.context import Explicit
from v6d3music.utils.aextract import * from v6d3music.utils.aextract import aextract
from v6d3music.utils.tor_extract import *
__all__ = ('entries_for_url',) __all__ = ("entries_for_url",)
async def entries_for_url(url: str, tor: bool) -> AsyncIterable[ async def entries_for_url(url: str) -> AsyncIterable[dict[str, Any]]:
dict[str, Any] info = await aextract({"logtostderr": True}, url, download=False, process=False)
]: if "__error__" in info:
ef = aextract raise Explicit("extraction error\n" + info.get("__error_str__"))
if tor: if "entries" in info:
ef = tor_extract for entry in info["entries"]:
info = await ef(
{
'logtostderr': True
},
url,
download=False,
process=False
)
if '__error__' in info:
raise Explicit('extraction error\n' + info.get('__error_str__'))
if 'entries' in info:
for entry in info['entries']:
yield entry yield entry
else: else:
yield info | {'url': url} yield info | {"url": url}

View File

@ -1,22 +0,0 @@
import discord.utils
import yt_dlp
__all__ = ('extract',)
def extract(params: dict, url: str, kwargs: dict):
try:
extracted = yt_dlp.YoutubeDL(params=params).extract_info(url, **kwargs)
if not isinstance(extracted, dict):
raise TypeError
if 'entries' in extracted:
extracted['entries'] = list(extracted['entries'])
return extracted
except Exception as e:
msg = str(e)
msg = discord.utils.escape_markdown(msg)
msg = msg.replace('\x1b[0;31m', '__')
msg = msg.replace('\x1b[0m', '__')
print(msg)
msg = 'unknown ytdl error'
return {'__error__': True, '__error_str__': msg}

View File

@ -1,9 +1,8 @@
import discord import discord
from v6d3music.utils.speed_quotient import * from v6d3music.utils.speed_quotient import speed_quotient
__all__ = ("sparq",)
__all__ = ('sparq',)
def sparq(options: str | None) -> float: def sparq(options: str | None) -> float:

View File

@ -2,24 +2,24 @@ import re
import discord import discord
__all__ = ('speed_quotient',) __all__ = ("speed_quotient",)
def speed_quotient(options: str | None) -> float: def speed_quotient(options: str | None) -> float:
options = options or '' options = options or ""
options = ''.join(c for c in options if not c.isspace()) options = "".join(c for c in options if not c.isspace())
options += ',' options += ","
quotient: float = 1.0 quotient: float = 1.0
asetrate: str asetrate: str
# noinspection RegExpSimplifiable # noinspection RegExpSimplifiable
for asetrate in re.findall(r'asetrate=([0-9.]+?),', options): for asetrate in re.findall(r"asetrate=([0-9.]+?),", options):
try: try:
quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE
except ValueError: except ValueError:
pass pass
atempo: str atempo: str
# noinspection RegExpSimplifiable # noinspection RegExpSimplifiable
for atempo in re.findall(r'atempo=([0-9.]+?),', options): for atempo in re.findall(r"atempo=([0-9.]+?),", options):
try: try:
quotient *= float(atempo) quotient *= float(atempo)
except ValueError: except ValueError:

View File

@ -1,22 +0,0 @@
import asyncio
import json
from v6d3music.utils.tor_prefix import *
__all__ = ('tor_extract',)
async def tor_extract(params: dict, url: str, **kwargs):
print(f'tor extracting {url}')
args = [*tor_prefix(), 'python', '-m', 'v6d3music.run-extract']
ap = await asyncio.create_subprocess_exec(*args, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE)
assert ap.stdin is not None
ap.stdin.write(f'{json.dumps(params)}\n'.encode())
ap.stdin.write(f'{json.dumps(url)}\n'.encode())
ap.stdin.write(f'{json.dumps(kwargs)}\n'.encode())
ap.stdin.write_eof()
code = await ap.wait()
if code:
raise RuntimeError(code)
assert ap.stdout is not None
return json.loads(await ap.stdout.read())

View File

@ -1,3 +0,0 @@
__all__ = ('tor_prefix',)
from adaas.tor_prefix import *