From 4c7a42d8d4c0fea76ce29bf64d7bd4255a9c207a Mon Sep 17 00:00:00 2001 From: LuanRT Date: Sat, 18 Jun 2022 05:16:21 -0300 Subject: [PATCH] fix: search continuations should return a `Search` class Why? To keep things consistent. --- lib/Innertube.js | 105 ++++----- lib/core/AccountManager.js | 11 - lib/core/Feed.js | 292 ++++++++++++++------------ lib/core/Music.js | 9 +- lib/parser/contents/classes/Button.js | 13 +- lib/parser/contents/index.js | 10 +- lib/parser/youtube/History.js | 4 +- lib/parser/youtube/Library.js | 4 +- lib/parser/youtube/Search.js | 16 +- 9 files changed, 244 insertions(+), 220 deletions(-) diff --git a/lib/Innertube.js b/lib/Innertube.js index 8ab7e93e..ee6e7bbd 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -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} - */ - 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.} + */ + 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.} + */ + 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} + */ + 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); } diff --git a/lib/core/AccountManager.js b/lib/core/AccountManager.js index d5475ea0..37f46135 100644 --- a/lib/core/AccountManager.js +++ b/lib/core/AccountManager.js @@ -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.} - */ - async getLibrary() { - const response = await this.#actions.browse('FElibrary'); - return new Library(response.data, this.#actions); - } } module.exports = AccountManager; \ No newline at end of file diff --git a/lib/core/Feed.js b/lib/core/Feed.js index 6a771d85..e36a6522 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -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} memo - * @returns {Array} - */ - 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} memo - * @returns {Array} - */ - 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} - */ - 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} - */ - 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} memo + * @returns {Array} + */ + 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} memo + * @returns {Array} + */ + 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} + */ + 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} + */ + 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; \ No newline at end of file diff --git a/lib/core/Music.js b/lib/core/Music.js index f4cc54b8..7740107d 100644 --- a/lib/core/Music.js +++ b/lib/core/Music.js @@ -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() || '' } diff --git a/lib/parser/contents/classes/Button.js b/lib/parser/contents/classes/Button.js index 56bfdd22..ed914bd2 100644 --- a/lib/parser/contents/classes/Button.js +++ b/lib/parser/contents/classes/Button.js @@ -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); } } diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index e9489f9f..f32881f9 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -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), diff --git a/lib/parser/youtube/History.js b/lib/parser/youtube/History.js index 6b84930b..220b3f53 100644 --- a/lib/parser/youtube/History.js +++ b/lib/parser/youtube/History.js @@ -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 }))); } diff --git a/lib/parser/youtube/Library.js b/lib/parser/youtube/Library.js index 6f29a7e9..7df36177 100644 --- a/lib/parser/youtube/Library.js +++ b/lib/parser/youtube/Library.js @@ -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) }))); } diff --git a/lib/parser/youtube/Search.js b/lib/parser/youtube/Search.js index 34539771..43e00046 100644 --- a/lib/parser/youtube/Search.js +++ b/lib/parser/youtube/Search.js @@ -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.} @@ -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.} + */ + async getContinuation() { + const continuation = await this.getContinuationData(); + return new Search(this.actions, continuation, true); + } } module.exports = Search; \ No newline at end of file