mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-07-01 02:15:42 +00:00
feat: add full support for refinement cards
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user