experiment: 내부 API 리졸버 후보 구현 채움(직전 커밋에 빈 파일이 올라감)
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
[실험 후보] 유튜브 내부 API(VISIONOS 클라이언트) 직접 호출로 오디오 URL을 얻는 리졸버 조각.
|
||||||
|
|
||||||
|
운영 리졸버(scripts/ytresolver.py)의 _do_resolve 맨 앞에 0순위로 끼워 넣어 쓴다.
|
||||||
|
yt-dlp 를 아예 거치지 않으므로 리졸브가 0.3초대로 떨어진다(상주 lib 1.7~2.3초).
|
||||||
|
|
||||||
|
동작 조건
|
||||||
|
- 대상이 유튜브 영상 URL 이어야 한다(영상 ID 필요). 검색어면 None 을 돌려 기존 경로로 폴백.
|
||||||
|
- visitorData 가 있어야 한다. 없으면 playabilityStatus=LOGIN_REQUIRED 로 실패한다.
|
||||||
|
운영 적용 시에는 봇이 2시간마다 생성하는 visitorData 를 공유받도록 배선해야 한다.
|
||||||
|
|
||||||
|
실측(2026-09-17)
|
||||||
|
추출 0.30~0.38s / itag 251(yt-dlp 와 동일 포맷) / Lavalink loadType=track
|
||||||
|
range 0·1MB·2MB·3MB 전부 206 (이어받기 정상) / 실제 재생 90초 끊김 0
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import urllib.request
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
# 운영 적용 시에는 파일 대신 봇이 갱신하는 값을 받아오도록 교체할 것.
|
||||||
|
VISITOR_DATA_PATH = os.environ.get("YT_VISITOR_DATA_PATH", "/tmp/vd.txt")
|
||||||
|
|
||||||
|
_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 "
|
||||||
|
"(KHTML, like Gecko) Version/26.0 Safari/605.1.15")
|
||||||
|
_VID_RE = re.compile(r"(?:v=|youtu\.be/|/shorts/|/embed/|/live/)([A-Za-z0-9_-]{11})")
|
||||||
|
|
||||||
|
|
||||||
|
def _visitor_data():
|
||||||
|
if not os.path.exists(VISITOR_DATA_PATH):
|
||||||
|
return ""
|
||||||
|
return unquote(open(VISITOR_DATA_PATH).read().strip())
|
||||||
|
|
||||||
|
|
||||||
|
def innertube_resolve(target):
|
||||||
|
"""유튜브 URL -> 최고 비트레이트 오디오 직링크. 불가하면 None(호출측이 폴백)."""
|
||||||
|
m = _VID_RE.search(target)
|
||||||
|
visitor = _visitor_data()
|
||||||
|
if not m or not visitor:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"clientName": "VISIONOS", "clientVersion": "1.02",
|
||||||
|
"deviceMake": "Apple", "deviceModel": "RealityDevice17,1",
|
||||||
|
"osName": "visionOS", "osVersion": "26.5.23O471",
|
||||||
|
"hl": "ko", "gl": "KR", "visitorData": visitor,
|
||||||
|
}
|
||||||
|
body = {"videoId": m.group(1), "context": {"client": ctx},
|
||||||
|
"contentCheckOk": True, "racyCheckOk": True}
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://www.youtube.com/youtubei/v1/player?prettyPrint=false",
|
||||||
|
data=json.dumps(body).encode(),
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": _UA,
|
||||||
|
"X-Goog-Visitor-Id": visitor,
|
||||||
|
"X-Youtube-Client-Name": "101",
|
||||||
|
"X-Youtube-Client-Version": "1.02",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
data = json.loads(urllib.request.urlopen(req, timeout=15).read())
|
||||||
|
if data.get("playabilityStatus", {}).get("status") != "OK":
|
||||||
|
return None
|
||||||
|
|
||||||
|
formats = (data.get("streamingData") or {}).get("adaptiveFormats") or []
|
||||||
|
audio = [f for f in formats
|
||||||
|
if str(f.get("mimeType", "")).startswith("audio") and f.get("url")]
|
||||||
|
if not audio:
|
||||||
|
return None
|
||||||
|
audio.sort(key=lambda f: f.get("bitrate", 0), reverse=True)
|
||||||
|
return audio[0]["url"]
|
||||||
|
|||||||
Reference in New Issue
Block a user