121 lines
4.8 KiB
Python
121 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
ytresolver - yt-dlp 오디오 URL 리졸버 (HTTP 서비스)
|
|
|
|
봇(GuildPlayer.resolvePlayable)이 유튜브/스포티파이-미러 트랙을 재생하기 직전에 호출한다.
|
|
Lavalink의 youtube-plugin은 집 IP의 SABR 차단/성인인증(연령제한) 때문에 실재생이 막히므로,
|
|
여기서 yt-dlp로 "직접 오디오 URL"을 뽑아 봇에 돌려주고 봇은 그 URL을 Lavalink http 소스로 재생한다.
|
|
|
|
동작:
|
|
1) 먼저 익명(쿠키 없이)으로 시도 -> 일반 영상은 계정을 태우지 않는다(밴 리스크 최소화).
|
|
2) 실패하면 쿠키 + node(EJS 시그니처 해독) 로 재시도 -> 성인인증(연령제한) 영상 우회.
|
|
|
|
환경변수:
|
|
YTRESOLVER_COOKIE : Netscape 쿠키 파일 경로 (기본 /root/bot/db/ytcookie.txt)
|
|
YTRESOLVER_PORT : 리슨 포트 (기본 8779)
|
|
YTDLP : yt-dlp 실행 경로 (기본 /usr/local/bin/yt-dlp)
|
|
|
|
HTTP API:
|
|
GET /resolve?url=<유튜브 URL | ytsearch: | 검색어> -> {"url": "<직접 오디오 URL>"}
|
|
GET /health -> {"ok": true}
|
|
"""
|
|
import json, subprocess, time, threading, os, sys
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
YTDLP = os.environ.get("YTDLP", "/usr/local/bin/yt-dlp")
|
|
COOKIE_FILE = os.environ.get("YTRESOLVER_COOKIE", "/root/bot/db/ytcookie.txt")
|
|
PORT = int(os.environ.get("YTRESOLVER_PORT", "8779"))
|
|
CACHE = {}
|
|
LOCK = threading.Lock()
|
|
TTL = 1800 # 리졸브된 URL 캐시 30분(googlevideo URL 자체는 약 6시간 유효)
|
|
|
|
# 속도/안정성 공통 플래그
|
|
COMMON = ["-f", "bestaudio/best", "--no-playlist", "--no-warnings",
|
|
"--no-check-certificate", "--socket-timeout", "15"]
|
|
|
|
|
|
def run_ytdlp(target, use_cookies=False):
|
|
if use_cookies and os.path.exists(COOKIE_FILE):
|
|
# 성인인증(연령제한): web 클라이언트 + 쿠키 + node(EJS) 시그니처 해독 필요
|
|
cmd = [YTDLP, "--js-runtimes", "node", "--cookies", COOKIE_FILE,
|
|
*COMMON, "--extractor-args", "youtube:player_client=web"]
|
|
else:
|
|
# 익명: 계정 미사용. 일반 영상에서 잘 나오는 클라이언트 조합.
|
|
cmd = [YTDLP, *COMMON,
|
|
"--extractor-args", "youtube:player_client=default,web_safari,tv"]
|
|
cmd += ["--get-url", target]
|
|
return subprocess.run(cmd, capture_output=True, text=True, timeout=45)
|
|
|
|
|
|
def first_url(out):
|
|
lines = (out.stdout or "").strip().splitlines()
|
|
return lines[0] if lines else None
|
|
|
|
|
|
def resolve(target):
|
|
now = time.time()
|
|
with LOCK:
|
|
hit = CACHE.get(target)
|
|
if hit and hit[1] > now:
|
|
return hit[0]
|
|
try:
|
|
out = run_ytdlp(target, use_cookies=False)
|
|
url = first_url(out)
|
|
if not (url and url.startswith("http")) and os.path.exists(COOKIE_FILE):
|
|
print(f"[retry-cookies] {(out.stderr or out.stdout or '')[:180]}", file=sys.stderr, flush=True)
|
|
out = run_ytdlp(target, use_cookies=True)
|
|
url = first_url(out)
|
|
except subprocess.TimeoutExpired:
|
|
print("[result] TIMEOUT", file=sys.stderr, flush=True)
|
|
return None
|
|
ok = bool(url and url.startswith("http"))
|
|
if not ok:
|
|
print(f"[result] FAIL {(out.stderr or out.stdout or '')[:240]}", file=sys.stderr, flush=True)
|
|
return None
|
|
print(f"[result] OK {url[:60]}", file=sys.stderr, flush=True)
|
|
with LOCK:
|
|
CACHE[target] = (url, now + TTL)
|
|
return url
|
|
|
|
|
|
class H(BaseHTTPRequestHandler):
|
|
def log_message(self, *a):
|
|
pass
|
|
|
|
def do_GET(self):
|
|
p = urlparse(self.path)
|
|
q = parse_qs(p.query)
|
|
if p.path == "/health":
|
|
self._send(200, {"ok": True, "cookie": os.path.exists(COOKIE_FILE)})
|
|
return
|
|
if p.path != "/resolve":
|
|
self._send(404, {"error": "not found"})
|
|
return
|
|
target = (q.get("url") or q.get("q") or [None])[0]
|
|
print(f"[resolve] {target[:80] if target else None}", file=sys.stderr, flush=True)
|
|
if not target:
|
|
self._send(400, {"error": "missing url/q"})
|
|
return
|
|
if not (target.startswith("http") or target.startswith("ytsearch")):
|
|
target = "ytsearch1:" + target
|
|
url = resolve(target)
|
|
if url:
|
|
self._send(200, {"url": url})
|
|
else:
|
|
self._send(502, {"error": "resolve failed"})
|
|
|
|
def _send(self, code, obj):
|
|
b = json.dumps(obj).encode()
|
|
self.send_response(code)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(b)))
|
|
self.end_headers()
|
|
self.wfile.write(b)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(f"ytresolver listening on :{PORT} (cookie={COOKIE_FILE}, exists={os.path.exists(COOKIE_FILE)})",
|
|
file=sys.stderr, flush=True)
|
|
ThreadingHTTPServer(("0.0.0.0", PORT), H).serve_forever()
|