From 3c6dbc96318207b5ab4989182fe07f54aa58e310 Mon Sep 17 00:00:00 2001 From: tkrmagid Date: Thu, 17 Sep 2026 19:07:16 +0900 Subject: [PATCH] =?UTF-8?q?experiment:=20=EB=82=B4=EB=B6=80=20API=20?= =?UTF-8?q?=EB=A6=AC=EC=A1=B8=EB=B2=84=20=ED=9B=84=EB=B3=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EC=B1=84=EC=9B=80(=EC=A7=81=EC=A0=84=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=EC=97=90=20=EB=B9=88=20=ED=8C=8C=EC=9D=BC=EC=9D=B4=20?= =?UTF-8?q?=EC=98=AC=EB=9D=BC=EA=B0=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/scripts/ytresolver_innertube.py | 72 +++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/bot/scripts/ytresolver_innertube.py b/bot/scripts/ytresolver_innertube.py index e69de29..8aaf91a 100644 --- a/bot/scripts/ytresolver_innertube.py +++ b/bot/scripts/ytresolver_innertube.py @@ -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"]