refactor
This commit is contained in:
parent
aad21d7135
commit
b9bfea0257
@ -5,13 +5,16 @@ from pathlib import Path
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
import discord
|
import discord
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from ptvp35 import Db
|
from ptvp35 import Db, KVJson
|
||||||
from v6d0auth.appfactory import AppFactory
|
from v6d0auth.appfactory import AppFactory
|
||||||
from v6d0auth.run_app import start_app
|
from v6d0auth.run_app import start_app
|
||||||
from v6d1tokens.client import request_token
|
from v6d1tokens.client import request_token
|
||||||
|
|
||||||
|
from v6d3music.config import myroot
|
||||||
from v6d3music.utils.bytes_hash import bytes_hash
|
from v6d3music.utils.bytes_hash import bytes_hash
|
||||||
|
|
||||||
|
session_db = Db(myroot / 'session.db', kvrequest_type=KVJson)
|
||||||
|
|
||||||
|
|
||||||
class MusicAppFactory(AppFactory):
|
class MusicAppFactory(AppFactory):
|
||||||
htmlroot = Path(__file__).parent / 'html'
|
htmlroot = Path(__file__).parent / 'html'
|
||||||
@ -19,7 +22,6 @@ class MusicAppFactory(AppFactory):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
secret: str,
|
secret: str,
|
||||||
db: Db,
|
|
||||||
client: discord.Client
|
client: discord.Client
|
||||||
):
|
):
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
@ -27,7 +29,6 @@ class MusicAppFactory(AppFactory):
|
|||||||
self.discord_auth = 'https://discord.com/api/oauth2/authorize?client_id=914432576926646322' \
|
self.discord_auth = 'https://discord.com/api/oauth2/authorize?client_id=914432576926646322' \
|
||||||
f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
|
f'&redirect_uri={urllib.parse.quote(self.redirect)}&response_type=code&scope=identify'
|
||||||
self.loop = asyncio.get_running_loop()
|
self.loop = asyncio.get_running_loop()
|
||||||
self.db = db
|
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
def _file(self, file: str):
|
def _file(self, file: str):
|
||||||
@ -106,7 +107,7 @@ class MusicAppFactory(AppFactory):
|
|||||||
discriminator = cls.user_discriminator(user)
|
discriminator = cls.user_discriminator(user)
|
||||||
if discriminator is None:
|
if discriminator is None:
|
||||||
return None
|
return None
|
||||||
return username + discriminator
|
return f'{username}#{discriminator}'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def user_username(cls, user: dict):
|
def user_username(cls, user: dict):
|
||||||
@ -147,8 +148,9 @@ class MusicAppFactory(AppFactory):
|
|||||||
'client': (None if sclient is None else self.client_status(sclient))
|
'client': (None if sclient is None else self.client_status(sclient))
|
||||||
}
|
}
|
||||||
|
|
||||||
def session_data(self, session: str) -> dict:
|
@classmethod
|
||||||
data = self.db.get(session, {})
|
def session_data(cls, session: str) -> dict:
|
||||||
|
data = session_db.get(session, {})
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
data = {}
|
data = {}
|
||||||
return data
|
return data
|
||||||
@ -177,7 +179,7 @@ class MusicAppFactory(AppFactory):
|
|||||||
data = self.session_data(session)
|
data = self.session_data(session)
|
||||||
data['code'] = code
|
data['code'] = code
|
||||||
data['token'] = await self.code_token(code)
|
data['token'] = await self.code_token(code)
|
||||||
await self.db.set(session, data)
|
await session_db.set(session, data)
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return await self.html_resp('auth')
|
return await self.html_resp('auth')
|
||||||
@ -186,7 +188,7 @@ class MusicAppFactory(AppFactory):
|
|||||||
async def state(request: web.Request) -> web.Response:
|
async def state(request: web.Request) -> web.Response:
|
||||||
session = str(request.query.get('session'))
|
session = str(request.query.get('session'))
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
data=f"{bytes_hash(session.encode())}"
|
data=f'{bytes_hash(session.encode())}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@routes.get('/status/')
|
@routes.get('/status/')
|
||||||
@ -209,6 +211,6 @@ class MusicAppFactory(AppFactory):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def start(cls, db: Db, client: discord.Client):
|
async def start(cls, client: discord.Client):
|
||||||
factory = cls(await request_token('music-client', 'token'), db, client)
|
factory = cls(await request_token('music-client', 'token'), client)
|
||||||
await start_app(factory.app())
|
await start_app(factory.app())
|
||||||
|
32
v6d3music/create_ytaudio.py
Normal file
32
v6d3music/create_ytaudio.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import string
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from v6d2ctx.context import Context, Explicit, escape
|
||||||
|
|
||||||
|
from v6d3music.real_url import real_url
|
||||||
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
|
from v6d3music.utils.presets import allowed_effects
|
||||||
|
from v6d3music.ytaudio import YTAudio
|
||||||
|
|
||||||
|
|
||||||
|
async def create_ytaudio(
|
||||||
|
ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int, tor: bool
|
||||||
|
) -> YTAudio:
|
||||||
|
if effects:
|
||||||
|
if effects not in allowed_effects:
|
||||||
|
assert_admin(ctx.member)
|
||||||
|
if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'):
|
||||||
|
raise Explicit('malformed effects')
|
||||||
|
options = options_for_effects(effects)
|
||||||
|
else:
|
||||||
|
options = None
|
||||||
|
return YTAudio(
|
||||||
|
await real_url(info['url'], False, tor),
|
||||||
|
info['url'],
|
||||||
|
f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}',
|
||||||
|
options,
|
||||||
|
ctx.member,
|
||||||
|
already_read,
|
||||||
|
tor
|
||||||
|
)
|
21
v6d3music/create_ytaudios.py
Normal file
21
v6d3music/create_ytaudios.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import AsyncIterable
|
||||||
|
|
||||||
|
from v6d2ctx.context import Context
|
||||||
|
|
||||||
|
from v6d3music.create_ytaudio import create_ytaudio
|
||||||
|
from v6d3music.utils.info_tuple import info_tuple
|
||||||
|
from v6d3music.ytaudio import YTAudio
|
||||||
|
|
||||||
|
|
||||||
|
async def create_ytaudios(ctx: Context, infos: list[info_tuple]) -> AsyncIterable[YTAudio]:
|
||||||
|
for audio in await asyncio.gather(
|
||||||
|
*[
|
||||||
|
create_ytaudio(ctx, info, effects, already_read, tor)
|
||||||
|
for
|
||||||
|
info, effects, already_read, tor
|
||||||
|
in
|
||||||
|
infos
|
||||||
|
]
|
||||||
|
):
|
||||||
|
yield audio
|
@ -3,11 +3,6 @@
|
|||||||
<script src="/main.js"></script>
|
<script src="/main.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(async () => {
|
(async () => {
|
||||||
const a = document.createElement('a');
|
root.append(await pageHome());
|
||||||
a.href = '/login/';
|
|
||||||
a.innerText = 'login';
|
|
||||||
root.append(a);
|
|
||||||
logEl(JSON.stringify(await sessionStatus(), undefined, 2));
|
|
||||||
root.append(await userAvatarImg());
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="/main.js"></script>
|
<script src="/main.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
authbase = '$$DISCORD_AUTH$$';
|
||||||
(async () => {
|
(async () => {
|
||||||
const a = document.createElement('a');
|
const a = await aAuth();
|
||||||
a.href = "$$DISCORD_AUTH$$&state=" + await sessionState();
|
|
||||||
a.innerText = 'auth';
|
|
||||||
root.append(a);
|
root.append(a);
|
||||||
logEl(sessionStr());
|
logEl(sessionStr());
|
||||||
logEl(await sessionState());
|
logEl(await sessionState());
|
||||||
|
a.click();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
html, body {
|
html, body {
|
||||||
color: white;
|
color: white;
|
||||||
background: black;
|
background: black;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,45 @@ const userUsername = async () => {
|
|||||||
return user && user['username'];
|
return user && user['username'];
|
||||||
};
|
};
|
||||||
const userAvatarImg = async () => {
|
const userAvatarImg = async () => {
|
||||||
const img = document.createElement('img');
|
const avatar = await userAvatarUrl();
|
||||||
img.src = await userAvatarUrl();
|
if (avatar) {
|
||||||
img.width = 128;
|
const img = document.createElement('img');
|
||||||
img.height = 128;
|
img.src = avatar;
|
||||||
img.alt = await userUsername();
|
img.width = 128;
|
||||||
return img;
|
img.height = 128;
|
||||||
|
img.alt = await userUsername();
|
||||||
|
return img;
|
||||||
|
} else {
|
||||||
|
return baseEl('span');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const userId = async () => {
|
||||||
|
const user = await sessionUser();
|
||||||
|
return user && user['id'];
|
||||||
|
};
|
||||||
|
const baseEl = (tag, ...appended) => {
|
||||||
|
const element = document.createElement(tag);
|
||||||
|
element.append(...appended);
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
const aLogin = () => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '/login/';
|
||||||
|
a.innerText = 'login';
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
const pageHome = async () => {
|
||||||
|
return baseEl(
|
||||||
|
'div',
|
||||||
|
baseEl('div', aLogin()),
|
||||||
|
baseEl('div', await userAvatarImg()),
|
||||||
|
await userId()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let authbase;
|
||||||
|
const aAuth = async () => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = authbase + '&state=' + await sessionState();
|
||||||
|
a.innerText = 'auth';
|
||||||
|
return a;
|
||||||
};
|
};
|
||||||
|
28
v6d3music/mainaudio.py
Normal file
28
v6d3music/mainaudio.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import discord
|
||||||
|
from ptvp35 import Db, KVJson
|
||||||
|
from v6d2ctx.context import Explicit
|
||||||
|
|
||||||
|
from v6d3music.config import myroot
|
||||||
|
from v6d3music.queueaudio import QueueAudio
|
||||||
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
|
|
||||||
|
volume_db = Db(myroot / 'volume.db', kvrequest_type=KVJson)
|
||||||
|
|
||||||
|
|
||||||
|
class MainAudio(discord.PCMVolumeTransformer):
|
||||||
|
def __init__(self, queue: QueueAudio, volume: float):
|
||||||
|
self.queue = queue
|
||||||
|
super().__init__(self.queue, volume=volume)
|
||||||
|
|
||||||
|
async def set(self, volume: float, member: discord.Member):
|
||||||
|
assert_admin(member)
|
||||||
|
if volume < 0.01:
|
||||||
|
raise Explicit('volume too small')
|
||||||
|
if volume > 1:
|
||||||
|
raise Explicit('volume too big')
|
||||||
|
self.volume = volume
|
||||||
|
await volume_db.set(member.guild.id, volume)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, guild: discord.Guild) -> 'MainAudio':
|
||||||
|
return cls(await QueueAudio.create(guild), volume=volume_db.get(guild.id, 0.2))
|
107
v6d3music/queueaudio.py
Normal file
107
v6d3music/queueaudio.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from ptvp35 import Db, KVJson
|
||||||
|
|
||||||
|
from v6d3music.config import myroot
|
||||||
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
|
from v6d3music.utils.fill import FILL
|
||||||
|
from v6d3music.ytaudio import YTAudio
|
||||||
|
|
||||||
|
queue_db = Db(myroot / 'queue.db', kvrequest_type=KVJson)
|
||||||
|
|
||||||
|
|
||||||
|
class QueueAudio(discord.AudioSource):
|
||||||
|
def __init__(self, guild: discord.Guild, respawned: list[YTAudio]):
|
||||||
|
self.queue: deque[YTAudio] = deque()
|
||||||
|
self.queue.extend(respawned)
|
||||||
|
self.guild = guild
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def respawned(guild: discord.Guild) -> list[YTAudio]:
|
||||||
|
respawned = []
|
||||||
|
try:
|
||||||
|
for audio_respawn in queue_db.get(guild.id, []):
|
||||||
|
try:
|
||||||
|
respawned.append(await YTAudio.respawn(guild, audio_respawn))
|
||||||
|
except Exception as e:
|
||||||
|
print('audio respawn failed', e)
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print('queue respawn failed', e)
|
||||||
|
return respawned
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create(cls, guild: discord.Guild):
|
||||||
|
return cls(guild, await QueueAudio.respawned(guild))
|
||||||
|
|
||||||
|
async def save(self):
|
||||||
|
hybernated = []
|
||||||
|
for audio in list(self.queue):
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
hybernated.append(audio.hybernate())
|
||||||
|
queue_db.set_nowait(self.guild.id, hybernated)
|
||||||
|
|
||||||
|
def append(self, audio: YTAudio):
|
||||||
|
self.queue.append(audio)
|
||||||
|
|
||||||
|
def read(self) -> bytes:
|
||||||
|
if not self.queue:
|
||||||
|
return FILL
|
||||||
|
audio = self.queue[0]
|
||||||
|
frame = audio.read()
|
||||||
|
if len(frame) != discord.opus.Encoder.FRAME_SIZE:
|
||||||
|
self.queue.popleft().cleanup()
|
||||||
|
frame = FILL
|
||||||
|
return frame
|
||||||
|
|
||||||
|
def skip_at(self, pos: int, member: discord.Member) -> bool:
|
||||||
|
if pos < len(self.queue):
|
||||||
|
audio = self.queue[pos]
|
||||||
|
if audio.can_be_skipped_by(member):
|
||||||
|
self.queue.remove(audio)
|
||||||
|
audio.cleanup()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool:
|
||||||
|
if audio in self.queue:
|
||||||
|
if audio.can_be_skipped_by(member):
|
||||||
|
self.queue.remove(audio)
|
||||||
|
audio.cleanup()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear(self, member: discord.Member) -> None:
|
||||||
|
assert_admin(member)
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def swap(self, member: discord.Member, a: int, b: int) -> None:
|
||||||
|
assert_admin(member)
|
||||||
|
if max(a, b) >= len(self.queue):
|
||||||
|
return
|
||||||
|
self.queue[a], self.queue[b] = self.queue[b], self.queue[a]
|
||||||
|
|
||||||
|
def move(self, member: discord.Member, a: int, b: int) -> None:
|
||||||
|
assert_admin(member)
|
||||||
|
if max(a, b) >= len(self.queue):
|
||||||
|
return
|
||||||
|
audio = self.queue[a]
|
||||||
|
self.queue.remove(audio)
|
||||||
|
self.queue.insert(b, audio)
|
||||||
|
|
||||||
|
async def format(self) -> str:
|
||||||
|
stream = StringIO()
|
||||||
|
for i, audio in enumerate(list(self.queue)):
|
||||||
|
stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n')
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
self.queue.clear()
|
||||||
|
for audio in self.queue:
|
||||||
|
try:
|
||||||
|
audio.cleanup()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
@ -1,34 +1,27 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import concurrent.futures
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import string
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
|
||||||
from io import StringIO
|
|
||||||
from typing import Any, AsyncIterable, Iterable, Optional, TypeAlias
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from ptvp35 import Db, KVJson
|
|
||||||
from v6d1tokens.client import request_token
|
from v6d1tokens.client import request_token
|
||||||
from v6d2ctx.context import Benchmark, Context, Explicit, Implicit, at, escape, monitor
|
from v6d2ctx.context import Benchmark, Context, Explicit, at, monitor
|
||||||
from v6d2ctx.handle_content import handle_content
|
from v6d2ctx.handle_content import handle_content
|
||||||
from v6d2ctx.lock_for import lock_for
|
from v6d2ctx.lock_for import lock_for
|
||||||
from v6d2ctx.serve import serve
|
from v6d2ctx.serve import serve
|
||||||
|
|
||||||
import v6d3music.extract
|
from v6d3music.app import MusicAppFactory, session_db
|
||||||
import v6d3music.ffmpegnormalaudio
|
|
||||||
from v6d3music.app import MusicAppFactory
|
|
||||||
from v6d3music.cache_url import cache_db
|
from v6d3music.cache_url import cache_db
|
||||||
from v6d3music.config import myroot, prefix
|
from v6d3music.config import prefix
|
||||||
from v6d3music.real_url import real_url
|
from v6d3music.mainaudio import MainAudio, volume_db
|
||||||
|
from v6d3music.queueaudio import QueueAudio, queue_db
|
||||||
from v6d3music.utils.assert_admin import assert_admin
|
from v6d3music.utils.assert_admin import assert_admin
|
||||||
from v6d3music.utils.fill import FILL
|
from v6d3music.utils.catch import catch
|
||||||
|
from v6d3music.utils.effects_for_preset import effects_for_preset
|
||||||
from v6d3music.utils.options_for_effects import options_for_effects
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
from v6d3music.utils.sparq import sparq
|
from v6d3music.utils.presets import allowed_presets
|
||||||
from v6d3music.ytaudio import YTAudio
|
from v6d3music.yt_audios import yt_audios
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
@ -45,9 +38,6 @@ client = discord.Client(
|
|||||||
reactions=True
|
reactions=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
volume_db = Db(myroot / 'volume.db', kvrequest_type=KVJson)
|
|
||||||
queue_db = Db(myroot / 'queue.db', kvrequest_type=KVJson)
|
|
||||||
session_db = Db(myroot / 'session.db', kvrequest_type=KVJson)
|
|
||||||
|
|
||||||
vcs_restored = False
|
vcs_restored = False
|
||||||
|
|
||||||
@ -85,8 +75,9 @@ async def on_ready():
|
|||||||
name='феноменально',
|
name='феноменально',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not vcs_restored:
|
async with lock_for('vcs_restored', '...'):
|
||||||
await restore_vcs()
|
if not vcs_restored:
|
||||||
|
await restore_vcs()
|
||||||
|
|
||||||
|
|
||||||
@at('commands', 'help')
|
@at('commands', 'help')
|
||||||
@ -98,283 +89,9 @@ async def help_(ctx: Context, args: list[str]) -> None:
|
|||||||
await ctx.reply(f'help for {name}: `{name} help`')
|
await ctx.reply(f'help for {name}: `{name} help`')
|
||||||
|
|
||||||
|
|
||||||
class QueueAudio(discord.AudioSource):
|
|
||||||
def __init__(self, guild: discord.Guild, respawned: list[YTAudio]):
|
|
||||||
self.queue: deque[YTAudio] = deque()
|
|
||||||
self.queue.extend(respawned)
|
|
||||||
self.guild = guild
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def respawned(guild: discord.Guild) -> list[YTAudio]:
|
|
||||||
respawned = []
|
|
||||||
try:
|
|
||||||
for audio_respawn in queue_db.get(guild.id, []):
|
|
||||||
try:
|
|
||||||
respawned.append(await YTAudio.respawn(guild, audio_respawn))
|
|
||||||
except Exception as e:
|
|
||||||
print('audio respawn failed', e)
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
print('queue respawn failed', e)
|
|
||||||
return respawned
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def create(cls, guild: discord.Guild):
|
|
||||||
return cls(guild, await QueueAudio.respawned(guild))
|
|
||||||
|
|
||||||
async def save(self):
|
|
||||||
hybernated = []
|
|
||||||
for audio in list(self.queue):
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
hybernated.append(audio.hybernate())
|
|
||||||
queue_db.set_nowait(self.guild.id, hybernated)
|
|
||||||
|
|
||||||
def append(self, audio: YTAudio):
|
|
||||||
self.queue.append(audio)
|
|
||||||
|
|
||||||
def read(self) -> bytes:
|
|
||||||
if not self.queue:
|
|
||||||
return FILL
|
|
||||||
audio = self.queue[0]
|
|
||||||
frame = audio.read()
|
|
||||||
if len(frame) != discord.opus.Encoder.FRAME_SIZE:
|
|
||||||
self.queue.popleft().cleanup()
|
|
||||||
frame = FILL
|
|
||||||
return frame
|
|
||||||
|
|
||||||
def skip_at(self, pos: int, member: discord.Member) -> bool:
|
|
||||||
if pos < len(self.queue):
|
|
||||||
audio = self.queue[pos]
|
|
||||||
if audio.can_be_skipped_by(member):
|
|
||||||
self.queue.remove(audio)
|
|
||||||
audio.cleanup()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def skip_audio(self, audio: YTAudio, member: discord.Member) -> bool:
|
|
||||||
if audio in self.queue:
|
|
||||||
if audio.can_be_skipped_by(member):
|
|
||||||
self.queue.remove(audio)
|
|
||||||
audio.cleanup()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clear(self, member: discord.Member) -> None:
|
|
||||||
assert_admin(member)
|
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
def swap(self, member: discord.Member, a: int, b: int) -> None:
|
|
||||||
assert_admin(member)
|
|
||||||
if max(a, b) >= len(self.queue):
|
|
||||||
return
|
|
||||||
self.queue[a], self.queue[b] = self.queue[b], self.queue[a]
|
|
||||||
|
|
||||||
def move(self, member: discord.Member, a: int, b: int) -> None:
|
|
||||||
assert_admin(member)
|
|
||||||
if max(a, b) >= len(self.queue):
|
|
||||||
return
|
|
||||||
audio = self.queue[a]
|
|
||||||
self.queue.remove(audio)
|
|
||||||
self.queue.insert(b, audio)
|
|
||||||
|
|
||||||
async def format(self) -> str:
|
|
||||||
stream = StringIO()
|
|
||||||
for i, audio in enumerate(list(self.queue)):
|
|
||||||
stream.write(f'`[{i}]` `{audio.source_timecode()} / {audio.duration()}` {audio.description}\n')
|
|
||||||
return stream.getvalue()
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.queue.clear()
|
|
||||||
for audio in self.queue:
|
|
||||||
try:
|
|
||||||
audio.cleanup()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MainAudio(discord.PCMVolumeTransformer):
|
|
||||||
def __init__(self, queue: QueueAudio, volume: float):
|
|
||||||
self.queue = queue
|
|
||||||
super().__init__(self.queue, volume=volume)
|
|
||||||
|
|
||||||
async def set(self, volume: float, member: discord.Member):
|
|
||||||
assert_admin(member)
|
|
||||||
if volume < 0.01:
|
|
||||||
raise Explicit('volume too small')
|
|
||||||
if volume > 1:
|
|
||||||
raise Explicit('volume too big')
|
|
||||||
self.volume = volume
|
|
||||||
await volume_db.set(member.guild.id, volume)
|
|
||||||
|
|
||||||
|
|
||||||
async def aextract(params: dict, url: str, **kwargs):
|
|
||||||
with Benchmark('AEX'):
|
|
||||||
with concurrent.futures.ProcessPoolExecutor() as pool:
|
|
||||||
return await loop.run_in_executor(
|
|
||||||
pool,
|
|
||||||
v6d3music.extract.extract,
|
|
||||||
params,
|
|
||||||
url,
|
|
||||||
kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def tor_extract(params: dict, url: str, **kwargs):
|
|
||||||
print(f'tor extracting {url}')
|
|
||||||
p = subprocess.Popen(
|
|
||||||
['torify', 'python', '-m', 'v6d3music.run-extract'],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
p.stdin.write(f'{json.dumps(params)}\n')
|
|
||||||
p.stdin.write(f'{json.dumps(url)}\n')
|
|
||||||
p.stdin.write(f'{json.dumps(kwargs)}\n')
|
|
||||||
p.stdin.flush()
|
|
||||||
p.stdin.close()
|
|
||||||
code = await loop.run_in_executor(None, p.wait)
|
|
||||||
if code:
|
|
||||||
raise RuntimeError(code)
|
|
||||||
return json.loads(p.stdout.read())
|
|
||||||
|
|
||||||
|
|
||||||
async def create_ytaudio(
|
|
||||||
ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int, tor: bool
|
|
||||||
) -> YTAudio:
|
|
||||||
if effects:
|
|
||||||
if effects not in allowed_effects:
|
|
||||||
assert_admin(ctx.member)
|
|
||||||
if not set(effects) <= set(string.ascii_letters + string.digits + '*,=+-/()|.^:_'):
|
|
||||||
raise Explicit('malformed effects')
|
|
||||||
options = options_for_effects(effects)
|
|
||||||
else:
|
|
||||||
options = None
|
|
||||||
return YTAudio(
|
|
||||||
await real_url(info['url'], False, tor),
|
|
||||||
info['url'],
|
|
||||||
f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}',
|
|
||||||
options,
|
|
||||||
ctx.member,
|
|
||||||
already_read,
|
|
||||||
tor
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def entries_for_url(url: str, tor: bool) -> AsyncIterable[
|
|
||||||
dict[str, Any]
|
|
||||||
]:
|
|
||||||
ef = aextract
|
|
||||||
if tor:
|
|
||||||
ef = tor_extract
|
|
||||||
info = await ef(
|
|
||||||
{
|
|
||||||
'playlistend': 128,
|
|
||||||
'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}
|
|
||||||
|
|
||||||
|
|
||||||
info_tuple: TypeAlias = tuple[dict[str, Any], str, int, bool]
|
|
||||||
|
|
||||||
|
|
||||||
async def create_ytaudios(ctx: Context, infos: list[info_tuple]) -> AsyncIterable[YTAudio]:
|
|
||||||
for audio in await asyncio.gather(
|
|
||||||
*[
|
|
||||||
create_ytaudio(ctx, info, effects, already_read, tor)
|
|
||||||
for
|
|
||||||
info, effects, already_read, tor
|
|
||||||
in
|
|
||||||
infos
|
|
||||||
]
|
|
||||||
):
|
|
||||||
yield audio
|
|
||||||
|
|
||||||
|
|
||||||
presets: dict[str, str] = {
|
|
||||||
'cursed': 'aeval=val(0)*2*sin(440*t)+val(1)*2*cos(622*t)|val(1)*2*sin(622*t)+val(0)*2*cos(440*t)',
|
|
||||||
'bassboost': 'bass=g=10',
|
|
||||||
'bassbooboost': 'bass=g=30',
|
|
||||||
'nightcore': 'asetrate=67882',
|
|
||||||
'daycore': 'atempo=.9,aecho=1.0:0.5:20:0.5',
|
|
||||||
'пришествие анимешне': 'bass=g=15,asetrate=67882,bass=g=15',
|
|
||||||
'difference': 'aeval=val(0)-val(1)|val(1)-val(0)',
|
|
||||||
'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)',
|
|
||||||
}
|
|
||||||
allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'daycore', 'mono']
|
|
||||||
allowed_effects = {'', *(presets[key] for key in allowed_presets)}
|
|
||||||
|
|
||||||
|
|
||||||
def effects_for_preset(preset: str) -> str:
|
|
||||||
if preset in presets:
|
|
||||||
return presets[preset]
|
|
||||||
else:
|
|
||||||
raise Explicit('unknown preset')
|
|
||||||
|
|
||||||
|
|
||||||
async def entries_effects_for_args(args: list[str]) -> AsyncIterable[info_tuple]:
|
|
||||||
while args:
|
|
||||||
match args:
|
|
||||||
case [url, '-', effects, *args]:
|
|
||||||
pass
|
|
||||||
case [url, '+', preset, *args]:
|
|
||||||
effects = effects_for_preset(preset)
|
|
||||||
case [url, *args]:
|
|
||||||
effects = None
|
|
||||||
case _:
|
|
||||||
raise RuntimeError
|
|
||||||
seconds = 0
|
|
||||||
match args:
|
|
||||||
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
|
|
||||||
seconds = 3600 * int(h) + 60 * int(m) + int(s)
|
|
||||||
case [m, s, *args] if m.isdecimal() and s.isdecimal():
|
|
||||||
seconds = 60 * int(m) + int(s)
|
|
||||||
case [s, *args] if s.isdecimal():
|
|
||||||
seconds = int(s)
|
|
||||||
case [*args]:
|
|
||||||
pass
|
|
||||||
already_read = round(seconds / sparq(options_for_effects(effects)))
|
|
||||||
tor = False
|
|
||||||
match args:
|
|
||||||
case ['tor', *args]:
|
|
||||||
tor = True
|
|
||||||
case [*args]:
|
|
||||||
pass
|
|
||||||
async for info in entries_for_url(url, tor):
|
|
||||||
yield info, effects, already_read, tor
|
|
||||||
|
|
||||||
|
|
||||||
async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
|
|
||||||
tuples: list[info_tuple] = []
|
|
||||||
async for info, effects, already_read, tor in entries_effects_for_args(args):
|
|
||||||
tuples.append((info, effects, already_read, tor))
|
|
||||||
if len(tuples) >= 5:
|
|
||||||
async for audio in create_ytaudios(ctx, tuples):
|
|
||||||
yield audio
|
|
||||||
tuples.clear()
|
|
||||||
async for audio in create_ytaudios(ctx, tuples):
|
|
||||||
yield audio
|
|
||||||
|
|
||||||
|
|
||||||
mainasrcs: dict[discord.Guild, MainAudio] = {}
|
mainasrcs: dict[discord.Guild, MainAudio] = {}
|
||||||
|
|
||||||
|
|
||||||
async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str)):
|
|
||||||
catched = {(case,) if isinstance(case, str) else tuple(case) for case in catched}
|
|
||||||
if tuple(args) in catched:
|
|
||||||
await ctx.reply(reply.strip())
|
|
||||||
raise Implicit
|
|
||||||
|
|
||||||
|
|
||||||
@at('commands', '/')
|
@at('commands', '/')
|
||||||
@at('commands', 'play')
|
@at('commands', 'play')
|
||||||
async def play(ctx: Context, args: list[str]) -> None:
|
async def play(ctx: Context, args: list[str]) -> None:
|
||||||
@ -422,7 +139,7 @@ async def main_for_raw_vc(vc: discord.VoiceClient, *, create: bool) -> MainAudio
|
|||||||
if create:
|
if create:
|
||||||
source = mainasrcs.setdefault(
|
source = mainasrcs.setdefault(
|
||||||
vc.guild,
|
vc.guild,
|
||||||
MainAudio(await QueueAudio.create(vc.guild), volume=volume_db.get(vc.guild.id, 0.2))
|
await MainAudio.create(vc.guild)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Explicit('not playing')
|
raise Explicit('not playing')
|
||||||
@ -644,7 +361,7 @@ async def save_job():
|
|||||||
|
|
||||||
|
|
||||||
async def start_app():
|
async def start_app():
|
||||||
await MusicAppFactory.start(session_db, client)
|
await MusicAppFactory.start(client)
|
||||||
|
|
||||||
|
|
||||||
async def setup_tasks():
|
async def setup_tasks():
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import v6d3music.extract
|
import v6d3music.utils.extract
|
||||||
|
|
||||||
params = json.loads(input())
|
params = json.loads(input())
|
||||||
url = json.loads(input())
|
url = json.loads(input())
|
||||||
kwargs = json.loads(input())
|
kwargs = json.loads(input())
|
||||||
print(json.dumps(v6d3music.extract.extract(params, url, kwargs)))
|
print(json.dumps(v6d3music.utils.extract.extract(params, url, kwargs)))
|
||||||
|
18
v6d3music/utils/aextract.py
Normal file
18
v6d3music/utils/aextract.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import asyncio
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
|
|
||||||
|
from v6d2ctx.context import Benchmark
|
||||||
|
|
||||||
|
from v6d3music.utils.extract import extract
|
||||||
|
|
||||||
|
|
||||||
|
async def aextract(params: dict, url: str, **kwargs):
|
||||||
|
with Benchmark('AEX'):
|
||||||
|
with ProcessPoolExecutor() as pool:
|
||||||
|
return await asyncio.get_running_loop().run_in_executor(
|
||||||
|
pool,
|
||||||
|
extract,
|
||||||
|
params,
|
||||||
|
url,
|
||||||
|
kwargs
|
||||||
|
)
|
10
v6d3music/utils/catch.py
Normal file
10
v6d3music/utils/catch.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from v6d2ctx.context import Context, Implicit
|
||||||
|
|
||||||
|
|
||||||
|
async def catch(ctx: Context, args: list[str], reply: str, *catched: (Iterable[str] | str)):
|
||||||
|
catched = {(case,) if isinstance(case, str) else tuple(case) for case in catched}
|
||||||
|
if tuple(args) in catched:
|
||||||
|
await ctx.reply(reply.strip())
|
||||||
|
raise Implicit
|
10
v6d3music/utils/effects_for_preset.py
Normal file
10
v6d3music/utils/effects_for_preset.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from v6d2ctx.context import Explicit
|
||||||
|
|
||||||
|
from v6d3music.utils.presets import presets
|
||||||
|
|
||||||
|
|
||||||
|
def effects_for_preset(preset: str) -> str:
|
||||||
|
if preset in presets:
|
||||||
|
return presets[preset]
|
||||||
|
else:
|
||||||
|
raise Explicit('unknown preset')
|
39
v6d3music/utils/entries_effects_for_args.py
Normal file
39
v6d3music/utils/entries_effects_for_args.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from typing import AsyncIterable
|
||||||
|
|
||||||
|
from v6d3music.utils.effects_for_preset import effects_for_preset
|
||||||
|
from v6d3music.utils.entries_for_url import entries_for_url
|
||||||
|
from v6d3music.utils.info_tuple import info_tuple
|
||||||
|
from v6d3music.utils.options_for_effects import options_for_effects
|
||||||
|
from v6d3music.utils.sparq import sparq
|
||||||
|
|
||||||
|
|
||||||
|
async def entries_effects_for_args(args: list[str]) -> AsyncIterable[info_tuple]:
|
||||||
|
while args:
|
||||||
|
match args:
|
||||||
|
case [url, '-', effects, *args]:
|
||||||
|
pass
|
||||||
|
case [url, '+', preset, *args]:
|
||||||
|
effects = effects_for_preset(preset)
|
||||||
|
case [url, *args]:
|
||||||
|
effects = None
|
||||||
|
case _:
|
||||||
|
raise RuntimeError
|
||||||
|
seconds = 0
|
||||||
|
match args:
|
||||||
|
case [h, m, s, *args] if h.isdecimal() and m.isdecimal() and s.isdecimal():
|
||||||
|
seconds = 3600 * int(h) + 60 * int(m) + int(s)
|
||||||
|
case [m, s, *args] if m.isdecimal() and s.isdecimal():
|
||||||
|
seconds = 60 * int(m) + int(s)
|
||||||
|
case [s, *args] if s.isdecimal():
|
||||||
|
seconds = int(s)
|
||||||
|
case [*args]:
|
||||||
|
pass
|
||||||
|
already_read = round(seconds / sparq(options_for_effects(effects)))
|
||||||
|
tor = False
|
||||||
|
match args:
|
||||||
|
case ['tor', *args]:
|
||||||
|
tor = True
|
||||||
|
case [*args]:
|
||||||
|
pass
|
||||||
|
async for info in entries_for_url(url, tor):
|
||||||
|
yield info, effects, already_read, tor
|
30
v6d3music/utils/entries_for_url.py
Normal file
30
v6d3music/utils/entries_for_url.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import Any, AsyncIterable
|
||||||
|
|
||||||
|
from v6d2ctx.context import Explicit
|
||||||
|
|
||||||
|
from v6d3music.utils.aextract import aextract
|
||||||
|
from v6d3music.utils.tor_extract import tor_extract
|
||||||
|
|
||||||
|
|
||||||
|
async def entries_for_url(url: str, tor: bool) -> AsyncIterable[
|
||||||
|
dict[str, Any]
|
||||||
|
]:
|
||||||
|
ef = aextract
|
||||||
|
if tor:
|
||||||
|
ef = tor_extract
|
||||||
|
info = await ef(
|
||||||
|
{
|
||||||
|
'playlistend': 128,
|
||||||
|
'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}
|
@ -1,10 +1,6 @@
|
|||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
import discord.utils
|
import discord.utils
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
eerror = namedtuple('eerror', ['content'])
|
|
||||||
|
|
||||||
|
|
||||||
def extract(params: dict, url: str, kwargs: dict):
|
def extract(params: dict, url: str, kwargs: dict):
|
||||||
try:
|
try:
|
3
v6d3music/utils/info_tuple.py
Normal file
3
v6d3music/utils/info_tuple.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from typing import Any, TypeAlias
|
||||||
|
|
||||||
|
info_tuple: TypeAlias = tuple[dict[str, Any], str, int, bool]
|
12
v6d3music/utils/presets.py
Normal file
12
v6d3music/utils/presets.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
presets: dict[str, str] = {
|
||||||
|
'cursed': 'aeval=val(0)*2*sin(440*t)+val(1)*2*cos(622*t)|val(1)*2*sin(622*t)+val(0)*2*cos(440*t)',
|
||||||
|
'bassboost': 'bass=g=10',
|
||||||
|
'bassbooboost': 'bass=g=30',
|
||||||
|
'nightcore': 'asetrate=67882',
|
||||||
|
'daycore': 'atempo=.9,aecho=1.0:0.5:20:0.5',
|
||||||
|
'пришествие анимешне': 'bass=g=15,asetrate=67882,bass=g=15',
|
||||||
|
'difference': 'aeval=val(0)-val(1)|val(1)-val(0)',
|
||||||
|
'mono': 'aeval=.5*val(0)+.5*val(1)|.5*val(1)+.5*val(0)',
|
||||||
|
}
|
||||||
|
allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'daycore', 'mono']
|
||||||
|
allowed_effects = {'', *(presets[key] for key in allowed_presets)}
|
22
v6d3music/utils/tor_extract.py
Normal file
22
v6d3music/utils/tor_extract.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
async def tor_extract(params: dict, url: str, **kwargs):
|
||||||
|
print(f'tor extracting {url}')
|
||||||
|
p = subprocess.Popen(
|
||||||
|
['torify', 'python', '-m', 'v6d3music.run-extract'],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
p.stdin.write(f'{json.dumps(params)}\n')
|
||||||
|
p.stdin.write(f'{json.dumps(url)}\n')
|
||||||
|
p.stdin.write(f'{json.dumps(kwargs)}\n')
|
||||||
|
p.stdin.flush()
|
||||||
|
p.stdin.close()
|
||||||
|
code = await asyncio.get_running_loop().run_in_executor(None, p.wait)
|
||||||
|
if code:
|
||||||
|
raise RuntimeError(code)
|
||||||
|
return json.loads(p.stdout.read())
|
20
v6d3music/yt_audios.py
Normal file
20
v6d3music/yt_audios.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from typing import AsyncIterable
|
||||||
|
|
||||||
|
from v6d2ctx.context import Context
|
||||||
|
|
||||||
|
from v6d3music.create_ytaudios import create_ytaudios
|
||||||
|
from v6d3music.utils.entries_effects_for_args import entries_effects_for_args
|
||||||
|
from v6d3music.utils.info_tuple import info_tuple
|
||||||
|
from v6d3music.ytaudio import YTAudio
|
||||||
|
|
||||||
|
|
||||||
|
async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
|
||||||
|
tuples: list[info_tuple] = []
|
||||||
|
async for info, effects, already_read, tor in entries_effects_for_args(args):
|
||||||
|
tuples.append((info, effects, already_read, tor))
|
||||||
|
if len(tuples) >= 5:
|
||||||
|
async for audio in create_ytaudios(ctx, tuples):
|
||||||
|
yield audio
|
||||||
|
tuples.clear()
|
||||||
|
async for audio in create_ytaudios(ctx, tuples):
|
||||||
|
yield audio
|
Loading…
Reference in New Issue
Block a user