YouTube Live Chat client library for Tampermonkey
Acest script nu ar trebui instalat direct. Aceasta este o bibliotecă pentru alte scripturi care este inclusă prin directiva meta a // @require https://update.greasyfork.org/scripts/575887/1811335/YTLiveChatClient.js
// ==UserScript==
// @name YTLiveChatClient
// @namespace https://greasyfork.org/
// @version 1.1.0
// @description YouTube Live Chat client library for Tampermonkey
// @author yourname
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect www.youtube.com
// ==/UserScript==
(function () {
'use strict';
async function getContinuationFromLiveId(liveId) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://www.youtube.com/watch?v=${liveId}`,
onload: (res) => {
const match = res.responseText.match(
/ytInitialData\s*=\s*({.+?})\s*;<\/script>/s
);
if (!match) return reject(new Error('ytInitialData not found'));
let data;
try {
data = JSON.parse(match[1]);
} catch (e) {
return reject(new Error('Failed to parse ytInitialData'));
}
const continuations = data
?.contents
?.twoColumnWatchNextResults
?.conversationBar
?.liveChatRenderer
?.continuations;
const continuation =
continuations?.[0]?.invalidationContinuationData?.continuation ??
continuations?.[0]?.reloadContinuationData?.continuation;
if (!continuation) return reject(new Error('continuation not found'));
resolve(continuation);
},
onerror: (e) => reject(new Error(`Network error: ${e.status}`)),
});
});
}
class YTLiveChatClient {
constructor({ liveId, onChat, onError } = {}) {
if (!liveId) throw new Error('liveId is required');
this._liveId = liveId;
this._onChat = onChat ?? (() => {});
this._onError = onError ?? console.error;
this._alive = false;
this._continuation = null;
this._timer = null;
this._startedAt = 0;
}
// skipExisting: true → start() 以前のチャットをコールバックしない(デフォルト)
// skipExisting: false → 既存チャットもコールバックする
async start({ skipExisting = true } = {}) {
if (this._alive) return;
try {
this._continuation = await getContinuationFromLiveId(this._liveId);
this._startedAt = skipExisting ? Date.now() * 1000 : 0; // マイクロ秒
this._alive = true;
this._poll();
} catch (e) {
this._onError(e);
}
}
stop() {
this._alive = false;
clearTimeout(this._timer);
this._continuation = null;
this._startedAt = 0;
}
isAlive() {
return this._alive;
}
_poll() {
if (!this._alive) return;
GM_xmlhttpRequest({
method: 'POST',
url: 'https://www.youtube.com/youtubei/v1/live_chat/get_live_chat',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
context: {
client: { clientName: 'WEB', clientVersion: '2.20240101' },
},
continuation: this._continuation,
}),
onload: (res) => {
try {
this._handle(JSON.parse(res.responseText));
} catch (e) {
this._onError(e);
}
},
onerror: (e) => this._onError(new Error(`Network error: ${e.status}`)),
});
}
_handle(json) {
const lcc = json?.continuationContents?.liveChatContinuation;
if (!lcc) {
this._alive = false;
return;
}
const cont = lcc.continuations?.[0];
const next =
cont?.invalidationContinuationData?.continuation ??
cont?.timedContinuationData?.continuation;
const timeout =
cont?.invalidationContinuationData?.timeoutMs ??
cont?.timedContinuationData?.timeoutMs ??
5000;
if (next) this._continuation = next;
const messages = (lcc.actions ?? [])
.map(a => a?.addChatItemAction?.item?.liveChatTextMessageRenderer)
.filter(Boolean)
.filter(r => Number(r.timestampUsec) > this._startedAt)
.map(r => ({
id: r.id,
author: r.authorName?.simpleText ?? '',
message: r.message?.runs?.map(run => run.text ?? '').join('') ?? '',
timestampUsec: r.timestampUsec,
isMember: !!r.authorBadges?.some(
b => b.liveChatAuthorBadgeRenderer?.icon?.iconType === 'MEMBER'
),
}));
if (messages.length > 0) this._onChat(messages);
if (this._alive && next) {
this._timer = setTimeout(() => this._poll(), timeout);
} else {
this._alive = false;
}
}
}
window.YTLiveChatClient = YTLiveChatClient;
})();