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`
446 lines
12 KiB
TypeScript
446 lines
12 KiB
TypeScript
import { deepCompare, ParsingError } from '../utils/Utils';
|
|
|
|
const isObserved = Symbol('ObservedArray.isObserved');
|
|
export class YTNode {
|
|
static readonly type: string = 'YTNode';
|
|
readonly type: string;
|
|
constructor() {
|
|
this.type = (this.constructor as YTNodeConstructor).type;
|
|
}
|
|
/**
|
|
* Check if the node is of the given type.
|
|
* @param type - The type to check
|
|
* @returns whether the node is of the given type
|
|
*/
|
|
#is<T extends YTNode>(type: YTNodeConstructor<T>): this is T {
|
|
return this.type === type.type;
|
|
}
|
|
/**
|
|
* Check if the node is of the given type.
|
|
* @param types - The type to check
|
|
* @returns whether the node is of the given type
|
|
*/
|
|
is<T extends YTNode, K extends YTNodeConstructor<T>[]>(...types: K): this is InstanceType<K[number]> {
|
|
return types.some((type) => this.#is(type));
|
|
}
|
|
/**
|
|
* Cast to one of the given types.
|
|
*/
|
|
as<T extends YTNode, K extends YTNodeConstructor<T>[]>(...types: K): InstanceType<K[number]> {
|
|
if (!this.is(...types)) {
|
|
throw new ParsingError(`Cannot cast ${this.type} to one of ${types.map((t) => t.type).join(', ')}`);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Check for a key without asserting the type.
|
|
* @param key - The key to check
|
|
* @returns Whether the node has the key
|
|
*/
|
|
hasKey<T extends string, R = any>(key: T): this is this & { [k in T]: R } {
|
|
return Reflect.has(this, key);
|
|
}
|
|
/**
|
|
* Assert that the node has the given key and return it.
|
|
* @param key - The key to check
|
|
* @returns The value of the key wrapped in a Maybe
|
|
* @throws If the node does not have the key
|
|
*/
|
|
key<T extends string, R = any>(key: T) {
|
|
if (!this.hasKey<T, R>(key)) {
|
|
throw new ParsingError(`Missing key ${key}`);
|
|
}
|
|
return new Maybe(this[key]);
|
|
}
|
|
}
|
|
|
|
export class Maybe {
|
|
#value;
|
|
constructor (value: any) {
|
|
this.#value = value;
|
|
}
|
|
|
|
#checkPrimative(type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function') {
|
|
if (typeof this.#value !== type) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#assertPrimative(type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function') {
|
|
if (!this.#checkPrimative(type)) {
|
|
throw new TypeError(`Expected ${type}, got ${this.typeof}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
get typeof() {
|
|
return typeof this.#value;
|
|
}
|
|
|
|
string(): string {
|
|
return this.#assertPrimative('string');
|
|
}
|
|
|
|
isString() {
|
|
return this.#checkPrimative('string');
|
|
}
|
|
|
|
number(): number {
|
|
return this.#assertPrimative('number');
|
|
}
|
|
|
|
isNumber() {
|
|
return this.#checkPrimative('number');
|
|
}
|
|
|
|
bigint(): bigint {
|
|
return this.#assertPrimative('bigint');
|
|
}
|
|
|
|
isBigint() {
|
|
return this.#checkPrimative('bigint');
|
|
}
|
|
|
|
boolean(): boolean {
|
|
return this.#assertPrimative('boolean');
|
|
}
|
|
|
|
isBoolean() {
|
|
return this.#checkPrimative('boolean');
|
|
}
|
|
|
|
symbol(): symbol {
|
|
return this.#assertPrimative('symbol');
|
|
}
|
|
|
|
isSymbol() {
|
|
return this.#checkPrimative('symbol');
|
|
}
|
|
|
|
undefined(): undefined {
|
|
return this.#assertPrimative('undefined');
|
|
}
|
|
|
|
isUndefined() {
|
|
return this.#checkPrimative('undefined');
|
|
}
|
|
|
|
null(): null {
|
|
if (this.#value !== null)
|
|
throw new TypeError(`Expected null, got ${typeof this.#value}`);
|
|
return this.#value;
|
|
}
|
|
|
|
isNull() {
|
|
return this.#value === null;
|
|
}
|
|
|
|
object(): object {
|
|
return this.#assertPrimative('object');
|
|
}
|
|
|
|
isObject() {
|
|
return this.#checkPrimative('object');
|
|
}
|
|
|
|
/* eslint-ignore */
|
|
function(): Function {
|
|
return this.#assertPrimative('function');
|
|
}
|
|
|
|
isFunction() {
|
|
return this.#checkPrimative('function');
|
|
}
|
|
|
|
/**
|
|
* Get the value as an array.
|
|
* @returns the value as any[]
|
|
* @throws If the value is not an array
|
|
*/
|
|
array(): any[] {
|
|
if (!Array.isArray(this.#value)) {
|
|
throw new TypeError(`Expected array, got ${typeof this.#value}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* More typesafe variant of {@link Maybe#array}.
|
|
* @returns a proxied array which returns all the values as {@link Maybe}
|
|
* @throws If the value is not an array
|
|
*/
|
|
arrayOfMaybe(): Maybe[] {
|
|
const arrayProps: any[] = [];
|
|
return new Proxy(this.array(), {
|
|
get(target, prop) {
|
|
if (Reflect.has(arrayProps, prop)) {
|
|
return Reflect.get(target, prop);
|
|
}
|
|
return new Maybe(Reflect.get(target, prop));
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check whether the value is an array.
|
|
* @returns whether the value is an array
|
|
*/
|
|
isArray() {
|
|
return Array.isArray(this.#value);
|
|
}
|
|
|
|
/**
|
|
* Get the value as a YTNode
|
|
* @returns the value as a YTNode
|
|
* @throws If the value is not a YTNode
|
|
*/
|
|
node() {
|
|
if (!(this.#value instanceof YTNode)) {
|
|
throw new TypeError(`Expected YTNode, got ${this.#value.constructor.name}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* Check if the value is a YTNode
|
|
* @returns Whether the value is a YTNode
|
|
*/
|
|
isNode() {
|
|
return this.#value instanceof YTNode;
|
|
}
|
|
|
|
/**
|
|
* Get the value as a YTNode of the given type.
|
|
* @param type - The type to cast to
|
|
* @returns The node casted to the given type
|
|
* @throws If the node is not of the given type
|
|
*/
|
|
nodeOfType<T extends YTNode, K extends YTNodeConstructor<T>[]>(...types: K) {
|
|
return this.node().as(...types);
|
|
}
|
|
|
|
/**
|
|
* Check if the value is a YTNode of the given type.
|
|
* @param type - the type to check
|
|
* @returns Whether the value is a YTNode of the given type
|
|
*/
|
|
isNodeOfType<T extends YTNode, K extends YTNodeConstructor<T>[]>(...types: K) {
|
|
return this.isNode() && this.node().is(...types);
|
|
}
|
|
|
|
/**
|
|
* Get the value as an ObservedArray.
|
|
* @returns the value of the Maybe as a ObservedArray
|
|
*/
|
|
observed(): ObservedArray<YTNode> {
|
|
if (!this.isObserved()) {
|
|
throw new TypeError(`Expected ObservedArray, got ${typeof this.#value}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* Check if the value is an ObservedArray.
|
|
*/
|
|
isObserved() {
|
|
return this.#value?.[isObserved];
|
|
}
|
|
|
|
/**
|
|
* Get the value of the Maybe as a SuperParsedResult.
|
|
* @returns the value as a SuperParsedResult
|
|
* @throws If the value is not a SuperParsedResult
|
|
*/
|
|
parsed(): SuperParsedResult {
|
|
if (!(this.#value instanceof SuperParsedResult)) {
|
|
throw new TypeError(`Expected SuperParsedResult, got ${typeof this.#value}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* Is the result a SuperParsedResult?
|
|
*/
|
|
isParsed() {
|
|
return this.#value instanceof SuperParsedResult;
|
|
}
|
|
|
|
/**
|
|
* @deprecated This call is not meant to be used outside of debugging. Please use the specific type getter instead.
|
|
*/
|
|
any(): any {
|
|
console.warn('This call is not meant to be used outside of debugging. Please use the specific type getter instead.');
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* Get the node as an instance of the given class.
|
|
* @param type - The type to check
|
|
* @returns the value as the given type
|
|
* @throws If the node is not of the given type
|
|
*/
|
|
instanceof<T extends object>(type: Constructor<T>): T {
|
|
if (!this.isInstanceof(type)) {
|
|
throw new TypeError(`Expected instance of ${type.name}, got ${this.#value.constructor.name}`);
|
|
}
|
|
return this.#value;
|
|
}
|
|
|
|
/**
|
|
* Check if the node is an instance of the given class.
|
|
* @param type - The type to check
|
|
* @returns Whether the node is an instance of the given type
|
|
*/
|
|
isInstanceof<T extends object>(type: Constructor<T>): this is this & T {
|
|
return this.#value instanceof type;
|
|
}
|
|
}
|
|
|
|
export interface Constructor<T> {
|
|
new (...args: any[]): T;
|
|
}
|
|
|
|
export interface YTNodeConstructor<T extends YTNode = YTNode> {
|
|
new(data: any): T;
|
|
readonly type: string;
|
|
}
|
|
|
|
/**
|
|
* Represents a parsed response in an unknown state. Either a YTNode or a YTNode[] or null.
|
|
*/
|
|
export class SuperParsedResult<T extends YTNode = YTNode> {
|
|
#result;
|
|
constructor(result: T | ObservedArray<T> | null) {
|
|
this.#result = result;
|
|
}
|
|
|
|
get is_null() {
|
|
return this.#result === null;
|
|
}
|
|
get is_array() {
|
|
return !this.is_null && Array.isArray(this.#result);
|
|
}
|
|
get is_node() {
|
|
return !this.is_array;
|
|
}
|
|
|
|
array() {
|
|
if (!this.is_array) {
|
|
throw new TypeError('Expected an array, got a node');
|
|
}
|
|
return this.#result as ObservedArray<T>;
|
|
}
|
|
|
|
item() {
|
|
if (!this.is_node) {
|
|
throw new TypeError('Expected a node, got an array');
|
|
}
|
|
return this.#result as T;
|
|
}
|
|
}
|
|
|
|
|
|
export type ObservedArray<T extends YTNode = YTNode> = Array<T> & {
|
|
/**
|
|
* Returns the first object to match the rule.
|
|
*/
|
|
get: (rule: object, del_item?: boolean) => T | undefined;
|
|
/**
|
|
* Returns all objects that match the rule.
|
|
*/
|
|
getAll: (rule: object, del_items?: boolean) => T[];
|
|
/**
|
|
* Removes the item at the given index.
|
|
*/
|
|
remove: (index: number) => T[];
|
|
/**
|
|
* Get all items of a specific type
|
|
*/
|
|
filterType<R extends YTNode, K extends YTNodeConstructor<R>[]>(...types: K): ObservedArray<InstanceType<K[number]>>;
|
|
/**
|
|
* Get the first of a specific type
|
|
*/
|
|
firstOfType<R extends YTNode, K extends YTNodeConstructor<R>[]>(...types: K): InstanceType<K[number]> | undefined;
|
|
/**
|
|
* This is similar to filter but throws if there's a type mismatch.
|
|
*/
|
|
as<R extends YTNode, K extends YTNodeConstructor<R>[]>(...types: K): ObservedArray<InstanceType<K[number]>>;
|
|
};
|
|
|
|
/**
|
|
* Creates a trap to intercept property access
|
|
* and add utilities to an object.
|
|
*/
|
|
export function observe<T extends YTNode>(obj: Array<T>) {
|
|
return new Proxy(obj, {
|
|
get(target, prop) {
|
|
if (prop == 'get') {
|
|
return (rule: object, del_item?: boolean) => (
|
|
target.find((obj, index) => {
|
|
const match = deepCompare(rule, obj);
|
|
if (match && del_item) {
|
|
target.splice(index, 1);
|
|
}
|
|
return match;
|
|
})
|
|
);
|
|
}
|
|
if (prop == isObserved) {
|
|
return true;
|
|
}
|
|
if (prop == 'getAll') {
|
|
return (rule: object, del_items: boolean) => (
|
|
target.filter((obj, index) => {
|
|
const match = deepCompare(rule, obj);
|
|
if (match && del_items) {
|
|
target.splice(index, 1);
|
|
}
|
|
return match;
|
|
})
|
|
);
|
|
}
|
|
if (prop == 'filterType') {
|
|
return (...types: YTNodeConstructor<YTNode>[]) => {
|
|
return observe(target.filter((node: YTNode) => {
|
|
if (node.is(...types))
|
|
return true;
|
|
return false;
|
|
|
|
}));
|
|
};
|
|
}
|
|
if (prop == 'firstOfType') {
|
|
return (...types: YTNodeConstructor<YTNode>[]) => {
|
|
return target.find((node: YTNode) => {
|
|
if (node.is(...types))
|
|
return true;
|
|
return false;
|
|
});
|
|
};
|
|
}
|
|
if (prop == 'as') {
|
|
return (...types: YTNodeConstructor<YTNode>[]) => {
|
|
return observe(target.map((node: YTNode) => {
|
|
if (node.is(...types))
|
|
return node;
|
|
throw new ParsingError(`Expected node of any type ${types.map((type) => type.type).join(', ')}, got ${(node as YTNode).type}`);
|
|
}));
|
|
};
|
|
}
|
|
if (prop == 'remove') {
|
|
return (index: number): any => target.splice(index, 1);
|
|
}
|
|
return Reflect.get(target, prop);
|
|
}
|
|
}) as ObservedArray<T>;
|
|
}
|
|
|
|
export class Memo extends Map<string, YTNode[]> {
|
|
getType<T extends YTNode>(type: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
|
if (Array.isArray(type))
|
|
return observe(type.flatMap((type) => (this.get(type.type) || []) as T[]));
|
|
return observe((this.get(type.type) || []) as T[]);
|
|
}
|
|
}
|