From 748e34758faf99e8b94f65a896a43e8945223ee2 Mon Sep 17 00:00:00 2001 From: LuanRT Date: Mon, 20 Jun 2022 03:02:42 -0300 Subject: [PATCH] feat: tidy things up and implement more renderers - Finished Library parser - Fixed search continuations - Improved channel parser - Improved playlist parser - Added support for posts of type poll - Improved History parser - Removed redundant code --- lib/Innertube.js | 15 +- lib/core/Feed.js | 162 ++++++++++++------ lib/core/FilterableFeed.js | 98 ++++++----- lib/core/TabbedFeed.js | 22 +-- lib/parser/contents/classes/Author.js | 70 +++----- lib/parser/contents/classes/BackstagePost.js | 17 +- lib/parser/contents/classes/C4TabbedHeader.js | 2 + .../classes/ChannelAboutFullMetadata.js | 9 +- .../classes/ChannelFeaturedContent.js | 15 ++ .../contents/classes/ChannelHeaderLinks.js | 6 +- lib/parser/contents/classes/EndScreenVideo.js | 3 +- .../contents/classes/ExpandedShelfContents.js | 6 +- lib/parser/contents/classes/Grid.js | 6 +- lib/parser/contents/classes/GridChannel.js | 6 +- lib/parser/contents/classes/GridPlaylist.js | 6 +- lib/parser/contents/classes/HorizontalList.js | 6 +- lib/parser/contents/classes/ItemSection.js | 4 +- lib/parser/contents/classes/Menu.js | 6 +- .../contents/classes/MerchandiseShelf.js | 6 +- lib/parser/contents/classes/Movie.js | 39 +++++ .../contents/classes/MovingThumbnail.js | 8 +- .../contents/classes/NavigationEndpoint.js | 6 + .../contents/classes/PlayerOverlayAutoplay.js | 2 +- lib/parser/contents/classes/Playlist.js | 8 +- .../contents/classes/PlaylistMetadata.js | 4 +- .../contents/classes/PlaylistSidebar.js | 6 +- .../classes/PlaylistSidebarPrimaryInfo.js | 2 + .../classes/PlaylistSidebarSecondaryInfo.js | 6 +- lib/parser/contents/classes/PlaylistVideo.js | 5 +- lib/parser/contents/classes/Poll.js | 27 +++ lib/parser/contents/classes/Post.js | 13 ++ lib/parser/contents/classes/ProfileColumn.js | 6 +- .../contents/classes/ProfileColumnStats.js | 6 +- lib/parser/contents/classes/ReelShelf.js | 6 +- lib/parser/contents/classes/RichItem.js | 2 +- lib/parser/contents/classes/RichSection.js | 7 +- lib/parser/contents/classes/RichShelf.js | 2 + lib/parser/contents/classes/SectionList.js | 7 +- .../classes/ThumbnailOverlayPlaybackStatus.js | 13 ++ lib/parser/contents/classes/ToggleButton.js | 18 +- lib/parser/contents/classes/VerticalList.js | 6 +- lib/parser/contents/classes/Video.js | 1 + lib/parser/contents/classes/VideoOwner.js | 2 +- lib/parser/contents/index.js | 6 +- lib/parser/youtube/Channel.js | 89 ++++++---- lib/parser/youtube/History.js | 59 +++---- lib/parser/youtube/Library.js | 19 +- lib/parser/youtube/Playlist.js | 71 ++++---- lib/parser/youtube/Search.js | 2 +- lib/parser/youtube/VideoInfo.js | 4 +- lib/utils/Utils.js | 6 +- 51 files changed, 567 insertions(+), 356 deletions(-) create mode 100644 lib/parser/contents/classes/ChannelFeaturedContent.js create mode 100644 lib/parser/contents/classes/Movie.js create mode 100644 lib/parser/contents/classes/Poll.js create mode 100644 lib/parser/contents/classes/Post.js create mode 100644 lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js diff --git a/lib/Innertube.js b/lib/Innertube.js index 441fa053..4b9cf1ee 100644 --- a/lib/Innertube.js +++ b/lib/Innertube.js @@ -22,7 +22,6 @@ 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'); @@ -298,7 +297,7 @@ class Innertube { */ async getHistory() { const response = await this.actions.browse('FEhistory'); - return new History(Parser.parseResponse(response.data), this.actions); + return new History(this.actions, response.data); } /** @@ -336,7 +335,7 @@ class Innertube { async getChannel(id) { Utils.throwIfMissing({ id }); const response = await this.actions.browse(id); - return new Channel(response.data, this.actions); + return new Channel(this.actions, response.data); } /** @@ -369,15 +368,11 @@ class Innertube { * Retrieves the contents of a given playlist. * * @param {string} playlist_id - the id of the playlist. - * @param {object} options - `YOUTUBE` | `YTMUSIC` - * @param {string} options.client - client used to parse the playlist, can be: `YTMUSIC` | `YOUTUBE` - * @returns {Promise.< - * { title: string, description: string, total_items: string, last_updated: string, views: string, items: object[] } | - * { title: string, description: string, total_items: number, duration: string, year: string, items: object[] }>} + * @returns {Promise.} */ - async getPlaylist(playlist_id, options = { client: 'YOUTUBE' }) { + async getPlaylist(playlist_id) { Utils.throwIfMissing({ playlist_id }); - const response = await this.actions.browse(`VL${playlist_id}`, { client: options.client }); + const response = await this.actions.browse(`VL${playlist_id.replace(/VL/g, '')}`); return new Playlist(this.actions, response.data); } diff --git a/lib/core/Feed.js b/lib/core/Feed.js index e36a6522..edea1474 100644 --- a/lib/core/Feed.js +++ b/lib/core/Feed.js @@ -13,8 +13,8 @@ class Feed { /** @type {import('../core/Actions')} */ #actions; - - memo; + + #memo; constructor(actions, data, already_parsed = false) { if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) { @@ -23,30 +23,23 @@ class Feed { this.#page = ResultsParser.parseResponse(data); } - this.memo = + 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; - + + this.#page.contents ? + this.#page.contents_memo: + + this.#page.on_response_received_actions ? + this.#page.on_response_received_actions_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 * @@ -87,25 +80,16 @@ class Feed { * 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); + return Feed.getVideosFromMemo(this.#memo); } /** * Get all the community posts in the feed * - * @returns {import('../parser/contents/classes/BackstagePost')[]} + * @returns {import('../parser/contents/classes/BackstagePost')[] | import('../parser/contents/classes/Post')[]} */ - get backstage_posts() { - return this.memo.get('BackstagePost'); + get posts() { + return this.#memo.get('BackstagePost') || this.#memo.get('Post') || []; } /** @@ -114,15 +98,93 @@ class Feed { * @returns {Array} */ get channels() { - const channels = this.memo.get('Channel') || []; - const grid_channels = this.memo.get('GridChannel') || []; + 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; + + /** + * Get all playlists in the feed + * + * @returns {Array} + */ + get playlists() { + return Feed.getPlaylistsFromMemo(this.#memo); } - + + get memo() { + return this.#memo; + } + + /** + * Returns contents from the page. + * + * @returns {*} + */ + get contents() { + const tab_content = this.#memo.get('Tab')?.[0]?.content; + const reload_continuation_items = this.#memo.get('reloadContinuationItemsCommand')?.[0]; + const append_continuation_items = this.#memo.get('appendContinuationItemsAction')?.[0]; + + return tab_content || reload_continuation_items || append_continuation_items; + } + + /** + * Returns all segments/sections from the page. + * + * @returns {import('../parser/contents/Shelf')[] | import('../parser/contents/RichShelf')[] | import('../parser/contents/ReelShelf')[]} + */ + get shelves() { + const shelf = this.#page.contents_memo.get('Shelf') || []; + const rich_shelf = this.#page.contents_memo.get('RichShelf') || []; + const reel_shelf = this.#page.contents_memo.get('ReelShelf') || []; + + return [ ...shelf, ...rich_shelf, ...reel_shelf ]; + } + + /** + * Finds shelf by title. + * + * @param {string} title + */ + getShelf(title) { + return this.shelves.find(shelf => shelf.title.toString() === title); + } + + /** + * Returns secondary contents from the page. + * + * @returns {*} + */ + get secondary_contents() { + return this.page.contents?.secondary_contents; + } + + get actions() { + return this.#actions; + } + + /** + * Get the original page data + */ + get page() { + return this.#page; + } + + /** + * Checks if the feed has continuation. + * + * @returns {boolean} + */ + get has_continuation() { + return (this.#memo.get('ContinuationItem') || []).length > 0; + } + + /** + * Retrieves continuation data as it is. + * + * @returns {Promise.} + */ async getContinuationData() { if (this.#continuation) { if (this.#continuation.length > 1) @@ -135,29 +197,19 @@ class Feed { return response; } - this.#continuation = this.memo.get('ContinuationItem'); + 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, - })); - } - + + /** + * Retrieves next batch of contents and returns a new {@link Feed} object. + * + * @returns {Promise.} + */ async getContinuation() { const continuation_data = await this.getContinuationData(); return new Feed(this.actions, continuation_data, true); diff --git a/lib/core/FilterableFeed.js b/lib/core/FilterableFeed.js index fcad2165..3fd0b91d 100644 --- a/lib/core/FilterableFeed.js +++ b/lib/core/FilterableFeed.js @@ -1,49 +1,67 @@ +'use strict'; + const { InnertubeError } = require('../utils/Utils'); const Feed = require('./Feed'); class FilterableFeed extends Feed { - /** - * @type {import('../parser/contents/ChipCloudChip')[]} - */ - #chips; - constructor(actions, data, already_parsed = false) { - super(actions, data, already_parsed) + /** + * @type {import('../parser/contents/ChipCloudChip')[]} + */ + #chips; + + constructor(actions, data, already_parsed = false) { + super(actions, data, already_parsed) + } + + /** + * Get filters for the feed + * + * @returns {import('../parser/contents/ChipCloudChip')[]} + */ + get filter_chips() { + if (this.#chips) return this.#chips || []; + + if (this.memo.get('FeedFilterChipBar')?.length > 1) + throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page'); + if (this.memo.get('FeedFilterChipBar')?.length === 0) + throw new InnertubeError('There are no feed filter chipbars'); + + this.#chips = this.memo.get('ChipCloudChip') || []; + + return this.#chips || []; + } + + get filters() { + return this.filter_chips.map(chip => chip.text.toString()) || []; + } + + /** + * Applies given filter and returns a new {@link Feed} object. + * + * @param {string | import('../parser/contents/classes/ChipCloudChip')} filter + * @returns {Promise.} + */ + async getFilteredFeed(filter) { + let target_filter; + + if (typeof filter === 'string') { + if (!this.filters.includes(filter)) + throw new InnertubeError('Filter not found', { + available_filters: this.filters + }); + target_filter = this.filter_chips.find(chip => chip.text.toString() === filter); + } else if (filter.type === 'ChipCloudChip') { + target_filter = filter; + } else { + throw new InnertubeError('Invalid filter'); } + + if (target_filter.is_selected) return this; - /** - * Get filters for the feed - * - * @returns {import('../parser/contents/ChipCloudChip')[]} - */ - get filter_chips() { - if (this.#chips) return this.#chips || []; - - if (this.memo.get('FeedFilterChipBar')?.length > 1) - throw new InnertubeError('There are too many feed filter chipbars, you\'ll need to find the correct one yourself in this.page'); - if (this.memo.get('FeedFilterChipBar')?.length === 0) - throw new InnertubeError('There are no feed filter chipbars'); - - this.#chips = this.memo.get('ChipCloudChip') || []; - - return this.#chips || []; - } - - get filters() { - return this.filter_chips.map(chip => chip.text.toString()) || []; - } - - async getFilteredFeed(name) { - if (!this.filters.includes(name)) - throw new InnertubeError('Invalid filter', { available_filters: this.filters }); - - const filter = this.filter_chips.find(chip => chip.text.toString() === name); - - if (filter.is_selected) return this; - - const response = await filter.endpoint.call(this.actions); - - return new Feed(this.actions, response, true); - } + const response = await target_filter.endpoint.call(this.actions); + + return new Feed(this.actions, response, true); + } } module.exports = FilterableFeed; \ No newline at end of file diff --git a/lib/core/TabbedFeed.js b/lib/core/TabbedFeed.js index c8077ed5..b529cf7d 100644 --- a/lib/core/TabbedFeed.js +++ b/lib/core/TabbedFeed.js @@ -1,28 +1,27 @@ -const ResultsParser = require('../parser/contents'); +'use strict'; + const { InnertubeError } = require('../utils/Utils'); const Feed = require('./Feed'); class TabbedFeed extends Feed { - #page; /** * @type {import('../parser/contents/classes/Tab')[]} */ #tabs; #actions; + constructor (actions, data, already_parsed = false) { super(actions, data, already_parsed); this.#actions = actions; - this.#page = already_parsed ? data : ResultsParser.parseResponse(data); - this.#tabs = this.#page.contents_memo.get('Tab'); + this.#tabs = this.page.contents_memo.get('Tab'); } get tabs() { - return this.#tabs.map(tab => tab.title.toString()); + return this.#tabs.map((tab) => tab.title.toString()); } /** - * - * @param {string} title title of the tab to get + * @param {string} title * @returns {Promise} */ async getTab(title) { @@ -32,16 +31,11 @@ class TabbedFeed extends Feed { if (tab.selected) return this; const response = await tab.endpoint.call(this.#actions); - return new TabbedFeed(this.#actions, response, true); } - + get title() { - return this.#page.contents_memo('Tab')?.find(tab => tab.selected)?.title.toString(); - } - - get page() { - return this.#page; + return this.page.contents_memo('Tab')?.find(tab => tab.selected)?.title.toString(); } } diff --git a/lib/parser/contents/classes/Author.js b/lib/parser/contents/classes/Author.js index 424509c1..7f9879ed 100644 --- a/lib/parser/contents/classes/Author.js +++ b/lib/parser/contents/classes/Author.js @@ -1,62 +1,32 @@ +'use strict'; + const Parser = require('..'); const NavigatableText = require('./NavigatableText'); const Thumbnail = require('./Thumbnail'); +const Constants = require('../../../utils/Constants'); class Author { #nav_text; - /** - * @type {import('./MetadataBadge')[]} - */ - badges; - /** - * @type {Thumbnail[]} - */ - thumbnails; + constructor(item, badges, thumbs) { this.#nav_text = new NavigatableText(item); + + this.id = + this.#nav_text.runs?.[0].endpoint.browse?.id || + this.#nav_text.endpoint?.browse?.id || 'N/A'; + + this.name = this.#nav_text.text || 'N/A'; + this.thumbnails = thumbs && Thumbnail.fromResponse(thumbs) || []; + this.endpoint = this.#nav_text.runs?.[0].endpoint || this.#nav_text.endpoint; this.badges = Array.isArray(badges) ? Parser.parse(badges) : []; - if (thumbs) { - this.thumbnails = Thumbnail.fromResponse(thumbs); - } - else { - this.thumbnails = []; - } - } - - get url() { - return this.#nav_text.endpoint.metadata.url; - } - - get name() { - return this.#nav_text.toString(); - } - - set name(name) { - this.#nav_text.text = name; - } - - get endpoint() { - return this.#nav_text.endpoint; - } - - get id() { - // XXX: maybe confirm that pageType == "WEB_PAGE_TYPE_CHANNEL"? - // TODO: this is outdated - return this.#nav_text.endpoint.browseId; - } - - /** - * @type {boolean} - */ - get is_verified() { - return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED'); - } - - /** - * @type {boolean} - */ - get is_verified_artist() { - return this.badges.some(badge => badge.style === 'BADGE_STYLE_TYPE_VERIFIED_ARTIST'); + this.is_verified = this.badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED') || null; + this.is_verified_artist = this.badges?.some((badge) => badge.style == 'BADGE_STYLE_TYPE_VERIFIED_ARTIST') || null; + + this.url = + this.#nav_text.runs?.[0].endpoint.browse && + `${Constants.URLS.YT_BASE}${this.#nav_text.runs[0].endpoint.browse?.base_url || '/u/' + this.#nav_text.runs[0].endpoint.browse?.id }` || + `${Constants.URLS.YT_BASE}${this.#nav_text.endpoint?.browse?.base_url || '/u/' + this.#nav_text.endpoint?.browse?.id }` || + null; } /** diff --git a/lib/parser/contents/classes/BackstagePost.js b/lib/parser/contents/classes/BackstagePost.js index 3ed92820..dafd6597 100644 --- a/lib/parser/contents/classes/BackstagePost.js +++ b/lib/parser/contents/classes/BackstagePost.js @@ -1,6 +1,9 @@ +'use strict'; + const Parser = require('..'); const Author = require('./Author'); const Text = require('./Text'); +const NavigationEndpoint = require('./NavigationEndpoint'); class BackstagePost { type = 'BackstagePost'; @@ -12,14 +15,16 @@ class BackstagePost { navigationEndpoint: data.authorEndpoint }, null, data.authorThumbnail); this.content = new Text(data.contentText, ''); - this.published_at = new Text(data.publishedTimeText); + this.published = new Text(data.publishedTimeText); + this.poll_status = data.pollStatus; + this.vote_status = data.voteStatus; this.likes = new Text(data.voteCount); + this.menu = Parser.parse(data.actionMenu) || null; this.actions = Parser.parse(data.actionButtons); - this.attachment = data.backstageAttachment ? Parser.parse(data.backstageAttachment) : null; - } - - get endpoint() { - return this.actions.reply.endpoint; + this.vote_button = Parser.parse(data.voteButton); + this.surface = data.surface; + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.attachment = Parser.parse(data.backstageAttachment) || null; } } diff --git a/lib/parser/contents/classes/C4TabbedHeader.js b/lib/parser/contents/classes/C4TabbedHeader.js index f9f3bc5f..fff05e9c 100644 --- a/lib/parser/contents/classes/C4TabbedHeader.js +++ b/lib/parser/contents/classes/C4TabbedHeader.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); const Author = require('./Author'); const Thumbnail = require('./Thumbnail'); diff --git a/lib/parser/contents/classes/ChannelAboutFullMetadata.js b/lib/parser/contents/classes/ChannelAboutFullMetadata.js index 6537a65c..b9228041 100644 --- a/lib/parser/contents/classes/ChannelAboutFullMetadata.js +++ b/lib/parser/contents/classes/ChannelAboutFullMetadata.js @@ -1,20 +1,25 @@ -const Author = require('./Author'); +'use strict'; + +const Thumbnail = require('./Thumbnail'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); +const Parser = require('..'); class ChannelAboutFullMetadata { type = 'ChannelAboutFullMetadata'; constructor(data) { this.id = data.channelId; + this.name = new Text(data.title); + this.avatar = Thumbnail.fromResponse(data.avatar); this.canonical_channel_url = data.canonicalChannelUrl; - this.author = new Author(data.title, null, data.avatar); this.views = new Text(data.viewCountText); this.joined = new Text(data.joinedDateText); this.description = new Text(data.description); this.email_reveal = new NavigationEndpoint(data.onBusinessEmailRevealClickCommand); this.can_reveal_email = !data.signInForBusinessEmail; this.country = new Text(data.country); + this.buttons = Parser.parse(data.actionButtons); } } diff --git a/lib/parser/contents/classes/ChannelFeaturedContent.js b/lib/parser/contents/classes/ChannelFeaturedContent.js new file mode 100644 index 00000000..34b80ea7 --- /dev/null +++ b/lib/parser/contents/classes/ChannelFeaturedContent.js @@ -0,0 +1,15 @@ +'use strict'; + +const Parser = require('..'); +const Text = require('./Text'); + +class ChannelFeaturedContent { + type = 'ChannelFeaturedContent'; + + constructor(data) { + this.title = new Text(data.title); + this.items = Parser.parse(data.items); + } +} + +module.exports = ChannelFeaturedContent; \ No newline at end of file diff --git a/lib/parser/contents/classes/ChannelHeaderLinks.js b/lib/parser/contents/classes/ChannelHeaderLinks.js index 76e857bb..3a0b8fd6 100644 --- a/lib/parser/contents/classes/ChannelHeaderLinks.js +++ b/lib/parser/contents/classes/ChannelHeaderLinks.js @@ -1,3 +1,5 @@ +'use strict'; + const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); const Thumbnail = require('./Thumbnail'); @@ -14,8 +16,8 @@ class ChannelHeaderLinks { type = 'ChannelHeaderLinks'; constructor(data) { - this.primary = data.primaryLinks.map(link => new HeaderLink(link)); - this.secondary = data.secondaryLinks.map(link => new HeaderLink(link)); + this.primary = data.primaryLinks?.map((link) => new HeaderLink(link)) || []; + this.secondary = data.secondaryLinks?.map((link) => new HeaderLink(link)) || []; } } diff --git a/lib/parser/contents/classes/EndScreenVideo.js b/lib/parser/contents/classes/EndScreenVideo.js index 33172031..6cc1cd56 100644 --- a/lib/parser/contents/classes/EndScreenVideo.js +++ b/lib/parser/contents/classes/EndScreenVideo.js @@ -14,9 +14,10 @@ class EndScreenVideo { this.title = new Text(data.title); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); - this.author = new Author({ nav_text: data.shortBylineText, badges: data.ownerBadges || data.badges }); + this.author = new Author(data.shortBylineText, data.ownerBadges); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.short_view_count_text = new Text(data.shortViewCountText); + this.badges = Parser.parse(data.badges); this.duration = { text: new Text(data.lengthText).toString(), seconds: data.lengthInSeconds diff --git a/lib/parser/contents/classes/ExpandedShelfContents.js b/lib/parser/contents/classes/ExpandedShelfContents.js index f24f12c5..fa3be02b 100644 --- a/lib/parser/contents/classes/ExpandedShelfContents.js +++ b/lib/parser/contents/classes/ExpandedShelfContents.js @@ -7,7 +7,11 @@ class ExpandedShelfContents { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/Grid.js b/lib/parser/contents/classes/Grid.js index 2426501b..534cbfbf 100644 --- a/lib/parser/contents/classes/Grid.js +++ b/lib/parser/contents/classes/Grid.js @@ -7,11 +7,15 @@ class Grid { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency this.is_collapsible = data.isCollapsible; this.visible_row_count = data.visibleRowCount; this.target_id = data.targetId; } + + // XXX: alias for consistency + get contents() { + return this.items; + } } module.exports = Grid; \ No newline at end of file diff --git a/lib/parser/contents/classes/GridChannel.js b/lib/parser/contents/classes/GridChannel.js index 33732482..eaf49bf3 100644 --- a/lib/parser/contents/classes/GridChannel.js +++ b/lib/parser/contents/classes/GridChannel.js @@ -1,4 +1,7 @@ +'use strict'; + const Author = require('./Author'); +const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); @@ -12,8 +15,9 @@ class GridChannel { navigationEndpoint: data.navigationEndpoint }, data.ownerBadges, data.thumbnail); this.subscribers = new Text(data.subscriberCountText); - this.videos = new Text(data.videoCountText); + this.video_count = new Text(data.videoCountText); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.subscribe_button = Parser.parse(data.subscribeButton); } } diff --git a/lib/parser/contents/classes/GridPlaylist.js b/lib/parser/contents/classes/GridPlaylist.js index 351dcd7a..888efd07 100644 --- a/lib/parser/contents/classes/GridPlaylist.js +++ b/lib/parser/contents/classes/GridPlaylist.js @@ -13,11 +13,15 @@ class GridPlaylist { constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); - this.author = new PlaylistAuthor({ nav_text: data.shortBylineText }); + + data.shortBylineText && + (this.author = new PlaylistAuthor(data.shortBylineText, data.ownerBadges)); + this.badges = Parser.parse(data.ownerBadges); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.view_playlist = new NavigatableText(data.viewPlaylistText); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); + this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer); this.sidebar_thumbnails = [].concat(...data.sidebarThumbnails?.map((thumbnail) => Thumbnail.fromResponse(thumbnail)) || []) || null; this.video_count = new Text(data.thumbnailText); this.video_count_short_text = new Text(data.videoCountShortText); diff --git a/lib/parser/contents/classes/HorizontalList.js b/lib/parser/contents/classes/HorizontalList.js index 607427ca..3c5a812b 100644 --- a/lib/parser/contents/classes/HorizontalList.js +++ b/lib/parser/contents/classes/HorizontalList.js @@ -6,7 +6,11 @@ class HorizontalList { constructor(data) { this.visible_item_count = data.visibleItemCount; this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/ItemSection.js b/lib/parser/contents/classes/ItemSection.js index f00ee1e8..295474d5 100644 --- a/lib/parser/contents/classes/ItemSection.js +++ b/lib/parser/contents/classes/ItemSection.js @@ -7,8 +7,10 @@ class ItemSection { constructor(data) { this.header = Parser.parse(data.header); - this.target_id = data.targetId || data.sectionIdentifier || null; this.contents = Parser.parse(data.contents); + + data.targetId || data.sectionIdentifier && + (this.target_id = data.targetId ? data.sectionIdentifier : null); } } diff --git a/lib/parser/contents/classes/Menu.js b/lib/parser/contents/classes/Menu.js index b8b8b7f6..5f59a515 100644 --- a/lib/parser/contents/classes/Menu.js +++ b/lib/parser/contents/classes/Menu.js @@ -7,10 +7,14 @@ class Menu { constructor(data) { this.items = Parser.parse(data.items) || []; - this.contents = this.items; // XXX: alias for consistency this.top_level_buttons = Parser.parse(data.topLevelButtons) || []; this.label = data.accessibility?.accessibilityData?.label || null; } + + // XXX: alias for consistency + get contents() { + return this.items; + } } module.exports = Menu; \ No newline at end of file diff --git a/lib/parser/contents/classes/MerchandiseShelf.js b/lib/parser/contents/classes/MerchandiseShelf.js index f6447d5e..db156dcc 100644 --- a/lib/parser/contents/classes/MerchandiseShelf.js +++ b/lib/parser/contents/classes/MerchandiseShelf.js @@ -9,7 +9,11 @@ class MerchandiseShelf { this.title = data.title; this.menu = Parser.parse(data.actionButton); this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/Movie.js b/lib/parser/contents/classes/Movie.js new file mode 100644 index 00000000..fe313706 --- /dev/null +++ b/lib/parser/contents/classes/Movie.js @@ -0,0 +1,39 @@ +'use strict'; + +const Parser = require('..'); +const Author = require('./Author'); +const Thumbnail = require('./Thumbnail'); +const NavigationEndpoint = require('./NavigationEndpoint'); +const Utils = require('../../../utils/Utils'); +const Text = require('./Text'); + +class Movie { + type = 'Movie'; + + constructor(data) { + const overlay_time_status = data.thumbnailOverlays + .find((overlay) => overlay.thumbnailOverlayTimeStatusRenderer) + ?.thumbnailOverlayTimeStatusRenderer.text || 'N/A'; + + this.id = data.videoId; + this.title = new Text(data.title); + this.description_snippet = data.descriptionSnippet ? new Text(data.descriptionSnippet, '') : null; + this.top_metadata_items = new Text(data.topMetadataItems); + this.thumbnails = Thumbnail.fromResponse(data.thumbnail); + this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); + this.author = new Author(data.longBylineText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); + + this.duration = { + text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, + seconds: Utils.timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text) + }; + + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + this.badges = Parser.parse(data.badges); + this.use_vertical_poster = data.useVerticalPoster; + this.show_action_menu = data.showActionMenu; + this.menu = Parser.parse(data.menu); + } +} + +module.exports = Movie; \ No newline at end of file diff --git a/lib/parser/contents/classes/MovingThumbnail.js b/lib/parser/contents/classes/MovingThumbnail.js index bf48a86e..c9ecd5b4 100644 --- a/lib/parser/contents/classes/MovingThumbnail.js +++ b/lib/parser/contents/classes/MovingThumbnail.js @@ -5,14 +5,8 @@ const Thumbnail = require('./Thumbnail'); class MovingThumbnail { type = 'MovingThumbnail'; - #data; - constructor(data) { - this.#data = data; - } - - get thumbnails() { - return this.#data.movingThumbnailDetails.thumbnails.map((thumbnail) => new Thumbnail(thumbnail)).sort((a, b) => b.width - a.width); + return data.movingThumbnailDetails.thumbnails.map((thumbnail) => new Thumbnail(thumbnail)).sort((a, b) => b.width - a.width); } } diff --git a/lib/parser/contents/classes/NavigationEndpoint.js b/lib/parser/contents/classes/NavigationEndpoint.js index c752b622..6885e0bc 100644 --- a/lib/parser/contents/classes/NavigationEndpoint.js +++ b/lib/parser/contents/classes/NavigationEndpoint.js @@ -84,6 +84,12 @@ class NavigationEndpoint { } } + if (data?.performCommentActionEndpoint) { + this.perform_comment_action = { + action: data?.performCommentActionEndpoint.action + } + } + if (data?.offlineVideoEndpoint) { this.offline_video = { video_id: data.offlineVideoEndpoint.videoId, diff --git a/lib/parser/contents/classes/PlayerOverlayAutoplay.js b/lib/parser/contents/classes/PlayerOverlayAutoplay.js index 4b7d874e..616d1377 100644 --- a/lib/parser/contents/classes/PlayerOverlayAutoplay.js +++ b/lib/parser/contents/classes/PlayerOverlayAutoplay.js @@ -18,7 +18,7 @@ class PlayerOverlayAutoplay { this.published = new Text(data.publishedTimeText); this.background = Thumbnail.fromResponse(data.background); this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); - this.author = new Author({ nav_text: data.byline }); + this.author = new Author(data.byline); this.cancel_button = Parser.parse(data.cancelButton); this.next_button = Parser.parse(data.nextButton); this.close_button = Parser.parse(data.closeButton); diff --git a/lib/parser/contents/classes/Playlist.js b/lib/parser/contents/classes/Playlist.js index 67526d68..5990d396 100644 --- a/lib/parser/contents/classes/Playlist.js +++ b/lib/parser/contents/classes/Playlist.js @@ -12,10 +12,10 @@ class Playlist { constructor(data) { this.id = data.playlistId; this.title = new Text(data.title); - - this.author = data.shortBylineText?.simpleText && - new Text(data.shortBylineText) || - new PlaylistAuthor({ nav_text: data.shortBylineText, badges: data.ownerBadges }); + + this.author = data.shortBylineText?.simpleText ? + new Text(data.shortBylineText) : + new PlaylistAuthor(data.longBylineText, data.ownerBadges, null); this.thumbnails = Thumbnail.fromResponse(data.thumbnail || { thumbnails: data.thumbnails.map((th) => th.thumbnails).flat(1) }); this.video_count = new Text(data.thumbnailText); diff --git a/lib/parser/contents/classes/PlaylistMetadata.js b/lib/parser/contents/classes/PlaylistMetadata.js index f8ec39ae..028ab3be 100644 --- a/lib/parser/contents/classes/PlaylistMetadata.js +++ b/lib/parser/contents/classes/PlaylistMetadata.js @@ -1,9 +1,11 @@ +'use strict'; + class PlaylistMetadata { type = 'PlaylistMetadata'; constructor(data) { this.title = data.title; - this.description = data.description; + this.description = data.description || null; // XXX: Appindexing should be in microformat } } diff --git a/lib/parser/contents/classes/PlaylistSidebar.js b/lib/parser/contents/classes/PlaylistSidebar.js index 5fe027bd..b935a010 100644 --- a/lib/parser/contents/classes/PlaylistSidebar.js +++ b/lib/parser/contents/classes/PlaylistSidebar.js @@ -7,7 +7,11 @@ class PlaylistSidebar { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js b/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js index 91b34225..7f3d397e 100644 --- a/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js +++ b/lib/parser/contents/classes/PlaylistSidebarPrimaryInfo.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); diff --git a/lib/parser/contents/classes/PlaylistSidebarSecondaryInfo.js b/lib/parser/contents/classes/PlaylistSidebarSecondaryInfo.js index 442f0622..8e8766e0 100644 --- a/lib/parser/contents/classes/PlaylistSidebarSecondaryInfo.js +++ b/lib/parser/contents/classes/PlaylistSidebarSecondaryInfo.js @@ -1,11 +1,13 @@ +'use strict'; + const Parser = require('..'); class PlaylistSidebarSecondaryInfo { type = 'PlaylistSidebarSecondaryInfo'; constructor(data) { - this.owner = data.videoOwner && Parser.parse(data.videoOwner); - this.button = data.button && Parser.parse(data.button); + this.owner = Parser.parse(data.videoOwner) || null; + this.button = Parser.parse(data.button) || null; } } diff --git a/lib/parser/contents/classes/PlaylistVideo.js b/lib/parser/contents/classes/PlaylistVideo.js index 4ae238c1..5c1b49ab 100644 --- a/lib/parser/contents/classes/PlaylistVideo.js +++ b/lib/parser/contents/classes/PlaylistVideo.js @@ -1,6 +1,7 @@ 'use strict'; const Text = require('./Text'); +const Parser = require('..'); const Thumbnail = require('./Thumbnail'); const PlaylistAuthor = require('./PlaylistAuthor'); const NavigationEndpoint = require('./NavigationEndpoint'); @@ -12,11 +13,13 @@ class PlaylistVideo { this.id = data.videoId; this.index = new Text(data.index); this.title = new Text(data.title); - this.author = new PlaylistAuthor({ nav_text: data.shortBylineText }); + this.author = new PlaylistAuthor(data.shortBylineText); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); + this.thumbnail_overlays = Parser.parse(data.thumbnailOverlays); this.set_video_id = data?.setVideoId; this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.is_playable = data.isPlayable; + this.menu = Parser.parse(data.menu); this.duration = { text: new Text(data.lengthText).text, seconds: parseInt(data.lengthSeconds) diff --git a/lib/parser/contents/classes/Poll.js b/lib/parser/contents/classes/Poll.js new file mode 100644 index 00000000..faa41b9b --- /dev/null +++ b/lib/parser/contents/classes/Poll.js @@ -0,0 +1,27 @@ +'use strict'; + +const Text = require('./Text'); +const Thumbnail = require('./Thumbnail'); +const NavigationEndpoint = require('./NavigationEndpoint'); + +class Poll { + type = 'Poll'; + + constructor(data) { + this.choices = data.choices.map((choice) => ({ + text: new Text(choice.text).toString(), + select_endpoint: new NavigationEndpoint(choice.selectServiceEndpoint), + deselect_endpoint: new NavigationEndpoint(choice.deselectServiceEndpoint), + vote_ratio_if_selected: choice.voteRatioIfSelected, + vote_percentage_if_selected: new Text(choice.votePercentageIfSelected), + vote_ratio_if_not_selected: choice.voteRatioIfSelected, + vote_percentage_if_not_selected: new Text(choice.votePercentageIfSelected), + image: Thumbnail.fromResponse(choice.image) + })); + + this.total_votes = new Text(data.totalVotes); + this.poll_type = data.type; + } +} + +module.exports = Poll; \ No newline at end of file diff --git a/lib/parser/contents/classes/Post.js b/lib/parser/contents/classes/Post.js new file mode 100644 index 00000000..73168bb2 --- /dev/null +++ b/lib/parser/contents/classes/Post.js @@ -0,0 +1,13 @@ +'use strict'; + +const BackstagePost = require('./BackstagePost'); + +class Post extends BackstagePost { + type = 'Post'; + + constructor(data) { + super(data); + } +} + +module.exports = Post; \ No newline at end of file diff --git a/lib/parser/contents/classes/ProfileColumn.js b/lib/parser/contents/classes/ProfileColumn.js index c96e7de6..88c84007 100644 --- a/lib/parser/contents/classes/ProfileColumn.js +++ b/lib/parser/contents/classes/ProfileColumn.js @@ -7,7 +7,11 @@ class ProfileColumn { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/ProfileColumnStats.js b/lib/parser/contents/classes/ProfileColumnStats.js index 7f50c78f..edc5a494 100644 --- a/lib/parser/contents/classes/ProfileColumnStats.js +++ b/lib/parser/contents/classes/ProfileColumnStats.js @@ -7,7 +7,11 @@ class ProfileColumnStats { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/ReelShelf.js b/lib/parser/contents/classes/ReelShelf.js index 8b95dfbf..11f25176 100644 --- a/lib/parser/contents/classes/ReelShelf.js +++ b/lib/parser/contents/classes/ReelShelf.js @@ -9,7 +9,11 @@ class ReelShelf { this.title = new Text(data.title); this.items = Parser.parse(data.items); this.endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null; - this.contents = this.items; // XXX: alias for consistency + } + + // XXX: alias for consistency + get contents() { + return this.items; } } diff --git a/lib/parser/contents/classes/RichItem.js b/lib/parser/contents/classes/RichItem.js index c823a5f5..e8369b91 100644 --- a/lib/parser/contents/classes/RichItem.js +++ b/lib/parser/contents/classes/RichItem.js @@ -4,7 +4,7 @@ class RichItem { type = 'RichItem'; constructor(data) { - this.content = Parser.parse(data.content); + return Parser.parse(data.content); } } diff --git a/lib/parser/contents/classes/RichSection.js b/lib/parser/contents/classes/RichSection.js index 8dea137b..da9de899 100644 --- a/lib/parser/contents/classes/RichSection.js +++ b/lib/parser/contents/classes/RichSection.js @@ -1,13 +1,12 @@ 'use strict'; -//const Parser = require('..'); +const Parser = require('..'); -// TODO: implement all renderers related to this class RichSection { type = 'RichSection'; - constructor(/* data */) { - // this.contents = Parser.parse(data.content); + constructor(data) { + this.contents = Parser.parse(data.content); } } diff --git a/lib/parser/contents/classes/RichShelf.js b/lib/parser/contents/classes/RichShelf.js index e33baf8e..9cd6d0c1 100644 --- a/lib/parser/contents/classes/RichShelf.js +++ b/lib/parser/contents/classes/RichShelf.js @@ -1,3 +1,5 @@ +'use strict'; + const Parser = require('..'); const NavigationEndpoint = require('./NavigationEndpoint'); const Text = require('./Text'); diff --git a/lib/parser/contents/classes/SectionList.js b/lib/parser/contents/classes/SectionList.js index 682d4b3f..bee232d5 100644 --- a/lib/parser/contents/classes/SectionList.js +++ b/lib/parser/contents/classes/SectionList.js @@ -6,7 +6,9 @@ class SectionList { type = 'SectionList'; constructor(data) { - this.target_id = data.targetId || null; + data.targetId + && (this.target_id = data.targetId); + this.contents = Parser.parse(data.contents); if (data.continuations) { @@ -17,7 +19,8 @@ class SectionList { } } - this.header = Parser.parse(data.header); + data.header && + (data.header = Parser.parse(data.header)); } } diff --git a/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js b/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js new file mode 100644 index 00000000..67097c9a --- /dev/null +++ b/lib/parser/contents/classes/ThumbnailOverlayPlaybackStatus.js @@ -0,0 +1,13 @@ +'use strict'; + +const Text = require('./Text'); + +class ThumbnailOverlayPlaybackStatus { + type = 'ThumbnailOverlayPlaybackStatus'; + + constructor(data) { + this.text = data.texts.map((text) => new Text(text))[0].toString(); + } +} + +module.exports = ThumbnailOverlayPlaybackStatus; \ No newline at end of file diff --git a/lib/parser/contents/classes/ToggleButton.js b/lib/parser/contents/classes/ToggleButton.js index 112f8c7f..70b39396 100644 --- a/lib/parser/contents/classes/ToggleButton.js +++ b/lib/parser/contents/classes/ToggleButton.js @@ -14,16 +14,24 @@ class ToggleButton { this.is_toggled = data.isToggled; this.is_disabled = data.isDisabled; this.icon_type = data.defaultIcon.iconType; - + + const acc_label = + data.defaultText?.accessibility.accessibilityData.label || + data?.accessibility?.label; + this.icon_type == 'LIKE' && - (this.like_count = parseInt(data.defaultText.accessibility.accessibilityData.label.replace(/\D/g, ''))) && + (this.like_count = parseInt(acc_label.replace(/\D/g, ''))) && (this.short_like_count = new Text(data.defaultText).toString()); - this.endpoint = data.defaultServiceEndpoint?.commandExecutorCommand?.commands && new NavigationEndpoint(data.defaultServiceEndpoint.commandExecutorCommand.commands.pop()); + this.endpoint = + data.defaultServiceEndpoint?.commandExecutorCommand?.commands && + new NavigationEndpoint(data.defaultServiceEndpoint.commandExecutorCommand.commands.pop()) || + new NavigationEndpoint(data.defaultServiceEndpoint); + this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); - this.button_id = data.toggleButtonSupportedData?.toggleButtonIdData?.id; - this.target_id = data.targetId; + this.button_id = data.toggleButtonSupportedData?.toggleButtonIdData?.id || null; + this.target_id = data.targetId || null; } } diff --git a/lib/parser/contents/classes/VerticalList.js b/lib/parser/contents/classes/VerticalList.js index 9a0bec55..f83e7e7f 100644 --- a/lib/parser/contents/classes/VerticalList.js +++ b/lib/parser/contents/classes/VerticalList.js @@ -8,10 +8,14 @@ class VerticalList { constructor(data) { this.items = Parser.parse(data.items); - this.contents = this.items; // XXX: alias for consistency this.collapsed_item_count = data.collapsedItemCount; this.collapsed_state_button_text = new Text(data.collapsedStateButtonText); } + + // XXX: alias for consistency + get contents() { + return this.items; + } } module.exports = VerticalList; \ No newline at end of file diff --git a/lib/parser/contents/classes/Video.js b/lib/parser/contents/classes/Video.js index 2ddee596..114bcd67 100644 --- a/lib/parser/contents/classes/Video.js +++ b/lib/parser/contents/classes/Video.js @@ -32,6 +32,7 @@ class Video { this.published = new Text(data.publishedTimeText); this.view_count_text = new Text(data.viewCountText); this.short_view_count_text = new Text(data.shortViewCountText); + const upcoming = data.upcomingEventData && Number(`${data.upcomingEventData.startTime}000`); if (upcoming) this.upcoming = new Date(upcoming); diff --git a/lib/parser/contents/classes/VideoOwner.js b/lib/parser/contents/classes/VideoOwner.js index 2389c991..1ce5f0d3 100644 --- a/lib/parser/contents/classes/VideoOwner.js +++ b/lib/parser/contents/classes/VideoOwner.js @@ -7,7 +7,7 @@ class VideoOwner { type = 'VideoOwner'; constructor(data) { - this.subscription_button = data.subscriptionButton; + this.subscription_button = data.subscriptionButton || null; this.subscriber_count = new Text(data.subscriberCountText); this.author = new Author({ ...data.title, diff --git a/lib/parser/contents/index.js b/lib/parser/contents/index.js index f32881f9..c2c93ca8 100644 --- a/lib/parser/contents/index.js +++ b/lib/parser/contents/index.js @@ -8,7 +8,7 @@ class AppendContinuationItemsAction { type = 'appendContinuationItemsAction'; constructor (data) { - this.continuation_items = Parser.parse(data.continuationItems); + this.contents = Parser.parse(data.continuationItems); } } @@ -17,7 +17,7 @@ class ReloadContinuationItemsCommand { constructor (data) { this.target_id = data.targetId; - this.continuation_items = Parser.parse(data.continuationItems); + this.contents = Parser.parse(data.continuationItems) } } @@ -127,7 +127,7 @@ class Parser { return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand); if (action.appendContinuationItemsAction) return new AppendContinuationItemsAction(action.appendContinuationItemsAction); - }).filter((item) => item)); + }).filter((item) => item)); } static parseFormats(formats) { diff --git a/lib/parser/youtube/Channel.js b/lib/parser/youtube/Channel.js index c770671e..31d66f7c 100644 --- a/lib/parser/youtube/Channel.js +++ b/lib/parser/youtube/Channel.js @@ -1,47 +1,66 @@ +'use strict'; + const TabbedFeed = require('../../core/TabbedFeed'); class Channel extends TabbedFeed { - /** - * @type {import('../parser/contents/ChannelMetadata')} - */ - metadata; - - constructor(actions, data, already_parsed = false) { - super(actions, data, already_parsed); - this.metadata = this.page.metadata; - this.title = this.metadata.title || ''; - this.description = this.metadata.description || ''; - } - - getVideos() { - return this.getTab('Videos'); + #tab; + + constructor(actions, data, already_parsed = false) { + super(actions, data, already_parsed); + this.header = { + author: this.page.header.author, + subscribers: this.page.header.subscribers.toString(), + banner: this.page.header.banner, + tv_banner: this.page.header.tv_banner, + mobile_banner: this.page.header.mobile_banner, + header_links: this.page.header.header_links } - getPlaylists() { - return this.getTab('Playlists'); - } + this.metadata = { ...this.page.metadata, ...this.page.microformat }; + + this.sponsor_button = this.page.header.sponsor_button || null; + this.subscribe_button = this.page.header.subscribe_button || null; + + const tab = this.page.contents.tabs.get({ selected: true }); + + this.current_tab = tab; + } + + async getVideos() { + const tab = await this.getTab('Videos'); + return new Channel(this.actions, tab.page, true); + } - getHome() { - return this.getTab('Home'); - } + async getPlaylists() { + const tab = await this.getTab('Playlists'); + return new Channel(this.actions, tab.page, true); + } - getCommunity() { - return this.getTab('Community'); - } + async getHome() { + const tab = await this.getTab('Home'); + return new Channel(this.actions, tab.page, true); + } - getChannels() { - return this.getTab('Channels'); - } + async getCommunity() { + const tab = await this.getTab('Community'); + return new Channel(this.actions, tab.page, true); + } - /** - * Get the channel about page - * - * @returns {Promise} - */ - async getAbout() { - const target_tab = await this.getTab('About'); - return target_tab.memo.get('ChannelAboutFullMetadata')?.[0]; - } + async getChannels() { + const tab = await this.getTab('Channels'); + return new Channel(this.actions, tab.page, true); + } + + /** + * Retrieves the channel about page. + * Note that this does not return a new {@link Channel} object. + * + * @returns {Promise} + */ + async getAbout() { + const tab = await this.getTab('About'); + return tab.memo.get('ChannelAboutFullMetadata')?.[0]; + } } module.exports = Channel; \ No newline at end of file diff --git a/lib/parser/youtube/History.js b/lib/parser/youtube/History.js index 220b3f53..848e310c 100644 --- a/lib/parser/youtube/History.js +++ b/lib/parser/youtube/History.js @@ -1,50 +1,31 @@ 'use strict'; -const { observe } = require('../../utils/Utils'); +const Feed = require('../../core/Feed'); + +// TODO: make filter actions usable /** @namespace */ -class History { - #page; - #actions; - #continuation; +class History extends Feed { + /** + * @param {import('../../core/Actions')} actions + * @param {object} data - parsed data. + * @param {boolean} already_parsed + */ + constructor(actions, data, already_parsed = false) { + super(actions, data, already_parsed) + + this.sections = this.memo.get('ItemSection'); + this.feed_actions = this.memo.get('BrowseFeedActions')?.[0] || []; + } /** - * @param {object} page - parsed data. - * @param {import('../../core/Actions')} actions - * @param {boolean} is_continuation + * Retrieves next batch of contents. + * + * @returns {Promise.} */ - constructor(page, actions, is_continuation) { - this.#page = page; - this.#actions = actions; - - const tab = page.contents?.tabs.get({ selected: true }); - const secondary_contents = page.contents?.secondary_contents; - - const contents = is_continuation - && page.continuation_items - || tab.content.contents; - - this.#continuation = contents.get({ type: 'ContinuationItem' }, true); - - this.feed_actions = secondary_contents?.contents || null; - - this.sections = observe(contents.map((section) => ({ - header: section.header, - contents: section.contents - }))); - } - async getContinuation() { - const response = await this.#continuation.endpoint.call(this.#actions); - return new History(response.on_response_received_actions[0], this.#actions, true); - } - - get has_continuation() { - return !!this.#continuation; - } - - get page() { - return this.#page; + const continuation = await this.getContinuationData(); + return new History(this.actions, continuation, true); } } diff --git a/lib/parser/youtube/Library.js b/lib/parser/youtube/Library.js index 7df36177..2032fc86 100644 --- a/lib/parser/youtube/Library.js +++ b/lib/parser/youtube/Library.js @@ -2,6 +2,8 @@ const Parser = require('../contents'); const History = require('./History'); +const Playlist = require('./Playlist'); +const Feed = require('../../core/Feed'); const { observe } = require('../../utils/Utils'); /** @namespace */ @@ -25,9 +27,10 @@ class Library { this.profile = { stats, user_info }; + /** @type {{ type: string, title: import('../contents/classes/Text'), contents: object[], getAll: Promise. }[] } */ this.sections = observe(shelves.map((shelf) => ({ type: shelf.icon_type, - title: shelf.title.toString(), + title: shelf.title, contents: shelf.content.items, getAll: () => this.#getAll(shelf) }))); @@ -41,17 +44,13 @@ class Library { const page = await button.endpoint.call(this.#actions); switch (shelf.icon_type) { - case 'WATCH_HISTORY': - return new History(page, this.#actions); + case 'LIKE': case 'WATCH_LATER': - // TODO - break; - case 'LIKE': - // TODO - break; + return new Playlist(this.#actions, page, true); + case 'WATCH_HISTORY': + return new History(this.#actions, page, true); case 'CONTENT_CUT': - // TODO - break; + return new Feed(this.#actions, page, true); default: } } diff --git a/lib/parser/youtube/Playlist.js b/lib/parser/youtube/Playlist.js index 174ff21e..9a00d9c5 100644 --- a/lib/parser/youtube/Playlist.js +++ b/lib/parser/youtube/Playlist.js @@ -1,47 +1,40 @@ +'use strict' + const Feed = require('../../core/Feed'); class Playlist extends Feed { - constructor(actions, data, already_parsed = false) { - super(actions, data, already_parsed); - this.primary_info = this.memo.get('PlaylistSidebarPrimaryInfo')?.[0]; + constructor(actions, data, already_parsed = false) { + super(actions, data, already_parsed); + + const primary_info = this.page.sidebar.contents.get({ type: 'PlaylistSidebarPrimaryInfo' }); + const secondary_info = this.page.sidebar.contents.get({ type: 'PlaylistSidebarSecondaryInfo' }); + + this.info = { + ...this.page.metadata, + ...{ + author: secondary_info.owner.author, + thumbnails: primary_info.thumbnail_renderer.thumbnail, + total_items: this.#getStat(0, primary_info), + views: this.#getStat(1, primary_info), + last_updated: this.#getStat(2, primary_info) + } } + + this.menu = primary_info.menu; + this.endpoint = primary_info.endpoint; + } - #getStat(index) { - if (!this.primary_info || !this.primary_info.stats) - return 'N/A'; - return this.primary_info.stats[index]?.toString() || 'N/A'; - } + #getStat(index, primary_info) { + if (!primary_info || !primary_info.stats) return 'N/A'; + return primary_info.stats[index]?.toString() || 'N/A'; + } - get title() { - if (!this.primary_info) - return ''; - return this.primary_info.title.toString(); - } - - get description() { - if (!this.primary_info) - return ''; - return this.primary_info.description.toString(); - } - - get total_items() { - return this.#getStat(0); - } - - get views() { - return this.#getStat(1); - } - - get last_updated() { - return this.#getStat(2); - } - - /** - * @alias videos - */ - get items () { - return this.videos; - } + /** + * @alias videos + */ + get items() { + return this.videos; + } } -module.exports = Playlist; +module.exports = Playlist; \ No newline at end of file diff --git a/lib/parser/youtube/Search.js b/lib/parser/youtube/Search.js index 43e00046..2639a1d1 100644 --- a/lib/parser/youtube/Search.js +++ b/lib/parser/youtube/Search.js @@ -14,7 +14,7 @@ class Search extends Feed { super(actions, data, already_parsed); const contents = this.page.contents?.primary_contents.contents - || this.page.on_response_received_commands[0].continuation_items; + || this.page.on_response_received_commands[0].contents; const secondary_contents = this.page.contents?.secondary_contents?.contents; diff --git a/lib/parser/youtube/VideoInfo.js b/lib/parser/youtube/VideoInfo.js index bfc458c8..11e98807 100644 --- a/lib/parser/youtube/VideoInfo.js +++ b/lib/parser/youtube/VideoInfo.js @@ -142,7 +142,7 @@ class VideoInfo { const response = await filter.endpoint.call(this.#actions); const data = response.on_response_received_endpoints.get({ target_id: 'watch-next-feed' }); - this.watch_next_feed = data.continuation_items; + this.watch_next_feed = data.contents; return this; } @@ -159,7 +159,7 @@ class VideoInfo { const response = await this.#watch_next_continuation.endpoint.call(this.#actions); const data = response.on_response_received_endpoints.get({ type: 'appendContinuationItemsAction' }); - this.watch_next_feed = data.continuation_items; + this.watch_next_feed = data.contents; this.#watch_next_continuation = this.watch_next_feed.pop(); return this.watch_next_feed; diff --git a/lib/utils/Utils.js b/lib/utils/Utils.js index fbddb2bb..74fa4490 100644 --- a/lib/utils/Utils.js +++ b/lib/utils/Utils.js @@ -119,11 +119,7 @@ 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]; - } + return obj1[key] === (obj2[key].constructor.name === 'Text' ? obj2[key].toString() : obj2[key]); }); }