fix: search continuations should return a Search class

Why? To keep things consistent.
This commit is contained in:
LuanRT
2022-06-18 05:16:21 -03:00
parent 1d2c1ed69b
commit 4c7a42d8d4
9 changed files with 244 additions and 220 deletions

View File

@@ -1,8 +1,5 @@
'use strict';
const Parser = require('./parser');
const EventEmitter = require('events');
const OAuth = require('./core/OAuth');
const Actions = require('./core/Actions');
const Livechat = require('./core/Livechat');
@@ -10,19 +7,27 @@ const SessionBuilder = require('./core/SessionBuilder');
const AccountManager = require('./core/AccountManager');
const PlaylistManager = require('./core/PlaylistManager');
const InteractionManager = require('./core/InteractionManager');
const YTMusic = require('./core/Music');
const VideoInfo = require('./parser/youtube/VideoInfo');
const Search = require('./parser/youtube/Search');
const Utils = require('./utils/Utils');
const Request = require('./utils/Request');
const Proto = require('./proto');
const Search = require('./parser/youtube/Search');
const VideoInfo = require('./parser/youtube/VideoInfo');
const Channel = require('./parser/youtube/Channel');
const Playlist = require('./parser/youtube/Playlist');
const Library = require('./parser/youtube/Library');
const History = require('./parser/youtube/History');
const YTMusic = require('./core/Music');
const FilterableFeed = require('./core/FilterableFeed');
const TabbedFeed = require('./core/TabbedFeed');
const Parser = require('./parser/contents');
const OldParser = require('./parser');
const Proto = require('./proto');
const EventEmitter = require('events');
const { PassThrough } = require('stream');
class Innertube {
@@ -159,7 +164,7 @@ class Innertube {
async getBasicInfo(video_id) {
Utils.throwIfMissing({ video_id });
const cpn = Utils.generateRandomString(16);
const response = await this.actions.getVideoInfo(video_id, cpn);
return new VideoInfo([ response, {} ], this.actions, this.#player, cpn);
@@ -197,7 +202,7 @@ class Innertube {
const response = await this.actions.getSearchSuggestions(options.client, query);
if (options.client === 'YTMUSIC' && !response.data.contents) return [];
const suggestions = new Parser(this, response.data, {
const suggestions = new OldParser(this, response.data, {
client: options.client,
data_type: 'SEARCH_SUGGESTIONS'
}).parse();
@@ -220,7 +225,7 @@ class Innertube {
const continuation = await this.actions.next({ video_id });
response.continuation = continuation.data;
const details = new Parser(this, response, {
const details = new OldParser(this, response, {
client: 'YOUTUBE',
data_type: 'VIDEO_INFO'
}).parse();
@@ -257,7 +262,7 @@ class Innertube {
});
const response = await this.actions.next({ ctoken: payload });
const comments = new Parser(this, response.data, {
const comments = new OldParser(this, response.data, {
video_id,
client: 'YOUTUBE',
data_type: 'COMMENTS'
@@ -266,36 +271,6 @@ class Innertube {
return comments;
}
/**
* Retrieves contents for a given channel. (WIP)
*
* @param {string} id - channel id
* @returns {Promise<Channel>}
*/
async getChannel(id) {
Utils.throwIfMissing({ id });
const response = await this.actions.browse(id);
return new Channel(response.data, this.actions);
}
/**
* Retrieves watch history.
*
* @returns {Promise.<{ items: Array.<{ date: string, videos: object[] }>}>}
*/
async getHistory() {
const response = await this.actions.browse('FEhistory');
const history = new Parser(this, response, {
client: 'YOUTUBE',
data_type: 'HISTORY'
}).parse();
return history;
}
/**
* Retrieves YouTube's home feed (aka recommendations).
*
@@ -303,10 +278,29 @@ class Innertube {
*/
async getHomeFeed() {
const response = await this.actions.browse('FEwhat_to_watch');
if (!response.success) throw new Utils.InnertubeError('Could not retrieve home feed', response);
return new FilterableFeed(this.actions, response.data);
}
/**
* Returns the account's library.
*
* @returns {Promise.<Library>}
*/
async getLibrary() {
const response = await this.actions.browse('FElibrary');
return new Library(response.data, this.actions);
}
/**
* Retrieves watch history.
* Which can also be achieved through {@link getLibrary()}.
*
* @returns {Promise.<History>}
*/
async getHistory() {
const response = await this.actions.browse('FEhistory');
return new History(Parser.parseResponse(response.data), this.actions);
}
/**
* Retrieves trending content.
@@ -315,8 +309,6 @@ class Innertube {
*/
async getTrending() {
const response = await this.actions.browse('FEtrending');
if (!response.success) throw new Utils.InnertubeError('Could not retrieve trending content', response);
return new TabbedFeed(this.actions, response.data);
}
@@ -328,14 +320,26 @@ class Innertube {
async getSubscriptionsFeed() {
const response = await this.actions.browse('FEsubscriptions');
const subsfeed = new Parser(this, response, {
const subsfeed = new OldParser(this, response, {
client: 'YOUTUBE',
data_type: 'SUBSFEED'
}).parse();
return subsfeed;
}
/**
* Retrieves contents for a given channel. (WIP)
*
* @param {string} id - channel id
* @returns {Promise<Channel>}
*/
async getChannel(id) {
Utils.throwIfMissing({ id });
const response = await this.actions.browse(id);
return new Channel(response.data, this.actions);
}
/**
* Retrieves notifications.
*
@@ -344,7 +348,7 @@ class Innertube {
async getNotifications() {
const response = await this.actions.notifications('get_notification_menu');
const notifications = new Parser(this, response.data, {
const notifications = new OldParser(this, response.data, {
client: 'YOUTUBE',
data_type: 'NOTIFICATIONS'
}).parse();
@@ -374,10 +378,7 @@ class Innertube {
*/
async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) {
Utils.throwIfMissing({ playlist_id });
const response = await this.actions.browse(`VL${playlist_id}`, { client: options.client });
if (!response.success) throw new Utils.InnertubeError('Could not get playlist', response);
return new Playlist(this.actions, response.data);
}

View File

@@ -2,7 +2,6 @@
const Utils = require('../utils/Utils');
const Constants = require('../utils/Constants');
const Library = require('../parser/youtube/Library');
const Analytics = require('../parser/youtube/Analytics');
const Proto = require('../proto');
@@ -210,16 +209,6 @@ class AccountManager {
return new Analytics(response.data);
}
/**
* Returns the account's library.
*
* @returns {Promise.<Library>}
*/
async getLibrary() {
const response = await this.#actions.browse('FElibrary');
return new Library(response.data, this.#actions);
}
}
module.exports = AccountManager;

View File

@@ -1,153 +1,167 @@
'use strict';
const ResultsParser = require('../parser/contents');
const { InnertubeError } = require('../utils/Utils');
// TODO: add a way subdivide into sections and return subfeeds?
class Feed {
#page;
/**
* @type {import('../parser/contents/classes/ContinuationItem')[]}
*/
#continuation;
/**
* @type {import('../core/Actions')}
*/
#actions;
memo;
constructor(actions, data, already_parsed = false) {
if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed)
this.#page = data;
else
this.#page = ResultsParser.parseResponse(data);
this.memo =
this.#page.on_response_received_actions ?
this.#page.on_response_received_actions_memo :
this.#page.on_response_received_endpoints ?
this.#page.on_response_received_endpoints_memo :
this.#page.contents_memo;
this.#actions = actions;
}
/**
* Get the original page data
*/
get page() {
return this.#page;
}
get actions() {
return this.#actions;
}
/**
* Get all videos on a given page via memo
*
* @param {Map<string, any[]>} memo
* @returns {Array<import('../parser/contents/classes/Video') | import('../parser/contents/classes/GridVideo') | import('../parser/contents/classes/CompactVideo') | import('../parser/contents/classes/PlaylistVideo') | import('../parser/contents/classes/PlaylistPanelVideo') | import('../parser/contents/classes/WatchCardCompactVideo')>}
*/
static getVideosFromMemo(memo) {
const videos = memo.get('Video') || [];
const grid_videos = memo.get('GridVideo') || [];
const compact_videos = memo.get('CompactVideo') || [];
const playlist_videos = memo.get('PlaylistVideo') || [];
const playlist_panel_videos = memo.get('PlaylistPanelVideo') || [];
const watch_card_compact_videos = memo.get('WatchCardCompactVideo') || [];
return [...videos, ...grid_videos, ...compact_videos, ...playlist_videos, ...playlist_panel_videos, ...watch_card_compact_videos];
}
/**
* Get all playlists on a given page via memo
*
* @param {Map<string, any[]>} memo
* @returns {Array<import('../parser/contents/classes/Playlist') | import('../parser/contents/classes/GridPlaylist')>}
*/
static getPlaylistsFromMemo(memo) {
const playlists = memo.get('Playlist') || [];
const grid_playlists = memo.get('GridPlaylist') || [];
return [...playlists, ...grid_playlists];
}
/**
* Get all the videos in the feed
*/
get videos() {
return Feed.getVideosFromMemo(this.memo);
}
/**
* Get all playlists in the feed
*
* @returns {Array<import('../parser/contents/classes/Playlist') | import('../parser/contents/classes/GridPlaylist')>}
*/
get playlists() {
return Feed.getPlaylistsFromPage(this.memo);
}
/**
* Get all the community posts in the feed
*
* @returns {import('../parser/contents/classes/BackstagePost')[]}
*/
get backstage_posts() {
return this.memo.get('BackstagePost');
}
/**
* Get all the channels in the feed
*
* @returns {Array<import('../parser/contents/Channel') | import('../parser/contents/GridChannel')>}
*/
get channels() {
const channels = this.memo.get('Channel') || [];
const grid_channels = this.memo.get('GridChannel') || [];
return [...channels, ...grid_channels];
}
get has_continuation() {
return (this.memo.get('ContinuationItem') || []).length > 0;
}
async getContinuationData() {
if (this.#continuation) {
if (this.#continuation.length > 1)
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
if (this.#continuation.length === 0)
throw new InnertubeError('There are no continuations');
const continuation = this.#continuation[0];
return await continuation.endpoint.call(this.#actions);
}
this.#continuation = this.memo.get('ContinuationItem');
if (this.#continuation)
return this.getContinuationData();
return null;
}
get shelves() {
return this.#page.contents_memo.get('Shelf');
}
#page;
getShelf(title) {
return this.shelves.find(shelf => shelf.title.toString() === title);
}
/** @type {import('../parser/contents/classes/ContinuationItem')[]} */
#continuation;
get shelf_content() {
return this.shelves.map(shelf => ({
title: shelf.title.toString(),
content: shelf.content.contents,
}));
/** @type {import('../core/Actions')} */
#actions;
memo;
constructor(actions, data, already_parsed = false) {
if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) {
this.#page = data;
} else {
this.#page = ResultsParser.parseResponse(data);
}
async getContinuation() {
const continuation_data = await this.getContinuationData();
this.memo =
this.#page.on_response_received_commands ?
this.#page.on_response_received_commands_memo:
this.#page.on_response_received_actions ?
this.#page.on_response_received_actions_memo:
this.#page.on_response_received_endpoints ?
this.#page.on_response_received_endpoints_memo:
this.#page.contents_memo;
return new Feed(this.actions, continuation_data, true);
this.#actions = actions;
}
/**
* Get the original page data
*/
get page() {
return this.#page;
}
get actions() {
return this.#actions;
}
/**
* Get all videos on a given page via memo
*
* @param {Map<string, any[]>} memo
* @returns {Array<import('../parser/contents/classes/Video') | import('../parser/contents/classes/GridVideo') | import('../parser/contents/classes/CompactVideo') | import('../parser/contents/classes/PlaylistVideo') | import('../parser/contents/classes/PlaylistPanelVideo') | import('../parser/contents/classes/WatchCardCompactVideo')>}
*/
static getVideosFromMemo(memo) {
const videos = memo.get('Video') || [];
const grid_videos = memo.get('GridVideo') || [];
const compact_videos = memo.get('CompactVideo') || [];
const playlist_videos = memo.get('PlaylistVideo') || [];
const playlist_panel_videos = memo.get('PlaylistPanelVideo') || [];
const watch_card_compact_videos = memo.get('WatchCardCompactVideo') || [];
return [
...videos,
...grid_videos,
...compact_videos,
...playlist_videos,
...playlist_panel_videos,
...watch_card_compact_videos
];
}
/**
* Get all playlists on a given page via memo
*
* @param {Map<string, any[]>} memo
* @returns {Array<import('../parser/contents/classes/Playlist') | import('../parser/contents/classes/GridPlaylist')>}
*/
static getPlaylistsFromMemo(memo) {
const playlists = memo.get('Playlist') || [];
const grid_playlists = memo.get('GridPlaylist') || [];
return [...playlists, ...grid_playlists];
}
/**
* Get all the videos in the feed
*/
get videos() {
return Feed.getVideosFromMemo(this.memo);
}
/**
* Get all playlists in the feed
*
* @returns {Array<import('../parser/contents/classes/Playlist') | import('../parser/contents/classes/GridPlaylist')>}
*/
get playlists() {
return Feed.getPlaylistsFromPage(this.memo);
}
/**
* Get all the community posts in the feed
*
* @returns {import('../parser/contents/classes/BackstagePost')[]}
*/
get backstage_posts() {
return this.memo.get('BackstagePost');
}
/**
* Get all the channels in the feed
*
* @returns {Array<import('../parser/contents/Channel') | import('../parser/contents/GridChannel')>}
*/
get channels() {
const channels = this.memo.get('Channel') || [];
const grid_channels = this.memo.get('GridChannel') || [];
return [...channels, ...grid_channels];
}
get has_continuation() {
return (this.memo.get('ContinuationItem') || []).length > 0;
}
async getContinuationData() {
if (this.#continuation) {
if (this.#continuation.length > 1)
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
if (this.#continuation.length === 0)
throw new InnertubeError('There are no continuations');
const response = await this.#continuation[0].endpoint.call(this.#actions);
return response;
}
this.#continuation = this.memo.get('ContinuationItem');
if (this.#continuation)
return this.getContinuationData();
return null;
}
get shelves() {
return this.#page.contents_memo.get('Shelf');
}
getShelf(title) {
return this.shelves.find(shelf => shelf.title.toString() === title);
}
get shelf_content() {
return this.shelves.map(shelf => ({
title: shelf.title.toString(),
content: shelf.content.contents,
}));
}
async getContinuation() {
const continuation_data = await this.getContinuationData();
return new Feed(this.actions, continuation_data, true);
}
}
module.exports = Feed;
module.exports = Feed;

View File

@@ -113,7 +113,7 @@ class Music {
/** @type {boolean} */
is_editable: upnext_content.is_editable,
/** @type {import('../parser/contents/classes/PlaylistPanelVideo')[]} */
items: observe(upnext_content.contents)
contents: observe(upnext_content.contents)
}
}
@@ -135,11 +135,8 @@ class Music {
const info = page.contents.contents.get({ type: 'MusicDescriptionShelf' });
return {
/** @type {Array.<{ header: import('../parser/contents/classes/MusicCarouselShelfBasicHeader'), items: object[] }>} */
sections: observe(shelves.map((shelf) => ({
header: shelf.header,
items: shelf.contents
}))),
/** @type {import('../parser/contents/classes/MusicCarouselShelf')[]} */
sections: shelves,
/** @type {string} */
info: info?.description.toString() || ''
}

View File

@@ -8,9 +8,16 @@ class Button {
constructor(data) {
this.text = new Text(data.text).toString();
this.label = data.accessibility?.label || null;
this.tooltip = data.tooltip || null;
this.icon_type = data.icon?.iconType || null;
data.accessibility?.label &&
(this.label = data.accessibility?.label);
data.tooltip &&
(this.tooltip = data.tooltip);
data.icon?.iconType &&
(this.icon_type = data.icon?.iconType);
this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint);
}
}

View File

@@ -61,7 +61,12 @@ class Parser {
const on_response_received_endpoints = data.onResponseReceivedEndpoints && Parser.parseRR(data.onResponseReceivedEndpoints) || null;
const on_response_received_endpoints_memo = Parser.#memo;
this.#clearMemo();
this.#createMemo();
const on_response_received_commands = data.onResponseReceivedCommands && Parser.parseRR(data.onResponseReceivedCommands) || null;
const on_response_received_commands_memo = Parser.#memo;
this.#clearMemo();
return {
contents,
contents_memo,
@@ -69,7 +74,8 @@ class Parser {
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,
on_response_received_commands,
on_response_received_commands_memo,
/** @type {*} */
continuation_contents: data.continuationContents && Parser.parseLC(data.continuationContents) || null,
metadata: Parser.parse(data.metadata),

View File

@@ -29,8 +29,8 @@ class History {
this.feed_actions = secondary_contents?.contents || null;
this.sections = observe(contents.map((section) => ({
title: section.header.title,
items: section.contents
header: section.header,
contents: section.contents
})));
}

View File

@@ -26,9 +26,9 @@ class Library {
this.profile = { stats, user_info };
this.sections = observe(shelves.map((shelf) => ({
title: shelf.title.toString(),
items: shelf.content.items,
type: shelf.icon_type,
title: shelf.title.toString(),
contents: shelf.content.items,
getAll: () => this.#getAll(shelf)
})));
}

View File

@@ -41,11 +41,11 @@ class Search extends Feed {
header: card_list?.header || null,
/** @type {import('../contents/classes/SearchRefinementCard')} */
cards: card_list?.cards || []
};
}
}
/**
* Applies given refinement card and returns a new {@link Feed} object.
* Applies given refinement card and returns a new {@link Search} object.
*
* @param {import('../contents/classes/SearchRefinementCard') | string} card - refinement card object or query
* @returns {Promise.<Feed>}
@@ -66,13 +66,23 @@ class Search extends Feed {
}
const page = await target_card.endpoint.call(this.actions);
return new Feed(this.actions, page, true);
return new Search(this.actions, page, true);
}
/** @type {string[]} */
get refinement_card_queries() {
return this.refinement_cards.cards.map((card) => card.query);
}
/**
* Retrieves next batch of results.
*
* @returns {Promise.<Search>}
*/
async getContinuation() {
const continuation = await this.getContinuationData();
return new Search(this.actions, continuation, true);
}
}
module.exports = Search;