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
RUN apt-get update
RUN apt-get install -y libopus0 opus-tools ffmpeg
RUN apt-get install -y tor
COPY base.requirements.txt base.requirements.txt
RUN pip install -r base.requirements.txt
COPY requirements.txt requirements.txt

View File

@ -1,29 +1 @@
# 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
discord.py[voice]~=2.2.2
yt-dlp~=2023.2.17
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::
?/play url [- effects | + preset] [[[h] m] s] [tor|ignore]* ...
?/play url [- effects | + preset] [[[h] m] s] [ignore]* ...
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 http://127.0.0.1/audio.mp3

View File

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

View File

@ -4,13 +4,13 @@ from typing import Callable
import discord
from v6d2ctx.at_of import AtOf
from v6d2ctx.context import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainservice import *
from v6d3music.utils.assert_admin import *
from v6d3music.utils.catch import *
from v6d3music.utils.effects_for_preset import *
from v6d3music.utils.presets import *
from v6d2ctx.context import Context, Explicit, command_type
from v6d3music.core.default_effects import DefaultEffects
from v6d3music.core.mainservice import MainService
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.presets import allowed_presets
__all__ = ("get_of",)
@ -37,7 +37,7 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
args,
f"""
`play ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args`
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [ignore] ...args`
`pause`
`resume`
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 v6d3music.core.real_url import *
from v6d3music.core.ytaservicing import *
from v6d3music.core.ytaudio import *
from v6d3music.utils.argctx import *
from v6d2ctx.context import Context
from v6d3music.core.real_url import real_url
from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaudio import YTAudio
from v6d3music.utils.argctx import BoundCtx, InfoCtx
__all__ = ('create_ytaudio',)
__all__ = ("create_ytaudio",)
async def _create_ytaudio(
servicing: YTAServicing, bound: BoundCtx
) -> YTAudio:
async def _create_ytaudio(servicing: YTAServicing, bound: BoundCtx) -> YTAudio:
return YTAudio(
servicing,
await real_url(servicing.caching, bound.url, False, bound.tor),
await real_url(bound.url, False),
bound.url,
bound.description,
bound.options,
bound.member,
bound.already_read,
bound.tor
)
async def create_ytaudio(
servicing: YTAServicing, ctx: Context, it: InfoCtx
) -> YTAudio:
async def create_ytaudio(servicing: YTAServicing, ctx: Context, it: InfoCtx) -> YTAudio:
bound = it.bind(ctx)
return await _create_ytaudio(servicing, bound)

View File

@ -1,11 +1,11 @@
from contextlib import AsyncExitStack
from ptvp35 import *
from v6d2ctx.context import *
from ptvp35 import DbFactory, KVJson
from v6d2ctx.context import Explicit
from v6d3music.config import myroot
from v6d3music.utils.presets import *
from v6d3music.utils.presets import allowed_effects
__all__ = ('DefaultEffects',)
__all__ = ("DefaultEffects",)
class DefaultEffects:
@ -18,12 +18,12 @@ class DefaultEffects:
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')
raise Explicit("these effects are not allowed")
await self.__db.set(gid, effects)
async def __aenter__(self) -> 'DefaultEffects':
async def __aenter__(self) -> "DefaultEffects":
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()
return self

View File

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

View File

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

View File

@ -6,29 +6,28 @@ from typing import AsyncIterable, Callable, TypeVar
import discord
import v6d3music.processing.pool
from ptvp35 import *
from v6d2ctx.context import *
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.targets import *
from v6d2ctx.lock_for import *
from ptvp35 import DbFactory, KVJson
from v6d2ctx.context import Context, Explicit
from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import ResponseType
from v6d2ctx.integration.targets import Async, Targets
from v6d2ctx.lock_for import Locks
from v6d3music.config import myroot
from v6d3music.core.caching import *
from v6d3music.core.default_effects import *
from v6d3music.core.mainaudio import *
from v6d3music.core.monitoring 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.core.default_effects import DefaultEffects
from v6d3music.core.mainaudio import MainAudio
from v6d3music.core.monitoring import Monitoring, PersistentMonitoring
from v6d3music.core.queueaudio import QueueAudio
from v6d3music.core.ystate import YState
from v6d3music.core.ytaservicing import YTAServicing
from v6d3music.core.ytaudio import YTAudio
from v6d3music.processing.pool import Pool, PoolEvent
from v6d3music.utils.argctx import ArgCtx
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):
@ -40,7 +39,7 @@ class _PMEvent(MainEvent):
self.event = event
def json(self) -> ResponseType:
return {'pool': self.event.json()}
return {"pool": self.event.json()}
class _PMSendable(SendableEvents[PoolEvent]):
@ -69,7 +68,7 @@ class MainService:
self.__ystates: dict[discord.Guild, YState] = {}
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
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():
vs: discord.VoiceState | None = member.voice
if vs is None:
raise Explicit('not connected')
raise Explicit("not connected")
vch: discord.abc.Connectable | None = vs.channel
if vch is None:
raise Explicit('not connected')
raise Explicit("not connected")
try:
vc = await vch.connect()
except discord.ClientException as e:
@ -89,32 +88,31 @@ class MainService:
assert vc is not None
await member.guild.fetch_channels()
await vc.disconnect(force=True)
raise Explicit('try again later') from e
raise Explicit("try again later") from e
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')
raise Explicit("not in a guild")
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)
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)
async def create(self, guild: discord.Guild) -> MainAudio:
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:
self.__locks = Locks()
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.__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.__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.__save_task = asyncio.create_task(self.save_daemon())
self.monitoring = await es.enter_async_context(Monitoring())
@ -143,7 +141,7 @@ 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()
@ -159,7 +157,7 @@ class MainService:
async def save_job(self):
await self.__vcs_restored
print('starting saving')
print("starting saving")
while True:
await asyncio.sleep(1)
await self.save_all(True, not self.client.is_closed())
@ -177,22 +175,14 @@ class MainService:
else:
try:
await self.save_all(False, False)
print('saved')
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
]
(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
@ -201,22 +191,22 @@ class MainService:
vc.pause()
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:
try:
print(f'vc restoring {vcgid}')
print(f"vc restoring {vcgid}")
guild: discord.Guild = await self.client.fetch_guild(vcgid)
async with self.lock_for(guild):
await self._restore_vc(guild, vccid, vc_is_paused)
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()
else:
print(f'vc restored {vcgid} {vccid}')
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:
@ -241,7 +231,7 @@ class MainService:
yield audio
finally:
del self.__ystates[ctx.guild]
def cancel_loading(self, ctx: Context) -> None:
assert ctx.guild is not None
ystate = self.__ystates.get(ctx.guild)
@ -266,17 +256,14 @@ class MainMode:
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)
)
source = self.mains.setdefault(vc.guild, await self.mainservice.create(vc.guild))
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()):
vc.play(source)
return source
def context(self, ctx: Context) -> 'MainContext':
def context(self, ctx: Context) -> "MainContext":
return MainContext(self, ctx)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
__all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged')
__all__ = ("AbstractRunner", "CoroEvent", "CoroContext", "CoroStatusChanged")
from abc import ABC, abstractmethod
from typing import Any, Callable, Coroutine, TypeVar
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import ResponseType
T = TypeVar('T')
T = TypeVar("T")
class CoroEvent(Event):
@ -23,7 +23,7 @@ class CoroStatusChanged(CoroEvent):
self.status = status
def json(self) -> ResponseType:
return {'status': self.status}
return {"status": self.status}
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
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
from v6d2ctx.integration.event import *
from v6d2ctx.integration.responsetype import *
from v6d2ctx.integration.event import Event, SendableEvents
from v6d2ctx.integration.responsetype import ResponseType
from .abstractrunner import *
from .abstractrunner import AbstractRunner, CoroContext, CoroEvent, CoroStatusChanged
class JobEvent(Event):
@ -22,25 +22,25 @@ class Job:
def __init__(self, future: asyncio.Future[None]) -> None:
self.future = future
async def run(self, context: JobContext, /) -> Union['Job', None]:
async def run(self, context: JobContext, /) -> Union["Job", None]:
raise NotImplementedError
def json(self) -> ResponseType:
return {'type': 'unknown'}
return {"type": "unknown"}
class JobUnit:
async def run(self, context: JobContext, /) -> Union['JobUnit', None]:
async def run(self, context: JobContext, /) -> Union["JobUnit", None]:
raise NotImplementedError
def wrap(self) -> Job:
return UnitJob(asyncio.Future(), self)
def at(self, pool: 'Pool') -> 'JDC':
def at(self, pool: "Pool") -> "JDC":
return JDC(self, pool)
def json(self) -> ResponseType:
return {'type': 'unknown'}
return {"type": "unknown"}
class JobStatusChanged(JobEvent):
@ -48,7 +48,7 @@ class JobStatusChanged(JobEvent):
self.job = job
def json(self) -> ResponseType:
return {'status': self.job.json()}
return {"status": self.job.json()}
class UnitJob(Job):
@ -76,7 +76,7 @@ class _JWEvent(WorkerEvent):
self.event = event
def json(self) -> ResponseType:
return {'job': self.event.json()}
return {"job": self.event.json()}
class _JWSendable(SendableEvents[JobEvent]):
@ -154,7 +154,7 @@ class Worker:
def _cancel_job(self, job: Job) -> None:
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:
pass
@ -176,19 +176,19 @@ class Worker:
async def _task(self) -> None:
await self._work()
if self.__working:
raise RuntimeError('worker left seemingly running after shutdown')
raise RuntimeError("worker left seemingly running after shutdown")
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]:
if self.__working:
raise RuntimeError('starting an already running worker')
raise RuntimeError("starting an already running worker")
self.__working = True
return asyncio.create_task(self._task())
def submit(self, job: Job | None) -> None:
if not self.__working:
raise RuntimeError('submitting to a non-working worker')
raise RuntimeError("submitting to a non-working worker")
self._put_nowait(job)
def working(self) -> bool:
@ -205,8 +205,8 @@ class Worker:
def json(self) -> ResponseType:
return {
'job': self._job_json(),
'qsize': self.__queue.qsize(),
"job": self._job_json(),
"qsize": self.__queue.qsize(),
}
@ -223,7 +223,7 @@ class Working:
self.__worker.submit(job)
@classmethod
def start(cls, events: SendableEvents[WorkerEvent], /) -> 'Working':
def start(cls, events: SendableEvents[WorkerEvent], /) -> "Working":
worker = Worker(events)
task = worker.start()
return cls(worker, task)
@ -238,7 +238,7 @@ class Working:
return self.__worker.json()
T = TypeVar('T')
T = TypeVar("T")
class CoroJD(JobUnit, Generic[T]):
@ -255,7 +255,7 @@ class CoroJD(JobUnit, Generic[T]):
return None
def json(self) -> ResponseType:
return {'coroutine': self.status}
return {"coroutine": self.status}
class _CJSendable(SendableEvents[CoroEvent]):
@ -281,7 +281,7 @@ class _WPEvent(PoolEvent):
self.event = event
def json(self) -> ResponseType:
return {'worker': self.event.json()}
return {"worker": self.event.json()}
class _WPSendable(SendableEvents[WorkerEvent]):
@ -295,7 +295,7 @@ class _WPSendable(SendableEvents[WorkerEvent]):
class Pool(AbstractRunner):
def __init__(self, workers: int, events: SendableEvents[PoolEvent], /) -> None:
if workers < 1:
raise ValueError('non-positive number of workers')
raise ValueError("non-positive number of workers")
self.__workers = workers
self.__working = False
self.__open = False
@ -305,11 +305,11 @@ class Pool(AbstractRunner):
def _start_worker(self) -> Working:
return Working.start(self.__worker_events)
async def __aenter__(self) -> 'Pool':
async def __aenter__(self) -> "Pool":
if self.__open:
raise RuntimeError('starting an already open pool')
raise RuntimeError("starting an already open pool")
if self.__working:
raise RuntimeError('starting an already running pool')
raise RuntimeError("starting an already running pool")
self.__working = True
self.__pool: set[Working] = set(self._start_worker() for _ in range(self.__workers))
self.__open = True
@ -317,9 +317,9 @@ class Pool(AbstractRunner):
async def __aexit__(self, exc_type, exc_val, exc_tb):
if not self.__working:
raise RuntimeError('stopping a non-working pool')
raise RuntimeError("stopping a non-working pool")
if not self.__open:
raise RuntimeError('stopping a closed pool')
raise RuntimeError("stopping a closed pool")
self.__open = False
for working in self.__pool:
await working.close()
@ -328,9 +328,9 @@ class Pool(AbstractRunner):
def submit(self, job: Job) -> None:
if not self.__working:
raise RuntimeError('submitting to a non-working pool')
raise RuntimeError("submitting to a non-working pool")
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)
def workers(self) -> int:
@ -353,7 +353,7 @@ class JDC:
self.__unit = unit
self.__pool = pool
async def __aenter__(self) -> 'JDC':
async def __aenter__(self) -> "JDC":
job = self.__unit.wrap()
self.__future = job.future
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
from concurrent.futures import ThreadPoolExecutor
import aiohttp
from v6d3music.utils.extract import *
from adaas.cachedb import RemoteCache
__all__ = ('aextract',)
__all__ = ("aextract",)
async def aextract(params: dict, url: str, **kwargs):
with ThreadPoolExecutor() as pool:
return await asyncio.get_running_loop().run_in_executor(
pool,
extract,
params,
url,
kwargs
)
async with aiohttp.ClientSession() as session:
base = RemoteCache().base
async with session.post(f"{base}/extract", json=dict(params=params, url=url, **kwargs)) as resp:
if resp.status != 200:
raise IOError(resp.status)
return await resp.json()

View File

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

View File

@ -1,12 +1,12 @@
import discord
from v6d2ctx.context import *
from v6d2ctx.context import Explicit
__all__ = ('assert_admin',)
__all__ = ("assert_admin",)
def assert_admin(member: discord.Member | None):
assert isinstance(member, discord.Member)
permissions: discord.Permissions = member.guild_permissions
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 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):

View File

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

View File

@ -1,30 +1,17 @@
from typing import Any, AsyncIterable
from v6d2ctx.context import *
from v6d3music.utils.aextract import *
from v6d3music.utils.tor_extract import *
from v6d2ctx.context import Explicit
from v6d3music.utils.aextract import aextract
__all__ = ('entries_for_url',)
__all__ = ("entries_for_url",)
async def entries_for_url(url: str, tor: bool) -> AsyncIterable[
dict[str, Any]
]:
ef = aextract
if tor:
ef = tor_extract
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']:
async def entries_for_url(url: str) -> AsyncIterable[dict[str, Any]]:
info = await aextract({"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
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
from v6d3music.utils.speed_quotient import *
from v6d3music.utils.speed_quotient import speed_quotient
__all__ = ('sparq',)
__all__ = ("sparq",)
def sparq(options: str | None) -> float:

View File

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