This commit is contained in:
AF 2022-04-09 02:46:58 +03:00
parent dc86f70c40
commit 8ccbed24bd
5 changed files with 144 additions and 38 deletions

View File

@ -6,5 +6,7 @@ RUN apt-get update
RUN apt-get install -y libopus0 opus-tools ffmpeg RUN apt-get install -y libopus0 opus-tools ffmpeg
COPY requirements.txt requirements.txt COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
RUN apt-get install -y tor obfs4proxy
COPY v6d3music v6d3music COPY v6d3music v6d3music
RUN printf "\nClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy\nBridge obfs4 65.108.56.114:55487 621BA99387F65441630DFBC8A403D11D126EBC72 cert=5HzsLradvYOipNky+aHrgo31KRtxq5Cb6tz3y5Ds7PbBeB0r+C4r15IPYppupCJgzuXgWw iat-mode=0\nUseBridges 1\n" >> "/etc/tor/torrc"
CMD ["python3", "-m", "v6d3music.run-bot"] CMD ["python3", "-m", "v6d3music.run-bot"]

8
v6d3music/extract.py Normal file
View File

@ -0,0 +1,8 @@
import youtube_dl
def extract(params: dict, url: str, kwargs: dict):
extracted = youtube_dl.YoutubeDL(params=params).extract_info(url, **kwargs)
if 'entries' in extracted:
extracted['entries'] = list(extracted['entries'])
return extracted

View File

