googlevideo 재생 403(스트림 거절)은 Lavalink youtube-plugin에 유효한 poToken/visitorData가 없어서 발생. 봇이 노드 ready 시점과 2시간 주기로 poToken을 생성해 Lavalink의 POST /youtube 엔드포인트에 주입한다. - scripts/potoken-refresh.mjs: youtubei.js+bgutils-js+jsdom로 생성 후 주입 (독립 ESM 프로세스) - LavalinkManager: ready 이벤트 + 2h 인터벌로 자식 프로세스 실행 - deps: bgutils-js 3.2.0, jsdom, youtubei.js 추가 / Docker base node:22-alpine Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
3.0 KiB
JavaScript
85 lines
3.0 KiB
JavaScript
// YouTube poToken/visitorData 생성 후 Lavalink youtube-plugin 런타임 엔드포인트(POST /youtube)에 주입.
|
|
// googlevideo 재생 403(스트림 거절) 우회용. 런타임 주입은 휘발성(Lavalink 재시작 시 소멸, 토큰도 수 시간 뒤 만료)이라
|
|
// 봇이 노드 ready 시점과 일정 주기로 이 스크립트를 자식 프로세스로 실행한다.
|
|
// 독립 ESM 프로세스로 돌려 BotGuard 생성 시의 메모리 스파이크를 봇 본체와 격리한다.
|
|
//
|
|
// 필요 env: LAVALINK_HOST, LAVALINK_PORT, LAVALINK_PW
|
|
import { Innertube } from "youtubei.js";
|
|
import { BG } from "bgutils-js";
|
|
import { JSDOM } from "jsdom";
|
|
|
|
const HOST = process.env.LAVALINK_HOST?.trim();
|
|
const PORT = process.env.LAVALINK_PORT?.trim();
|
|
const PW = process.env.LAVALINK_PW?.trim();
|
|
const REQUEST_KEY = "O43z0dpjhgX20SCx4KAo";
|
|
|
|
function log(msg) {
|
|
process.stdout.write(`[potoken] ${msg}\n`);
|
|
}
|
|
|
|
if (!HOST || !PORT || !PW) {
|
|
log("FAIL: LAVALINK_HOST/PORT/PW env 누락");
|
|
process.exit(1);
|
|
}
|
|
|
|
async function generate() {
|
|
const innertube = await Innertube.create({ retrieve_player: false });
|
|
const visitorData = innertube.session.context.client.visitorData;
|
|
if (!visitorData) throw new Error("visitorData 획득 실패");
|
|
|
|
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", { url: "https://www.youtube.com/" });
|
|
Object.assign(globalThis, {
|
|
window: dom.window,
|
|
document: dom.window.document,
|
|
location: dom.window.location,
|
|
origin: dom.window.origin,
|
|
});
|
|
if (!Reflect.has(globalThis, "navigator"))
|
|
Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator });
|
|
|
|
const bgConfig = {
|
|
fetch: (url, options) => fetch(url, options),
|
|
globalObj: globalThis,
|
|
identifier: visitorData,
|
|
requestKey: REQUEST_KEY,
|
|
};
|
|
|
|
const challenge = await BG.Challenge.create(bgConfig);
|
|
if (!challenge) throw new Error("BotGuard challenge 생성 실패");
|
|
|
|
const js = challenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
|
if (js) new Function(js)();
|
|
else throw new Error("BotGuard interpreter 로드 실패");
|
|
|
|
const pot = await BG.PoToken.generate({
|
|
program: challenge.program,
|
|
globalName: challenge.globalName,
|
|
bgConfig,
|
|
});
|
|
if (!pot?.poToken) throw new Error("poToken 생성 실패");
|
|
return { visitorData, poToken: pot.poToken };
|
|
}
|
|
|
|
async function inject({ visitorData, poToken }) {
|
|
const res = await fetch(`http://${HOST}:${PORT}/youtube`, {
|
|
method: "POST",
|
|
headers: { Authorization: PW, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ poToken, visitorData }),
|
|
});
|
|
return res.status;
|
|
}
|
|
|
|
try {
|
|
const pair = await generate();
|
|
const status = await inject(pair);
|
|
if (status === 204) {
|
|
log(`OK poToken 주입 완료 (visitorData ${pair.visitorData.length}자, poToken ${pair.poToken.length}자)`);
|
|
process.exit(0);
|
|
}
|
|
log(`WARN 주입 응답 http ${status}`);
|
|
process.exit(2);
|
|
} catch (e) {
|
|
log(`FAIL ${e && e.stack ? e.stack : e}`);
|
|
process.exit(1);
|
|
}
|