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
|
ENV v6root=/v6data
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install -y libopus0 opus-tools ffmpeg
|
RUN apt-get install -y libopus0 opus-tools ffmpeg
|
||||||
RUN apt-get install -y tor
|
|
||||||
COPY base.requirements.txt base.requirements.txt
|
COPY base.requirements.txt base.requirements.txt
|
||||||
RUN pip install -r base.requirements.txt
|
RUN pip install -r base.requirements.txt
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
|
28
README.md
28
README.md
@ -1,29 +1 @@
|
|||||||
# music bot
|
# music bot
|
||||||
## try for yourself
|
|
||||||
default prefix is `?/`
|
|
||||||
### install docker compose
|
|
||||||
follow instructions at https://docs.docker.com/compose/install/
|
|
||||||
from https://docs.docker.com/compose/install/linux/ :
|
|
||||||
```sh
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install docker-compose-plugin
|
|
||||||
```
|
|
||||||
or
|
|
||||||
```sh
|
|
||||||
sudo yum update
|
|
||||||
sudo yum install docker-compose-plugin
|
|
||||||
```
|
|
||||||
### clone repo
|
|
||||||
```sh
|
|
||||||
git clone https://gitea.parrrate.ru/PTV/v6d3music.git
|
|
||||||
cd v6d3music
|
|
||||||
```
|
|
||||||
### set token
|
|
||||||
```sh
|
|
||||||
cp .trial_token_example.env .trial_token.env
|
|
||||||
vim .trial_token.env
|
|
||||||
```
|
|
||||||
### start or update
|
|
||||||
```sh
|
|
||||||
docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
aiohttp>=3.7.4,<4
|
aiohttp>=3.7.4,<4
|
||||||
discord.py[voice]~=2.2.2
|
discord.py[voice]~=2.2.2
|
||||||
yt-dlp~=2023.2.17
|
|
||||||
typing_extensions~=4.4.0
|
typing_extensions~=4.4.0
|
||||||
|
@ -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::
|
command syntax::
|
||||||
|
|
||||||
?/play url [- effects | + preset] [[[h] m] s] [tor|ignore]* ...
|
?/play url [- effects | + preset] [[[h] m] s] [ignore]* ...
|
||||||
|
|
||||||
examples::
|
examples::
|
||||||
|
|
||||||
?/play http://127.0.0.1/audio.mp3 + bassboost tor
|
?/play http://127.0.0.1/audio.mp3 + bassboost
|
||||||
?/play http://127.0.0.1/audio.mp3 - "bass=g=10" 23 59 59 ignore
|
?/play http://127.0.0.1/audio.mp3 - "bass=g=10" 23 59 59 ignore
|
||||||
?/play http://127.0.0.1/audio.mp3 http://127.0.0.1/audio.mp3
|
?/play http://127.0.0.1/audio.mp3 http://127.0.0.1/audio.mp3
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ import discord
|
|||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
from rainbowadn.instrument import Instrumentation
|
from rainbowadn.instrument import Instrumentation
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d2ctx.integration.responsetype import *
|
from v6d2ctx.integration.responsetype import ResponseType
|
||||||
from v6d2ctx.integration.targets import *
|
from v6d2ctx.integration.targets import Async, JsonLike, Targets
|
||||||
from v6d3music.core.mainaudio import *
|
from v6d3music.core.mainaudio import MainAudio
|
||||||
from v6d3music.core.mainservice import *
|
from v6d3music.core.mainservice import MainService
|
||||||
|
|
||||||
__all__ = ("Api",)
|
__all__ = ("Api",)
|
||||||
|
|
||||||
|
@ -4,13 +4,13 @@ from typing import Callable
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
from v6d2ctx.at_of import AtOf
|
from v6d2ctx.at_of import AtOf
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Context, Explicit, command_type
|
||||||
from v6d3music.core.default_effects import *
|
from v6d3music.core.default_effects import DefaultEffects
|
||||||
from v6d3music.core.mainservice import *
|
from v6d3music.core.mainservice import MainService
|
||||||
from v6d3music.utils.assert_admin import *
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
from v6d3music.utils.catch import *
|
from v6d3music.utils.catch import catch
|
||||||
from v6d3music.utils.effects_for_preset import *
|
from v6d3music.utils.effects_for_preset import effects_for_preset
|
||||||
from v6d3music.utils.presets import *
|
from v6d3music.utils.presets import allowed_presets
|
||||||
|
|
||||||
__all__ = ("get_of",)
|
__all__ = ("get_of",)
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ def get_of(mainservice: MainService) -> Callable[[str], command_type]:
|
|||||||
args,
|
args,
|
||||||
f"""
|
f"""
|
||||||
`play ...args`
|
`play ...args`
|
||||||
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [tor] ...args`
|
`play url [- effects]/[+ preset] [[[h]]] [[m]] [s] [ignore] ...args`
|
||||||
`pause`
|
`pause`
|
||||||
`resume`
|
`resume`
|
||||||
presets: {shlex.join(allowed_presets)}
|
presets: {shlex.join(allowed_presets)}
|
||||||
|
@ -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 v6d2ctx.context import Context
|
||||||
from v6d3music.core.real_url import *
|
from v6d3music.core.real_url import real_url
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.core.ytaudio import *
|
from v6d3music.core.ytaudio import YTAudio
|
||||||
from v6d3music.utils.argctx import *
|
from v6d3music.utils.argctx import BoundCtx, InfoCtx
|
||||||
|
|
||||||
__all__ = ('create_ytaudio',)
|
__all__ = ("create_ytaudio",)
|
||||||
|
|
||||||
|
|
||||||
async def _create_ytaudio(
|
async def _create_ytaudio(servicing: YTAServicing, bound: BoundCtx) -> YTAudio:
|
||||||
servicing: YTAServicing, bound: BoundCtx
|
|
||||||
) -> YTAudio:
|
|
||||||
return YTAudio(
|
return YTAudio(
|
||||||
servicing,
|
servicing,
|
||||||
await real_url(servicing.caching, bound.url, False, bound.tor),
|
await real_url(bound.url, False),
|
||||||
bound.url,
|
bound.url,
|
||||||
bound.description,
|
bound.description,
|
||||||
bound.options,
|
bound.options,
|
||||||
bound.member,
|
bound.member,
|
||||||
bound.already_read,
|
bound.already_read,
|
||||||
bound.tor
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_ytaudio(
|
async def create_ytaudio(servicing: YTAServicing, ctx: Context, it: InfoCtx) -> YTAudio:
|
||||||
servicing: YTAServicing, ctx: Context, it: InfoCtx
|
|
||||||
) -> YTAudio:
|
|
||||||
bound = it.bind(ctx)
|
bound = it.bind(ctx)
|
||||||
return await _create_ytaudio(servicing, bound)
|
return await _create_ytaudio(servicing, bound)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
|
|
||||||
from ptvp35 import *
|
from ptvp35 import DbFactory, KVJson
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.config import myroot
|
from v6d3music.config import myroot
|
||||||
from v6d3music.utils.presets import *
|
from v6d3music.utils.presets import allowed_effects
|
||||||
|
|
||||||
__all__ = ('DefaultEffects',)
|
__all__ = ("DefaultEffects",)
|
||||||
|
|
||||||
|
|
||||||
class DefaultEffects:
|
class DefaultEffects:
|
||||||
@ -18,12 +18,12 @@ class DefaultEffects:
|
|||||||
|
|
||||||
async def set(self, gid: int, effects: str | None) -> None:
|
async def set(self, gid: int, effects: str | None) -> None:
|
||||||
if effects is not None and effects not in allowed_effects:
|
if effects is not None and effects not in allowed_effects:
|
||||||
raise Explicit('these effects are not allowed')
|
raise Explicit("these effects are not allowed")
|
||||||
await self.__db.set(gid, effects)
|
await self.__db.set(gid, effects)
|
||||||
|
|
||||||
async def __aenter__(self) -> 'DefaultEffects':
|
async def __aenter__(self) -> "DefaultEffects":
|
||||||
async with AsyncExitStack() as es:
|
async with AsyncExitStack() as es:
|
||||||
self.__db = await es.enter_async_context(DbFactory(myroot / 'effects.db', kvfactory=KVJson()))
|
self.__db = await es.enter_async_context(DbFactory(myroot / "effects.db", kvfactory=KVJson()))
|
||||||
self.__es = es.pop_all()
|
self.__es = es.pop_all()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -6,37 +6,37 @@ from typing import Optional
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from v6d3music.utils.fill import *
|
from v6d3music.utils.fill import FILL
|
||||||
from v6d3music.utils.tor_prefix import *
|
|
||||||
|
|
||||||
__all__ = ('FFmpegNormalAudio',)
|
__all__ = ("FFmpegNormalAudio",)
|
||||||
|
|
||||||
|
|
||||||
class FFmpegNormalAudio(discord.FFmpegAudio):
|
class FFmpegNormalAudio(discord.FFmpegAudio):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None,
|
self,
|
||||||
tor: bool
|
source,
|
||||||
|
*,
|
||||||
|
executable="ffmpeg",
|
||||||
|
pipe=False,
|
||||||
|
stderr=None,
|
||||||
|
before_options=None,
|
||||||
|
options=None,
|
||||||
):
|
):
|
||||||
self.source = source
|
self.source = source
|
||||||
args = []
|
args = []
|
||||||
if tor:
|
subprocess_kwargs = {"stdin": source if pipe else subprocess.DEVNULL, "stderr": stderr}
|
||||||
_tor_prefix = tor_prefix()
|
|
||||||
args.extend([*_tor_prefix[1:], executable])
|
|
||||||
executable = _tor_prefix[0]
|
|
||||||
|
|
||||||
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
|
|
||||||
|
|
||||||
if isinstance(before_options, str):
|
if isinstance(before_options, str):
|
||||||
args.extend(shlex.split(before_options))
|
args.extend(shlex.split(before_options))
|
||||||
|
|
||||||
args.append('-i')
|
args.append("-i")
|
||||||
args.append('-' if pipe else source)
|
args.append("-" if pipe else source)
|
||||||
args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning'))
|
args.extend(("-f", "s16le", "-ar", "48000", "-ac", "2", "-loglevel", "warning"))
|
||||||
|
|
||||||
if isinstance(options, str):
|
if isinstance(options, str):
|
||||||
args.extend(shlex.split(options))
|
args.extend(shlex.split(options))
|
||||||
|
|
||||||
args.append('pipe:1')
|
args.append("pipe:1")
|
||||||
|
|
||||||
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
|
super().__init__(source, executable=executable, args=args, **subprocess_kwargs)
|
||||||
|
|
||||||
@ -85,9 +85,9 @@ class FFmpegNormalAudio(discord.FFmpegAudio):
|
|||||||
ret = self._raw_read()
|
ret = self._raw_read()
|
||||||
if len(ret) != discord.opus.Encoder.FRAME_SIZE:
|
if len(ret) != discord.opus.Encoder.FRAME_SIZE:
|
||||||
if self._process.poll() is None:
|
if self._process.poll() is None:
|
||||||
print('poll')
|
print("poll")
|
||||||
return FILL
|
return FILL
|
||||||
return b''
|
return b""
|
||||||
self.loaded_at = time.time()
|
self.loaded_at = time.time()
|
||||||
self._loaded = True
|
self._loaded = True
|
||||||
return ret
|
return ret
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
from ptvp35 import *
|
from ptvp35 import DbConnection
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.core.queueaudio import *
|
from v6d3music.core.queueaudio import QueueAudio
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.utils.assert_admin import *
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
|
|
||||||
__all__ = ('MainAudio',)
|
__all__ = ("MainAudio",)
|
||||||
|
|
||||||
|
|
||||||
class MainAudio(discord.PCMVolumeTransformer):
|
class MainAudio(discord.PCMVolumeTransformer):
|
||||||
@ -18,9 +18,9 @@ class MainAudio(discord.PCMVolumeTransformer):
|
|||||||
async def set(self, volume: float, member: discord.Member):
|
async def set(self, volume: float, member: discord.Member):
|
||||||
assert_admin(member)
|
assert_admin(member)
|
||||||
if volume < 0.01:
|
if volume < 0.01:
|
||||||
raise Explicit('volume too small')
|
raise Explicit("volume too small")
|
||||||
if volume > 1:
|
if volume > 1:
|
||||||
raise Explicit('volume too big')
|
raise Explicit("volume too big")
|
||||||
self.volume = volume
|
self.volume = volume
|
||||||
await self.db.set(member.guild.id, volume)
|
await self.db.set(member.guild.id, volume)
|
||||||
|
|
||||||
@ -28,5 +28,7 @@ class MainAudio(discord.PCMVolumeTransformer):
|
|||||||
return self.db.get(self.queue.guild.id, 0.2)
|
return self.db.get(self.queue.guild.id, 0.2)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, servicing: YTAServicing, db: DbConnection, queues: DbConnection, guild: discord.Guild) -> 'MainAudio':
|
async def create(
|
||||||
|
cls, servicing: YTAServicing, db: DbConnection, queues: DbConnection, guild: discord.Guild
|
||||||
|
) -> "MainAudio":
|
||||||
return cls(db, await QueueAudio.create(servicing, queues, guild))
|
return cls(db, await QueueAudio.create(servicing, queues, guild))
|
||||||
|
@ -6,29 +6,28 @@ from typing import AsyncIterable, Callable, TypeVar
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
import v6d3music.processing.pool
|
import v6d3music.processing.pool
|
||||||
from ptvp35 import *
|
from ptvp35 import DbFactory, KVJson
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Context, Explicit
|
||||||
from v6d2ctx.integration.event import *
|
from v6d2ctx.integration.event import Event, SendableEvents
|
||||||
from v6d2ctx.integration.responsetype import *
|
from v6d2ctx.integration.responsetype import ResponseType
|
||||||
from v6d2ctx.integration.targets import *
|
from v6d2ctx.integration.targets import Async, Targets
|
||||||
from v6d2ctx.lock_for import *
|
from v6d2ctx.lock_for import Locks
|
||||||
from v6d3music.config import myroot
|
from v6d3music.config import myroot
|
||||||
from v6d3music.core.caching import *
|
from v6d3music.core.default_effects import DefaultEffects
|
||||||
from v6d3music.core.default_effects import *
|
from v6d3music.core.mainaudio import MainAudio
|
||||||
from v6d3music.core.mainaudio import *
|
from v6d3music.core.monitoring import Monitoring, PersistentMonitoring
|
||||||
from v6d3music.core.monitoring import *
|
from v6d3music.core.queueaudio import QueueAudio
|
||||||
from v6d3music.core.queueaudio import *
|
from v6d3music.core.ystate import YState
|
||||||
from v6d3music.core.ystate import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaudio import YTAudio
|
||||||
from v6d3music.core.ytaudio import *
|
from v6d3music.processing.pool import Pool, PoolEvent
|
||||||
from v6d3music.processing.pool import *
|
from v6d3music.utils.argctx import ArgCtx
|
||||||
from v6d3music.utils.assert_admin import assert_admin
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
from v6d3music.utils.argctx import *
|
|
||||||
|
|
||||||
__all__ = ('MainService', 'MainMode', 'MainContext', 'MainEvent')
|
__all__ = ("MainService", "MainMode", "MainContext", "MainEvent")
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class MainEvent(Event):
|
class MainEvent(Event):
|
||||||
@ -40,7 +39,7 @@ class _PMEvent(MainEvent):
|
|||||||
self.event = event
|
self.event = event
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'pool': self.event.json()}
|
return {"pool": self.event.json()}
|
||||||
|
|
||||||
|
|
||||||
class _PMSendable(SendableEvents[PoolEvent]):
|
class _PMSendable(SendableEvents[PoolEvent]):
|
||||||
@ -69,7 +68,7 @@ class MainService:
|
|||||||
self.__ystates: dict[discord.Guild, YState] = {}
|
self.__ystates: dict[discord.Guild, YState] = {}
|
||||||
|
|
||||||
def register_instrumentation(self):
|
def register_instrumentation(self):
|
||||||
self.targets.register_type(v6d3music.processing.pool.UnitJob, 'run', Async)
|
self.targets.register_type(v6d3music.processing.pool.UnitJob, "run", Async)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
|
async def raw_vc_for_member(member: discord.Member) -> discord.VoiceClient:
|
||||||
@ -77,10 +76,10 @@ class MainService:
|
|||||||
if vc is None or vc.channel is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
|
if vc is None or vc.channel is None or isinstance(vc, discord.VoiceClient) and not vc.is_connected():
|
||||||
vs: discord.VoiceState | None = member.voice
|
vs: discord.VoiceState | None = member.voice
|
||||||
if vs is None:
|
if vs is None:
|
||||||
raise Explicit('not connected')
|
raise Explicit("not connected")
|
||||||
vch: discord.abc.Connectable | None = vs.channel
|
vch: discord.abc.Connectable | None = vs.channel
|
||||||
if vch is None:
|
if vch is None:
|
||||||
raise Explicit('not connected')
|
raise Explicit("not connected")
|
||||||
try:
|
try:
|
||||||
vc = await vch.connect()
|
vc = await vch.connect()
|
||||||
except discord.ClientException as e:
|
except discord.ClientException as e:
|
||||||
@ -89,32 +88,31 @@ class MainService:
|
|||||||
assert vc is not None
|
assert vc is not None
|
||||||
await member.guild.fetch_channels()
|
await member.guild.fetch_channels()
|
||||||
await vc.disconnect(force=True)
|
await vc.disconnect(force=True)
|
||||||
raise Explicit('try again later') from e
|
raise Explicit("try again later") from e
|
||||||
assert isinstance(vc, discord.VoiceClient)
|
assert isinstance(vc, discord.VoiceClient)
|
||||||
return vc
|
return vc
|
||||||
|
|
||||||
async def raw_vc_for(self, ctx: Context) -> discord.VoiceClient:
|
async def raw_vc_for(self, ctx: Context) -> discord.VoiceClient:
|
||||||
if ctx.member is None:
|
if ctx.member is None:
|
||||||
raise Explicit('not in a guild')
|
raise Explicit("not in a guild")
|
||||||
return await self.raw_vc_for_member(ctx.member)
|
return await self.raw_vc_for_member(ctx.member)
|
||||||
|
|
||||||
def mode(self, *, create: bool, force_play: bool) -> 'MainMode':
|
def mode(self, *, create: bool, force_play: bool) -> "MainMode":
|
||||||
return MainMode(self, create=create, force_play=force_play)
|
return MainMode(self, create=create, force_play=force_play)
|
||||||
|
|
||||||
def context(self, ctx: Context, *, create: bool, force_play: bool) -> 'MainContext':
|
def context(self, ctx: Context, *, create: bool, force_play: bool) -> "MainContext":
|
||||||
return self.mode(create=create, force_play=force_play).context(ctx)
|
return self.mode(create=create, force_play=force_play).context(ctx)
|
||||||
|
|
||||||
async def create(self, guild: discord.Guild) -> MainAudio:
|
async def create(self, guild: discord.Guild) -> MainAudio:
|
||||||
return await MainAudio.create(self.__servicing, self.__volumes, self.__queues, guild)
|
return await MainAudio.create(self.__servicing, self.__volumes, self.__queues, guild)
|
||||||
|
|
||||||
async def __aenter__(self) -> 'MainService':
|
async def __aenter__(self) -> "MainService":
|
||||||
async with AsyncExitStack() as es:
|
async with AsyncExitStack() as es:
|
||||||
self.__locks = Locks()
|
self.__locks = Locks()
|
||||||
self.__volumes = await es.enter_async_context(DbFactory(myroot / 'volume.db', kvfactory=KVJson()))
|
self.__volumes = await es.enter_async_context(DbFactory(myroot / "volume.db", kvfactory=KVJson()))
|
||||||
self.__queues = await es.enter_async_context(DbFactory(myroot / 'queue.db', kvfactory=KVJson()))
|
self.__queues = await es.enter_async_context(DbFactory(myroot / "queue.db", kvfactory=KVJson()))
|
||||||
self.__caching = await es.enter_async_context(Caching())
|
|
||||||
self.__pool = await es.enter_async_context(Pool(5, self.__pool_events))
|
self.__pool = await es.enter_async_context(Pool(5, self.__pool_events))
|
||||||
self.__servicing = YTAServicing(self.__caching, self.__pool)
|
self.__servicing = YTAServicing(self.__pool)
|
||||||
self.__vcs_restored: asyncio.Future[None] = asyncio.Future()
|
self.__vcs_restored: asyncio.Future[None] = asyncio.Future()
|
||||||
self.__save_task = asyncio.create_task(self.save_daemon())
|
self.__save_task = asyncio.create_task(self.save_daemon())
|
||||||
self.monitoring = await es.enter_async_context(Monitoring())
|
self.monitoring = await es.enter_async_context(Monitoring())
|
||||||
@ -143,7 +141,7 @@ class MainService:
|
|||||||
if vc.is_playing():
|
if vc.is_playing():
|
||||||
if vc.guild is not None and vc.channel is not None:
|
if vc.guild is not None and vc.channel is not None:
|
||||||
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
|
vcs.append((vc.guild.id, vc.channel.id, vc.is_paused()))
|
||||||
self.__queues.set_nowait('vcs', vcs)
|
self.__queues.set_nowait("vcs", vcs)
|
||||||
|
|
||||||
async def save_commit(self) -> None:
|
async def save_commit(self) -> None:
|
||||||
await self.__queues.commit()
|
await self.__queues.commit()
|
||||||
@ -159,7 +157,7 @@ class MainService:
|
|||||||
|
|
||||||
async def save_job(self):
|
async def save_job(self):
|
||||||
await self.__vcs_restored
|
await self.__vcs_restored
|
||||||
print('starting saving')
|
print("starting saving")
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await self.save_all(True, not self.client.is_closed())
|
await self.save_all(True, not self.client.is_closed())
|
||||||
@ -177,22 +175,14 @@ class MainService:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
await self.save_all(False, False)
|
await self.save_all(False, False)
|
||||||
print('saved')
|
print("saved")
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
async def _restore_vc(self, guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None:
|
async def _restore_vc(self, guild: discord.Guild, vccid: int, vc_is_paused: bool) -> None:
|
||||||
channels = await guild.fetch_channels()
|
channels = await guild.fetch_channels()
|
||||||
channel: discord.VoiceChannel
|
channel: discord.VoiceChannel
|
||||||
channel, = [
|
(channel,) = [ch for ch in (chc for chc in channels if isinstance(chc, discord.VoiceChannel)) if ch.id == vccid]
|
||||||
ch for ch in
|
|
||||||
(
|
|
||||||
chc for chc in channels
|
|
||||||
if
|
|
||||||
isinstance(chc, discord.VoiceChannel)
|
|
||||||
)
|
|
||||||
if ch.id == vccid
|
|
||||||
]
|
|
||||||
vp: discord.VoiceProtocol = await channel.connect()
|
vp: discord.VoiceProtocol = await channel.connect()
|
||||||
assert isinstance(vp, discord.VoiceClient)
|
assert isinstance(vp, discord.VoiceClient)
|
||||||
vc = vp
|
vc = vp
|
||||||
@ -201,22 +191,22 @@ class MainService:
|
|||||||
vc.pause()
|
vc.pause()
|
||||||
|
|
||||||
def lock_for(self, guild: discord.Guild | None) -> asyncio.Lock:
|
def lock_for(self, guild: discord.Guild | None) -> asyncio.Lock:
|
||||||
return self.__locks.lock_for(guild, 'not in a guild')
|
return self.__locks.lock_for(guild, "not in a guild")
|
||||||
|
|
||||||
async def restore_vc(self, vcgid: int, vccid: int, vc_is_paused: bool) -> None:
|
async def restore_vc(self, vcgid: int, vccid: int, vc_is_paused: bool) -> None:
|
||||||
try:
|
try:
|
||||||
print(f'vc restoring {vcgid}')
|
print(f"vc restoring {vcgid}")
|
||||||
guild: discord.Guild = await self.client.fetch_guild(vcgid)
|
guild: discord.Guild = await self.client.fetch_guild(vcgid)
|
||||||
async with self.lock_for(guild):
|
async with self.lock_for(guild):
|
||||||
await self._restore_vc(guild, vccid, vc_is_paused)
|
await self._restore_vc(guild, vccid, vc_is_paused)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'vc {vcgid} {vccid} {vc_is_paused} failed')
|
print(f"vc {vcgid} {vccid} {vc_is_paused} failed")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
print(f'vc restored {vcgid} {vccid}')
|
print(f"vc restored {vcgid} {vccid}")
|
||||||
|
|
||||||
async def restore_vcs(self) -> None:
|
async def restore_vcs(self) -> None:
|
||||||
vcs: list[tuple[int, int, bool]] = self.__queues.get('vcs', [])
|
vcs: list[tuple[int, int, bool]] = self.__queues.get("vcs", [])
|
||||||
try:
|
try:
|
||||||
tasks = []
|
tasks = []
|
||||||
for vcgid, vccid, vc_is_paused in vcs:
|
for vcgid, vccid, vc_is_paused in vcs:
|
||||||
@ -266,17 +256,14 @@ class MainMode:
|
|||||||
if vc.guild in self.mains:
|
if vc.guild in self.mains:
|
||||||
source = self.mains[vc.guild]
|
source = self.mains[vc.guild]
|
||||||
elif self.create:
|
elif self.create:
|
||||||
source = self.mains.setdefault(
|
source = self.mains.setdefault(vc.guild, await self.mainservice.create(vc.guild))
|
||||||
vc.guild,
|
|
||||||
await self.mainservice.create(vc.guild)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise Explicit('not playing, use `queue pause` or `queue resume`')
|
raise Explicit("not playing, use `queue pause` or `queue resume`")
|
||||||
if vc.source != source or self.create and not vc.is_playing() and (self.force_play or not vc.is_paused()):
|
if vc.source != source or self.create and not vc.is_playing() and (self.force_play or not vc.is_paused()):
|
||||||
vc.play(source)
|
vc.play(source)
|
||||||
return source
|
return source
|
||||||
|
|
||||||
def context(self, ctx: Context) -> 'MainContext':
|
def context(self, ctx: Context) -> "MainContext":
|
||||||
return MainContext(self, ctx)
|
return MainContext(self, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ from typing import MutableSequence
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from ptvp35 import *
|
from ptvp35 import DbConnection
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.core.ytaudio import *
|
from v6d3music.core.ytaudio import YTAudio
|
||||||
from v6d3music.utils.assert_admin import *
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
from v6d3music.utils.fill import *
|
from v6d3music.utils.fill import FILL
|
||||||
|
|
||||||
__all__ = ('QueueAudio',)
|
__all__ = ("QueueAudio",)
|
||||||
|
|
||||||
|
|
||||||
PRE_SET_LENGTH = 6
|
PRE_SET_LENGTH = 6
|
||||||
@ -45,15 +45,15 @@ class QueueAudio(discord.AudioSource):
|
|||||||
try:
|
try:
|
||||||
respawned.append(await YTAudio.respawn(servicing, guild, audio_respawn))
|
respawned.append(await YTAudio.respawn(servicing, guild, audio_respawn))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('audio respawn failed', e)
|
print("audio respawn failed", e)
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('queue respawn failed', e)
|
print("queue respawn failed", e)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return respawned
|
return respawned
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> 'QueueAudio':
|
async def create(cls, servicing: YTAServicing, db: DbConnection, guild: discord.Guild) -> "QueueAudio":
|
||||||
return cls(db, guild, await cls.respawned(servicing, db, guild))
|
return cls(db, guild, await cls.respawned(servicing, db, guild))
|
||||||
|
|
||||||
async def save(self, delay: bool) -> None:
|
async def save(self, delay: bool) -> None:
|
||||||
@ -139,18 +139,18 @@ class QueueAudio(discord.AudioSource):
|
|||||||
|
|
||||||
async def format(self, limit=100) -> str:
|
async def format(self, limit=100) -> str:
|
||||||
if limit > 100 or limit < -100:
|
if limit > 100 or limit < -100:
|
||||||
raise Explicit('queue limit is too large')
|
raise Explicit("queue limit is too large")
|
||||||
stream = StringIO()
|
stream = StringIO()
|
||||||
lst = list(self.queue)
|
lst = list(self.queue)
|
||||||
llst = len(lst)
|
llst = len(lst)
|
||||||
|
|
||||||
def write():
|
def write():
|
||||||
stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n')
|
stream.write(f"`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n")
|
||||||
|
|
||||||
if limit >= 0:
|
if limit >= 0:
|
||||||
for i, audio in enumerate(lst):
|
for i, audio in enumerate(lst):
|
||||||
if i >= limit:
|
if i >= limit:
|
||||||
stream.write(f'cutting queue at {limit} results, {llst - limit} remaining.\n')
|
stream.write(f"cutting queue at {limit} results, {llst - limit} remaining.\n")
|
||||||
break
|
break
|
||||||
write()
|
write()
|
||||||
else:
|
else:
|
||||||
@ -169,18 +169,19 @@ class QueueAudio(discord.AudioSource):
|
|||||||
|
|
||||||
async def pubjson(self, member: discord.Member, limit: int) -> list:
|
async def pubjson(self, member: discord.Member, limit: int) -> list:
|
||||||
import random
|
import random
|
||||||
|
|
||||||
audios = list(self.queue)
|
audios = list(self.queue)
|
||||||
return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))]
|
return [await audio.pubjson(member) for audio, _ in zip(audios, range(limit))]
|
||||||
|
|
||||||
def repeat(self, n: int, p: int, t: int | None) -> None:
|
def repeat(self, n: int, p: int, t: int | None) -> None:
|
||||||
if not self.queue:
|
if not self.queue:
|
||||||
raise Explicit('empty queue')
|
raise Explicit("empty queue")
|
||||||
if n > 99:
|
if n > 99:
|
||||||
raise Explicit('too long')
|
raise Explicit("too long")
|
||||||
try:
|
try:
|
||||||
audio = self.queue[p]
|
audio = self.queue[p]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise Explicit('no track at that index')
|
raise Explicit("no track at that index")
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
if t is None:
|
if t is None:
|
||||||
self.queue.append(audio.copy())
|
self.queue.append(audio.copy())
|
||||||
@ -196,7 +197,7 @@ class QueueAudio(discord.AudioSource):
|
|||||||
|
|
||||||
def branch(self, effects: str | None) -> None:
|
def branch(self, effects: str | None) -> None:
|
||||||
if not self.queue:
|
if not self.queue:
|
||||||
raise Explicit('empty queue')
|
raise Explicit("empty queue")
|
||||||
audio = self.queue[0].branch()
|
audio = self.queue[0].branch()
|
||||||
if effects is not None:
|
if effects is not None:
|
||||||
audio.set_effects(effects or None)
|
audio.set_effects(effects or None)
|
||||||
|
@ -1,44 +1,22 @@
|
|||||||
import asyncio
|
import aiohttp
|
||||||
import os
|
|
||||||
|
|
||||||
from adaas.cachedb import *
|
from adaas.cachedb import RemoteCache
|
||||||
from v6d3music.core.caching import *
|
|
||||||
from v6d3music.utils.bytes_hash import *
|
|
||||||
from v6d3music.utils.tor_prefix import *
|
|
||||||
|
|
||||||
__all__ = ('real_url',)
|
__all__ = ("real_url",)
|
||||||
|
|
||||||
adaas_available = bool(os.getenv('adaasurl'))
|
|
||||||
if adaas_available:
|
|
||||||
print('running real_url through adaas')
|
|
||||||
|
|
||||||
|
|
||||||
async def _resolve_url(url: str, tor: bool) -> str:
|
async def real_url(url: str, override: bool) -> str:
|
||||||
args = []
|
base = RemoteCache()
|
||||||
if tor:
|
async with aiohttp.ClientSession() as session:
|
||||||
args.extend(tor_prefix())
|
async with session.post(
|
||||||
args.extend(
|
f"{base}/resolve",
|
||||||
[
|
json={
|
||||||
'yt-dlp', '--no-playlist', '-f', 'bestaudio', '-g', '--', url,
|
"base": base,
|
||||||
]
|
"url": url,
|
||||||
)
|
"uncached": override,
|
||||||
ap = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE)
|
"x": True,
|
||||||
code = await ap.wait()
|
},
|
||||||
if code:
|
) as resp:
|
||||||
raise RuntimeError(code)
|
if resp.status != 200:
|
||||||
assert ap.stdout is not None
|
raise IOError(resp.status)
|
||||||
return (await ap.stdout.readline()).decode()[:-1]
|
return await resp.text()
|
||||||
|
|
||||||
|
|
||||||
async def real_url(caching: Caching, url: str, override: bool, tor: bool) -> str:
|
|
||||||
if adaas_available and not tor:
|
|
||||||
return await RemoteCache().real_url(url, override, tor, True)
|
|
||||||
hurl: str = bytes_hash(url.encode())
|
|
||||||
if not override:
|
|
||||||
curl: str | None = caching.get(hurl)
|
|
||||||
if curl is not None:
|
|
||||||
print('using cached', hurl)
|
|
||||||
return curl
|
|
||||||
rurl: str = await _resolve_url(url, tor)
|
|
||||||
caching.schedule_cache(hurl, rurl, override, tor)
|
|
||||||
return rurl
|
|
||||||
|
@ -3,13 +3,13 @@ from collections import deque
|
|||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Any, AsyncIterable, Callable, Coroutine, Iterable
|
from typing import Any, AsyncIterable, Callable, Coroutine, Iterable
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Context, Explicit
|
||||||
from v6d2ctx.integration.responsetype import *
|
from v6d2ctx.integration.responsetype import ResponseType, cast_to_response
|
||||||
from v6d3music.core.create_ytaudio import *
|
from v6d3music.core.create_ytaudio import create_ytaudio
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.core.ytaudio import *
|
from v6d3music.core.ytaudio import YTAudio
|
||||||
from v6d3music.processing.pool import *
|
from v6d3music.processing.pool import JobContext, JobStatusChanged, JobUnit, Pool
|
||||||
from v6d3music.utils.argctx import *
|
from v6d3music.utils.argctx import InfoCtx, UrlCtx
|
||||||
|
|
||||||
__all__ = ("YState",)
|
__all__ = ("YState",)
|
||||||
|
|
||||||
@ -130,7 +130,6 @@ class YStream(JobUnit):
|
|||||||
"info": cast_to_response(entry.info),
|
"info": cast_to_response(entry.info),
|
||||||
"effects": entry.effects,
|
"effects": entry.effects,
|
||||||
"already_read": entry.already_read,
|
"already_read": entry.already_read,
|
||||||
"tor": entry.tor,
|
|
||||||
"ignore": entry.ignore,
|
"ignore": entry.ignore,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -155,7 +154,6 @@ class YStream(JobUnit):
|
|||||||
"url": source.url,
|
"url": source.url,
|
||||||
"effects": source.effects,
|
"effects": source.effects,
|
||||||
"already_read": source.already_read,
|
"already_read": source.already_read,
|
||||||
"tor": source.tor,
|
|
||||||
"ignore": source.ignore,
|
"ignore": source.ignore,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
from v6d3music.core.caching import *
|
from v6d3music.processing.abstractrunner import AbstractRunner
|
||||||
from v6d3music.processing.abstractrunner import *
|
|
||||||
|
|
||||||
|
__all__ = ("YTAServicing",)
|
||||||
__all__ = ('YTAServicing',)
|
|
||||||
|
|
||||||
|
|
||||||
class YTAServicing:
|
class YTAServicing:
|
||||||
def __init__(self, caching: Caching, runner: AbstractRunner) -> None:
|
def __init__(self, runner: AbstractRunner) -> None:
|
||||||
self.caching = caching
|
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
|
@ -6,15 +6,14 @@ from typing import Optional
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.core.ffmpegnormalaudio import *
|
from v6d3music.core.ffmpegnormalaudio import FFmpegNormalAudio
|
||||||
from v6d3music.core.real_url import *
|
from v6d3music.core.real_url import real_url
|
||||||
from v6d3music.core.ytaservicing import *
|
from v6d3music.core.ytaservicing import YTAServicing
|
||||||
from v6d3music.processing.abstractrunner import *
|
from v6d3music.processing.abstractrunner import CoroContext, CoroStatusChanged
|
||||||
from v6d3music.utils.fill import *
|
from v6d3music.utils.fill import FILL
|
||||||
from v6d3music.utils.options_for_effects import *
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
from v6d3music.utils.sparq import *
|
from v6d3music.utils.sparq import sparq
|
||||||
from v6d3music.utils.tor_prefix import *
|
|
||||||
|
|
||||||
__all__ = ("YTAudio",)
|
__all__ = ("YTAudio",)
|
||||||
|
|
||||||
@ -31,7 +30,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
options: Optional[str],
|
options: Optional[str],
|
||||||
rby: discord.Member | None,
|
rby: discord.Member | None,
|
||||||
already_read: int,
|
already_read: int,
|
||||||
tor: bool,
|
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
stop_at: int | None = None,
|
stop_at: int | None = None,
|
||||||
@ -46,7 +44,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
self.options = options
|
self.options = options
|
||||||
self.rby = rby
|
self.rby = rby
|
||||||
self.already_read = already_read
|
self.already_read = already_read
|
||||||
self.tor = tor
|
|
||||||
self.regenerating = False
|
self.regenerating = False
|
||||||
# self.set_source()
|
# self.set_source()
|
||||||
self._durations: dict[str, str] = {}
|
self._durations: dict[str, str] = {}
|
||||||
@ -74,9 +71,7 @@ class YTAudio(discord.AudioSource):
|
|||||||
|
|
||||||
def set_source(self):
|
def set_source(self):
|
||||||
self.schedule_duration_update()
|
self.schedule_duration_update()
|
||||||
self.source = FFmpegNormalAudio(
|
self.source = FFmpegNormalAudio(self.url, options=self.options, before_options=self.before_options())
|
||||||
self.url, options=self.options, before_options=self.before_options(), tor=self.tor
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_already_read(self, already_read: int):
|
def set_already_read(self, already_read: int):
|
||||||
self.already_read = already_read
|
self.already_read = already_read
|
||||||
@ -110,9 +105,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
if url in self._durations:
|
if url in self._durations:
|
||||||
return
|
return
|
||||||
self._durations.setdefault(url, "")
|
self._durations.setdefault(url, "")
|
||||||
if self.tor:
|
|
||||||
args = [*tor_prefix()]
|
|
||||||
else:
|
|
||||||
args = []
|
args = []
|
||||||
args += [
|
args += [
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
@ -225,7 +217,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
"options": self.options,
|
"options": self.options,
|
||||||
"rby": None if self.rby is None else self.rby.id,
|
"rby": None if self.rby is None else self.rby.id,
|
||||||
"already_read": self.already_read,
|
"already_read": self.already_read,
|
||||||
"tor": self.tor,
|
|
||||||
"stop_at": self.stop_at,
|
"stop_at": self.stop_at,
|
||||||
"durations": self._reduced_durations(),
|
"durations": self._reduced_durations(),
|
||||||
}
|
}
|
||||||
@ -251,7 +242,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
respawn["options"],
|
respawn["options"],
|
||||||
member,
|
member,
|
||||||
respawn["already_read"],
|
respawn["already_read"],
|
||||||
respawn.get("tor", False),
|
|
||||||
stop_at=respawn.get("stop_at", None),
|
stop_at=respawn.get("stop_at", None),
|
||||||
)
|
)
|
||||||
audio._durations |= respawn.get("durations", {})
|
audio._durations |= respawn.get("durations", {})
|
||||||
@ -260,7 +250,7 @@ class YTAudio(discord.AudioSource):
|
|||||||
async def regenerate(self, reason: str):
|
async def regenerate(self, reason: str):
|
||||||
try:
|
try:
|
||||||
print(f"regenerating {self.origin} {reason=}")
|
print(f"regenerating {self.origin} {reason=}")
|
||||||
self.url = await real_url(self.servicing.caching, self.origin, True, self.tor)
|
self.url = await real_url(self.origin, True)
|
||||||
if hasattr(self, "source"):
|
if hasattr(self, "source"):
|
||||||
self.source.cleanup()
|
self.source.cleanup()
|
||||||
self.set_source()
|
self.set_source()
|
||||||
@ -286,7 +276,6 @@ class YTAudio(discord.AudioSource):
|
|||||||
self.options,
|
self.options,
|
||||||
self.rby,
|
self.rby,
|
||||||
0,
|
0,
|
||||||
self.tor,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def branch(self) -> "YTAudio":
|
def branch(self) -> "YTAudio":
|
||||||
@ -301,6 +290,5 @@ class YTAudio(discord.AudioSource):
|
|||||||
self.options,
|
self.options,
|
||||||
self.rby,
|
self.rby,
|
||||||
stop_at,
|
stop_at,
|
||||||
self.tor,
|
|
||||||
)
|
)
|
||||||
return audio
|
return audio
|
||||||
|
@ -1,29 +1,26 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
import struct
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from ptvp35 import *
|
from ptvp35 import DbConnection
|
||||||
from rainbowadn.instrument import Instrumentation
|
from rainbowadn.instrument import Instrumentation
|
||||||
from v6d1tokens.client import *
|
from v6d1tokens.client import request_token
|
||||||
from v6d2ctx.handle_content import *
|
from v6d2ctx.handle_content import handle_content
|
||||||
from v6d2ctx.integration.event import *
|
from v6d2ctx.integration.event import Events
|
||||||
from v6d2ctx.integration.targets import *
|
from v6d2ctx.integration.targets import Targets
|
||||||
from v6d2ctx.pain import ABlockMonitor, ALog
|
from v6d2ctx.pain import ABlockMonitor
|
||||||
from v6d2ctx.serve import *
|
from v6d2ctx.serve import serve
|
||||||
from v6d3music.api import *
|
from v6d3music.api import Api
|
||||||
from v6d3music.app import *
|
from v6d3music.app import AppContext
|
||||||
from v6d3music.commands import *
|
from v6d3music.commands import get_of
|
||||||
from v6d3music.config import prefix
|
from v6d3music.config import prefix
|
||||||
from v6d3music.core.caching import *
|
from v6d3music.core.default_effects import DefaultEffects
|
||||||
from v6d3music.core.default_effects import *
|
from v6d3music.core.mainservice import MainService
|
||||||
from v6d3music.core.mainservice import *
|
|
||||||
from v6d3music.core.set_config import set_config
|
from v6d3music.core.set_config import set_config
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
@ -169,13 +166,9 @@ async def amain(client: discord.Client):
|
|||||||
else:
|
else:
|
||||||
token = input("token:")
|
token = input("token:")
|
||||||
tokenpath.write_text(token)
|
tokenpath.write_text(token)
|
||||||
elif token_ := os.getenv("trial_token"):
|
|
||||||
token = token_
|
|
||||||
else:
|
else:
|
||||||
token = await request_token("music", "token")
|
token = await request_token("music", "token")
|
||||||
await client.login(token)
|
await client.login(token)
|
||||||
if os.getenv("v6tor", None) is None:
|
|
||||||
print("no tor")
|
|
||||||
await client.connect()
|
await client.connect()
|
||||||
print("exited")
|
print("exited")
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
__all__ = ('AbstractRunner', 'CoroEvent', 'CoroContext', 'CoroStatusChanged')
|
__all__ = ("AbstractRunner", "CoroEvent", "CoroContext", "CoroStatusChanged")
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable, Coroutine, TypeVar
|
from typing import Any, Callable, Coroutine, TypeVar
|
||||||
|
|
||||||
from v6d2ctx.integration.event import *
|
from v6d2ctx.integration.event import Event, SendableEvents
|
||||||
from v6d2ctx.integration.responsetype import *
|
from v6d2ctx.integration.responsetype import ResponseType
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class CoroEvent(Event):
|
class CoroEvent(Event):
|
||||||
@ -23,7 +23,7 @@ class CoroStatusChanged(CoroEvent):
|
|||||||
self.status = status
|
self.status = status
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'status': self.status}
|
return {"status": self.status}
|
||||||
|
|
||||||
|
|
||||||
class AbstractRunner(ABC):
|
class AbstractRunner(ABC):
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
__all__ = ('Job', 'Pool', 'JobUnit', 'JobContext', 'JobStatusChanged', 'PoolEvent')
|
__all__ = ("Job", "Pool", "JobUnit", "JobContext", "JobStatusChanged", "PoolEvent")
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
|
from typing import Any, Callable, Coroutine, Generic, TypeVar, Union
|
||||||
|
|
||||||
from v6d2ctx.integration.event import *
|
from v6d2ctx.integration.event import Event, SendableEvents
|
||||||
from v6d2ctx.integration.responsetype import *
|
from v6d2ctx.integration.responsetype import ResponseType
|
||||||
|
|
||||||
from .abstractrunner import *
|
from .abstractrunner import AbstractRunner, CoroContext, CoroEvent, CoroStatusChanged
|
||||||
|
|
||||||
|
|
||||||
class JobEvent(Event):
|
class JobEvent(Event):
|
||||||
@ -22,25 +22,25 @@ class Job:
|
|||||||
def __init__(self, future: asyncio.Future[None]) -> None:
|
def __init__(self, future: asyncio.Future[None]) -> None:
|
||||||
self.future = future
|
self.future = future
|
||||||
|
|
||||||
async def run(self, context: JobContext, /) -> Union['Job', None]:
|
async def run(self, context: JobContext, /) -> Union["Job", None]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'type': 'unknown'}
|
return {"type": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
class JobUnit:
|
class JobUnit:
|
||||||
async def run(self, context: JobContext, /) -> Union['JobUnit', None]:
|
async def run(self, context: JobContext, /) -> Union["JobUnit", None]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def wrap(self) -> Job:
|
def wrap(self) -> Job:
|
||||||
return UnitJob(asyncio.Future(), self)
|
return UnitJob(asyncio.Future(), self)
|
||||||
|
|
||||||
def at(self, pool: 'Pool') -> 'JDC':
|
def at(self, pool: "Pool") -> "JDC":
|
||||||
return JDC(self, pool)
|
return JDC(self, pool)
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'type': 'unknown'}
|
return {"type": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
class JobStatusChanged(JobEvent):
|
class JobStatusChanged(JobEvent):
|
||||||
@ -48,7 +48,7 @@ class JobStatusChanged(JobEvent):
|
|||||||
self.job = job
|
self.job = job
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'status': self.job.json()}
|
return {"status": self.job.json()}
|
||||||
|
|
||||||
|
|
||||||
class UnitJob(Job):
|
class UnitJob(Job):
|
||||||
@ -76,7 +76,7 @@ class _JWEvent(WorkerEvent):
|
|||||||
self.event = event
|
self.event = event
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'job': self.event.json()}
|
return {"job": self.event.json()}
|
||||||
|
|
||||||
|
|
||||||
class _JWSendable(SendableEvents[JobEvent]):
|
class _JWSendable(SendableEvents[JobEvent]):
|
||||||
@ -154,7 +154,7 @@ class Worker:
|
|||||||
|
|
||||||
def _cancel_job(self, job: Job) -> None:
|
def _cancel_job(self, job: Job) -> None:
|
||||||
try:
|
try:
|
||||||
job.future.set_exception(RuntimeError('task left in the worker after shutdown'))
|
job.future.set_exception(RuntimeError("task left in the worker after shutdown"))
|
||||||
except asyncio.InvalidStateError:
|
except asyncio.InvalidStateError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -176,19 +176,19 @@ class Worker:
|
|||||||
async def _task(self) -> None:
|
async def _task(self) -> None:
|
||||||
await self._work()
|
await self._work()
|
||||||
if self.__working:
|
if self.__working:
|
||||||
raise RuntimeError('worker left seemingly running after shutdown')
|
raise RuntimeError("worker left seemingly running after shutdown")
|
||||||
if not self.__queue.empty():
|
if not self.__queue.empty():
|
||||||
raise RuntimeError('worker failed to finish all its jobs')
|
raise RuntimeError("worker failed to finish all its jobs")
|
||||||
|
|
||||||
def start(self) -> asyncio.Future[None]:
|
def start(self) -> asyncio.Future[None]:
|
||||||
if self.__working:
|
if self.__working:
|
||||||
raise RuntimeError('starting an already running worker')
|
raise RuntimeError("starting an already running worker")
|
||||||
self.__working = True
|
self.__working = True
|
||||||
return asyncio.create_task(self._task())
|
return asyncio.create_task(self._task())
|
||||||
|
|
||||||
def submit(self, job: Job | None) -> None:
|
def submit(self, job: Job | None) -> None:
|
||||||
if not self.__working:
|
if not self.__working:
|
||||||
raise RuntimeError('submitting to a non-working worker')
|
raise RuntimeError("submitting to a non-working worker")
|
||||||
self._put_nowait(job)
|
self._put_nowait(job)
|
||||||
|
|
||||||
def working(self) -> bool:
|
def working(self) -> bool:
|
||||||
@ -205,8 +205,8 @@ class Worker:
|
|||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {
|
return {
|
||||||
'job': self._job_json(),
|
"job": self._job_json(),
|
||||||
'qsize': self.__queue.qsize(),
|
"qsize": self.__queue.qsize(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ class Working:
|
|||||||
self.__worker.submit(job)
|
self.__worker.submit(job)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls, events: SendableEvents[WorkerEvent], /) -> 'Working':
|
def start(cls, events: SendableEvents[WorkerEvent], /) -> "Working":
|
||||||
worker = Worker(events)
|
worker = Worker(events)
|
||||||
task = worker.start()
|
task = worker.start()
|
||||||
return cls(worker, task)
|
return cls(worker, task)
|
||||||
@ -238,7 +238,7 @@ class Working:
|
|||||||
return self.__worker.json()
|
return self.__worker.json()
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class CoroJD(JobUnit, Generic[T]):
|
class CoroJD(JobUnit, Generic[T]):
|
||||||
@ -255,7 +255,7 @@ class CoroJD(JobUnit, Generic[T]):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'coroutine': self.status}
|
return {"coroutine": self.status}
|
||||||
|
|
||||||
|
|
||||||
class _CJSendable(SendableEvents[CoroEvent]):
|
class _CJSendable(SendableEvents[CoroEvent]):
|
||||||
@ -281,7 +281,7 @@ class _WPEvent(PoolEvent):
|
|||||||
self.event = event
|
self.event = event
|
||||||
|
|
||||||
def json(self) -> ResponseType:
|
def json(self) -> ResponseType:
|
||||||
return {'worker': self.event.json()}
|
return {"worker": self.event.json()}
|
||||||
|
|
||||||
|
|
||||||
class _WPSendable(SendableEvents[WorkerEvent]):
|
class _WPSendable(SendableEvents[WorkerEvent]):
|
||||||
@ -295,7 +295,7 @@ class _WPSendable(SendableEvents[WorkerEvent]):
|
|||||||
class Pool(AbstractRunner):
|
class Pool(AbstractRunner):
|
||||||
def __init__(self, workers: int, events: SendableEvents[PoolEvent], /) -> None:
|
def __init__(self, workers: int, events: SendableEvents[PoolEvent], /) -> None:
|
||||||
if workers < 1:
|
if workers < 1:
|
||||||
raise ValueError('non-positive number of workers')
|
raise ValueError("non-positive number of workers")
|
||||||
self.__workers = workers
|
self.__workers = workers
|
||||||
self.__working = False
|
self.__working = False
|
||||||
self.__open = False
|
self.__open = False
|
||||||
@ -305,11 +305,11 @@ class Pool(AbstractRunner):
|
|||||||
def _start_worker(self) -> Working:
|
def _start_worker(self) -> Working:
|
||||||
return Working.start(self.__worker_events)
|
return Working.start(self.__worker_events)
|
||||||
|
|
||||||
async def __aenter__(self) -> 'Pool':
|
async def __aenter__(self) -> "Pool":
|
||||||
if self.__open:
|
if self.__open:
|
||||||
raise RuntimeError('starting an already open pool')
|
raise RuntimeError("starting an already open pool")
|
||||||
if self.__working:
|
if self.__working:
|
||||||
raise RuntimeError('starting an already running pool')
|
raise RuntimeError("starting an already running pool")
|
||||||
self.__working = True
|
self.__working = True
|
||||||
self.__pool: set[Working] = set(self._start_worker() for _ in range(self.__workers))
|
self.__pool: set[Working] = set(self._start_worker() for _ in range(self.__workers))
|
||||||
self.__open = True
|
self.__open = True
|
||||||
@ -317,9 +317,9 @@ class Pool(AbstractRunner):
|
|||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
if not self.__working:
|
if not self.__working:
|
||||||
raise RuntimeError('stopping a non-working pool')
|
raise RuntimeError("stopping a non-working pool")
|
||||||
if not self.__open:
|
if not self.__open:
|
||||||
raise RuntimeError('stopping a closed pool')
|
raise RuntimeError("stopping a closed pool")
|
||||||
self.__open = False
|
self.__open = False
|
||||||
for working in self.__pool:
|
for working in self.__pool:
|
||||||
await working.close()
|
await working.close()
|
||||||
@ -328,9 +328,9 @@ class Pool(AbstractRunner):
|
|||||||
|
|
||||||
def submit(self, job: Job) -> None:
|
def submit(self, job: Job) -> None:
|
||||||
if not self.__working:
|
if not self.__working:
|
||||||
raise RuntimeError('submitting to a non-working pool')
|
raise RuntimeError("submitting to a non-working pool")
|
||||||
if not self.__open:
|
if not self.__open:
|
||||||
raise RuntimeError('submitting to a closed pool')
|
raise RuntimeError("submitting to a closed pool")
|
||||||
min(self.__pool, key=lambda working: working.busy()).submit(job)
|
min(self.__pool, key=lambda working: working.busy()).submit(job)
|
||||||
|
|
||||||
def workers(self) -> int:
|
def workers(self) -> int:
|
||||||
@ -353,7 +353,7 @@ class JDC:
|
|||||||
self.__unit = unit
|
self.__unit = unit
|
||||||
self.__pool = pool
|
self.__pool = pool
|
||||||
|
|
||||||
async def __aenter__(self) -> 'JDC':
|
async def __aenter__(self) -> "JDC":
|
||||||
job = self.__unit.wrap()
|
job = self.__unit.wrap()
|
||||||
self.__future = job.future
|
self.__future = job.future
|
||||||
self.__pool.submit(job)
|
self.__pool.submit(job)
|
||||||
|
@ -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
|
import aiohttp
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from v6d3music.utils.extract import *
|
from adaas.cachedb import RemoteCache
|
||||||
|
|
||||||
__all__ = ('aextract',)
|
__all__ = ("aextract",)
|
||||||
|
|
||||||
|
|
||||||
async def aextract(params: dict, url: str, **kwargs):
|
async def aextract(params: dict, url: str, **kwargs):
|
||||||
with ThreadPoolExecutor() as pool:
|
async with aiohttp.ClientSession() as session:
|
||||||
return await asyncio.get_running_loop().run_in_executor(
|
base = RemoteCache().base
|
||||||
pool,
|
async with session.post(f"{base}/extract", json=dict(params=params, url=url, **kwargs)) as resp:
|
||||||
extract,
|
if resp.status != 200:
|
||||||
params,
|
raise IOError(resp.status)
|
||||||
url,
|
return await resp.json()
|
||||||
kwargs
|
|
||||||
)
|
|
||||||
|
@ -1,51 +1,49 @@
|
|||||||
import string
|
import string
|
||||||
from typing import Any, AsyncIterable
|
from typing import Any, AsyncIterable
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Context, Explicit, escape
|
||||||
from v6d3music.utils.assert_admin import *
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
from v6d3music.utils.effects_for_preset import *
|
from v6d3music.utils.effects_for_preset import effects_for_preset
|
||||||
from v6d3music.utils.entries_for_url import *
|
from v6d3music.utils.entries_for_url import entries_for_url
|
||||||
from v6d3music.utils.options_for_effects import *
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
from v6d3music.utils.presets import *
|
from v6d3music.utils.presets import allowed_effects, presets
|
||||||
from v6d3music.utils.sparq import *
|
from v6d3music.utils.sparq import sparq
|
||||||
|
|
||||||
__all__ = ('InfoCtx', 'BoundCtx', 'UrlCtx', 'ArgCtx',)
|
__all__ = (
|
||||||
|
"InfoCtx",
|
||||||
|
"BoundCtx",
|
||||||
|
"UrlCtx",
|
||||||
|
"ArgCtx",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PostCtx:
|
class PostCtx:
|
||||||
def __init__(
|
def __init__(self, effects: str | None) -> None:
|
||||||
self, effects: str | None
|
|
||||||
) -> None:
|
|
||||||
self.effects: str | None = effects
|
self.effects: str | None = effects
|
||||||
self.already_read: int = 0
|
self.already_read: int = 0
|
||||||
self.tor: bool = False
|
|
||||||
self.ignore: bool = False
|
self.ignore: bool = False
|
||||||
|
|
||||||
|
|
||||||
class InfoCtx:
|
class InfoCtx:
|
||||||
def __init__(
|
def __init__(self, info: dict[str, Any], post: PostCtx) -> None:
|
||||||
self, info: dict[str, Any], post: PostCtx
|
|
||||||
) -> None:
|
|
||||||
self.info = info
|
self.info = info
|
||||||
self.post = post
|
self.post = post
|
||||||
self.effects = post.effects
|
self.effects = post.effects
|
||||||
self.already_read = post.already_read
|
self.already_read = post.already_read
|
||||||
self.tor = post.tor
|
|
||||||
self.ignore = post.ignore
|
self.ignore = post.ignore
|
||||||
|
|
||||||
def bind(self, ctx: Context) -> 'BoundCtx':
|
def bind(self, ctx: Context) -> "BoundCtx":
|
||||||
return BoundCtx(self, ctx)
|
return BoundCtx(self, ctx)
|
||||||
|
|
||||||
|
|
||||||
class BoundCtx:
|
class BoundCtx:
|
||||||
def __init__(self, it: InfoCtx, ctx: Context, /) -> None:
|
def __init__(self, it: InfoCtx, ctx: Context, /) -> None:
|
||||||
if ctx.member is None:
|
if ctx.member is None:
|
||||||
raise Explicit('not in a guild')
|
raise Explicit("not in a guild")
|
||||||
self.member = ctx.member
|
self.member = ctx.member
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.url = it.info['url']
|
self.url = it.info["url"]
|
||||||
self.description = f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}'
|
self.description = f'{escape(it.info.get("title", "unknown"))} `Rby` {ctx.member}'
|
||||||
self.tor = it.tor
|
|
||||||
self.effects = it.effects
|
self.effects = it.effects
|
||||||
self.already_read = it.already_read
|
self.already_read = it.already_read
|
||||||
self.options = self._options()
|
self.options = self._options()
|
||||||
@ -55,8 +53,8 @@ class BoundCtx:
|
|||||||
if self.effects:
|
if self.effects:
|
||||||
if self.effects not in allowed_effects:
|
if self.effects not in allowed_effects:
|
||||||
assert_admin(self.ctx.member)
|
assert_admin(self.ctx.member)
|
||||||
if not set(self.effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'):
|
if not set(self.effects) <= set(string.ascii_letters + string.digits + "*,=+-/()|.^:_"):
|
||||||
raise Explicit('malformed effects')
|
raise Explicit("malformed effects")
|
||||||
return options_for_effects(self.effects)
|
return options_for_effects(self.effects)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@ -68,12 +66,11 @@ class UrlCtx:
|
|||||||
self.post = post
|
self.post = post
|
||||||
self.effects: str | None = post.effects
|
self.effects: str | None = post.effects
|
||||||
self.already_read = post.already_read
|
self.already_read = post.already_read
|
||||||
self.tor = post.tor
|
|
||||||
self.ignore = post.ignore
|
self.ignore = post.ignore
|
||||||
|
|
||||||
async def entries(self) -> AsyncIterable[InfoCtx]:
|
async def entries(self) -> AsyncIterable[InfoCtx]:
|
||||||
try:
|
try:
|
||||||
async for info in entries_for_url(self.url, self.tor):
|
async for info in entries_for_url(self.url):
|
||||||
yield InfoCtx(info, self.post)
|
yield InfoCtx(info, self.post)
|
||||||
except Exception:
|
except Exception:
|
||||||
if not self.ignore:
|
if not self.ignore:
|
||||||
@ -85,32 +82,32 @@ class ArgCtx:
|
|||||||
self.sources: list[UrlCtx] = []
|
self.sources: list[UrlCtx] = []
|
||||||
while args:
|
while args:
|
||||||
match args:
|
match args:
|
||||||
case ['[[', *args]:
|
case ["[[", *args]:
|
||||||
try:
|
try:
|
||||||
close_ix = args.index(']]')
|
close_ix = args.index("]]")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Explicit('expected closing `]]`, not found')
|
raise Explicit("expected closing `]]`, not found")
|
||||||
urls = args[:close_ix]
|
urls = args[:close_ix]
|
||||||
args = args[close_ix + 1:]
|
args = args[close_ix + 1 :]
|
||||||
case [']]', *args]:
|
case ["]]", *args]:
|
||||||
raise Explicit('unexpected `]]`')
|
raise Explicit("unexpected `]]`")
|
||||||
case [_url, *args]:
|
case [_url, *args]:
|
||||||
urls = [_url]
|
urls = [_url]
|
||||||
case _:
|
case _:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
for url in urls:
|
for url in urls:
|
||||||
if url in presets:
|
if url in presets:
|
||||||
raise Explicit('expected url, got preset. maybe you are missing `+`?')
|
raise Explicit("expected url, got preset. maybe you are missing `+`?")
|
||||||
if url in {'+', '-'}:
|
if url in {"+", "-"}:
|
||||||
raise Explicit('expected url, got `+` or `-`. maybe you tried to use multiple effects?')
|
raise Explicit("expected url, got `+` or `-`. maybe you tried to use multiple effects?")
|
||||||
if url.startswith('+') or url.startswith('-"') or url.startswith('-\''):
|
if url.startswith("+") or url.startswith('-"') or url.startswith("-'"):
|
||||||
raise Explicit(
|
raise Explicit(
|
||||||
'expected url, got `+` or `-"` or `-\'`. maybe you forgot to separate control symbol from the effects?'
|
"expected url, got `+` or `-\"` or `-'`. maybe you forgot to separate control symbol from the effects?"
|
||||||
)
|
)
|
||||||
match args:
|
match args:
|
||||||
case ['-', effects, *args]:
|
case ["-", effects, *args]:
|
||||||
pass
|
pass
|
||||||
case ['+', preset, *args]:
|
case ["+", preset, *args]:
|
||||||
effects = effects_for_preset(preset)
|
effects = effects_for_preset(preset)
|
||||||
case [*args]:
|
case [*args]:
|
||||||
effects = default_effects
|
effects = default_effects
|
||||||
@ -129,13 +126,11 @@ class ArgCtx:
|
|||||||
post.already_read = round(seconds / sparq(options_for_effects(effects)))
|
post.already_read = round(seconds / sparq(options_for_effects(effects)))
|
||||||
while True:
|
while True:
|
||||||
match args:
|
match args:
|
||||||
case ['tor', *args]:
|
case ["tor", *args]:
|
||||||
if post.tor:
|
raise Explicit("tor support is temporarily suspended")
|
||||||
raise Explicit('duplicate tor')
|
case ["ignore", *args]:
|
||||||
post.tor = True
|
|
||||||
case ['ignore', *args]:
|
|
||||||
if post.ignore:
|
if post.ignore:
|
||||||
raise Explicit('duplicate ignore')
|
raise Explicit("duplicate ignore")
|
||||||
post.ignore = True
|
post.ignore = True
|
||||||
case [*args]:
|
case [*args]:
|
||||||
break
|
break
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
|
|
||||||
__all__ = ('assert_admin',)
|
__all__ = ("assert_admin",)
|
||||||
|
|
||||||
|
|
||||||
def assert_admin(member: discord.Member | None):
|
def assert_admin(member: discord.Member | None):
|
||||||
assert isinstance(member, discord.Member)
|
assert isinstance(member, discord.Member)
|
||||||
permissions: discord.Permissions = member.guild_permissions
|
permissions: discord.Permissions = member.guild_permissions
|
||||||
if not permissions.administrator:
|
if not permissions.administrator:
|
||||||
raise Explicit('not an administrator')
|
raise Explicit("not an administrator")
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Context, Implicit
|
||||||
|
|
||||||
__all__ = ('catch',)
|
__all__ = ("catch",)
|
||||||
|
|
||||||
|
|
||||||
async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str), attachments_ok=True):
|
async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str), attachments_ok=True):
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.utils.presets import *
|
from v6d3music.utils.presets import presets
|
||||||
|
|
||||||
__all__ = ('effects_for_preset',)
|
__all__ = ("effects_for_preset",)
|
||||||
|
|
||||||
|
|
||||||
def effects_for_preset(preset: str) -> str:
|
def effects_for_preset(preset: str) -> str:
|
||||||
if preset in presets:
|
if preset in presets:
|
||||||
return presets[preset]
|
return presets[preset]
|
||||||
else:
|
else:
|
||||||
raise Explicit('unknown preset')
|
raise Explicit("unknown preset")
|
||||||
|
@ -1,30 +1,17 @@
|
|||||||
from typing import Any, AsyncIterable
|
from typing import Any, AsyncIterable
|
||||||
|
|
||||||
from v6d2ctx.context import *
|
from v6d2ctx.context import Explicit
|
||||||
from v6d3music.utils.aextract import *
|
from v6d3music.utils.aextract import aextract
|
||||||
from v6d3music.utils.tor_extract import *
|
|
||||||
|
|
||||||
__all__ = ('entries_for_url',)
|
__all__ = ("entries_for_url",)
|
||||||
|
|
||||||
|
|
||||||
async def entries_for_url(url: str, tor: bool) -> AsyncIterable[
|
async def entries_for_url(url: str) -> AsyncIterable[dict[str, Any]]:
|
||||||
dict[str, Any]
|
info = await aextract({"logtostderr": True}, url, download=False, process=False)
|
||||||
]:
|
if "__error__" in info:
|
||||||
ef = aextract
|
raise Explicit("extraction error\n" + info.get("__error_str__"))
|
||||||
if tor:
|
if "entries" in info:
|
||||||
ef = tor_extract
|
for entry in info["entries"]:
|
||||||
info = await ef(
|
|
||||||
{
|
|
||||||
'logtostderr': True
|
|
||||||
},
|
|
||||||
url,
|
|
||||||
download=False,
|
|
||||||
process=False
|
|
||||||
)
|
|
||||||
if '__error__' in info:
|
|
||||||
raise Explicit('extraction error\n' + info.get('__error_str__'))
|
|
||||||
if 'entries' in info:
|
|
||||||
for entry in info['entries']:
|
|
||||||
yield entry
|
yield entry
|
||||||
else:
|
else:
|
||||||
yield info | {'url': url}
|
yield info | {"url": url}
|
||||||
|
@ -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
|
import discord
|
||||||
|
|
||||||
from v6d3music.utils.speed_quotient import *
|
from v6d3music.utils.speed_quotient import speed_quotient
|
||||||
|
|
||||||
|
__all__ = ("sparq",)
|
||||||
__all__ = ('sparq',)
|
|
||||||
|
|
||||||
|
|
||||||
def sparq(options: str | None) -> float:
|
def sparq(options: str | None) -> float:
|
||||||
|
@ -2,24 +2,24 @@ import re
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
__all__ = ('speed_quotient',)
|
__all__ = ("speed_quotient",)
|
||||||
|
|
||||||
|
|
||||||
def speed_quotient(options: str | None) -> float:
|
def speed_quotient(options: str | None) -> float:
|
||||||
options = options or ''
|
options = options or ""
|
||||||
options = ''.join(c for c in options if not c.isspace())
|
options = "".join(c for c in options if not c.isspace())
|
||||||
options += ','
|
options += ","
|
||||||
quotient: float = 1.0
|
quotient: float = 1.0
|
||||||
asetrate: str
|
asetrate: str
|
||||||
# noinspection RegExpSimplifiable
|
# noinspection RegExpSimplifiable
|
||||||
for asetrate in re.findall(r'asetrate=([0-9.]+?),', options):
|
for asetrate in re.findall(r"asetrate=([0-9.]+?),", options):
|
||||||
try:
|
try:
|
||||||
quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE
|
quotient *= float(asetrate) / discord.opus.Encoder.SAMPLING_RATE
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
atempo: str
|
atempo: str
|
||||||
# noinspection RegExpSimplifiable
|
# noinspection RegExpSimplifiable
|
||||||
for atempo in re.findall(r'atempo=([0-9.]+?),', options):
|
for atempo in re.findall(r"atempo=([0-9.]+?),", options):
|
||||||
try:
|
try:
|
||||||
quotient *= float(atempo)
|
quotient *= float(atempo)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -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