feat: add full support for refinement cards

This commit is contained in:
LuanRT
2022-06-13 04:20:49 -03:00
parent 797c545b80
commit b6a898f733
10 changed files with 134 additions and 72 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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' });
}

View File

@@ -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.
*