move extract
, remove tor
This commit is contained in:
parent
8e52889d8f
commit
715d4fbf61
@ -1 +0,0 @@
|
||||
trial_token=paste here
|
@ -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
|
||||
|
28
README.md
28
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
|
||||
```
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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
|
||||
|
||||
|
@ -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",)
|
||||
|
||||
|
@ -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)}
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
@ -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)))
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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")
|
||||
|
@ -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}
|
||||
|
@ -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}
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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())
|
@ -1,3 +0,0 @@
|
||||
__all__ = ('tor_prefix',)
|
||||
|
||||
from adaas.tor_prefix import *
|
Loading…
Reference in New Issue
Block a user