UNPKG

@imput/youtubei.js

Version:

A JavaScript client for YouTube's private API, known as InnerTube. Fork of youtubei.js

652 lines 27.9 kB
import * as YTNodes from './nodes.js'; import { InnertubeError, ParsingError, Platform } from '../utils/Utils.js'; import { Memo, observe, SuperParsedResult } from './helpers.js'; import { camelToSnake, generateRuntimeClass, generateTypescriptClass } from './generator.js'; import { Log } from '../utils/index.js'; import { Continuation, ContinuationCommand, GridContinuation, ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation, NavigateAction, PlaylistPanelContinuation, ReloadContinuationItemsCommand, SectionListContinuation, ShowMiniplayerCommand } from './continuations.js'; import AudioOnlyPlayability from './classes/AudioOnlyPlayability.js'; import CardCollection from './classes/CardCollection.js'; import Endscreen from './classes/Endscreen.js'; import PlayerAnnotationsExpanded from './classes/PlayerAnnotationsExpanded.js'; import PlayerCaptionsTracklist from './classes/PlayerCaptionsTracklist.js'; import PlayerLiveStoryboardSpec from './classes/PlayerLiveStoryboardSpec.js'; import PlayerStoryboardSpec from './classes/PlayerStoryboardSpec.js'; import Alert from './classes/Alert.js'; import AlertWithButton from './classes/AlertWithButton.js'; import EngagementPanelSectionList from './classes/EngagementPanelSectionList.js'; import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem.js'; import MacroMarkersListEntity from './classes/MacroMarkersListEntity.js'; import Format from './classes/misc/Format.js'; import VideoDetails from './classes/misc/VideoDetails.js'; import NavigationEndpoint from './classes/NavigationEndpoint.js'; import CommentView from './classes/comments/CommentView.js'; import MusicThumbnail from './classes/MusicThumbnail.js'; import OpenPopupAction from './classes/actions/OpenPopupAction.js'; import AppendContinuationItemsAction from './classes/actions/AppendContinuationItemsAction.js'; const TAG = 'Parser'; const IGNORED_LIST = new Set([ 'AdSlot', 'DisplayAd', 'SearchPyv', 'MealbarPromo', 'PrimetimePromo', 'PromotedSparklesWeb', 'CompactPromotedVideo', 'BrandVideoShelf', 'BrandVideoSingleton', 'StatementBanner', 'GuideSigninPromo', 'AdsEngagementPanelContent', 'MiniGameCardView' ]); const RUNTIME_NODES = new Map(Object.entries(YTNodes)); const DYNAMIC_NODES = new Map(); let MEMO = null; let ERROR_HANDLER = ({ classname, ...context }) => { switch (context.error_type) { case 'parse': if (context.error instanceof Error) { Log.warn(TAG, new InnertubeError(`Something went wrong at ${classname}!\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}`, { stack: context.error.stack, classdata: JSON.stringify(context.classdata, null, 2) })); } break; case 'typecheck': Log.warn(TAG, new ParsingError(`Type mismatch, got ${classname} expected ${Array.isArray(context.expected) ? context.expected.join(' | ') : context.expected}.`, context.classdata)); break; case 'mutation_data_missing': Log.warn(TAG, new InnertubeError(`Mutation data required for processing ${classname}, but none found.\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}`)); break; case 'mutation_data_invalid': Log.warn(TAG, new InnertubeError(`Mutation data missing or invalid for ${context.failed} out of ${context.total} MusicMultiSelectMenuItems. ` + `The titles of the failed items are: ${context.titles.join(', ')}.\n` + `This is a bug, please report it at ${Platform.shim.info.bugs_url}`)); break; case 'class_not_found': Log.warn(TAG, new InnertubeError(`${classname} not found!\n` + `This is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!\n` + `Introspected and JIT generated this class in the meantime:\n${generateTypescriptClass(classname, context.key_info)}`)); break; case 'class_changed': Log.warn(TAG, `${classname} changed!\n` + `The following keys where altered: ${context.changed_keys.map(([key]) => camelToSnake(key)).join(', ')}\n` + `The class has changed to:\n${generateTypescriptClass(classname, context.key_info)}`); break; default: Log.warn(TAG, 'Unreachable code reached at ParserErrorHandler'); break; } }; export function setParserErrorHandler(handler) { ERROR_HANDLER = handler; } function _clearMemo() { MEMO = null; } function _createMemo() { MEMO = new Memo(); } function _addToMemo(classname, result) { if (!MEMO) return; const list = MEMO.get(classname); if (!list) return MEMO.set(classname, [result]); list.push(result); } function _getMemo() { if (!MEMO) throw new Error('Parser#getMemo() called before Parser#createMemo()'); return MEMO; } export function shouldIgnore(classname) { return IGNORED_LIST.has(classname); } export function sanitizeClassName(input) { return (input.charAt(0).toUpperCase() + input.slice(1)) .replace(/Renderer|Model/g, '') .replace(/Radio/g, 'Mix').trim(); } export function getParserByName(classname) { const ParserConstructor = RUNTIME_NODES.get(classname); if (!ParserConstructor) { const error = new Error(`Module not found: ${classname}`); error.code = 'MODULE_NOT_FOUND'; throw error; } return ParserConstructor; } export function hasParser(classname) { return RUNTIME_NODES.has(classname); } export function addRuntimeParser(classname, ParserConstructor) { RUNTIME_NODES.set(classname, ParserConstructor); DYNAMIC_NODES.set(classname, ParserConstructor); } export function getDynamicParsers() { return Object.fromEntries(DYNAMIC_NODES); } /** * Parses a given InnerTube response. * @param data - Raw data. */ export function parseResponse(data) { const parsed_data = {}; _createMemo(); const contents = parse(data.contents); const contents_memo = _getMemo(); if (contents) { parsed_data.contents = contents; parsed_data.contents_memo = contents_memo; } _clearMemo(); _createMemo(); const on_response_received_actions = data.onResponseReceivedActions ? parseRR(data.onResponseReceivedActions) : null; const on_response_received_actions_memo = _getMemo(); if (on_response_received_actions) { parsed_data.on_response_received_actions = on_response_received_actions; parsed_data.on_response_received_actions_memo = on_response_received_actions_memo; } _clearMemo(); _createMemo(); const on_response_received_endpoints = data.onResponseReceivedEndpoints ? parseRR(data.onResponseReceivedEndpoints) : null; const on_response_received_endpoints_memo = _getMemo(); if (on_response_received_endpoints) { parsed_data.on_response_received_endpoints = on_response_received_endpoints; parsed_data.on_response_received_endpoints_memo = on_response_received_endpoints_memo; } _clearMemo(); _createMemo(); const on_response_received_commands = data.onResponseReceivedCommands ? parseRR(data.onResponseReceivedCommands) : null; const on_response_received_commands_memo = _getMemo(); if (on_response_received_commands) { parsed_data.on_response_received_commands = on_response_received_commands; parsed_data.on_response_received_commands_memo = on_response_received_commands_memo; } _clearMemo(); _createMemo(); const continuation_contents = data.continuationContents ? parseLC(data.continuationContents) : null; const continuation_contents_memo = _getMemo(); if (continuation_contents) { parsed_data.continuation_contents = continuation_contents; parsed_data.continuation_contents_memo = continuation_contents_memo; } _clearMemo(); _createMemo(); const actions = data.actions ? parseActions(data.actions) : null; const actions_memo = _getMemo(); if (actions) { parsed_data.actions = actions; parsed_data.actions_memo = actions_memo; } _clearMemo(); _createMemo(); const live_chat_item_context_menu_supported_renderers = data.liveChatItemContextMenuSupportedRenderers ? parseItem(data.liveChatItemContextMenuSupportedRenderers) : null; const live_chat_item_context_menu_supported_renderers_memo = _getMemo(); if (live_chat_item_context_menu_supported_renderers) { parsed_data.live_chat_item_context_menu_supported_renderers = live_chat_item_context_menu_supported_renderers; parsed_data.live_chat_item_context_menu_supported_renderers_memo = live_chat_item_context_menu_supported_renderers_memo; } _clearMemo(); _createMemo(); const header = data.header ? parse(data.header) : null; const header_memo = _getMemo(); if (header) { parsed_data.header = header; parsed_data.header_memo = header_memo; } _clearMemo(); _createMemo(); const sidebar = data.sidebar ? parseItem(data.sidebar) : null; const sidebar_memo = _getMemo(); if (sidebar) { parsed_data.sidebar = sidebar; parsed_data.sidebar_memo = sidebar_memo; } _clearMemo(); _createMemo(); const items = parse(data.items); if (items) { parsed_data.items = items; parsed_data.items_memo = _getMemo(); } _clearMemo(); applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations); if (on_response_received_endpoints_memo) { applyCommentsMutations(on_response_received_endpoints_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations); } const continuation = data.continuation ? parseC(data.continuation) : null; if (continuation) { parsed_data.continuation = continuation; } const continuation_endpoint = data.continuationEndpoint ? parseLC(data.continuationEndpoint) : null; if (continuation_endpoint) { parsed_data.continuation_endpoint = continuation_endpoint; } const metadata = parse(data.metadata); if (metadata) { parsed_data.metadata = metadata; } const microformat = parseItem(data.microformat); if (microformat) { parsed_data.microformat = microformat; } const overlay = parseItem(data.overlay); if (overlay) { parsed_data.overlay = overlay; } const alerts = parseArray(data.alerts, [Alert, AlertWithButton]); if (alerts.length) { parsed_data.alerts = alerts; } const refinements = data.refinements; if (refinements) { parsed_data.refinements = refinements; } const estimated_results = data.estimatedResults ? parseInt(data.estimatedResults) : null; if (estimated_results) { parsed_data.estimated_results = estimated_results; } const player_overlays = parse(data.playerOverlays); if (player_overlays) { parsed_data.player_overlays = player_overlays; } const background = parseItem(data.background, MusicThumbnail); if (background) { parsed_data.background = background; } const playback_tracking = data.playbackTracking ? { videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl, videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl } : null; if (playback_tracking) { parsed_data.playback_tracking = playback_tracking; } const playability_status = data.playabilityStatus ? { status: data.playabilityStatus.status, reason: data.playabilityStatus.reason || '', embeddable: !!data.playabilityStatus.playableInEmbed || false, audio_only_playability: parseItem(data.playabilityStatus.audioOnlyPlayability, AudioOnlyPlayability), error_screen: parseItem(data.playabilityStatus.errorScreen) } : null; if (playability_status) { parsed_data.playability_status = playability_status; } if (data.streamingData) { // Currently each response with streaming data only has two n param values // One for the adaptive formats and another for the combined formats // As they are the same for a response, we only need to decipher them once // For all further deciphering calls on formats from that response, we can use the cached output, given the same input n param const this_response_nsig_cache = new Map(); parsed_data.streaming_data = { expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000), formats: parseFormats(data.streamingData.formats, this_response_nsig_cache), adaptive_formats: parseFormats(data.streamingData.adaptiveFormats, this_response_nsig_cache), dash_manifest_url: data.streamingData.dashManifestUrl, hls_manifest_url: data.streamingData.hlsManifestUrl, server_abr_streaming_url: data.streamingData.serverAbrStreamingUrl }; } if (data.playerConfig) { parsed_data.player_config = { audio_config: { loudness_db: data.playerConfig.audioConfig?.loudnessDb, perceptual_loudness_db: data.playerConfig.audioConfig?.perceptualLoudnessDb, enable_per_format_loudness: data.playerConfig.audioConfig?.enablePerFormatLoudness }, stream_selection_config: { max_bitrate: data.playerConfig.streamSelectionConfig?.maxBitrate || '0' }, media_common_config: { dynamic_readahead_config: { max_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.maxReadAheadMediaTimeMs || 0, min_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.minReadAheadMediaTimeMs || 0, read_ahead_growth_rate_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.readAheadGrowthRateMs || 0 }, media_ustreamer_request_config: { video_playback_ustreamer_config: data.playerConfig.mediaCommonConfig?.mediaUstreamerRequestConfig?.videoPlaybackUstreamerConfig } } }; } const current_video_endpoint = data.currentVideoEndpoint ? new NavigationEndpoint(data.currentVideoEndpoint) : null; if (current_video_endpoint) { parsed_data.current_video_endpoint = current_video_endpoint; } const endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null; if (endpoint) { parsed_data.endpoint = endpoint; } const captions = parseItem(data.captions, PlayerCaptionsTracklist); if (captions) { parsed_data.captions = captions; } const video_details = data.videoDetails ? new VideoDetails(data.videoDetails) : null; if (video_details) { parsed_data.video_details = video_details; } const annotations = parseArray(data.annotations, PlayerAnnotationsExpanded); if (annotations.length) { parsed_data.annotations = annotations; } const storyboards = parseItem(data.storyboards, [PlayerStoryboardSpec, PlayerLiveStoryboardSpec]); if (storyboards) { parsed_data.storyboards = storyboards; } const endscreen = parseItem(data.endscreen, Endscreen); if (endscreen) { parsed_data.endscreen = endscreen; } const cards = parseItem(data.cards, CardCollection); if (cards) { parsed_data.cards = cards; } const engagement_panels = parseArray(data.engagementPanels, EngagementPanelSectionList); if (engagement_panels.length) { parsed_data.engagement_panels = engagement_panels; } if (data.bgChallenge) { const interpreter_url = { private_do_not_access_or_else_trusted_resource_url_wrapped_value: data.bgChallenge.interpreterUrl.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue, private_do_not_access_or_else_safe_script_wrapped_value: data.bgChallenge.interpreterUrl.privateDoNotAccessOrElseSafeScriptWrappedValue }; parsed_data.bg_challenge = { interpreter_url, interpreter_hash: data.bgChallenge.interpreterHash, program: data.bgChallenge.program, global_name: data.bgChallenge.globalName, client_experiments_state_blob: data.bgChallenge.clientExperimentsStateBlob }; } if (data.challenge) { parsed_data.challenge = data.challenge; } if (data.playerResponse) { parsed_data.player_response = parseResponse(data.playerResponse); } if (data.watchNextResponse) { parsed_data.watch_next_response = parseResponse(data.watchNextResponse); } if (data.cpnInfo) { parsed_data.cpn_info = { cpn: data.cpnInfo.cpn, cpn_source: data.cpnInfo.cpnSource }; } if (data.entries) { parsed_data.entries = data.entries.map((entry) => new NavigationEndpoint(entry)); } if (data.targetId) { parsed_data.target_id = data.targetId; } return parsed_data; } export function parseItem(data, validTypes) { if (!data) return null; const keys = Object.keys(data); if (!keys.length) return null; const classname = sanitizeClassName(keys[0]); if (!shouldIgnore(classname)) { try { const has_target_class = hasParser(classname); const TargetClass = has_target_class ? getParserByName(classname) : generateRuntimeClass(classname, data[keys[0]], ERROR_HANDLER); if (validTypes) { if (Array.isArray(validTypes)) { if (!validTypes.some((type) => type.type === TargetClass.type)) { ERROR_HANDLER({ classdata: data[keys[0]], classname, error_type: 'typecheck', expected: validTypes.map((type) => type.type) }); return null; } } else if (TargetClass.type !== validTypes.type) { ERROR_HANDLER({ classdata: data[keys[0]], classname, error_type: 'typecheck', expected: validTypes.type }); return null; } } const result = new TargetClass(data[keys[0]]); _addToMemo(classname, result); return result; } catch (err) { ERROR_HANDLER({ classname, classdata: data[keys[0]], error: err, error_type: 'parse' }); return null; } } return null; } export function parseArray(data, validTypes) { if (Array.isArray(data)) { const results = []; for (const item of data) { const result = parseItem(item, validTypes); if (result) { results.push(result); } } return observe(results); } else if (!data) { return observe([]); } throw new ParsingError('Expected array but got a single item'); } export function parse(data, requireArray, validTypes) { if (!data) return null; if (Array.isArray(data)) { const results = []; for (const item of data) { const result = parseItem(item, validTypes); if (result) { results.push(result); } } const res = observe(results); return requireArray ? res : new SuperParsedResult(res); } else if (requireArray) { throw new ParsingError('Expected array but got a single item'); } return new SuperParsedResult(parseItem(data, validTypes)); } const command_regexp = /Command$/; const endpoint_regexp = /Endpoint$/; const action_regexp = /Action$/; /** * Parses an InnerTube command and returns a YTNode instance if applicable. * @param data - The raw node data to parse * @returns A YTNode instance if parsing is successful, undefined otherwise */ export function parseCommand(data) { let keys = []; try { keys = Object.keys(data); } catch { /** NO-OP */ } for (const key of keys) { const value = data[key]; if (command_regexp.test(key) || endpoint_regexp.test(key) || action_regexp.test(key)) { const classname = sanitizeClassName(key); if (shouldIgnore(classname)) return undefined; try { const has_target_class = hasParser(classname); if (has_target_class) return new (getParserByName(classname))(value); } catch (error) { ERROR_HANDLER({ error, classname, classdata: value, error_type: 'parse' }); } } } } /** * Parses an array of InnerTube command nodes. * @param commands - Array of raw command nodes to parse * @returns An observed array of parsed YTNodes */ export function parseCommands(commands) { if (Array.isArray(commands)) { const results = []; for (const item of commands) { const result = parseCommand(item); if (result) { results.push(result); } } return observe(results); } else if (!commands) return observe([]); throw new ParsingError('Expected array but got a single item'); } export function parseC(data) { if (data.timedContinuationData) return new Continuation({ continuation: data.timedContinuationData, type: 'timed' }); return null; } export function parseLC(data) { if (data.itemSectionContinuation) return new ItemSectionContinuation(data.itemSectionContinuation); if (data.sectionListContinuation) return new SectionListContinuation(data.sectionListContinuation); if (data.liveChatContinuation) return new LiveChatContinuation(data.liveChatContinuation); if (data.musicPlaylistShelfContinuation) return new MusicPlaylistShelfContinuation(data.musicPlaylistShelfContinuation); if (data.musicShelfContinuation) return new MusicShelfContinuation(data.musicShelfContinuation); if (data.gridContinuation) return new GridContinuation(data.gridContinuation); if (data.playlistPanelContinuation) return new PlaylistPanelContinuation(data.playlistPanelContinuation); if (data.continuationCommand) return new ContinuationCommand(data.continuationCommand); return null; } export function parseRR(actions) { return observe(actions.map((action) => { if (action.navigateAction) return new NavigateAction(action.navigateAction); else if (action.showMiniplayerCommand) return new ShowMiniplayerCommand(action.showMiniplayerCommand); else if (action.reloadContinuationItemsCommand) return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand); else if (action.appendContinuationItemsAction) return new AppendContinuationItemsAction(action.appendContinuationItemsAction); else if (action.openPopupAction) return new OpenPopupAction(action.openPopupAction); }).filter((item) => item)); } export function parseActions(data) { if (Array.isArray(data)) { return parse(data.map((action) => { delete action.clickTrackingParams; return action; })); } return new SuperParsedResult(parseItem(data)); } export function parseFormats(formats, this_response_nsig_cache) { return formats?.map((format) => new Format(format, this_response_nsig_cache)) || []; } export function applyMutations(memo, mutations) { // Apply mutations to MusicMultiSelectMenuItems const music_multi_select_menu_items = memo.getType(MusicMultiSelectMenuItem); if (music_multi_select_menu_items.length > 0 && !mutations) { ERROR_HANDLER({ error_type: 'mutation_data_missing', classname: 'MusicMultiSelectMenuItem' }); } else { const missing_or_invalid_mutations = []; for (const menu_item of music_multi_select_menu_items) { const mutation = mutations .find((mutation) => mutation.payload?.musicFormBooleanChoice?.id === menu_item.form_item_entity_key); const choice = mutation?.payload.musicFormBooleanChoice; if (choice?.selected !== undefined && choice?.opaqueToken) { menu_item.selected = choice.selected; } else { missing_or_invalid_mutations.push(`'${menu_item.title}'`); } } if (missing_or_invalid_mutations.length > 0) { ERROR_HANDLER({ error_type: 'mutation_data_invalid', classname: 'MusicMultiSelectMenuItem', total: music_multi_select_menu_items.length, failed: missing_or_invalid_mutations.length, titles: missing_or_invalid_mutations }); } } // Apply mutations to MacroMarkersListEntity if (mutations) { const heat_map_mutations = mutations.filter((mutation) => mutation.payload?.macroMarkersListEntity && mutation.payload.macroMarkersListEntity.markersList?.markerType === 'MARKER_TYPE_HEATMAP'); for (const mutation of heat_map_mutations) { const macro_markers_entity = new MacroMarkersListEntity(mutation.payload.macroMarkersListEntity); const list = memo.get('MacroMarkersListEntity'); if (!list) { memo.set('MacroMarkersListEntity', [macro_markers_entity]); } else { list.push(macro_markers_entity); } } } } export function applyCommentsMutations(memo, mutations) { const comment_view_items = memo.getType(CommentView); if (comment_view_items.length > 0) { if (!mutations) { ERROR_HANDLER({ error_type: 'mutation_data_missing', classname: 'CommentView' }); } for (const comment_view of comment_view_items) { const comment_mutation = mutations .find((mutation) => mutation.payload?.commentEntityPayload?.key === comment_view.keys.comment) ?.payload?.commentEntityPayload; const toolbar_state_mutation = mutations .find((mutation) => mutation.payload?.engagementToolbarStateEntityPayload?.key === comment_view.keys.toolbar_state) ?.payload?.engagementToolbarStateEntityPayload; const engagement_toolbar = mutations.find((mutation) => mutation.entityKey === comment_view.keys.toolbar_surface) ?.payload?.engagementToolbarSurfaceEntityPayload; const comment_surface_mutation = mutations .find((mutation) => mutation.payload?.commentSurfaceEntityPayload?.key === comment_view.keys.comment_surface) ?.payload?.commentSurfaceEntityPayload; comment_view.applyMutations(comment_mutation, toolbar_state_mutation, engagement_toolbar, comment_surface_mutation); } } } //# sourceMappingURL=parser.js.map