mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-16 11:02:10 +00:00
308 lines
9.3 KiB
JavaScript
308 lines
9.3 KiB
JavaScript
'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; |