mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-19 04:21:35 +00:00
* refactor: remove dependancies removes node-forge and uuid in favor of Web APIs * refactor!: commonjs to es6 To aid with #93 I will make all my changes in TypeScript instead. This is the first step into making that happen. Used: https://github.com/wessberg/cjstoesm * refactor!: NToken and Signature TS files Bring this PR up to speed with #93 * feat: cross platform cache (WIP) this is untested! should remove idb as dependecy. * feat: EventEmitter polyfill * refactor: remove events * feat: HTTPClient based on Fetch API (WIP) * refactor!: parsers refactor (WIP) Initial TS support for parsers as per #93 This adds several type safety checks to the parser which'll help to ensure valid data is returned by the parser. * refactor!: parsers refactor (WIP) Bring more in line with the existing implementations & make less verbose * refactor!: parser refactor I was overcomplicating things, this is much simpler and compatible with the existing JS API * fix: some missed parsers while refactoring * fix: better type inferance for parseResponse * feat(TS): typesafe YTNode casts * feat: more type safety in YTNode and Parser * refactor: VideoInfo download with fetch & TS (WIP) Again, this also does some work for #93 * fix: LiveChat in VideoInfo * refactor!: more typesafety in parser * refactor!: VideoInfo almost completed * refactor!: player and session refactors - Remove the Player class' dependance on Session. - Add additional context to the Session. * refactor!: move auth logic to Session (WIP) * refactor: TS port for Actions and Innertube My fingers hurt from typing out all those types :-P * refactor: NavigationEndpoint TS this is still a WIP and should be improved. NavigationEndpoint should probably be refactored further. * refactor!: VideoInfo compiles without errors * chore: delete old player * fix: import errors It compiles and runs!! * fix: Utils import fixes * fix: several runtime errors * fix: video streaming * chore: remove console.log debugging Whoops, forgot to remove these before I pushed the previous commit * chore: remove old unused dependencies * fix: typescript errors Now emitting declarations and source maps * refactor: TS feed * chore: delete old Feed * refactor: move streamToIterable into Utils * refactor: AccountManager TS * refactor: FilterableFeed to TS * refactor: InteractionManager to TS * refactor: PlaylistManager to TS * refactor: TabbedFeed to TS * refactor: Music to TS (WIP) more work to be done, see TODO comments * fix: getting the tests to pass (6/12) YouTube.js Tests Search ✓ Should search on YouTube (1152 ms) ✕ Should search on YouTube Music (705 ms) ✕ Should retrieve YouTube search suggestions (722 ms) ✓ Should retrieve YouTube Music search suggestions (233 ms) Comments ✓ Should retrieve comments (585 ms) ✕ Should retrieve next batch of comments (221 ms) ✕ Should retrieve comment replies (1 ms) General ✕ Should retrieve playlist with YouTube (732 ms) ✓ Should retrieve home feed (838 ms) ✓ Should retrieve trending content (543 ms) ✓ Should retrieve video info (639 ms) ✕ Should download video (5 ms) * fix: tests (7/12) YouTube.js Tests Search ✓ Should search on YouTube (1984 ms) ✕ Should search on YouTube Music (1139 ms) ✕ Should retrieve YouTube search suggestions (1433 ms) ✓ Should retrieve YouTube Music search suggestions (529 ms) Comments ✓ Should retrieve comments (324 ms) ✓ Should retrieve next batch of comments (395 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (653 ms) ✓ Should retrieve home feed (1085 ms) ✓ Should retrieve trending content (513 ms) ✓ Should retrieve video info (921 ms) ✕ Should download video (3 ms) * fix: download tests (8/12) YouTube.js Tests Search ✓ Should search on YouTube (1293 ms) ✕ Should search on YouTube Music (927 ms) ✕ Should retrieve YouTube search suggestions (1250 ms) ✓ Should retrieve YouTube Music search suggestions (258 ms) Comments ✓ Should retrieve comments (803 ms) ✓ Should retrieve next batch of comments (511 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (528 ms) ✓ Should retrieve home feed (1047 ms) ✓ Should retrieve trending content (548 ms) ✓ Should retrieve video info (825 ms) ✓ Should download video (1779 ms) * fix: tests (9/12) YouTube.js Tests Search ✓ Should search on YouTube (1276 ms) ✕ Should search on YouTube Music (955 ms) ✓ Should retrieve YouTube search suggestions (661 ms) ✓ Should retrieve YouTube Music search suggestions (491 ms) Comments ✓ Should retrieve comments (624 ms) ✓ Should retrieve next batch of comments (353 ms) ✕ Should retrieve comment replies General ✕ Should retrieve playlist with YouTube (672 ms) ✓ Should retrieve home feed (1277 ms) ✓ Should retrieve trending content (999 ms) ✓ Should retrieve video info (1106 ms) ✓ Should download video (2514 ms) * feat: key based type validation for parsers * fix: comments tests pass (10/12) YouTube.js Tests Search ✓ Should search on YouTube (938 ms) ✕ Should search on YouTube Music (850 ms) ✓ Should retrieve YouTube search suggestions (528 ms) ✓ Should retrieve YouTube Music search suggestions (224 ms) Comments ✓ Should retrieve comments (518 ms) ✓ Should retrieve next batch of comments (337 ms) ✓ Should retrieve comment replies (358 ms) General ✕ Should retrieve playlist with YouTube (466 ms) ✓ Should retrieve home feed (1051 ms) ✓ Should retrieve trending content (623 ms) ✓ Should retrieve video info (863 ms) ✓ Should download video (2656 ms) * refactor: type safety checks removing @ts-ignore * fix: playlist tests pass (11/12) YouTube.js Tests Search ✓ Should search on YouTube (991 ms) ✕ Should search on YouTube Music (924 ms) ✓ Should retrieve YouTube search suggestions (606 ms) ✓ Should retrieve YouTube Music search suggestions (225 ms) Comments ✓ Should retrieve comments (393 ms) ✓ Should retrieve next batch of comments (284 ms) ✓ Should retrieve comment replies (252 ms) General ✓ Should retrieve playlist with YouTube (578 ms) ✓ Should retrieve home feed (1148 ms) ✓ Should retrieve trending content (541 ms) ✓ Should retrieve video info (799 ms) ✓ Should download video (1419 ms) * fix: all tests pass for node 🎉 YouTube.js Tests Search ✓ Should search on YouTube (1053 ms) ✓ Should search on YouTube Music (761 ms) ✓ Should retrieve YouTube search suggestions (453 ms) ✓ Should retrieve YouTube Music search suggestions (221 ms) Comments ✓ Should retrieve comments (627 ms) ✓ Should retrieve next batch of comments (412 ms) ✓ Should retrieve comment replies (268 ms) General ✓ Should retrieve playlist with YouTube (565 ms) ✓ Should retrieve home feed (775 ms) ✓ Should retrieve trending content (498 ms) ✓ Should retrieve video info (875 ms) ✓ Should download video (1364 ms) * build: working Deno bundle Still need to test whether this bundle works in the browser * docs: update deno example to download video * refactor: MusicResponsiveListItem to TS * docs: TSDoc for Parser helpers * docs: Parser documentation for TS * docs: add note about parseItem and parseArray * test: remove browser tests since they're identical * feat: browser support and proxy example * fix: PlaylistManager TS after merge * feat: in-browser video streaming * refactor: cleanup the Dash example * feat: allow custom fetch implementations * feat: fetch debugger * fix: OAuth login * refactor: remove file extensions from imports * refactor: build scripts * fix: CustomEvent on node * fix: LiveChat * fix: linting * fix: liniting in build-parser-json * chore: update test workflow * fix: NToken errors after lint fixes * fix: codacy complaints * docs: update to reflect changes Definitly needs more work but its a start * refactor: cleanup imports/exports * fix: browser example - Remove user-agent before making request. - Fix cache on browsers * fix: cache on node * fix: stupid mistake * refactor: Session#signIn to wait untill success This also splits the 'auth' event up into 3 distinct events: - 'auth' -> fired on success - 'auth-pending' -> fired when pending authentication - 'auth-error' -> fired when an error occurred * refactor: freeze Constants * refactor: cleanup HTTPClient Request * refactor: debugFetch readability * chore: lint * refactor: replace jsdoc with tsdoc eslint plugin remove @param annotations without descriptions * fix: bunch of liniting warnings * refactor: better inference on YTNode#is As suggested by @MasterOfBob777 * fix: linting warnings * revert: undici import * refactor: rename `list_type` to `item_type`
304 lines
12 KiB
TypeScript
304 lines
12 KiB
TypeScript
import { InnertubeError, ParsingError } from '../utils/Utils';
|
|
import Format from './classes/misc/Format';
|
|
import VideoDetails from './classes/misc/VideoDetails';
|
|
import GetParserByName from './map';
|
|
import package_json from '../../package.json';
|
|
import Endscreen from './classes/Endscreen';
|
|
import CardCollection from './classes/CardCollection';
|
|
import { YTNode, YTNodeConstructor, SuperParsedResult, ObservedArray, observe, Memo } from './helpers';
|
|
|
|
export class AppendContinuationItemsAction extends YTNode {
|
|
static readonly type = 'appendContinuationItemsAction';
|
|
|
|
contents: ObservedArray<YTNode> | null;
|
|
|
|
constructor(data: any) {
|
|
super();
|
|
this.contents = Parser.parse(data.continuationItems, true);
|
|
}
|
|
}
|
|
export class ReloadContinuationItemsCommand extends YTNode {
|
|
static readonly type = 'reloadContinuationItemsCommand';
|
|
target_id: string;
|
|
contents: ObservedArray<YTNode> | null;
|
|
constructor(data: any) {
|
|
super();
|
|
this.target_id = data.targetId;
|
|
this.contents = Parser.parse(data.continuationItems, true);
|
|
}
|
|
}
|
|
export class SectionListContinuation extends YTNode {
|
|
static readonly type = 'sectionListContinuation';
|
|
continuation: string;
|
|
contents: ObservedArray<YTNode> | null;
|
|
constructor(data: any) {
|
|
super();
|
|
this.contents = Parser.parse(data.contents, true);
|
|
this.continuation = data.continuations[0].nextContinuationData.continuation;
|
|
}
|
|
}
|
|
export class TimedContinuation extends YTNode {
|
|
static readonly type = 'timedContinuationData';
|
|
timeout_ms: number; // TODO: is this a number or a string?
|
|
token: string;
|
|
constructor(data: any) {
|
|
super();
|
|
this.timeout_ms = data.timeoutMs || data.timeUntilLastMessageMsec;
|
|
this.token = data.continuation;
|
|
}
|
|
}
|
|
export class LiveChatContinuation extends YTNode {
|
|
static readonly type = 'liveChatContinuation';
|
|
actions: ObservedArray<YTNode>;
|
|
action_panel: YTNode | null;
|
|
item_list: YTNode | null;
|
|
header: YTNode | null;
|
|
participants_list: YTNode | null;
|
|
popout_message: YTNode | null;
|
|
emojis: any[] | null; // TODO: give this an actual type
|
|
continuation: TimedContinuation;
|
|
viewer_name: string;
|
|
constructor(data: any) {
|
|
super();
|
|
this.actions = Parser.parse(data.actions?.map((action: any) => {
|
|
delete action.clickTrackingParams;
|
|
return action;
|
|
}), true) || observe<YTNode>([]);
|
|
this.action_panel = Parser.parseItem(data.actionPanel);
|
|
this.item_list = Parser.parseItem(data.itemList);
|
|
this.header = Parser.parseItem(data.header);
|
|
this.participants_list = Parser.parseItem(data.participantsList);
|
|
this.popout_message = Parser.parseItem(data.popoutMessage);
|
|
this.emojis = data.emojis?.map((emoji: any) => ({
|
|
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;
|
|
}
|
|
}
|
|
|
|
export default class Parser {
|
|
static #memo: Memo | null = null;
|
|
static #clearMemo() {
|
|
Parser.#memo = null;
|
|
}
|
|
static #createMemo() {
|
|
Parser.#memo = new Memo();
|
|
}
|
|
static #addToMemo(classname: string, result: YTNode) {
|
|
if (!Parser.#memo)
|
|
return;
|
|
|
|
const list = Parser.#memo.get(classname);
|
|
if (!list)
|
|
return Parser.#memo.set(classname, [ result ]);
|
|
|
|
list.push(result);
|
|
}
|
|
static #getMemo() {
|
|
if (!Parser.#memo)
|
|
throw new Error('Parser#getMemo() called before Parser#createMemo()');
|
|
return Parser.#memo;
|
|
}
|
|
/**
|
|
* Parses InnerTube response.
|
|
*/
|
|
static parseResponse(data: any) {
|
|
// Memoize the response objects by classname
|
|
this.#createMemo();
|
|
// TODO: is this parseItem?
|
|
const contents = Parser.parse(data.contents);
|
|
const contents_memo = this.#getMemo();
|
|
// 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 = this.#getMemo();
|
|
this.#clearMemo();
|
|
this.#createMemo();
|
|
const on_response_received_endpoints = data.onResponseReceivedEndpoints ? Parser.parseRR(data.onResponseReceivedEndpoints) : null;
|
|
const on_response_received_endpoints_memo = this.#getMemo();
|
|
this.#clearMemo();
|
|
this.#createMemo();
|
|
const on_response_received_commands = data.onResponseReceivedCommands ? Parser.parseRR(data.onResponseReceivedCommands) : null;
|
|
const on_response_received_commands_memo = this.#getMemo();
|
|
this.#clearMemo();
|
|
this.#createMemo();
|
|
const actions = data.actions ? Parser.parseActions(data.actions) : null;
|
|
const actions_memo = this.#getMemo();
|
|
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,
|
|
continuation: data.continuation ? Parser.parseC(data.continuation) : null,
|
|
continuation_contents: data.continuationContents ? Parser.parseLC(data.continuationContents) : null,
|
|
metadata: Parser.parse(data.metadata),
|
|
header: Parser.parse(data.header),
|
|
microformat: data.microformat ? Parser.parseItem(data.microformat) : null,
|
|
sidebar: Parser.parseItem(data.sidebar),
|
|
overlay: Parser.parseItem(data.overlay),
|
|
refinements: data.refinements || null,
|
|
estimated_results: data.estimatedResults || null,
|
|
player_overlays: Parser.parse(data.playerOverlays),
|
|
playability_status: data.playabilityStatus ? {
|
|
status: data.playabilityStatus.status as string,
|
|
error_screen: Parser.parse(data.playabilityStatus.errorScreen),
|
|
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
|
reason: `${data.reason}` || ''
|
|
} : undefined,
|
|
streaming_data: data.streamingData ? {
|
|
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
|
|
formats: Parser.parseFormats(data.streamingData.formats),
|
|
adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats),
|
|
dash_manifest_url: data.streamingData?.dashManifestUrl || null,
|
|
dls_manifest_url: data.streamingData?.dashManifestUrl || null
|
|
} : undefined,
|
|
// TODO: PlayerCaptionsTracklist ?
|
|
captions: Parser.parse(data.captions),
|
|
video_details: data.videoDetails ? new VideoDetails(data.videoDetails) : undefined,
|
|
// TODO: might want to type check these two and use parseItem
|
|
annotations: Parser.parse(data.annotations),
|
|
storyboards: Parser.parse(data.storyboards),
|
|
endscreen: Parser.parseItem<Endscreen>(data.endscreen, Endscreen),
|
|
cards: Parser.parseItem<CardCollection>(data.cards, CardCollection)
|
|
};
|
|
}
|
|
static parseC(data: any) {
|
|
if (data.timedContinuationData)
|
|
return new TimedContinuation(data.timedContinuationData);
|
|
}
|
|
static parseLC(data: any) {
|
|
if (data.sectionListContinuation)
|
|
return new SectionListContinuation(data.sectionListContinuation);
|
|
if (data.liveChatContinuation)
|
|
return new LiveChatContinuation(data.liveChatContinuation);
|
|
}
|
|
static parseRR(actions: any[]) {
|
|
return observe(actions.map((action: any) => {
|
|
if (action.reloadContinuationItemsCommand)
|
|
return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand);
|
|
if (action.appendContinuationItemsAction)
|
|
return new AppendContinuationItemsAction(action.appendContinuationItemsAction);
|
|
}).filter((item) => item) as (ReloadContinuationItemsCommand | AppendContinuationItemsAction)[]);
|
|
}
|
|
static parseActions(data: any) {
|
|
if (Array.isArray(data)) {
|
|
return Parser.parse(data.map((action) => {
|
|
delete action.clickTrackingParams;
|
|
return action;
|
|
}));
|
|
}
|
|
return new SuperParsedResult(Parser.parseItem(data));
|
|
}
|
|
static parseFormats(formats: any[]) {
|
|
return formats?.map((format) => new Format(format)) || [];
|
|
}
|
|
|
|
static parseItem<T extends YTNode = YTNode>(data: any, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
|
if (!data) return null;
|
|
const keys = Object.keys(data);
|
|
const classname = this.sanitizeClassName(keys[0]);
|
|
if (!this.shouldIgnore(classname)) {
|
|
try {
|
|
const TargetClass = GetParserByName(classname);
|
|
if (validTypes) {
|
|
if (Array.isArray(validTypes)) {
|
|
if (!validTypes.some((type) => type.type === TargetClass.type))
|
|
throw new ParsingError(`Type mismatch, got ${classname} but expected one of ${validTypes.map((type) => type.type).join(', ')}`);
|
|
} else if (TargetClass.type !== validTypes.type)
|
|
throw new ParsingError(`Type mismatch, got ${classname} but expected ${validTypes.type}`);
|
|
}
|
|
const result = new TargetClass(data[keys[0]]);
|
|
this.#addToMemo(classname, result);
|
|
return result as T;
|
|
} catch (err) {
|
|
this.formatError({ classname, classdata: data[keys[0]], err });
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static parseArray<T extends YTNode = YTNode>(data: any[], validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
|
if (Array.isArray(data)) {
|
|
const results: T[] = [];
|
|
for (const item of data) {
|
|
const result = this.parseItem(item, validTypes);
|
|
if (result) {
|
|
results.push(result);
|
|
}
|
|
}
|
|
return observe(results);
|
|
} else if (!data) {
|
|
return observe([] as T[]);
|
|
}
|
|
throw new ParsingError('Expected array but got a single item');
|
|
}
|
|
|
|
static parse<T extends YTNode = YTNode>(data: any, requireArray: true, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) : ObservedArray<T> | null;
|
|
static parse<T extends YTNode = YTNode>(data: any, requireArray?: false | undefined, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) : SuperParsedResult<T>;
|
|
/**
|
|
* Parses the `contents` property of the response.
|
|
*/
|
|
static parse<T extends YTNode = YTNode>(data: any, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
|
if (!data)
|
|
return null;
|
|
if (Array.isArray(data)) {
|
|
const results: T[] = [];
|
|
for (const item of data) {
|
|
const result = this.parseItem(item, validTypes);
|
|
if (result) {
|
|
results.push(result);
|
|
}
|
|
}
|
|
const res = observe(results);
|
|
return requireArray ? res : new SuperParsedResult(observe(results));
|
|
} else if (requireArray) {
|
|
throw new ParsingError('Expected array but got a single item');
|
|
}
|
|
return new SuperParsedResult(this.parseItem(data, validTypes));
|
|
}
|
|
|
|
static formatError({ classname, classdata, err }: { classname: string, classdata: any, err: any }) {
|
|
if (err.code == 'MODULE_NOT_FOUND') {
|
|
return console.warn(new InnertubeError(`${classname} not found!\n` +
|
|
`This is a bug, please report it at ${package_json.bugs.url}`, classdata));
|
|
}
|
|
console.warn(new InnertubeError(`Something went wrong at ${classname}!\n` +
|
|
`This is a bug, please report it at ${package_json.bugs.url}`, { stack: err.stack }));
|
|
}
|
|
static sanitizeClassName(input: string) {
|
|
return (input.charAt(0).toUpperCase() + input.slice(1))
|
|
.replace(/Renderer|Model/g, '')
|
|
.replace(/Radio/g, 'Mix').trim();
|
|
}
|
|
static ignore_list = new Set<string>([
|
|
'DisplayAd',
|
|
'SearchPyv',
|
|
'MealbarPromo',
|
|
'BackgroundPromo',
|
|
'PromotedSparklesWeb',
|
|
'RunAttestationCommand',
|
|
'StatementBanner'
|
|
]);
|
|
static shouldIgnore(classname: string) {
|
|
return this.ignore_list.has(classname);
|
|
}
|
|
}
|
|
|
|
export type ParsedResponse = ReturnType<typeof Parser.parseResponse>;
|