'use strict'; const { InnertubeError, observe } = require('../utils/Utils'); const Format = require('./classes/Format'); const VideoDetails = require('./classes/VideoDetails'); const requireParserClass = require('./map'); class AppendContinuationItemsAction { type = 'appendContinuationItemsAction'; constructor (data) { this.contents = Parser.parse(data.continuationItems); } } class ReloadContinuationItemsCommand { type = 'reloadContinuationItemsCommand'; constructor (data) { this.target_id = data.targetId; this.contents = Parser.parse(data.continuationItems); } } class SectionListContinuation { type = 'sectionListContinuation'; constructor(data) { this.contents = Parser.parse(data.contents); this.continuation = data.continuations[0].nextContinuationData.continuation; } } class TimedContinuation { type = 'timedContinuationData'; constructor(data) { this.timeout_ms = data.timeoutMs || data.timeUntilLastMessageMsec; this.token = data.continuation; } } class LiveChatContinuation { type = 'liveChatContinuation'; constructor(data) { this.actions = Parser.parse(data.actions?.map((action) => { delete action.clickTrackingParams; return action; })) || []; this.action_panel = Parser.parse(data.actionPanel); this.item_list = Parser.parse(data.itemList); this.header = Parser.parse(data.header); this.participants_list = Parser.parse(data.participantsList); this.popout_message = Parser.parse(data.popoutMessage); this.emojis = data.emojis?.map((emoji) => ({ emoji_id: emoji.emojiId, shortcuts: emoji.shortcuts, search_terms: emoji.searchTerms, image: emoji.image, is_custom_emoji: emoji.isCustomEmoji })) || null; this.continuation = new TimedContinuation( data.continuations?.[0].timedContinuationData || data.continuations?.[0].invalidationContinuationData || data.continuations?.[0].liveChatReplayContinuationData); this.viewer_name = data.viewerName; } } class Parser { static #memo = new Map(); static #clearMemo() { Parser.#memo = null; } static #createMemo() { Parser.#memo = new Map(); } static #addToMemo(classname, result) { if (!Parser.#memo) return; if (!Parser.#memo.has(classname)) return Parser.#memo.set(classname, [ result ]); Parser.#memo.get(classname).push(result); } /** * Parses InnerTube response. * * @param {object} data * @returns {*} */ static parseResponse(data) { // Memoize the response objects by classname this.#createMemo(); const contents = Parser.parse(data.contents); const contents_memo = Parser.#memo; // End of memoization this.#clearMemo(); this.#createMemo(); const on_response_received_actions = data.onResponseReceivedActions ? Parser.parseRR(data.onResponseReceivedActions) : null; const on_response_received_actions_memo = Parser.#memo; this.#clearMemo(); this.#createMemo(); const on_response_received_endpoints = data.onResponseReceivedEndpoints ? Parser.parseRR(data.onResponseReceivedEndpoints) : null; const on_response_received_endpoints_memo = Parser.#memo; this.#clearMemo(); this.#createMemo(); const on_response_received_commands = data.onResponseReceivedCommands ? Parser.parseRR(data.onResponseReceivedCommands) : null; const on_response_received_commands_memo = Parser.#memo; this.#clearMemo(); this.#createMemo(); const actions = data.actions ? Parser.parseActions(data.actions) : null; const actions_memo = Parser.#memo; this.#clearMemo(); return { actions, actions_memo, contents, contents_memo, on_response_received_actions, on_response_received_actions_memo, on_response_received_endpoints, on_response_received_endpoints_memo, on_response_received_commands, on_response_received_commands_memo, /** @type {*} */ continuation: data.continuation ? Parser.parseC(data.continuation) : null, /** @type {*} */ continuation_contents: data.continuationContents ? Parser.parseLC(data.continuationContents) : null, metadata: Parser.parse(data.metadata), header: Parser.parse(data.header), /** @type {import('./classes/PlayerMicroformat')} */ microformat: data.microformat && Parser.parse(data.microformat), /** @type {import('./classes/PlaylistSidebar')} */ sidebar: Parser.parse(data.sidebar), /** @type {import('./classes/PlayerOverlay')} */ overlay: Parser.parse(data.overlay), refinements: data.refinements || null, estimated_results: data.estimatedResults || null, player_overlays: Parser.parse(data.playerOverlays), playability_status: data.playabilityStatus && { /** @type {number} */ status: data.playabilityStatus.status, error_screen: Parser.parse(data.playabilityStatus.errorScreen), /** @type {boolean} */ embeddable: data.playabilityStatus.playableInEmbed || null, /** @type {string} */ reason: data.reason || '' }, streaming_data: data.streamingData && { expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000), /** @type {import('./classes/Format')[]} */ formats: Parser.parseFormats(data.streamingData.formats), /** @type {import('./classes/Format')[]} */ adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats), dash_manifest_url: data.streamingData?.dashManifestUrl || null, dls_manifest_url: data.streamingData?.dashManifestUrl || null }, captions: Parser.parse(data.captions), /** @type {import('./classes/VideoDetails')} */ video_details: data.videoDetails && new VideoDetails(data.videoDetails), annotations: Parser.parse(data.annotations), storyboards: Parser.parse(data.storyboards), /** @type {import('./classes/Endscreen')} */ endscreen: Parser.parse(data.endscreen), /** @type {import('./classes/CardCollection')} */ cards: Parser.parse(data.cards) }; } static parseC(data) { if (data.timedContinuationData) return new TimedContinuation(data.timedContinuationData); } static parseLC(data) { if (data.sectionListContinuation) return new SectionListContinuation(data.sectionListContinuation); if (data.liveChatContinuation) return new LiveChatContinuation(data.liveChatContinuation); } static parseRR(actions) { return observe(actions.map((action) => { if (action.reloadContinuationItemsCommand) return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand); if (action.appendContinuationItemsAction) return new AppendContinuationItemsAction(action.appendContinuationItemsAction); }).filter((item) => item)); } static parseActions(data) { if (Array.isArray(data)) { return Parser.parse(data.map((action) => { delete action.clickTrackingParams; return action; })); } return Parser.parse(data) || null; } static parseFormats(formats) { return observe(formats?.map((format) => new Format(format)) || []); } /** * Parses the `contents` property of the response. * * @param {object} data - contents to be parsed. * @returns {*} */ static parse(data) { if (!data) return null; if (Array.isArray(data)) { const results = []; for (const item of data) { const keys = Object.keys(item); const classname = this.sanitizeClassName(keys[0]); if (!this.shouldIgnore(classname)) { try { const TargetClass = requireParserClass(classname); const result = new TargetClass(item[keys[0]]); results.push(result); this.#addToMemo(classname, result); } catch (err) { this.formatError({ classname, classdata: item[keys[0]], err }); } } } return observe(results); } const keys = Object.keys(data); const classname = this.sanitizeClassName(keys[0]); if (!this.shouldIgnore(classname)) { try { const TargetClass = requireParserClass(classname); const result = new TargetClass(data[keys[0]]); this.#addToMemo(classname, result); return result; } catch (err) { this.formatError({ classname, classdata: data[keys[0]], err }); return null; } } } static formatError({ classname, classdata, err }) { if (err.code == 'MODULE_NOT_FOUND') { return console.warn( new InnertubeError(`${classname} not found!\n` + `This is a bug, please report it at ${require('../../package.json').bugs.url}`, classdata) ); } console.warn( new InnertubeError(`Something went wrong at ${classname}!\n` + `This is a bug, please report it at ${require('../../package.json').bugs.url}`, { stack: err.stack }) ); } static sanitizeClassName(input) { return (input.charAt(0).toUpperCase() + input.slice(1)) .replace(/Renderer|Model/g, '') .replace(/Radio/g, 'Mix').trim(); } static shouldIgnore(classname) { return [ 'DisplayAd', 'SearchPyv', 'MealbarPromo', 'BackgroundPromo', 'PromotedSparklesWeb', 'RunAttestationCommand', 'StatementBanner' ].includes(classname); } } module.exports = Parser;