@ -0,0 +1,33 @@
import shlex
import subprocess
import discord
class FFmpegTorAudio(discord.FFmpegAudio):
def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None):
args = [executable]
subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr}
if isinstance(before_options, str):
args.extend(shlex.split(before_options))
args.append('-i')
args.append('-' if pipe else source)
args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning'))
if isinstance(options, str):
args.extend(shlex.split(options))
args.append('pipe:1')
super().__init__(source, executable='torify', args=args, **subprocess_kwargs)
def read(self):
ret = self._stdout.read(discord.opus.Encoder.FRAME_SIZE)
if len(ret) != discord.opus.Encoder.FRAME_SIZE:
return b''
return ret
def is_opus(self):
return False

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import concurrent.futures import concurrent.futures
import json
import os import os
import random import random
import re import re
@ -9,12 +10,11 @@ import subprocess
import time import time
from collections import deque from collections import deque
from io import StringIO from io import StringIO
from typing import Optional, AsyncIterable, Any, Iterable from typing import Optional, AsyncIterable, Any, Iterable, TypeAlias
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import discord import discord
import nacl.hash import nacl.hash
import youtube_dl
from ptvp35 import Db, KVJson from ptvp35 import Db, KVJson
from v6d1tokens.client import request_token from v6d1tokens.client import request_token
from v6d2ctx.context import Context, at, escape, monitor, Benchmark, Explicit, Implicit from v6d2ctx.context import Context, at, escape, monitor, Benchmark, Explicit, Implicit
@ -22,6 +22,8 @@ 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
import v6d3music.ffmpegtoraudio
from v6d3music.config import prefix, myroot from v6d3music.config import prefix, myroot
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
@ -118,7 +120,7 @@ def sparq(options: str) -> float:
class YTAudio(discord.AudioSource): class YTAudio(discord.AudioSource):
source: discord.FFmpegPCMAudio source: discord.FFmpegAudio
def __init__( def __init__(
self, self,
@ -128,6 +130,7 @@ class YTAudio(discord.AudioSource):
options: Optional[str], options: Optional[str],
rby: discord.Member, rby: discord.Member,
already_read: int, already_read: int,
tor: bool
): ):
self.url = url self.url = url
self.origin = origin self.origin = origin
@ -135,6 +138,7 @@ 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.loaded = False self.loaded = False
self.regenerating = False self.regenerating = False
self.set_source() self.set_source()
@ -142,7 +146,10 @@ class YTAudio(discord.AudioSource):
def set_source(self): def set_source(self):
self.schedule_duration_update() self.schedule_duration_update()
self.source = discord.FFmpegPCMAudio( audio_class = discord.FFmpegPCMAudio
if self.tor:
audio_class = v6d3music.ffmpegtoraudio.FFmpegTorAudio
self.source = audio_class(
self.url, self.url,
options=self.options, options=self.options,
before_options=self.before_options() before_options=self.before_options()
@ -172,9 +179,15 @@ class YTAudio(discord.AudioSource):
if url in self._durations: if url in self._durations:
return return
self._durations.setdefault(url, '') self._durations.setdefault(url, '')
p = subprocess.Popen( prompt = ''
if self.tor:
prompt = 'torify '
prompt += (
f'ffprobe -i {shlex.quote(url)}' f'ffprobe -i {shlex.quote(url)}'
' -show_entries format=duration -v quiet -of csv="p=0" -sexagesimal', ' -show_entries format=duration -v quiet -of csv="p=0" -sexagesimal'
)
p = subprocess.Popen(
prompt,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
shell=True shell=True
) )
@ -245,6 +258,7 @@ class YTAudio(discord.AudioSource):
'options': self.options, 'options': self.options,
'rby': self.rby.id, 'rby': self.rby.id,
'already_read': self.already_read, 'already_read': self.already_read,
'tor': self.tor,
} }
@classmethod @classmethod
@ -255,13 +269,14 @@ class YTAudio(discord.AudioSource):
respawn['description'], respawn['description'],
respawn['options'], respawn['options'],
guild.get_member(respawn['rby']) or await guild.fetch_member(respawn['rby']), guild.get_member(respawn['rby']) or await guild.fetch_member(respawn['rby']),
respawn['already_read'] respawn['already_read'],
respawn.get('tor', False),
) )
async def regenerate(self): async def regenerate(self):
try: try:
print(f'regenerating {self.origin}') print(f'regenerating {self.origin}')
self.url = await real_url(self.origin, True) self.url = await real_url(self.origin, True, self.tor)
self.source.cleanup() self.source.cleanup()
self.set_source() self.set_source()
print(f'regenerated {self.origin}') print(f'regenerated {self.origin}')
@ -387,13 +402,6 @@ class MainAudio(discord.PCMVolumeTransformer):
await volume_db.set(member.guild.id, volume) await volume_db.set(member.guild.id, volume)
def extract(params: dict, url: str, kwargs: dict):
extracted = youtube_dl.YoutubeDL(params=params).extract_info(url, **kwargs)
if 'entries' in extracted:
extracted['entries'] = list(extracted['entries'])
return extracted
def bytes_hash(b: bytes) -> str: def bytes_hash(b: bytes) -> str:
return nacl.hash.sha256(b).decode() return nacl.hash.sha256(b).decode()
@ -414,14 +422,33 @@ async def aextract(params: dict, url: str, **kwargs):
with concurrent.futures.ProcessPoolExecutor() as pool: with concurrent.futures.ProcessPoolExecutor() as pool:
return await loop.run_in_executor( return await loop.run_in_executor(
pool, pool,
extract, v6d3music.extract.extract,
params, params,
url, url,
kwargs kwargs
) )
async def cache_url(hurl: str, rurl: str, override: bool) -> None: 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 cache_url(hurl: str, rurl: str, override: bool, tor: bool) -> None:
async with lock_for(('cache', hurl), 'cache failed'): async with lock_for(('cache', hurl), 'cache failed'):
if not override and cache_db.get(f'url:{hurl}', None) is not None: if not override and cache_db.get(f'url:{hurl}', None) is not None:
return return
@ -429,15 +456,19 @@ async def cache_url(hurl: str, rurl: str, override: bool) -> None:
if cachable: if cachable:
print('caching', hurl) print('caching', hurl)
path = cache_root / f'{hurl}.opus' path = cache_root / f'{hurl}.opus'
p = subprocess.Popen( args = []
if tor:
args.append('torify')
args.extend(
[ [
'ffmpeg', '-hide_banner', '-loglevel', 'warning', 'ffmpeg', '-hide_banner', '-loglevel', 'warning',
'-reconnect', '1', '-reconnect_at_eof', '0', '-reconnect', '1', '-reconnect_at_eof', '0',
'-reconnect_streamed', '1', '-reconnect_delay_max', '10', '-copy_unknown', '-reconnect_streamed', '1', '-reconnect_delay_max', '10', '-copy_unknown',
'-y', '-i', rurl, str(path) '-y', '-i', rurl, str(path)
], ]
# stdout=subprocess.PIPE, )
# stderr=subprocess.PIPE, p = subprocess.Popen(
args,
) )
with Benchmark('CCH'): with Benchmark('CCH'):
code = await loop.run_in_executor(None, p.wait) code = await loop.run_in_executor(None, p.wait)
@ -450,15 +481,23 @@ async def cache_url(hurl: str, rurl: str, override: bool) -> None:
await cache_db.set(f'cachable:{hurl}', True) await cache_db.set(f'cachable:{hurl}', True)
async def real_url(url: str, override: bool) -> str: async def real_url(url: str, override: bool, tor: bool) -> str:
hurl: str = bytes_hash(url.encode()) hurl: str = bytes_hash(url.encode())
if not override: if not override:
curl: Optional[str] = cache_db.get(f'url:{hurl}', None) curl: Optional[str] = cache_db.get(f'url:{hurl}', None)
if curl is not None: if curl is not None:
print('using cached', hurl) print('using cached', hurl)
return curl return curl
args = []
if tor:
args.append('torify')
args.extend(
[
'youtube-dl', '--no-playlist', '-f', 'bestaudio', '-g', '--', url
]
)
p = subprocess.Popen( p = subprocess.Popen(
['youtube-dl', '--no-playlist', '-f', 'bestaudio', '-g', '--', url], args,
stdout=subprocess.PIPE stdout=subprocess.PIPE
) )
with Benchmark('URL'): with Benchmark('URL'):
@ -466,7 +505,7 @@ async def real_url(url: str, override: bool) -> str:
if code: if code:
raise RuntimeError(code) raise RuntimeError(code)
rurl: str = p.stdout.readline().decode()[:-1] rurl: str = p.stdout.readline().decode()[:-1]
loop.create_task(cache_url(hurl, rurl, override)) loop.create_task(cache_url(hurl, rurl, override, tor))
return rurl return rurl
@ -474,7 +513,9 @@ def options_for_effects(effects: str) -> Optional[str]:
return f'-af {shlex.quote(effects)}' if effects else None return f'-af {shlex.quote(effects)}' if effects else None
async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int) -> YTAudio: async def create_ytaudio(
ctx: Context, info: dict[str, Any], effects: Optional[str], already_read: int, tor: bool
) -> YTAudio:
if effects: if effects:
if effects not in allowed_effects: if effects not in allowed_effects:
assert_admin(ctx.member) assert_admin(ctx.member)
@ -484,19 +525,23 @@ async def create_ytaudio(ctx: Context, info: dict[str, Any], effects: Optional[s
else: else:
options = None options = None
return YTAudio( return YTAudio(
await real_url(info['url'], False), await real_url(info['url'], False, tor),
info['url'], info['url'],
f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}', f'{escape(info.get("title", "unknown"))} `Rby` {ctx.member}',
options, options,
ctx.member, ctx.member,
already_read already_read,
tor
) )
async def entries_for_url(url: str) -> AsyncIterable[ async def entries_for_url(url: str, tor: bool) -> AsyncIterable[
dict[str, Any] dict[str, Any]
]: ]:
info = await aextract( ef = aextract
if tor:
ef = tor_extract
info = await ef(
{ {
'playlistend': 128, 'playlistend': 128,
'logtostderr': True 'logtostderr': True
@ -512,12 +557,15 @@ async def entries_for_url(url: str) -> AsyncIterable[
yield info | {'url': url} yield info | {'url': url}
async def create_ytaudios(ctx: Context, infos: list[tuple[dict[str, Any], str, int]]) -> AsyncIterable[YTAudio]: 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( for audio in await asyncio.gather(
*[ *[
create_ytaudio(ctx, info, effects, already_read) create_ytaudio(ctx, info, effects, already_read, tor)
for for
info, effects, already_read info, effects, already_read, tor
in in
infos infos
] ]
@ -538,7 +586,7 @@ allowed_presets = ['bassboost', 'bassbooboost', 'nightcore', 'mono']
allowed_effects = {'', *(presets[key] for key in allowed_presets)} allowed_effects = {'', *(presets[key] for key in allowed_presets)}
async def entries_effects_for_args(args: list[str]) -> AsyncIterable[tuple[dict[str, Any], str, int]]: async def entries_effects_for_args(args: list[str]) -> AsyncIterable[info_tuple]:
while args: while args:
match args: match args:
case [url, '-', effects, *args]: case [url, '-', effects, *args]:
@ -560,14 +608,20 @@ async def entries_effects_for_args(args: list[str]) -> AsyncIterable[tuple[dict[
case [*args]: case [*args]:
pass pass
already_read = round(seconds / sparq(options_for_effects(effects))) already_read = round(seconds / sparq(options_for_effects(effects)))
async for info in entries_for_url(url): tor = False
yield info, effects, already_read 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]: async def yt_audios(ctx: Context, args: list[str]) -> AsyncIterable[YTAudio]:
tuples: list[tuple[dict[str, Any], str, int]] = [] tuples: list[info_tuple] = []
async for info, effects, already_read in entries_effects_for_args(args): async for info, effects, already_read, tor in entries_effects_for_args(args):
tuples.append((info, effects, already_read)) tuples.append((info, effects, already_read, tor))
if len(tuples) >= 5: if len(tuples) >= 5:
async for audio in create_ytaudios(ctx, tuples): async for audio in create_ytaudios(ctx, tuples):
yield audio yield audio
@ -846,6 +900,7 @@ async def main():
loop.create_task(save_job()) loop.create_task(save_job())
if os.getenv('v6monitor'): if os.getenv('v6monitor'):
loop.create_task(monitor()) loop.create_task(monitor())
subprocess.Popen('tor')
await client.connect() await client.connect()

8
v6d3music/run-extract.py Normal file
View File

@ -0,0 +1,8 @@
import json
import v6d3music.extract
params = json.loads(input())
url = json.loads(input())
kwargs = json.loads(input())
print(json.dumps(v6d3music.extract.extract(params, url, kwargs)))