From b6a898f7333e460f321fbe06bad6a5fe0232f6a7 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Mon, 13 Jun 2022 04:20:49 -0300 Subject: [PATCH] feat: add full support for refinement cards --- lib/Innertube.js | 4 +- lib/parser/contents/classes/RichListHeader.js | 2 +- .../contents/classes/SearchRefinementCard.js | 4 +- lib/parser/contents/classes/Shelf.js | 13 +++- .../classes/TwoColumnBrowseResults.js | 13 +--- .../classes/TwoColumnSearchResults.js | 13 +--- .../classes/TwoColumnWatchNextResults.js | 18 +---- lib/parser/contents/index.js | 8 +-- lib/parser/youtube/Search.js | 65 ++++++++++++++---- lib/utils/Utils.js | 66 +++++++++++++++---- 10 files changed, 134 insertions(+), 72 deletions(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 1e94da91..7357ae6b 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -174,9 +174,9 @@ class Innertube { const initial_info = this.actions.getVideoInfo(video_id); const continuation = this.actions.next({ video_id }); - + console.time('') const response = await Promise.all([ initial_info, continuation ]); - + console.timeEnd('') return new VideoInfo(response, this.actions, this.#player); } diff --git a/lib/parser/contents/classes/RichListHeader.js b/lib/parser/contents/classes/RichListHeader.js index a4ad3f71..9d6ea9c5 100644 --- a/lib/parser/contents/classes/RichListHeader.js +++ b/lib/parser/contents/classes/RichListHeader.js @@ -5,7 +5,7 @@ const Text = require('./Text'); class RichListHeader { constructor(data) { this.title = new Text(data.title); - this.type = data.icon.iconType; + this.icon_type = data.icon.iconType; } } diff --git a/lib/parser/contents/classes/SearchRefinementCard.js b/lib/parser/contents/classes/SearchRefinementCard.js index 3003ffa3..2d7427c5 100644 --- a/lib/parser/contents/classes/SearchRefinementCard.js +++ b/lib/parser/contents/classes/SearchRefinementCard.js @@ -8,9 +8,9 @@ class SearchRefinementCard { type = 'searchRefinementCardRenderer'; constructor(data) { - this.thumbnail = new Thumbnail(data.thumbnail).thumbnails; + this.thumbnails = new Thumbnail(data.thumbnail).thumbnails; this.endpoint = new NavigationEndpoint(data.searchEndpoint); - this.query = new Text(data.query); + this.query = new Text(data.query).toString(); } } diff --git a/lib/parser/contents/classes/Shelf.js b/lib/parser/contents/classes/Shelf.js index d1848c96..d013a378 100644 --- a/lib/parser/contents/classes/Shelf.js +++ b/lib/parser/contents/classes/Shelf.js @@ -9,10 +9,17 @@ class Shelf { constructor(data) { this.title = new Text(data.title); - this.endpoint = new NavigationEndpoint(data.endpoint); + + data.endpoint && + (this.endpoint = new NavigationEndpoint(data.endpoint)); + this.content = Parser.parse(data.content) || []; - this.icon_type = data.icon?.iconType || null; - this.menu = Parser.parse(data.menu); + + data.icon?.iconType && + (this.icon_type = data.icon?.iconType); + + data.menu && + (this.menu = Parser.parse(data.menu)); } } diff --git a/lib/parser/contents/classes/TwoColumnBrowseResults.js b/lib/parser/contents/classes/TwoColumnBrowseResults.js index f50cbb98..29e14eb7 100644 --- a/lib/parser/contents/classes/TwoColumnBrowseResults.js +++ b/lib/parser/contents/classes/TwoColumnBrowseResults.js @@ -5,18 +5,9 @@ const Parser = require('..'); class TwoColumnBrowseResults { type = 'twoColumnBrowseResultsRenderer'; - #data; - constructor(data) { - this.#data = data; - } - - get tabs() { - return Parser.parse(this.#data.tabs); - } - - get secondary_contents() { - return Parser.parse(this.#data.secondaryContents); + this.tabs = Parser.parse(data.tabs); + this.secondary_contents = Parser.parse(data.secondaryContents); } } diff --git a/lib/parser/contents/classes/TwoColumnSearchResults.js b/lib/parser/contents/classes/TwoColumnSearchResults.js index 4756bf1a..cf7b9e96 100644 --- a/lib/parser/contents/classes/TwoColumnSearchResults.js +++ b/lib/parser/contents/classes/TwoColumnSearchResults.js @@ -5,18 +5,9 @@ const Parser = require('..'); class TwoColumnSearchResults { type = 'twoColumnSearchResultsRenderer'; - #data; - constructor(data) { - this.#data = data; - } - - get primary_contents() { - return Parser.parse(this.#data.primaryContents); - } - - get secondary_contents() { - return Parser.parse(this.#data.secondaryContents); + this.primary_contents = Parser.parse(data.primaryContents); + this.secondary_contents = Parser.parse(data.secondaryContents); } } diff --git a/lib/parser/contents/classes/TwoColumnWatchNextResults.js b/lib/parser/contents/classes/TwoColumnWatchNextResults.js index 84dc5ee7..3b916c01 100644 --- a/lib/parser/contents/classes/TwoColumnWatchNextResults.js +++ b/lib/parser/contents/classes/TwoColumnWatchNextResults.js @@ -5,22 +5,10 @@ const Parser = require('..'); class TwoColumnWatchNextResults { type = 'twoColumnWatchNextResults'; - #data; - constructor(data) { - this.#data = data; - } - - get results() { - return Parser.parse(this.#data.results.results.contents); - } - - get secondary_results() { - return Parser.parse(this.#data.secondaryResults?.secondaryResults.results); - } - - get conversation_bar() { - return Parser.parse(this.#data?.conversationBar); + this.results = Parser.parse(data.results?.results.contents); + this.secondary_results = Parser.parse(data.secondaryResults?.secondaryResults.results); + this.conversation_bar = Parser.parse(data?.conversationBar); } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index f504e4b4..66f7db04 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -82,7 +82,7 @@ class Parser { return formats?.map((format) => new Format(format)) || []; } - static parse(data, ctx) { + static parse(data) { if (!data) return null; @@ -97,7 +97,7 @@ class Parser { try { const TargetClass = require('./classes/' + classname); - results.push(new TargetClass(item[keys[0]], ctx)); + results.push(new TargetClass(item[keys[0]])); } catch (err) { this.formatError({ classname, classdata: item[keys[0]], err }); return null; @@ -109,11 +109,11 @@ class Parser { const keys = Object.keys(data); const classname = this.sanitizeClassName(keys[0]); - if (this.shouldIgnore(classname))return; + if (this.shouldIgnore(classname)) return; try { const TargetClass = require('./classes/' + classname); - return new TargetClass(data[keys[0]], ctx); + return new TargetClass(data[keys[0]]); } catch (err) { this.formatError({ classname, classdata: data[keys[0]], err }); return null; diff --git a/lib/parser/youtube/Search.js b/lib/parser/youtube/Search.js index cf4c82ef..0dc956ac 100644 --- a/lib/parser/youtube/Search.js +++ b/lib/parser/youtube/Search.js @@ -1,7 +1,8 @@ 'use strict'; const Parser = require('../contents'); - +const { InnertubeError } = require('../../utils/Utils'); + /** @namespace */ class Search { #page; @@ -11,36 +12,78 @@ class Search { /** * @param {object} response - API response. * @param {import('../../core/Actions')} actions - * @param {boolean} is_continuation + * @param {object} [args] + * @param {boolean} [args.is_continuation] * @constructor */ - constructor(response, actions, is_continuation) { + constructor(response, actions, args = {}) { this.#actions = actions; - this.#page = is_continuation + this.#page = args.is_continuation && response || Parser.parseResponse(response.data); - this.estimated_results = this.#page.estimated_results; - this.refinements = this.#page.refinements || []; - - const contents = is_continuation - && this.#page.on_response_received_commands[0].continuation_items - || this.#page.contents.primary_contents.contents; + const contents = this.#page.contents?.primary_contents.contents + || this.#page.on_response_received_commands[0].continuation_items; this.results = contents.get({ type: 'itemSectionRenderer' }).contents; + + const shelves = this.results.findAll({ type: 'shelfRenderer' }, true); + const card_list = this.results.get({ type: 'horizontalCardListRenderer' }, true); + + this.refinements = this.#page.refinements || []; + this.estimated_results = this.#page.estimated_results; + + this.sections = shelves.map((shelf) => ({ + title: shelf.title.toString(), + items: shelf.content.items + })); + + this.refinement_cards = { + header: card_list?.header || null, + cards: card_list?.cards || [] + }; + this.#continuation = contents.get({ type: 'continuationItemRenderer' }); } async getContinuation() { const response = await this.#continuation.endpoint.call(this.#actions); - return new Search(response, this.#actions, true); + return new Search(response, this.#actions, { is_continuation: true }); + } + + /** + * Applies given refinement card and returns a new {@link Search} object. + * @param {SearchRefinementCard || string} refinement card object or query + * @returns {Search} + */ + async selectRefinementCard(card) { + let target_card; + + if (typeof card === 'string') { + target_card = this.refinement_cards.cards.get({ query: card }); + if (!target_card) + throw new InnertubeError('Refinement card not found!', + { available_cards: this.refinement_card_query_list } + ); + } else if (card.type === 'searchRefinementCardRenderer') { + target_card = card; + } else { + throw new InnertubeError('Invalid refinement card!'); + } + + const page = await target_card.endpoint.call(this.#actions); + return new Search(page, this.#actions, { is_continuation: true }); } get has_continuation() { return !!this.#continuation; } + get refinement_card_queries() { + return this.refinement_cards.cards.map((card) => card.query); + } + get videos() { return this.results.findAll({ type: 'videoRenderer' }); } diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index 0ded9935..78d9bc2a 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -44,7 +44,7 @@ function findNode(obj, key, target, depth, safe = true) { * Creates a trap to intercept property access * and add utilities to an object. * - * @param {*} obj + * @param {object} obj * @returns */ function observe(obj) { @@ -55,11 +55,17 @@ function observe(obj) { * Returns the first object to match the rule. * @name get * @param {object} rule + * @param {boolean} del_item */ - return (rule) => target - .find((obj) => { - const rule_keys = Reflect.ownKeys(rule); - return rule_keys.some((key) => obj[key] === rule[key]); + return (rule, del_item) => target + .find((obj, index) => { + const match = deepCompare(rule, obj); + + if (match && del_item) { + target.splice(index, 1); + } + + return match; }); } @@ -68,21 +74,53 @@ function observe(obj) { * Returns all objects that match the rule. * @name findAll * @param {object} rule + * @param {boolean} del_items */ - return (rule) => target - .filter((obj) => { - const rule_keys = Reflect.ownKeys(rule); - return rule_keys.some((key) => obj[key] === rule[key]); + return (rule, del_items) => target + .filter((obj, index) => { + const match = deepCompare(rule, obj); + + if (match && del_items) { + target.splice(index, 1); + } + + return match; }); } - + + if (prop == 'remove') { + /** + * Removes the item at the given index. + * @name remove + * @param {number} index + */ + return (index) => target.splice(index, 1); + } + return Reflect.get(...arguments); } }); } -function escapeStringRegexp(string) { - return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'); + +/** + * Compares given objects. May not work correctly for + * objects with methods. + * + * @param {object} obj1 + * @param {object} obj2 + * @returns {boolean} + */ +function deepCompare(obj1, obj2) { + const keys = Reflect.ownKeys(obj1); + + return keys.some((key) => { + if (typeof obj1[key] == 'object') { + return JSON.stringify(obj1[key]) === JSON.stringify(obj2[key]); + } else { + return obj1[key] === obj2[key]; + } + }); } /** @@ -100,6 +138,10 @@ function getStringBetweenStrings(data, start_string, end_string) { return match ? match[1] : undefined; } +function escapeStringRegexp(string) { + return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'); +} + /** * Returns a random user agent. *