diff --git a/.trial_token_example.env b/.trial_token_example.env deleted file mode 100644 index 847ce27..0000000 --- a/.trial_token_example.env +++ /dev/null @@ -1 +0,0 @@ -trial_token=paste here diff --git a/Dockerfile b/Dockerfile index 98d95d4..58d280a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index b063ab1..1c90847 100644 --- a/README.md +++ b/README.md @@ -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 -``` diff --git a/base.requirements.txt b/base.requirements.txt index 12e88b7..34e2074 100644 --- a/base.requirements.txt +++ b/base.requirements.txt @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index a4c2b60..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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 diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 7e6fbbc..baa2e50 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -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 diff --git a/v6d3music/api.py b/v6d3music/api.py index 8a3e089..eeede89 100644 --- a/v6d3music/api.py +++ b/v6d3music/api.py @@ -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",) diff --git a/v6d3music/commands.py b/v6d3music/commands.py index bc22313..ef18b92 100644 --- a/v6d3music/commands.py +++ b/v6d3music/commands.py @@ -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)} diff --git a/v6d3music/core/caching.py b/v6d3music/core/caching.py deleted file mode 100644 index 164c645..0000000 --- a/v6d3music/core/caching.py +++ /dev/null @@ -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) diff --git a/v6d3music/core/create_ytaudio.py b/v6d3music/core/create_ytaudio.py index 0ad3e3c..674a8a8 100644 --- a/v6d3music/core/create_ytaudio.py +++ b/v6d3music/core/create_ytaudio.py @@ -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) diff --git a/v6d3music/core/default_effects.py b/v6d3music/core/default_effects.py index d2ebc76..eb20feb 100644 --- a/v6d3music/core/default_effects.py +++ b/v6d3music/core/default_effects.py @@ -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 diff --git a/v6d3music/core/ffmpegnormalaudio.py b/v6d3music/core/ffmpegnormalaudio.py index 026ccab..5ca5d86 100644 --- a/v6d3music/core/ffmpegnormalaudio.py +++ b/v6d3music/core/ffmpegnormalaudio.py @@ -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 diff --git a/v6d3music/core/mainaudio.py b/v6d3music/core/mainaudio.py index 096dfe0..cc33b43 100644 --- a/v6d3music/core/mainaudio.py +++ b/v6d3music/core/mainaudio.py @@ -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)) diff --git a/v6d3music/core/mainservice.py b/v6d3music/core/mainservice.py index 43b1864..98c078e 100644 --- a/v6d3music/core/mainservice.py +++ b/v6d3music/core/mainservice.py @@ -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) diff --git a/v6d3music/core/queueaudio.py b/v6d3music/core/queueaudio.py index a7fe280..c0dc3cc 100644 --- a/v6d3music/core/queueaudio.py +++ b/v6d3music/core/queueaudio.py @@ -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) diff --git a/v6d3music/core/real_url.py b/v6d3music/core/real_url.py index a72874d..517b20d 100644 --- a/v6d3music/core/real_url.py +++ b/v6d3music/core/real_url.py @@ -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() diff --git a/v6d3music/core/ystate.py b/v6d3music/core/ystate.py index 636d6ce..4ee5516 100644 --- a/v6d3music/core/ystate.py +++ b/v6d3music/core/ystate.py @@ -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, }, ) diff --git a/v6d3music/core/ytaservicing.py b/v6d3music/core/ytaservicing.py index bf26430..11a00bd 100644 --- a/v6d3music/core/ytaservicing.py +++ b/v6d3music/core/ytaservicing.py @@ -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 diff --git a/v6d3music/core/ytaudio.py b/v6d3music/core/ytaudio.py index a98df31..dfd4f87 100644 --- a/v6d3music/core/ytaudio.py +++ b/v6d3music/core/ytaudio.py @@ -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 diff --git a/v6d3music/main.py b/v6d3music/main.py index d8fa08f..f672beb 100644 --- a/v6d3music/main.py +++ b/v6d3music/main.py @@ -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") diff --git a/v6d3music/processing/abstractrunner.py b/v6d3music/processing/abstractrunner.py index 1b03f29..759deb0 100644 --- a/v6d3music/processing/abstractrunner.py +++ b/v6d3music/processing/abstractrunner.py @@ -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): diff --git a/v6d3music/processing/pool.py b/v6d3music/processing/pool.py index 317c27b..9fa1ab9 100644 --- a/v6d3music/processing/pool.py +++ b/v6d3music/processing/pool.py @@ -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) diff --git a/v6d3music/processing/yprocess.py b/v6d3music/processing/yprocess.py deleted file mode 100644 index 7257857..0000000 --- a/v6d3music/processing/yprocess.py +++ /dev/null @@ -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) diff --git a/v6d3music/run-extract.py b/v6d3music/run-extract.py deleted file mode 100644 index 0085de5..0000000 --- a/v6d3music/run-extract.py +++ /dev/null @@ -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))) diff --git a/v6d3music/utils/aextract.py b/v6d3music/utils/aextract.py index 7994b5d..0484be0 100644 --- a/v6d3music/utils/aextract.py +++ b/v6d3music/utils/aextract.py @@ -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() diff --git a/v6d3music/utils/argctx.py b/v6d3music/utils/argctx.py index 03d1176..e00be46 100644 --- a/v6d3music/utils/argctx.py +++ b/v6d3music/utils/argctx.py @@ -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 diff --git a/v6d3music/utils/assert_admin.py b/v6d3music/utils/assert_admin.py index 8307c98..57ca0f7 100644 --- a/v6d3music/utils/assert_admin.py +++ b/v6d3music/utils/assert_admin.py @@ -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") diff --git a/v6d3music/utils/catch.py b/v6d3music/utils/catch.py index fc090b3..962a84d 100644 --- a/v6d3music/utils/catch.py +++ b/v6d3music/utils/catch.py @@ -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): diff --git a/v6d3music/utils/effects_for_preset.py b/v6d3music/utils/effects_for_preset.py index be9f988..f5efd6b 100644 --- a/v6d3music/utils/effects_for_preset.py +++ b/v6d3music/utils/effects_for_preset.py @@ -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") diff --git a/v6d3music/utils/entries_for_url.py b/v6d3music/utils/entries_for_url.py index 2f32170..5d605ec 100644 --- a/v6d3music/utils/entries_for_url.py +++ b/v6d3music/utils/entries_for_url.py @@ -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} diff --git a/v6d3music/utils/extract.py b/v6d3music/utils/extract.py deleted file mode 100644 index ee881cf..0000000 --- a/v6d3music/utils/extract.py +++ /dev/null @@ -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} diff --git a/v6d3music/utils/sparq.py b/v6d3music/utils/sparq.py index 49b78d7..a5cf6f9 100644 --- a/v6d3music/utils/sparq.py +++ b/v6d3music/utils/sparq.py @@ -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: diff --git a/v6d3music/utils/speed_quotient.py b/v6d3music/utils/speed_quotient.py index 4b000d6..4132b77 100644 --- a/v6d3music/utils/speed_quotient.py +++ b/v6d3music/utils/speed_quotient.py @@ -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: diff --git a/v6d3music/utils/tor_extract.py b/v6d3music/utils/tor_extract.py deleted file mode 100644 index a815427..0000000 --- a/v6d3music/utils/tor_extract.py +++ /dev/null @@ -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()) diff --git a/v6d3music/utils/tor_prefix.py b/v6d3music/utils/tor_prefix.py deleted file mode 100644 index 7891571..0000000 --- a/v6d3music/utils/tor_prefix.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = ('tor_prefix',) - -from adaas.tor_prefix import *