UNPKG

@natsume0304/livechat

Version:

Get YouTube Streaming Message

497 lines (496 loc) 27.3 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; const langs = { en: { gl: "US", hl: "en" }, zh: { gl: "TW", hl: "zh-TW" }, jp: { gl: "JP", hl: "ja" }, kr: { gl: "KR", hl: "ko" } }; const Localize = { en: { ChatGeneral: "General", ChatSuperChat: "Super Chat", ChatSuperSticker: "Super Sticker", ChatJoinMember: "Join Member", ChatMemberUpgrade: "Member Upgrade", ChatMemberMilestone: "Member Milestone", ChatMemberGift: "Member Gift", ChatReceivedMemberGift: "Received Member Gift", ChatRedirect: "Redirect", ChatPinned: "Pinned", MemberUpgrade: "Upgraded membership to", MemberMilestone: "Member for", }, zh: { ChatGeneral: "一般", ChatSuperChat: "超級留言", ChatSuperSticker: "超級貼圖", ChatJoinMember: "加入會員", ChatMemberUpgrade: "會員升級", ChatMemberMilestone: "會員里程碑", ChatMemberGift: "贈送會員", ChatReceivedMemberGift: "接收會員贈送", ChatRedirect: "重新導向", ChatPinned: "置頂留言", MemberUpgrade: "頻道會員等級已升級至", MemberMilestone: "已加入會員", }, jp: { ChatGeneral: "一般", ChatSuperChat: "スーパーチャット", ChatSuperSticker: "スーパーステッカー", ChatJoinMember: "メンバー登録", ChatMemberUpgrade: "会員アップグレード", ChatMemberMilestone: "会員マイルストーン", ChatMemberGift: "会員ギフト", ChatReceivedMemberGift: "会員プレゼントを受け取る", ChatRedirect: "リダイレクト", ChatPinned: "ピン留め", MemberUpgrade: "にアップグレードされました", MemberMilestone: "メンバー歴", }, kr: { ChatGeneral: "일반", ChatSuperChat: "슈퍼 채팅", ChatSuperSticker: "슈퍼 스티커", ChatJoinMember: "회원 가입", ChatMemberUpgrade: "회원 업그레이드", ChatMemberMilestone: "회원 마일스톤", ChatMemberGift: "회원 선물", ChatReceivedMemberGift: "회원 선물 받기", ChatRedirect: "리디렉션", ChatPinned: "고정", MemberUpgrade: "멤버십을", MemberMilestone: "회원 가입 기간", } }; //#region YTDataType class Message { } class AuthorBadgesData { } class BadgeData { } class EmojiData { constructor() { /** 是否為自定義表情符號 */ this.isCustomEmoji = false; } } class MessageData { } class StickerData { } class RunsData { } //#endregion class YoutubeChat { //#region YTDataParser ParseActions(jsonElement) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; if (!jsonElement) return; const output = []; let actions = (_b = (_a = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.continuationContents) === null || _a === void 0 ? void 0 : _a.liveChatContinuation) === null || _b === void 0 ? void 0 : _b.actions; if (!actions) { actions = (_d = (_c = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.contents) === null || _c === void 0 ? void 0 : _c.liveChatRenderer) === null || _d === void 0 ? void 0 : _d.actions; } if (Array.isArray(actions)) { for (const singleAction of actions) { // Handle `addChatItemAction`. const item = (_e = singleAction === null || singleAction === void 0 ? void 0 : singleAction.addChatItemAction) === null || _e === void 0 ? void 0 : _e.item; if (item) output.push(...this.parseRenderer(item)); // Handle `addBannerToLiveChatCommand`. const singleBannerRenderer = (_f = singleAction === null || singleAction === void 0 ? void 0 : singleAction.addBannerToLiveChatCommand) === null || _f === void 0 ? void 0 : _f.bannerRenderer; if (singleBannerRenderer) output.push(...this.parseRenderer(singleBannerRenderer)); // Handle `videoOffsetTimeMsec`. const videoOffsetTimeMsec = (_g = singleAction === null || singleAction === void 0 ? void 0 : singleAction.addChatItemAction) === null || _g === void 0 ? void 0 : _g.videoOffsetTimeMsec; const videoOffsetTimeText = this.getVideoOffsetTimeMsec(videoOffsetTimeMsec); // Handle `replayChatItemAction`. const replayActions = (_h = singleAction === null || singleAction === void 0 ? void 0 : singleAction.replayChatItemAction) === null || _h === void 0 ? void 0 : _h.actions; if (Array.isArray(replayActions)) { for (const replayAction of replayActions) { const replayItem = (_j = replayAction === null || replayAction === void 0 ? void 0 : replayAction.addChatItemAction) === null || _j === void 0 ? void 0 : _j.item; if (replayItem) { const rendererDatas = this.parseRenderer(replayItem); rendererDatas.forEach((rendererData) => { if (!rendererData.timestampText && !rendererData.timestampUsec) { rendererData.timestampText = videoOffsetTimeText; } }); output.push(...rendererDatas); } const replayBannerRenderer = (_k = replayAction === null || replayAction === void 0 ? void 0 : replayAction.addBannerToLiveChatCommand) === null || _k === void 0 ? void 0 : _k.bannerRenderer; if (replayBannerRenderer) { const rendererDatas = this.parseRenderer(replayBannerRenderer); rendererDatas.forEach((rendererData) => { if (!rendererData.timestampText && !rendererData.timestampUsec) { rendererData.timestampText = videoOffsetTimeText; } }); output.push(...rendererDatas); } } } } } return output.filter(x => x.type); } ParseContinuation(jsonElement) { var _a, _b; const output = [null, null]; const continuations = (_b = (_a = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.continuationContents) === null || _a === void 0 ? void 0 : _a.liveChatContinuation) === null || _b === void 0 ? void 0 : _b.continuations; if (Array.isArray(continuations)) { for (const singleContinuation of continuations) { const invalidationContinuationData = singleContinuation.invalidationContinuationData; if (invalidationContinuationData) { const continuation = invalidationContinuationData.continuation; if (continuation) { output[0] = continuation; } const timeoutMs = invalidationContinuationData.timeoutMs; if (timeoutMs) { output[1] = timeoutMs; } break; } const timedContinuationData = singleContinuation.timedContinuationData; if (timedContinuationData) { const continuation = timedContinuationData.continuation; if (continuation) { output[0] = continuation; } const timeoutMs = timedContinuationData.timeoutMs; if (timeoutMs) { output[1] = timeoutMs; } break; } const liveChatReplayContinuationData = singleContinuation.liveChatReplayContinuationData; if (liveChatReplayContinuationData) { const continuation = liveChatReplayContinuationData.continuation; if (continuation) { output[0] = continuation; } const timeUntilLastMessageMsec = liveChatReplayContinuationData.timeUntilLastMessageMsec; if (timeUntilLastMessageMsec) { output[1] = timeUntilLastMessageMsec; } break; } } } return output; } getVideoOffsetTimeMsec(jsonElement) { const videoOffsetTimeMsec = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.videoOffsetTimeMsec; if (!videoOffsetTimeMsec) return; const milliseconds = Number(videoOffsetTimeMsec); // 將 Unix 毫秒時間轉換為 "HH:mm:ss" 格式 const date = new Date(milliseconds); const hours = date.getUTCHours().toString().padStart(2, '0'); const minutes = date.getUTCMinutes().toString().padStart(2, '0'); const seconds = date.getUTCSeconds().toString().padStart(2, '0'); return `${hours}:${minutes}:${seconds}`; } parseRenderer(jsonElement) { const output = []; if ((this.setRendererData(output, jsonElement, "liveChatTextMessageRenderer") || this.setRendererData(output, jsonElement, "liveChatPaidMessageRenderer") || this.setRendererData(output, jsonElement, "liveChatPaidStickerRenderer") || this.setRendererData(output, jsonElement, "liveChatMembershipItemRenderer") || this.setRendererData(output, jsonElement, "liveChatViewerEngagementMessageRenderer") || this.setRendererData(output, jsonElement, "liveChatModeChangeMessageRenderer") || this.setRendererData(output, jsonElement, "liveChatSponsorshipsGiftPurchaseAnnouncementRenderer") || this.setRendererData(output, jsonElement, "liveChatSponsorshipsGiftRedemptionAnnouncementRenderer"))) return output; const liveChatBannerRenderer = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.liveChatBannerRenderer; if (this.setRendererData(output, liveChatBannerRenderer === null || liveChatBannerRenderer === void 0 ? void 0 : liveChatBannerRenderer.header, "liveChatBannerHeaderRenderer")) return output; const contents = liveChatBannerRenderer === null || liveChatBannerRenderer === void 0 ? void 0 : liveChatBannerRenderer.contents; this.setRendererData(output, contents, "liveChatTextMessageRenderer") || this.setRendererData(output, contents, "liveChatBannerRedirectRenderer"); return output; } getRendererDataType(rendererName) { var _a; return (_a = { "liveChatViewerEngagementMessageRenderer": "[YouTube]", "liveChatModeChangeMessageRenderer": "[YouTube]", "liveChatTextMessageRenderer": Localize[this.lang].ChatGeneral, "liveChatPaidMessageRenderer": Localize[this.lang].ChatSuperChat, "liveChatPaidStickerRenderer": Localize[this.lang].ChatSuperSticker, "liveChatMembershipItemRenderer": Localize[this.lang].ChatJoinMember, "liveChatSponsorshipsGiftPurchaseAnnouncementRenderer": Localize[this.lang].ChatMemberGift, "liveChatSponsorshipsGiftRedemptionAnnouncementRenderer": Localize[this.lang].ChatReceivedMemberGift, "liveChatBannerHeaderRenderer": Localize[this.lang].ChatPinned, "liveChatBannerRedirectRenderer": Localize[this.lang].ChatRedirect, }[rendererName]) !== null && _a !== void 0 ? _a : ""; } setRendererData(dataSet, jsonElement, rendererName) { var _a, _b, _c, _d, _e, _f; if (!(jsonElement = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement[rendererName])) return; let data = new Message(); const messageData = this.parseMessageData(jsonElement); data.type = this.getRendererDataType(rendererName); data.channelID = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.authorExternalChannelId; data.name = this.getAuthorName(jsonElement); data.avatarUrl = this.getAuthorPhoto(jsonElement); data.authorBadges = (_a = this.parseAuthorBadges(jsonElement)) === null || _a === void 0 ? void 0 : _a.text; data.content = messageData === null || messageData === void 0 ? void 0 : messageData.text; data.purchaseAmountText = (_b = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.purchaseAmountText) === null || _b === void 0 ? void 0 : _b.simpleText; data.foregroundColor = messageData === null || messageData === void 0 ? void 0 : messageData.textColor; data.backgroundColor = this.getColorHexCode((_c = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.backgroundColor) !== null && _c !== void 0 ? _c : jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.bodyBackgroundColor); data.timestampUsec = Number(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.timestampUsec); if (data.timestampUsec) data.timestampText = new Date(data.timestampUsec / 1000).toLocaleString(); if (rendererName === "liveChatMembershipItemRenderer") { // Update type based on message content if (data.content.includes(Localize[this.lang].MemberUpgrade)) { data.type = Localize[this.lang].ChatMemberUpgrade; } else if (data.content.includes(Localize[this.lang].MemberMilestone)) { data.type = Localize[this.lang].ChatMemberMilestone; } } else if (rendererName === "liveChatSponsorshipsGiftPurchaseAnnouncementRenderer") { const headerRenderer = (_d = jsonElement.header) === null || _d === void 0 ? void 0 : _d.liveChatSponsorshipsHeaderRenderer; if (headerRenderer) { data.name = this.getAuthorName(headerRenderer); data.avatarUrl = this.getAuthorPhoto(headerRenderer); data.authorBadges = (_e = this.parseAuthorBadges(headerRenderer)) === null || _e === void 0 ? void 0 : _e.text; data.content = (_f = this.parseMessageData(headerRenderer)) === null || _f === void 0 ? void 0 : _f.text; } } dataSet.push(data); return true; } getAuthorName(jsonElement) { var _a; return (_a = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.authorName) === null || _a === void 0 ? void 0 : _a.simpleText; } getAuthorPhoto(jsonElement) { return this.getThumbnailUrl(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.authorPhoto); } parseAuthorBadges(jsonElement) { var _a, _b, _c, _d, _e, _f, _g; const authorBadges = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.authorBadges; if (!Array.isArray(authorBadges)) return; const output = new AuthorBadgesData(); const tempBadges = []; for (const singleAuthorBadge of authorBadges) { const badgeData = new BadgeData(); badgeData.url = this.getThumbnailUrl((_a = singleAuthorBadge === null || singleAuthorBadge === void 0 ? void 0 : singleAuthorBadge.liveChatAuthorBadgeRenderer) === null || _a === void 0 ? void 0 : _a.customThumbnail); badgeData.iconType = (_c = (_b = singleAuthorBadge === null || singleAuthorBadge === void 0 ? void 0 : singleAuthorBadge.liveChatAuthorBadgeRenderer) === null || _b === void 0 ? void 0 : _b.icon) === null || _c === void 0 ? void 0 : _c.iconType; badgeData.tooltip = (_d = singleAuthorBadge === null || singleAuthorBadge === void 0 ? void 0 : singleAuthorBadge.liveChatAuthorBadgeRenderer) === null || _d === void 0 ? void 0 : _d.tooltip; badgeData.label = (_g = (_f = (_e = singleAuthorBadge === null || singleAuthorBadge === void 0 ? void 0 : singleAuthorBadge.liveChatAuthorBadgeRenderer) === null || _e === void 0 ? void 0 : _e.accessibility) === null || _f === void 0 ? void 0 : _f.accessibilityData) === null || _g === void 0 ? void 0 : _g.label; tempBadges.push(badgeData); } output.text = this.getBadgeName(tempBadges); output.badges = tempBadges; return output; } getBadgeName(list) { var array = list.map(n => n.label); if (Array.isArray(array)) return array.join("、"); } parseMessageData(jsonElement) { var _a, _b, _c, _d, _e, _f, _g, _h; const output = new MessageData(); let tempText = ''; let tempTextColor = ''; let tempFontFace = ''; let isBold = false; const tempStickers = []; const tempEmojis = []; function addRunData(runsData) { if (!runsData) return; tempText += runsData.text || ''; isBold = runsData.bold || false; tempTextColor = runsData.textColor || ''; tempFontFace = runsData.fontFace || ''; if (runsData.emojis) tempEmojis.push(...runsData.emojis); } ; const headerPrimaryText = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.headerPrimaryText; if (headerPrimaryText) { const runsData = this.parseRunData(headerPrimaryText); tempText += ` [${runsData.text}] `; addRunData(runsData); } const headerSubtext = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.headerSubtext; if (headerSubtext) { const simpleText = headerSubtext === null || headerSubtext === void 0 ? void 0 : headerSubtext.simpleText; if (simpleText) tempText += ` [${simpleText}] `; const runsData = this.parseRunData(headerSubtext); if (runsData === null || runsData === void 0 ? void 0 : runsData.text) tempText += ` ${runsData.text} `; addRunData(runsData); } addRunData(this.parseRunData(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.primaryText)); addRunData(this.parseRunData(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.text)); addRunData(this.parseRunData(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.subtext)); const sticker = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.sticker; if (sticker) { const stickerData = new StickerData(); const label = (_b = (_a = sticker === null || sticker === void 0 ? void 0 : sticker.accessibility) === null || _a === void 0 ? void 0 : _a.accessibilityData) === null || _b === void 0 ? void 0 : _b.label; if (label) tempText += `:${label}:`; const content = (_g = (_f = (_e = (_d = (_c = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.lowerBumper) === null || _c === void 0 ? void 0 : _c.liveChatItemBumperViewModel) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.bumperUserEduContentViewModel) === null || _f === void 0 ? void 0 : _f.text) === null || _g === void 0 ? void 0 : _g.content; if (content) tempText += ` [${content}] `; stickerData.id = label || ''; stickerData.url = this.getThumbnailUrl(sticker); stickerData.text = label ? `:${label}:` : ''; stickerData.label = label || ''; tempStickers.push(stickerData); } const purchaseAmountText = (_h = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.purchaseAmountText) === null || _h === void 0 ? void 0 : _h.simpleText; if (purchaseAmountText) tempText += ` [${purchaseAmountText}] `; addRunData(this.parseRunData(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.message)); addRunData(this.parseRunData(jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.bannerMessage)); output.text = tempText; output.bold = isBold; output.textColor = tempTextColor; output.fontFace = tempFontFace; output.stickers = tempStickers; output.emojis = tempEmojis; return output; } parseRunData(jsonElement) { var _a, _b, _c; const runs = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.runs; if (!Array.isArray(runs)) return; const output = new RunsData(); let tempText = ''; let tempTextColor = ''; let tempFontFace = ''; let isBold = false; const tempEmojis = []; for (const singleRun of runs) { const text = singleRun === null || singleRun === void 0 ? void 0 : singleRun.text; if (text) tempText += text; const bold = singleRun === null || singleRun === void 0 ? void 0 : singleRun.bold; if (bold) isBold = bold; const textColor = singleRun === null || singleRun === void 0 ? void 0 : singleRun.textColor; if (textColor) tempTextColor += this.getColorHexCode(textColor); const fontFace = singleRun === null || singleRun === void 0 ? void 0 : singleRun.fontFace; if (fontFace) tempFontFace += fontFace; const emoji = singleRun === null || singleRun === void 0 ? void 0 : singleRun.emoji; if (emoji) { const emojiData = new EmojiData(); const emojiId = emoji === null || emoji === void 0 ? void 0 : emoji.emojiId; if (emojiId) emojiData.id = emojiId; emojiData.url = this.getThumbnailUrl(emoji === null || emoji === void 0 ? void 0 : emoji.image); const label = (_c = (_b = (_a = emoji === null || emoji === void 0 ? void 0 : emoji.image) === null || _a === void 0 ? void 0 : _a.accessibility) === null || _b === void 0 ? void 0 : _b.accessibilityData) === null || _c === void 0 ? void 0 : _c.label; if (label) tempText += `:${label}:`; const shortcuts = emoji === null || emoji === void 0 ? void 0 : emoji.shortcuts; if (shortcuts === null || shortcuts === void 0 ? void 0 : shortcuts.length) emojiData.text = shortcuts[0]; emojiData.label = label || ''; emojiData.isCustomEmoji = (emoji === null || emoji === void 0 ? void 0 : emoji.isCustomEmoji) || false; tempEmojis.push(emojiData); } } output.text = tempText; output.bold = isBold; output.textColor = tempTextColor; output.fontFace = tempFontFace; output.emojis = tempEmojis; return output; } getThumbnailUrl(jsonElement) { var _a; const thumbnails = jsonElement === null || jsonElement === void 0 ? void 0 : jsonElement.thumbnails; if (Array.isArray(thumbnails) && thumbnails.length > 0) { const url = (_a = thumbnails[0]) === null || _a === void 0 ? void 0 : _a.url.split('=')[0]; if (url === null || url === void 0 ? void 0 : url.startsWith('//')) return `https:${url}`; return url; } } getColorHexCode(color) { if (color) return `#${color.toString(16).padStart(6, '0')}`; } //#endregion constructor(lang) { this.lang = lang; } resloveStreamUrl(input) { input = input.replace(/https?:\/\/(?:www\.)?youtu\.?be(?:\.com)?\/(?:channel\/|embed\/|live\/|watch.*v=)?/, "").split(/\/|\?/)[0]; if (!input) return; if (input.startsWith("UC")) return `https://www.youtube.com/channel/${input}/live`; if (input.startsWith("@")) return `https://www.youtube.com/${input}/live`; if (input.length == 11) return `https://www.youtube.com/watch?v=${input}`; } LiveChatMessage(YouTubeURLorID_1) { return __awaiter(this, arguments, void 0, function* (YouTubeURLorID, action = (id, m) => console.log(`[${m.name}] ${m.content}`)) { var _a; var url = this.resloveStreamUrl(YouTubeURLorID); console.log(url); if (!url) { console.error(`NotYoutubeURL ${url}`); return false; } const html = yield (yield fetch(url)).text(); const videoId = (_a = html.match(/<link rel="canonical" href="(?:.*?)v=(.*?)">/)) === null || _a === void 0 ? void 0 : _a[1]; console.log(url); if (/LIVE_STREAM_OFFLINE/.test(html)) return console.error('Not Streaming Now'); let [apiKey, continuation] = [...html.matchAll(/"INNERTUBE_API_KEY":"(.*?)"|"continuation":"(.*?)"/g)].map(x => x[1] || x[2]); if (!(apiKey && continuation)) return console.error('Failed to fetch required parameters.'); const chatUrl = `https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=${apiKey}`; // 替換為實際 API 密鑰 console.log(1); while (true) { var data = yield (yield fetch(chatUrl, { method: "post", body: JSON.stringify({ context: { client: Object.assign({ clientName: "WEB", clientVersion: "2.20230228.00.00" }, langs[this.lang]), }, continuation }) })).json(); if (!data) break; const next = this.ParseContinuation(data); continuation = next[0]; const messages = this.ParseActions(data); if (!Array.isArray(messages)) break; for (const msg of messages) action(videoId, msg); yield new Promise(res => { var _a; return setTimeout(res, (((_a = next[1]) !== null && _a !== void 0 ? _a : 5000) / 2)); }); } console.log("Stream End"); }); } } module.exports = YoutubeChat;