Files
YouTube.js/lib/parser/contents/index.js
Daniel Wykerd 4a102878d8 refactor!: feature complete contents parser
* feat: allow setting search params to custom value

This is useful for getting results other than videos, like playlists and
channels.

* feat: add initial parsers for common renderers

* feat: artist search renderers

Added common renderers used when searching artists

* refactor: snake_case

* feat: channel home page renderers

* feat: parsers for more channel tabs

These are needed for channel tabs: Videos, Playlists, Community,
Channels

Additionally, do not merely return text as string, since they may
include links which may be navigated to

* feat: channel full metadata

* feat: renderers for playlists

* refactor!: Actions.browse

Channels should be viewable when not logged in, also added 'navigation'
type for use in NagivationEndpoint in the future.

* feat: home feed parsers

* feat: watch page renderers

* feat: start implementing HomeFeed API

The HomeFeed class remains compatible with the existing API

* feat: generate types using tsc and jsdoc

* feat: browse continuations from navigationEndpoint

* fix: Actions moved to session

This follows commit 1bfe2676d8

* fix: add more typescript config

* chore: use correct spaces and quotes

* feat: Trending API

* feat: reimplement existing channel API

* feat: add base video feed class

* feat: get channel videos

* feat: channel playlists

* feat: get channel community posts

* feat: get channels from channel

* feat: get channel about page data

* feat: add missing channel parsers

this commit also adds regenerated types I've neglected to push

* feat: initial playlist reimplementation

* feat: complete playlist reimplementation

* refactor: change InnertubeError to ES6 class

* fix: some unresolved types

* chore: update types

* feat: wip video details

* feat: get music tracks in video

Possibly an implementation for issue #48

* refactor:  merge parsers (wip)

This is a work in progress.

* fix: add pnpm to ignore

* fix: merge issues

* fix: merge Video and VideoInfo

VideoInfo should be working again.
Also remove the old parsers.

* feat: set matching in Simplify

Still looking into removing Simplify

* fix: ContinuationItem

This `call` method allows for traversal of continuations with the Simplify API
but may be removed in the future

* fix: optionally returned data

* revert: replace ContinuationItem with main

* feat(parser): contents memoization by classname

* feat(channel): working without Simplify

* feat(feed): working continuations

* fix: liniting issues

* feat(feed): filterable feed for home

* feat(feed): tabbed feed for trending & channel

* refactor: remove Simplify completely

* chore: lint

* refactor: alias `items` with `contents`

* refactor: `Search` to extend `Feed`

* fix: Search working

Also added MenuServiceItemDownload

* refactor: move `Channel` and `Playlist`

* fix: pass all tests

* fix: linting errors
2022-06-15 18:31:34 -03:00

202 lines
6.8 KiB
JavaScript

'use strict';
const { InnertubeError, observe } = require('../../utils/Utils');
const Format = require('./classes/Format');
const VideoDetails = require('./classes/VideoDetails');
class AppendContinuationItemsAction {
type = 'appendContinuationItemsAction';
constructor (data) {
this.continuation_items = Parser.parse(data.continuationItems);
}
}
class ReloadContinuationItemsCommand {
type = 'reloadContinuationItemsCommand';
constructor (data) {
this.target_id = data.targetId;
this.continuation_items = Parser.parse(data.continuationItems);
}
}
class SectionListContinuation {
type = 'sectionListContinuation';
constructor(data) {
this.contents = Parser.parse(data.contents);
this.continuation = data.continuations[0].nextContinuationData.continuation;
}
}
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);
}
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();
return {
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: data.onResponseReceivedCommands && Parser.parseRR(data.onResponseReceivedCommands) || 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),
sidebar: Parser.parse(data.sidebar),
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),
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 parseLC(data) {
if (data.sectionListContinuation)
return new SectionListContinuation(data.sectionListContinuation);
}
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 parseFormats(formats) {
return observe(formats?.map((format) => new Format(format)) || []);
}
static parse(data) {
if (!data)
return null;
if (Array.isArray(data)) {
let results = [];
for (let item of data) {
const keys = Object.keys(item);
const classname = this.sanitizeClassName(keys[0]);
if (!this.shouldIgnore(classname)) {
try {
const TargetClass = require('./classes/' + 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);
} else {
const keys = Object.keys(data);
const classname = this.sanitizeClassName(keys[0]);
if (!this.shouldIgnore(classname)) {
try {
const TargetClass = require('./classes/' + 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')
console.warn(
new InnertubeError(classname + ' not found!\n' +
'This is a bug, please report it at ' + require('../../../package.json').bugs.url,
classdata)
);
else
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 [
'MealbarPromo',
'PromotedSparklesWeb'
].includes(classname);
}
}
module.exports = Parser;