Files
YouTube.js/lib/parser/helpers.ts
Daniel Wykerd fb68e6bcfe feat!: better cross runtime support (#97)
* 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`
2022-07-20 14:06:12 -03:00

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[]);
}
}