""" [실험 후보] 유튜브 내부 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"]